[FEAT] 이미지 예외 처리 및 S3 Config 이동 그리고 예외 원인 추가#28
Conversation
이미지 관련 작업에 대한 사용자 지정 예외 처리를 도입합니다. 특정 오류 코드와 해당 HTTP 상태 및 메시지를 정의하여 더욱 유용한 오류 응답을 제공하고 디버깅을 개선합니다.
IllegalArgumentExceptions를 사용자 지정 ImageException으로 대체하여 더욱 구체적인 오류 컨텍스트를 제공하고 더 나은 오류 처리 및 관리를 가능하게 합니다. 이미지 관련 예외 처리를 중앙 집중화하여 코드 명확성과 유지 관리 용이성을 향상합니다.
AppException에 두 개의 새로운 생성자를 추가하여 원인을 가진 예외를 처리합니다. 원래 예외(cause)를 보존하면서, 우리 시스템 표준 예 (AppException 계층)로 감싸는 패턴입니다. 로그에는 진짜 원인(IOException 등)을 남기고, 클라이언트에는 우리가 정의한 에러코드 + 포맷된 메시지만 보여주기 위한 구조입니다. 기존에는, - 생성자 1: ErrorCode + args → errorCode.format(args)로 메시지 만듦 - 생성자 2: ErrorCode만 → 템플릿 그대로 메시지 사용 즉, AppException은 message를 ErrorCode에서만 만들고, 원래 IOException, SQLException 같은 cause 정보는 받지 않고 있어요. 그래서 지금 GlobalExceptionHandler에서는, ``` @ExceptionHandler(AppException.class) public ResponseEntity<ErrorResponse> handleApp(AppException ex, HttpServletRequest request) { ErrorCode code = ex.getErrorCode(); ... return ResponseEntity.status(code.getHttpStatus()) .body(ErrorResponse.of( type, title, code.getHttpStatus(), ex.getMessage(), // <-- 여기엔 ErrorCode 기반 메시지만 들어감 instance, errorCode, null )); } ``` 클라이언트 쪽에는 **포맷된 메시지(ex.getMessage())**만 나가고, cause는 애초에 안 넣었기 때문에 존재하지 않습니다. ### cause를 받는 생성자를 왜 쓰는 이유! 예를 들어 이미지 처리에서 이런 코드가 있다고 해볼게요. ``` try { BufferedImage img = ImageIO.read(file.getInputStream()); ... } catch (IOException e) { throw new ImageException(ImageExceptionCode.IMAGE_IO_ERROR, e, "WebP 변환"); } ``` 이때 하고 싶은 건 1. 클라이언트에는 `"이미지: WebP 변환 중 입출력 오류가 발생했습니다."` 같은 깔끔한 메시지 + HTTP 상태 코드 전송 2. 서버 로그에는 진짜 원인인 `IOException`의 스택 트레이스를 그대로 남기기 즉, “로깅용 원인 예외(cause)를 붙여두고, 외부 응답은 ErrorCode 기반으로 통제”하려는 거예요. 이걸 위해 필요한 게 바로, ``` public AppException(ErrorCode errorCode, Throwable cause, Object... args) { super(errorCode.format(args), cause); // 메시지 + 원인 예외 같이 전달 this.errorCode = errorCode; this.args = args; } ``` 이런 생성자고, 그 위에 얹힌 게, ``` public ImageException(ErrorCode errorCode, Throwable cause, Object... args) { super(errorCode, cause, args); } ``` 이 패턴입니다. GlobalExceptionHandler와의 연동에서 응답 직렬화 실패를 처리하는 메서드가 있습니다. `log.error("응답 직렬화 실패(500): {}", rootCauseMessage(ex), ex);` 이렇게 쓰고 있죠. 만약 AppException이 cause를 지원하면, new ImageException(..., e, "WebP 변환") 같이 감싼 예외가 들어왔을 때 rootCauseMessage(ex)는 IOException까지 타고 내려가서 진짜 메시지를 뽑아줍니다. 로그는 ex 자체를 함께 넘기기 때문에, 스택 트레이스에는 cause 체인이 전부 찍힙니다. 하지만 클라이언트 응답은 여전히 ex.getMessage() (ErrorCode 기반 포맷 메시지)만 쓰기 때문에 내부 기술적인 메시지(Broken pipe, Connection reset, 파일 경로 등)는 외부로 안 나가요. 이게 이 패턴의 핵심 장점입니다. 이미지뿐 아니라 다른 도메인에서도 IO, DB, 외부 API 예외를 감쌀 일이 생기면 도메인별 XxxException에서 편하게 super(errorCode, cause, args) 부르는 식으로 확장해보시면 딱 좋습니다.
|
Caution Review failedThe pull request is closed. WalkthroughAppException에 원인(Throwable cause)을 받는 두 개의 새로운 생성자를 추가하고, ImageException 예외 클래스와 ImageExceptionCode 열거형을 도입하여 이미지 업로드 서비스의 예외 처리를 일관된 도메인 예외로 통합했습니다. S3Config 패키지를 이동하고 유효성 검사 로직을 강화했습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 주의 필요 영역:
Possibly related PRs
Poem
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (6)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces comprehensive exception handling for image operations by replacing generic IllegalArgumentException and RuntimeException with domain-specific ImageException, moves the S3 configuration to a more appropriate package location, and enhances the base AppException class to support exception chaining with cause information.
- Adds cause parameter support to
AppExceptionfor better exception chain preservation in logging - Introduces
ImageExceptionandImageExceptionCodefor structured, type-safe image operation error handling - Replaces all generic exceptions in
ImageUploadServicewith specificImageExceptioninstances
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/team/wego/wegobackend/infrastructure/aws/S3Config.java | Moves S3 configuration from image.infrastructure to infrastructure.aws package for better architectural organization |
| src/main/java/team/wego/wegobackend/common/exception/AppException.java | Adds two new constructors supporting Throwable cause parameter for exception chaining |
| src/main/java/team/wego/wegobackend/image/domain/exception/ImageExceptionCode.java | Defines comprehensive error codes for image operations with appropriate HTTP status codes and message templates |
| src/main/java/team/wego/wegobackend/image/domain/exception/ImageException.java | Implements domain-specific exception class extending AppException with all constructor variants |
| src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java | Replaces IllegalArgumentException with ImageException throughout, adds validateDir method, and wraps IOExceptions with ImageException for consistent error handling |
| src/test/http/image/image-api.http | Adds comprehensive HTTP test cases for validating exception scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public enum ImageExceptionCode implements ErrorCode { | ||
|
|
||
| INVALID_IMAGE_SIZE(HttpStatus.BAD_REQUEST, | ||
| "이미지: 크기가 너무 큽니다. 최대 %,d bytes 까지만 허용됩니다."), |
There was a problem hiding this comment.
The format specifier %,d uses comma grouping for thousands separators, which might not work correctly with String.format() depending on the default locale. For consistent behavior across different locales, consider using %d instead, or explicitly set a locale in the format method.
However, if comma grouping is intentional for better readability (e.g., "10,000,000 bytes"), ensure that the ErrorCode.format() method handles locale properly.
| "이미지: 크기가 너무 큽니다. 최대 %,d bytes 까지만 허용됩니다."), | |
| "이미지: 크기가 너무 큽니다. 최대 %d bytes 까지만 허용됩니다."), // NOTE: Use %d for consistent formatting across locales |
| --boundary-- | ||
|
|
||
| ### (예외 발생)dir에 허용되지 않는 문자 포함 → DIR_INVALID_PATTERN | ||
| POST http://localhost:8080/api/v1/images/original?dir=test 이미지 |
There was a problem hiding this comment.
The URL parameter contains a space character which may not be properly encoded in HTTP requests. The value test 이미지 should be URL-encoded as test%20%EC%9D%B4%EB%AF%B8%EC%A7%80 or use a hyphen/underscore instead (e.g., test-이미지 or test_이미지) for better compatibility.
| POST http://localhost:8080/api/v1/images/original?dir=test 이미지 | |
| POST http://localhost:8080/api/v1/images/original?dir=test%20%EC%9D%B4%EB%AF%B8%EC%A7%80 |
| @RequiredArgsConstructor(access = AccessLevel.PUBLIC) | ||
| @Getter(AccessLevel.PUBLIC) |
There was a problem hiding this comment.
[nitpick] The @RequiredArgsConstructor(access = AccessLevel.PUBLIC) and @Getter(AccessLevel.PUBLIC) annotations specify AccessLevel.PUBLIC explicitly, which is the default for enum constructors and getters. These can be simplified to @RequiredArgsConstructor and @Getter without the access level specification, or removed entirely since the manual getter implementations below already provide the same functionality.
| @RequiredArgsConstructor(access = AccessLevel.PUBLIC) | |
| @Getter(AccessLevel.PUBLIC) |
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
🔍 관련 이슈
🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
AppException에 두 개의 새로운 생성자를 추가하여 원인을 가진 예외를 처리합니다. 원래 예외(cause)를 보존하면서, 우리 시스템 표준 예 (AppException 계층)로 감싸는 패턴입니다. 로그에는 진짜 원인(IOException 등)을 남기고, 클라이언트에는 우리가 정의한 에러코드 + 포맷된 메시지만 보여주기 위한 구조입니다.
기존에는,
즉, AppException은 message를 ErrorCode에서만 만들고, 원래 IOException, SQLException 같은 cause 정보는 받지 않고 있어요.
그래서 지금 GlobalExceptionHandler에서는,
클라이언트 쪽에는 **포맷된 메시지(ex.getMessage())**만 나가고, cause는 애초에 안 넣었기 때문에 존재하지 않습니다.
cause를 받는 생성자를 왜 쓰는 이유!
예를 들어 이미지 처리에서 이런 코드가 있다고 해볼게요.
이때 하고 싶은 건
"이미지: WebP 변환 중 입출력 오류가 발생했습니다."같은 깔끔한 메시지 + HTTP 상태 코드 전송IOException의 스택 트레이스를 그대로 남기기즉, “로깅용 원인 예외(cause)를 붙여두고, 외부 응답은 ErrorCode 기반으로 통제”하려는 거예요.
이걸 위해 필요한 게 바로,
이런 생성자고, 그 위에 얹힌 게,
이 패턴입니다.
GlobalExceptionHandler와의 연동에서 응답 직렬화 실패를 처리하는 메서드가 있습니다.
log.error("응답 직렬화 실패(500): {}", rootCauseMessage(ex), ex);이렇게 쓰고 있죠. 만약 AppException이 cause를 지원하면, new ImageException(..., e, "WebP 변환") 같이 감싼 예외가 들어왔을 때 rootCauseMessage(ex)는 IOException까지 타고 내려가서 진짜 메시지를 뽑아줍니다. 로그는 ex 자체를 함께 넘기기 때문에, 스택 트레이스에는 cause 체인이 전부 찍힙니다.하지만 클라이언트 응답은 여전히 ex.getMessage() (ErrorCode 기반 포맷 메시지)만 쓰기 때문에 내부 기술적인 메시지(Broken pipe, Connection reset, 파일 경로 등)는 외부로 안 나가요.
이게 이 패턴의 핵심 장점입니다. 이미지뿐 아니라 다른 도메인에서도 IO, DB, 외부 API 예외를 감쌀 일이 생기면 도메인별 XxxException에서 편하게 super(errorCode, cause, args) 부르는 식으로 확장해보시면 딱 좋습니다.
Summary by CodeRabbit
릴리스 노트
Bug Fixes & Improvements
Tests
✏️ Tip: You can customize this high-level summary in your review settings.