사용기술
- 스프링부트
- 제이쿼리
- 타임리프 살짝
서론
디테일하게 파일을 저장하는 레포지토리를 만들어서 해당 레포지토리에서 파일을 꺼내오는 것이 아닌 파일 입출력을 스프링 부트에서 어떻게 구현할 수 있는지에 대한 궁금증으로 인해
단순히 account 엔티티에 프로필 이미지 경로와 이름 변수를 선언해 단건으로 구현해보기로 했습니다.
해당 글에서 작성한 코드들은 다른분들의 글을 많이 참고했음을 알려드립니다.
$ 참고글
- Spring Boot | multipart/form-data 파일 업로드 ( + React , Axios, REST API, multiple files)
- [Spring Boot] 게시판 구현 하기 (4) - 파일 업로드 & 다운로드
본론
전체적인 흐름은 이렇습니다.
프로필 이미지 필드 변수가 존재하지 않는 계정 엔티티에 프로필 이미지용 경로/ 이름 변수 선언해주기
👇🏻
엔티티에 필드변수가 추가되었으니 관련된 웹 계층/서비스 계층 테스트 코드 작성 후 다듬기
👉🏻 프로필 이미지가 업로드가 되었을 때 안되었을 때두 가지 케이스로 살펴보자.
👉 스프링 시큐리티에서 리소스에 대한 접근을 허용하도록 코드를 추가해주어야 한다.
👇🏻
앞단에서 제이쿼리로 파일 업로드받아서 뒷단으로 넘겨주기
👇🏻
타임리 프로 해당 프로필 이미지 출력하기
계정 엔티티 수정하기
지금 상황은 게시판 홈페이지에 프로필 이미지 관련 기능이 하나도 구현되어있지 않은 상황이므로 무에서 유를 창조해야 합니다. 👀
일단 파일 업로드 관련 디펜던시를 build.gradle 에 추가해주고 어느 경로에 업로드된 파일을 저장해줄지 yaml파일에 지정해줍니다.
dependencies {
implementation('commons-io:commons-io:2.11.0')
}
multipart:
max-file-size: 10MB
max-request-size: 10MB
location: /Users/brinc/Desktop
enabled: true
com.example.upload.path.profileImg : /Users/brinc/Desktop
절대 경로로 작성해야 합니다.
그리고 기존에 생성돼있는 계정 엔티티에 이미지 파일의 경로와 이름을 필드 변수로 선언해줍니다.
...
@Setter private String profileImgName;
@Setter private String profileImgPath;
...
계정 엔티티와 연관된 Dto에도 파일 경로를 추가해줄 수 있지 않을까? 하는 생각이 있었는데, 여러 시도 결과 @RequestPart로 회원가입 리퀘스트 Dto와 파일을 따로 요청받도록 했습니다.
제 실력선에서는 프런트단에서 업로드 시에 폼 데이터를 하나의 Dto로 전달받을 때 파일은 파일로, 양식은 json 타입으로 전달받는 방법을 찾지 못했습니다 😿
컨트롤러 내 회원가입 메서드 수정하기
이제 디펜던시와 엔티티도 수정을 해주었고, 뷰는 단순히 태그만 추가해줘도 되기 때문에 컨트롤러 내에 있는 메서드부터 수정해줍니다.
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestPart("signupDto") @Valid UserAccount.SignupDto signupDto, BindingResult bindingResult
,@RequestPart(value = "imgFile",required = false) MultipartFile imgFile) throws IOException {
if (bindingResult.hasErrors()) {
return new ResponseEntity<>(controllerUtil.getErrors(bindingResult), HttpStatus.BAD_REQUEST);
}
else if(!signupDto.getPassword1().equals(signupDto.getPassword2())){
bindingResult.addError(new FieldError("userCreateForm","password2","비밀번호가 일치하지 않습니다."));
return new ResponseEntity<>(controllerUtil.getErrors(bindingResult), HttpStatus.BAD_REQUEST);
}// -----추가된 부분-------
if(imgFile==null){
userService.saveUserAccountWithoutProfile(signupDto);
}
else {
userService.saveUserAccount(signupDto, imgFile);
}
return new ResponseEntity<>("success", HttpStatus.OK);
}
제 목적은 최대한 기존의 코드의 큰 변경이 없이 기능만 추가할 수 있는 걸 원했기 때문에, 회원가입 관련 메서드만 수정해주면 되었습니다.
기존의 코드에서 이미지 파일이 존재한다면 디폴트 이미지로 프로필 이미지를 설정하도록 했고, 존재한다면 이미지 파일을 같이 받아와서 서비스 계층으로 넘겨주도록 코드를 추가했습니다.
비즈니스 로직 수정하기
이제 컨트롤러에서 서비스 계층으로 전달하는 값이 추가되었으니, 해당 값을 또 가공해서 디비에 반영해야 하니 메서드를 수정해야 합니다.
public void saveUserAccount(UserAccount.SignupDto user,MultipartFile imgFile) throws IOException {
String password = user.getPassword1();
UserAccount account = userAccountRepository.save(user.toEntity());
account.setUserPassword(new BCryptPasswordEncoder().encode(password));
// ----- 추가된 부분 -----
UUID uuid = UUID.randomUUID();
String fileName = uuid.toString() + "_" + imgFile.getOriginalFilename();
File profileImg= new File(uploadPath,fileName);
imgFile.transferTo(profileImg);
account.setProfileImgName(fileName);
account.setProfileImgPath(uploadPath+"/"+fileName);
}
파일을 저장할 때는 랜덤 한 UUID를 생성해 이름에 부여합니다. (같은 이름을 가진 파일을 업로드할 때 오류가 생기면 안 되겠죠?)
그리고 UUID를 붙여서 새로 지정한 이미지 파일 이름과 경로를 기준으로 파일 객체를 생성합니다.
그리고 뷰에서 전달받은 파일을 multipartfile 클래스가 제공하는 transferTo 메서드로 파일 객체로 변환해줍니다.
그리고 간단하게 해당 엔티티의 프로필 이미지 경로와 이름을 업데이트해주면 됩니다!
회원가입 폼에 이미지 파일 업로드 태그 추가하기
이제 대충 뒷단은 구현했으니 앞단을 수정할 차례입니다.
<form method="post" style="width:700px" enctype="multipart/form-data">
<p class="register">회원가입</p>
<label for="imgFile">프로필 사진</label>
<input type="file" name="imgFile" id="imgFile" accept=".jpg, .png" class="form-control">
...
단순히 기존에 작성돼있던 폼에 enctype을 추가해서 폼 데이터로 전송할 것을 명시해주고, 프로필 사진을 업로드해야 할 부분에 input 태그로 type을 file로 지정해주면 됩니다.
accept 에는 확장자를 입력하면 해당 확장자가 아닐 경우에 업로드를 하지 못하게 해 줍니다.
그리고 해당 값을 제이쿼리를 이용해서 컨트롤러로 전송합니다.
function signupCheck() {
var formData = new FormData();
formData.append("imgFile", $("#imgFile")[0].files[0]);
var data = {
info: {
userId: $("#userId").val(),
password1: $("#password1").val(),
password2: $("#password2").val(),
nickname: $("#nickname").val(),
email: $("#email").val(),
memo: $("#memo").val()
}
}
formData.append(
"signupDto",
new Blob([JSON.stringify(data.info)], { type: "application/json" })
);
console.log(formData);
//위에서 만든 오브젝트를 json 타입으로 바꾼다.
$.ajax({
type : 'POST',
url : '/signup',
data : formData,
processData : false,
contentType : false,
success: function ( ){
alert("회원가입 성공");
},
저는 기존에 작성해뒀던 스크립트를 수정해서 사용했습니다.
formData 객체를 새로 생성해서 사용합니다.
객체의 첫 인덱스에는 이미지 파일을 담아주고, 두 번째 인덱스에 입력했던 값들을 json형태로 변환해서 담아줍니다.
키값에 입력되는 이름이 RequestPart 어노테이션의 이름에 들어갈 값과 같아야 합니다.
컨트롤러에 프로필 이미지를 가져와줄 메서드를 선언하자
프런트에서 해결해야 할 부분이 하나 더 남았는데, GET 메서드로 프로필 이미지를 바이트 배열로 전달받아야 합니다.
단순히 src태그에 경로를 붙여버리면, 빌드 전에 이미 존재하던 파일이 아닌 이상 읽어오지 못합니다.
@GetMapping("/accounts/{username}")
public ResponseEntity<?> getProfileImg (@PathVariable String username) throws IOException {
UserAccount.UserAccountDto accountDto = userService.getUserAccount(username);
InputStream inputStream = new FileInputStream(accountDto.profileImgPath());
byte[] imageByteArray = IOUtils.toByteArray(inputStream);
inputStream.close();
return new ResponseEntity<>(imageByteArray, HttpStatus.OK);
}
inputstream으로 파일을 읽어와서 바이트 배열로 전달해줍니다.
그리고 해당 프로필 이미지가 출력되어야 하는 부분에는 타임리프 문법인 th:src를 사용해서 이미지를 출력해줄 겁니다.
<img id="imgId" th:src="@{/accounts/}+${accountDto.userId}" width="100" height="100" alt="첨부이미지" th:if="${accountDto.profileImgPath() != null}" />
동작하는지 확인해보기
파일을 업로드해줍니다. accept에 png와 jpg 만 받도록 명시해놨기 때문에 해당 파일 외의 파일들은 업로드하지 못합니다.
이렇게 새로 생성한 계정에 yaml 파일에 지정해둔 경로에 이미지가 업로드된 모습을 볼 수 있습니다. (파일을 업로드하지 않을 때는 단순히 같은 메서드에 default 이미지의 경로와 이름을 대입해주면 됩니다 🤓)
제가 임시로 만들어둔 페이지입니다. 이미지를 잘 출력하는 모습을 볼 수 있습니다
마찬가지로 이렇게 비슷한 패턴으로 프로필 수정 페이지를 구현해서 프로필 이미지를 변경할 수도 있습니다.
잘 변경이 되네요 😎
마치며
- 입출력 스트림을 활용해보고 싶어서 시도해보았으나, 딥한 원리를 이해하지 못하고 구현한 느낌이 들어서 게시글 이미지/파일 첨부 기능 구현시에 좀더 딥하게 공부해볼 예정입니다.
- 해당 기능을 구현하면서 겪었던 오류는 절대경로를 제대로 지정하지 않아 발생한 오류와, 이미지 파일 출력을 단순 절대경로로 출력하려고 할때 없는 파일로 인식이 되어 발생한 오류 이렇게 총 두가지 입니다.
- 해당 기능을 구현할때 좀 더 살펴보면 좋은것은 MultipartFile 인터페이스가 어떠한 기능을 제공해주고 어떻게 돌아가는지 공식 문서를 참고하면 좋을 것 같습니다.
- 단순히 파일 자체를 dto 로 전달받아 업로드 하는 방식도 있습니다만 저는 제이쿼리로 기존의 회원가입 폼을 만들었기 때문에 제이쿼리로 전달하는 방법을 찾아보았습니다.
- 제이쿼리 스크립트 속 blob 은 [Javascript] API 활용 - Blob 해당 글을 참고하시면 좋을것 같습니다 :)
(해당 글은 블로그 이전으로 인하여 붙여넣기 된 글입니다.)
'Spring' 카테고리의 다른 글
스프링부트 + 타임리프로 동기식 댓글수정 기능 구현하기 (0) | 2022.12.13 |
---|---|
스프링부트 + JPA 양방향 맵핑 해시태그 기능으로 구현하기 (0) | 2022.12.13 |
스프링 부트+ JPA 로 파일 입출력을 쉽게 구현해보자 (Spring Data JPA) (0) | 2022.12.13 |
스프링부트 Toast UI 에디터 / 뷰어 적용 + 이미지 입출력 -2 (0) | 2022.12.13 |
스프링부트 Toast UI 에디터 / 뷰어 적용 + 이미지 입출력 -1 (0) | 2022.12.12 |