본문 바로가기

CodeLap 프로젝트

[CodeLap] IOExcpetion 언체크 예외로 변환 (리팩토링)

최근에 스프링을 딥하게 공부하고있다.

 

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 인터페이스이다.