Codeit/스프린트 과제

[sprint4] IOException과 @Transactional (멘토님 피드백)

leejunkim 2025. 7. 14. 23:11

스프린트 4 완료 후, 멘토님한테서 이런 피드백을 받았다:

 

내 코드는 이렇게 쓰여져 있었다:

import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;

@Component
public class BinaryContentMapper {

    // DTO를 Entity로 변환하는 메소드
    public BinaryContent toBinaryContent(BinaryContentRequestDto binaryContentRequestDto) {
        return new BinaryContent(
                binaryContentRequestDto.userId(),
                binaryContentRequestDto.messageId(),
                binaryContentRequestDto.bytes(),
                binaryContentRequestDto.fileName(),
                binaryContentRequestDto.fileType()
        );
    } 

    // MultipartFile을 Entity로 변환하는 메소드
    public BinaryContent toBinaryContent(UUID userId, UUID messageId, MultipartFile file) {
        if (file == null || file.isEmpty()) {
            return null;
        }

        try {
            return new BinaryContent(
                    userId,
                    messageId,
                    file.getBytes(),
                    file.getOriginalFilename(),
                    file.getContentType()
            );
        } catch (IOException e) {
            throw new RuntimeException("파일 바이트를 읽는 중 오류가 발생했습니다.", e);
        }
    }
}

 

하지만 RuntimeException을 IOException으로 바꿔준다면

  • 먼저, 이 매퍼 클래스를 호출하는 모든 상위 레이어에도 IOException을 붙혀주어야 했다 (너무 번거로웠다 ㅠㅠ)
    • IOException은 오직 레포지토리에서만 발생을 하므로, 상위 계층인 서비스 레이어는 알아서는 안 될 예외다
  • 나중에 @Transactional 어노테이션을 사용할때 골치가 아파진다.

@Transactional과 예외처리

@Transactional 어노테이션은 unchecked exception들을 잡는데, IOException은 checked exception이다.

  • 중간에 이 매퍼 클래스를 호출한 상위 계층에 @Transaction 어노테이션이 붙고 어떠한 에러가 생겨 롤백을 해야 할때, 이 에러가 만약에 IOException같은 checked exception이면, 이 에러가 중간에 발생해도 롤백을 하지 않는다

여기서 간단하게 checked, unchecked exception을 비교하자면:

  • checked - 개발자가 직접 핸들링
  • unchecked - 런타임

GlobalExceptionHandler 

그래서 결국 sprint5로 넘어가면서 따로 GlobalExceptionHandler 클래스를 만들어주었다.

  • GlobalExceptionHandler(@RestControllerAdvice)는 애플리케이션 전역에서 발생한 예외들을 중간에 가로채서 처리한다
  • 또한 어떤 exception에 어떠한 response를 보낼지 커스터마이징이 가능하다
@Hidden // swagger과 restcontrolleradvice 같이 사용하고 싶어서 추가
@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(NoSuchElementException.class)
  public ResponseEntity<ErrorResponse> handleException(NoSuchElementException e) {
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getMessage());
    return ResponseEntity
        .status(HttpStatus.NOT_FOUND)
        .body(errorResponse);
  }

  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<ErrorResponse> handleException(IllegalArgumentException e) {
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage());
    return ResponseEntity
        .status(HttpStatus.BAD_REQUEST)
        .body(errorResponse);
  }

  @ExceptionHandler(Exception.class)
  public ResponseEntity<String> handleException(Exception e) {
    return ResponseEntity
        .status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body(e.getMessage());
  }

  @ExceptionHandler(FileAccessException.class)
  public ResponseEntity<String> handleFileAccessException(FileAccessException e) {
    return ResponseEntity
        .status(e.getStatus())
        .body(e.getMessage());
  }
}

 

또한 매퍼 부분도 이렇게 수정이 되었다.

@Component
public class BinaryContentMapper {

  public BinaryContentResponse toResponseDto(BinaryContent binaryContent) {
    return new BinaryContentResponse(
        binaryContent.getId(),
        binaryContent.getCreatedAt(),
        binaryContent.getFileName(),
        binaryContent.getSize(),
        binaryContent.getContentType(),
        Base64.getEncoder().encodeToString(binaryContent.getBytes())
    );
  }
}

 

이 경험 덕분에 @Transaction과 exception에 대해서 더 자세히 공부할 수 있었다.