외부 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.error | S3 쪽 문제 → 개발자가 즉시 확인해야 한다 |
클라이언트 오류 (SdkClientException) | log.warn | 네트워크 등 일시적 문제일 수 있다 |
catch 순서 주의사항
S3Exception과 SdkClientException은 서로 상속 관계가 아니므로 순서는 상관없다. 다만 습관적으로 서버 오류를 먼저 쓰는 것이 일반적이다.
} catch (S3Exception e) { // 서버 오류 먼저
} catch (SdkClientException e) { // 클라이언트 오류
}
확장 예시
이 구조는 다른 외부 SDK에서도 동일하게 적용할 수 있다.
예를 들어 Redis SDK를 추가하면 이렇게 된다.
클라이언트 오류 서버 오류
(RedisClientException) (RedisException)
put() RedisWriteException RedisWriteException
get() RedisReadException RedisReadException
축 구분법 자체는 동일하고, 예외 클래스와 SDK 종류만 바뀐다.