Skip to content

Comments

[FEAT] 이미지 예외 처리 및 S3 Config 이동 그리고 예외 원인 추가#28

Merged
LimdaeIl merged 6 commits intomainfrom
feat/image-exception
Dec 6, 2025
Merged

[FEAT] 이미지 예외 처리 및 S3 Config 이동 그리고 예외 원인 추가#28
LimdaeIl merged 6 commits intomainfrom
feat/image-exception

Conversation

@LimdaeIl
Copy link
Collaborator

@LimdaeIl LimdaeIl commented Dec 6, 2025

📝 Pull Request

📌 PR 종류

해당하는 항목에 체크해주세요.

  • 기능 추가 (Feature)
  • 버그 수정 (Fix)
  • 문서 수정 (Docs)
  • 코드 리팩터링 (Refactor)
  • 테스트 추가 (Test)
  • 기타 변경 (Chore)

✨ 변경 내용

  • S3 Config 경로 이동(wegobackend/infratructure/aws)
  • AppException에 cause 추가
  • IllegalArgumentExceptions를 사용자 지정 ImageException으로 대체
  • 이미지 관련 작업에 대한 사용자 지정 예외 처리를 도입

🔍 관련 이슈

🧪 테스트

변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.

  • 유닛 테스트 추가 / 수정
  • 통합 테스트 검증
  • 수동 테스트 완료

🚨 확인해야 할 사항 (Checklist)

PR을 제출하기 전에 아래 항목들을 확인해주세요.

  • 코드 포매팅 완료
  • 불필요한 파일/코드 제거
  • 로직 검증 완료
  • 프로젝트 빌드 성공
  • 린트/정적 분석 통과 (해당 시)

🙋 기타 참고 사항

AppException에 두 개의 새로운 생성자를 추가하여 원인을 가진 예외를 처리합니다. 원래 예외(cause)를 보존하면서, 우리 시스템 표준 예 (AppException 계층)로 감싸는 패턴입니다. 로그에는 진짜 원인(IOException 등)을 남기고, 클라이언트에는 우리가 정의한 에러코드 + 포맷된 메시지만 보여주기 위한 구조입니다.

package team.wego.wegobackend.common.exception;

public class AppException extends RuntimeException {
    ....
    public AppException(ErrorCode errorCode, Throwable cause, Object... args) {
        super(errorCode.format(args), cause);
        this.errorCode = errorCode;
        this.args = args;
    }

    public AppException(ErrorCode errorCode, Throwable cause) {
        super(errorCode.getMessageTemplate(), cause);
        this.errorCode = errorCode;
        this.args = null;
    }
}

기존에는,

  • 생성자 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) 부르는 식으로 확장해보시면 딱 좋습니다.

Summary by CodeRabbit

릴리스 노트

  • Bug Fixes & Improvements

    • 이미지 업로드 시 더 자세한 오류 메시지 제공 (이미지 크기, 형식, 확장자, 디렉토리 유효성 검사 등)
    • 이미지 처리 중 발생하는 오류에 대한 개선된 에러 코드 시스템 도입
    • 업로드 실패 시 명확한 이유 구분 (잘못된 형식, 지원하지 않는 콘텐츠 타입, 경로 문제 등)
  • Tests

    • 이미지 업로드 오류 케이스에 대한 테스트 케이스 추가

✏️ Tip: You can customize this high-level summary in your review settings.

이미지 관련 작업에 대한 사용자 지정 예외 처리를 도입합니다. 특정 오류 코드와 해당 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) 부르는 식으로 확장해보시면 딱 좋습니다.
@LimdaeIl LimdaeIl self-assigned this Dec 6, 2025
Copilot AI review requested due to automatic review settings December 6, 2025 13:11
@LimdaeIl LimdaeIl added the ✨enhancement New feature or request label Dec 6, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

AppException에 원인(Throwable cause)을 받는 두 개의 새로운 생성자를 추가하고, ImageException 예외 클래스와 ImageExceptionCode 열거형을 도입하여 이미지 업로드 서비스의 예외 처리를 일관된 도메인 예외로 통합했습니다. S3Config 패키지를 이동하고 유효성 검사 로직을 강화했습니다.

Changes

Cohort / File(s) 변경 사항
예외 처리 기반 확장
src/main/java/team/wego/wegobackend/common/exception/AppException.java
Throwable cause를 매개변수로 받는 두 개의 새로운 생성자 추가: AppException(ErrorCode, Throwable, Object...)AppException(ErrorCode, Throwable)
도메인 예외 정의
src/main/java/team/wego/wegobackend/image/domain/exception/ImageException.java, src/main/java/team/wego/wegobackend/image/domain/exception/ImageExceptionCode.java
ImageException 클래스 신규 추가 (4개 오버로드 생성자); ImageExceptionCode 열거형 신규 추가 (INVALID_IMAGE_SIZE, UNSUPPORTED_IMAGE_CONTENT_TYPE, DIR_REQUIRED 등 12개 에러 코드)
이미지 업로드 서비스 예외 처리 개선
src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java
RuntimeException/IllegalArgumentException을 ImageException으로 대체; 디렉토리 유효성 검사 메서드 추가 (validateDir); WebP 변환, 이미지 크기, 콘텐츠 타입, 확장자 검증에 도메인 예외 코드 적용; 공개 API 시그니처는 변경 없음
S3 구성 파일 이동
src/main/java/team/wego/wegobackend/infrastructure/aws/S3Config.java
패키지 선언 변경: team.wego.wegobackend.image.infrastructureteam.wego.wegobackend.infrastructure.aws
오류 케이스 테스트 추가
src/test/http/image/image-api.http
디렉토리 검증(DIR_REQUIRED, DIR_INVALID_TRAVERSAL, DIR_TRAILING_SLASH, DIR_INVALID_PATTERN), 이미지 크기, 콘텐츠 타입, 확장자, 이미지 형식 관련 오류 시나리오 테스트 블록 추가

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

주의 필요 영역:

  • ImageUploadService.java: 여러 검증 로직과 예외 처리 로직이 재구성되었으므로, 기존 동작과의 호환성 및 예외 코드 매핑이 정확한지 확인 필요
  • validateDir 메서드: 새로 추가된 디렉토리 유효성 검사 로직의 모든 엣지 케이스(null, 빈 문자열, 경로 트래버설 등) 검증
  • S3Config 패키지 이동: 다른 파일에서의 import 경로 변경 및 빈 스캔 설정 영향도 확인

Possibly related PRs

Poem

🐰 이미지 처리가 깔끔해졌네, 예외 처리는 이제 깔끔하네,
도메인 예외로 통합되고, 유효성 검사도 늘어났네,
S3 설정 자리도 잡았으니, 이제 업로드는 안전하네! ✨

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/image-exception

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fac492 and 5df31e0.

📒 Files selected for processing (6)
  • src/main/java/team/wego/wegobackend/common/exception/AppException.java (1 hunks)
  • src/main/java/team/wego/wegobackend/image/application/service/ImageUploadService.java (7 hunks)
  • src/main/java/team/wego/wegobackend/image/domain/exception/ImageException.java (1 hunks)
  • src/main/java/team/wego/wegobackend/image/domain/exception/ImageExceptionCode.java (1 hunks)
  • src/main/java/team/wego/wegobackend/infrastructure/aws/S3Config.java (1 hunks)
  • src/test/http/image/image-api.http (1 hunks)

Comment @coderabbitai help to get the list of available commands and usage tips.

@LimdaeIl LimdaeIl moved this from Backlog to In progress in WeGo-Together Backend Dec 6, 2025
@LimdaeIl LimdaeIl merged commit 6ab1268 into main Dec 6, 2025
4 of 5 checks passed
@LimdaeIl LimdaeIl deleted the feat/image-exception branch December 6, 2025 13:12
@github-project-automation github-project-automation bot moved this from In progress to Done in WeGo-Together Backend Dec 6, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AppException for better exception chain preservation in logging
  • Introduces ImageException and ImageExceptionCode for structured, type-safe image operation error handling
  • Replaces all generic exceptions in ImageUploadService with specific ImageException instances

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 까지만 허용됩니다."),
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"이미지: 크기가 너무 큽니다. 최대 %,d bytes 까지만 허용됩니다."),
"이미지: 크기가 너무 큽니다. 최대 %d bytes 까지만 허용됩니다."), // NOTE: Use %d for consistent formatting across locales

Copilot uses AI. Check for mistakes.
--boundary--

### (예외 발생)dir에 허용되지 않는 문자 포함 → DIR_INVALID_PATTERN
POST http://localhost:8080/api/v1/images/original?dir=test 이미지
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +10
@RequiredArgsConstructor(access = AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
@RequiredArgsConstructor(access = AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 이미지 예외 처리 및 S3 Config 이동 그리고 예외 원인 추가

1 participant