최근에 스프링을 딥하게 공부하고있다.
API 예외처리, 체크 예외 -> 언체크 예외로 변환 등 배운 건 많지만 실제 프로젝트에 적용할 기회가 잘 없었다.
오늘은 뭔가 고칠게 없나 싶어서 프로젝트를 둘러보고있는데.. 발견해버렸다.
@PostMapping("/image-upload")
public void imageUpload(
@RequestPart(value = "multipartFile") MultipartFile multipartFile,
) throws IOException {
studyAppService.imageUpload(multipartFile);
}
컨트롤러에서, imageUpload 메소드에서 체크 예외를 던지고 있어 throws IOException을 던지고 있다.
먼저 수정하기전, 자기 자신한테 질문을 던져봤다.
1. IOException을 컨트롤러단에서 해결할 수 있나?
- 입출력 관련 에러라서 컨트롤러에서 해줄 수 있는게 없다.
2. IOException에 의존적이면 안되는가?
- 파일업로드 로직이 변경되면 예외 또한 변할 수 있다. 하지만 이 업로드 로직은 개발이 끝날때 까지 변하지 않을 것 같아서 의존적이어도 될 수도 있다. 그래서 서비스 쪽 인터페이스에서 의존해도 문제가 없다고 생각한다.
두 가지 생각이 부딪혔는데, 이 IOException을 서비스쪽이나 컨트롤러에서 해결해 줄 수 없다고 판단해서, RuntimeException, 즉 언체크 예외로 변환 후 적용하게됐다.
예외의 근원이 되는 곳으로 찾아들어가 보겠다.
@Override
public StudyFile imageUpload(MultipartFile multipartFile) throws IOException {
return (StudyFile) fileUpload.upload(multipartFile, dirName, create());
}
.
.
.
@Override
public FileStandard upload(MultipartFile multipartFile, String dirName, FileStandard file) throws IOException {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
return upload(uploadFile, multipartFile.getOriginalFilename(), dirName, file);
}
.
.
.
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(file.getOriginalFilename());
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
convert 메서드 내의 createNewFile() 메서드에서 IOException이 발생한다.
이쪽에서 던져지는 예외를, catch에서 새로 만든 예외를 던지게 할 것이다.
public class RuntimeIOException extends RuntimeException{
public RuntimeIOException(Throwable cause) {
super(cause);
}
}
Throwable 객체를 받는 생성자를 선언한다.
private Optional<File> convert(MultipartFile file) {
File convertFile = new File(file.getOriginalFilename());
try {
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
} catch (IOException e) {
throw new RuntimeIOException(e);
}
return Optional.empty();
}
catch에서 IOException을 그대로 받아서 던지는 것으로 처리했다.
이 던져지는 언체크 예외는 APIAdvice에서 처리할 것이다.
@Log4j2
@RestControllerAdvice
@RequiredArgsConstructor
public class ApiAdvice {
private final MessageSource messageSource;
@ExceptionHandler({RuntimeIOException.class})
public ErrorResponse uploadException(Throwable ex) {
log.info(ex.getMessage(), ex);
return new ErrorResponse(new ErrorData("파일 업로드에 실패했습니다."));
}
@Getter
@AllArgsConstructor
public static class ErrorResponse {
private ErrorData error;
}
@Getter
@AllArgsConstructor
public static class ErrorData {
private String message;
}
이런식으로 예외가 발생했을 때, ApiAdvice에서 받아준 후 ErrorData를 반환해주는 것으로 처리했다.
만약 업로드 중 예외가 발생하면 해당 예외메세지와 함께 statusCode가 반환될것이다.
이렇게 서비스, 컨트롤러단을 깔끔하게 정리했다.
package com.codelap.api.service.study;
import com.codelap.api.service.study.dto.GetStudiesDto.GetStudiesStudyDto;
import com.codelap.common.study.dto.GetOpenedStudiesDto;
import com.codelap.common.study.dto.GetStudiesCardDto;
import com.codelap.common.support.TechStack;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
public interface StudyAppService {
List<GetStudiesStudyDto> getStudies(Long userId);
List<GetStudiesCardDto.GetStudyInfo> getAttendedStudiesByUser(Long userId, String statusCond, List<TechStack> techStackList);
List<GetOpenedStudiesDto> getOpenedStudies();
void imageUpload(Long leaderId, Long studyId, MultipartFile multipartFile) throws IOException;
}
.
.
.
package com.codelap.api.service.study;
import com.codelap.api.service.study.dto.GetStudiesDto.GetStudiesStudyDto;
import com.codelap.common.study.domain.StudyFile;
import com.codelap.common.study.dto.GetOpenedStudiesDto;
import com.codelap.common.study.dto.GetStudiesCardDto.GetStudyInfo;
import com.codelap.common.support.TechStack;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface StudyAppService {
List<GetStudiesStudyDto> getStudies(Long userId);
List<GetStudyInfo> findStudyCardsByCond(Long userId, String statusCond, List<TechStack> techStackList);
List<GetOpenedStudiesDto> getOpenedStudies();
StudyFile imageUpload(MultipartFile multipartFile);
}
더 이상 예외를 던지지않는 AppSerivce 인터페이스이다.
'CodeLap 프로젝트' 카테고리의 다른 글
CodeLap - S3로 파일 업로드(자바, 스프링부트) (0) | 2023.05.12 |
---|---|
[CodeLap] Github Action, AWS(EC2, S3, RDS, CodeDeploy)를 활용한 자바 + 스프링부트 백앤드 서버 배포 - 2편 (0) | 2023.04.28 |
[CodeLap] DIP와 OCP를 지켜서 코드를 짜게 되면? (0) | 2023.04.27 |
[CodeLap] Github Action, AWS(EC2, S3, RDS, CodeDeploy)를 활용한 자바 + 스프링부트 백앤드 서버 배포 - 1편 (0) | 2023.04.27 |
[CodeLap] 조회 쿼리 성능 최적화 및 DTO 이너클래스 리팩토링 (0) | 2023.04.22 |