외부 SDK 예외를 잡을 때, "어떤 작업에서 실패했는지"와 "왜 실패했는지"를 동시에 구분해야 할 때가 있다. 이 두 가지를 축으로 나눠서 catch를 구성하면, 예외 처리가 깔끔하게 정리된다.


두 축의 개념

예외는 두 축으로 나뉜다.

축 1 - 작업 종류 (무엇이 실패했는지)

코드에서 무엇을 하려고 했는지를 기준으로 구분한다.

  • 업로드 (put)
  • 다운로드 (get, download)
  • 삭제 (delete)

축 2 - 오류 종류 (왜 실패했는지)

SDK가 던진 예외의 계층을 기준으로 구분한다.

  • 서버 오류 (S3Exception) — S3 쪽 문제 (권한 없음, 파일 없음 등)
  • 클라이언트 오류 (SdkClientException) — 우리 쪽 문제 (네트워크, 타임아웃 등)

두 축을 교차하여 잡는 구조

각 메서드에서 두 종류의 catch를 쓰면, 4가지를 동시에 구분할 수 있다.

try {
    s3Client.putObject(...);
} catch (S3Exception e) {                    // 축 2: 서버 오류
    log.error("S3 서버 오류 - 업로드 실패");  // 축 1: 업로드
    throw new S3UploadException(...);
} catch (SdkClientException e) {             // 축 2: 클라이언트 오류
    log.warn("클라이언트 오류 - 업로드 실패"); // 축 1: 업로드
    throw new S3UploadException(...);
}

적용 예시 - S3BinaryContentStorage

메서드별 예외 매핑 구조

                    클라이언트 오류              서버 오류
                  (SdkClientException)        (S3Exception)
put()            S3UploadException           S3UploadException
get()            S3DownloadException         S3DownloadException
download()       S3DownloadException         S3DownloadException
delete()         S3OperationException        S3OperationException

실제 코드

// put() — 업로드
try {
    s3Client.putObject(...);
} catch (S3Exception e) {
    log.error("S3 서버 오류 - 업로드 실패 : {}", file.getFileName());
    throw new S3UploadException(uuid);
} catch (SdkClientException e) {
    log.warn("클라이언트 오류 - 업로드 실패 : {}", file.getFileName());
    throw new S3UploadException(uuid);
}

// get() — 파일 조회
try {
    return s3Client.getObject(...);
} catch (S3Exception e) {
    log.error("S3 서버 오류 - 다운로드 실패 : {}", fileName);
    throw new S3DownloadException(fileName);
} catch (SdkClientException e) {
    log.warn("클라이언트 오류 - 다운로드 실패 : {}", fileName);
    throw new S3DownloadException(fileName);
}

// download() — presigned URL 생성
try {
    return ResponseEntity.status(HttpStatus.FOUND)
            .header(HttpHeaders.LOCATION, generatePresignedUrl(dto.fileName()))
            .build();
} catch (S3Exception e) {
    log.error("S3 서버 오류 - 다운로드 실패 : {}", dto.fileName());
    throw new S3DownloadException(dto.fileName());
} catch (SdkClientException e) {
    log.warn("클라이언트 오류 - 다운로드 실패 : {}", dto.fileName());
    throw new S3DownloadException(dto.fileName());
}

// delete() — 삭제
try {
    s3Client.deleteObject(...);
} catch (S3Exception e) {
    log.error("S3 서버 오류 - 삭제 실패 : {}", fileName);
    throw new S3OperationException(fileName);
} catch (SdkClientException e) {
    log.warn("클라이언트 오류 - 삭제 실패 : {}", fileName);
    throw new S3OperationException(fileName);
}

오류 종류별 로그 레벨 기준

오류 종류로그 레벨이유
서버 오류 (S3Exception)log.errorS3 쪽 문제 → 개발자가 즉시 확인해야 한다
클라이언트 오류 (SdkClientException)log.warn네트워크 등 일시적 문제일 수 있다

catch 순서 주의사항

S3ExceptionSdkClientException은 서로 상속 관계가 아니므로 순서는 상관없다. 다만 습관적으로 서버 오류를 먼저 쓰는 것이 일반적이다.

} catch (S3Exception e) {        // 서버 오류 먼저
} catch (SdkClientException e) { // 클라이언트 오류
}

확장 예시

이 구조는 다른 외부 SDK에서도 동일하게 적용할 수 있다.

예를 들어 Redis SDK를 추가하면 이렇게 된다.

                    클라이언트 오류              서버 오류
                  (RedisClientException)      (RedisException)
put()            RedisWriteException         RedisWriteException
get()            RedisReadException          RedisReadException

축 구분법 자체는 동일하고, 예외 클래스와 SDK 종류만 바뀐다.