백엔드 개발자가 반드시 잡는 예외 모음 (feat. IDE가 시키는 try~catch 탈출)

공부하는 개발자

about 오늘도 뚝딱🔨 뚝딱 하는 주니어 개발자👩🏻‍💻

북마크

고정글

최근 글

백엔드 개발자가 반드시 잡는 예외 모음 (feat. IDE가 시키는 try~catch 탈출)

date
read
반응형

 

네트워크 타임아웃 예외가 발생하는 원리 (feat. ConnectException, SocketTimeoutException)

주제 정의 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션| 김영한 - 인프런 강의현재 평점 5.0점 수강생 7,020명인 강의를 만나보세요. I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실

yurizzy.tistory.com

 

 

수집 대상 정의

IDE의 빨간 줄을 없애기 위한 try~catch가 아니라, 실무에서 의도를 갖고 처리해야 하는 예외들을 모았다.

지난 편에서 네트워크 타임아웃 예외의 발생 원리를 다뤘고, 이번 편은 그 확장판이다.

수집 기준은 세 가지다.

 

첫째, 잡아서 복구(재시도, 대체 동작)가 가능한 예외.

둘째, 사용자에게 다른 응답을 내려줘야 하는 예외.

셋째, 놓치면 장애로 번지는 예외.

 

항목을 보기 전에 원칙 세 개를 먼저 적어둔다.

잡아서 할 수 있는 것이 없으면 잡지 않고 던진다.

잡았다면 반드시 로그나 대체 동작을 남긴다(빈 catch 금지).

개별 try~catch는 복구 지점에만 두고, 나머지는 공통 처리(@RestControllerAdvice)로 모은다.

 

 

 

 


 

 

 

 

네트워크·외부 연동

 

1. ResourceAccessException

외부 API 호출 중 연결 실패, 응답 지연 같은 I/O 오류가 발생하면 Spring이 이 예외로 감싸서 던진다.

getCause()로 ConnectException인지 SocketTimeoutException인지 구분해 재시도 여부를 판단한다.

 

 

I/O error on GET request for "https://api.example.com/orders/1": Connect timed out

 

상세한 발생 원리와 타임아웃 설정 방법은 지난 편에 정리했다. 

 

네트워크 타임아웃 예외가 발생하는 원리 (feat. ConnectException, SocketTimeoutException)

주제 정의 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션| 김영한 - 인프런 강의현재 평점 5.0점 수강생 7,020명인 강의를 만나보세요. I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실

yurizzy.tistory.com

 

 

 

 

 

2. HttpClientErrorException / HttpServerErrorException

외부 API가 응답은 줬지만 상태 코드가 4xx/5xx인 경우다.

4xx는 우리 요청이 잘못된 것이라 재시도해도 결과가 같고, 5xx는 상대 서버 문제라 재시도 후보가 된다.

같은 "호출 실패"라도 원인의 위치가 달라 처리도 달라진다.

404 Not Found on GET request for "https://api.example.com/orders/1"

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

DB

 

3. CannotGetJdbcConnectionException

커넥션 풀에서 커넥션을 얻지 못했을 때 발생한다.

원인은 풀 크기 부족보다, 커넥션 누수나 느린 쿼리가 커넥션을 오래 점유하는 경우가 대부분이다.

HikariPool-1 - Connection is not available, request timed out after 30000ms

30000ms라는 숫자는 HikariCP의 connectionTimeout 기본값이 30초이기 때문에 찍힌다. 공식 문서 참고

 

GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP

github.com

 

 

 

 

 

 

4. DuplicateKeyException

유니크 제약 조건 충돌이다.

"조회해서 없으면 저장" 로직은 동시 요청이 들어오면 뚫리기 때문에

최후의 방어선인 유니크 제약에서 이 예외가 발생한다

catch해서 "이미 처리된 요청"이라는 비즈니스 응답으로 바꿔주는 것이 정석이다.

Duplicate entry 'user@example.com' for key 'users.uk_email'

 

 

 

 

 

 

 

5. DeadlockLoserDataAccessException

DB가 데드락을 감지하면 한쪽 트랜잭션을 강제 롤백하는데

그 패배자가 받는 예외다. 짧은 대기 후 재시도하면 대부분 성공한다.

Deadlock found when trying to get lock; try restarting transaction

 

 

 

 

 

 

6. QueryTimeoutException

지정한 쿼리 타임아웃을 초과했을 때 발생한다.

재시도보다는 인덱스와 실행 계획을 점검하라는 신호로 받아들이는 편이 맞다.

참고로 SQLException이 아니라 이런 구체적인 예외로 잡을 수 있는 이유는

Spring이 DB별 에러 코드를 DataAccessException 계층으로 변환해주기 때문이다.

 

DAO Support :: Spring Framework

The Data Access Object (DAO) support in Spring is aimed at making it easy to work with data access technologies (such as JDBC, Hibernate, or JPA) in a consistent way. This lets you switch between the aforementioned persistence technologies fairly easily, a

docs.spring.io

 

 

DB 계열 예외의 재시도는 아래처럼 복구 지점에서만 처리한다.

재시도 대기 중 발생하는 InterruptedException 처리까지 포함해야 완성이다.

import org.springframework.dao.DeadlockLoserDataAccessException;

public class RetryExecutor {

    public void executeWithRetry(Runnable task) {
        int maxAttempts = 3;
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                task.run();
                return;
            } catch (DeadlockLoserDataAccessException e) {
                if (attempt == maxAttempts) {
                    throw e; // 더 이상 할 수 있는 것이 없으면 던진다
                }
                sleep(100L * attempt);
            }
        }
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 인터럽트 상태 복원
            throw new IllegalStateException("재시도 대기 중 인터럽트 발생", e);
        }
    }
}

 

 

 

 

 

 

 


 

 

 

 

 

 

 

동시성

 

7. OptimisticLockingFailureException

@Version 기반 낙관적 락에서, 내가 읽은 버전과 커밋 시점의 버전이 다르면 발생한다.

재고 차감이나 좌석 예약처럼 동시 수정이 흔한 도메인에서 만난다.

재시도하거나 사용자에게 "다시 시도해 달라"는 응답으로 처리한다.

Row was updated or deleted by another transaction

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

웹 계층 (공통 처리 대상)

 

여기부터는 개별 try~catch가 아니라

@RestControllerAdvice에서 한 번에 처리하는 예외들이다.

Spring Boot가 기본 처리를 해주기는 하지만

에러 응답 포맷을 서비스 전체에서 통일하려면 공통 처리에 포함해야 한다

 

Exceptions :: Spring Framework

The exception may match against a top-level exception being propagated (for example, a direct IOException being thrown) or against a nested cause within a wrapper exception (for example, an IOException wrapped inside an IllegalStateException). As of 5.3, t

docs.spring.io

 

 

 

 

8. MethodArgumentNotValidException

@Valid 검증 실패다. 어떤 필드가 왜 틀렸는지를 담아 400으로 내려준다.

이걸 처리하지 않으면 클라이언트는 무엇을 고쳐야 하는지 알 수 없다.

 

 

 

9. HttpMessageNotReadableException

요청 본문이 JSON으로 파싱조차 되지 않는 경우다.

깨진 JSON, 타입이 맞지 않는 값이 원인이며 400으로 처리한다.

JSON parse error: Unexpected character ('}' (code 125))

 

 

 

 

10. NoResourceFoundException / HttpRequestMethodNotSupportedException

없는 URL 호출과 지원하지 않는 HTTP 메서드 호출이다.

각각 404, 405로 내려준다. 봇들이 아무 URL이나 찔러보는 운영 환경에서는

이 예외가 에러 알림을 오염시키지 않도록 로그 레벨 조정도 함께 해준다.

 

세 가지를 묶은 공통 처리 코드다.

import java.util.stream.Collectors;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
        String detail = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest().body(new ErrorResponse("INVALID_REQUEST", detail));
    }

    @ExceptionHandler(DuplicateKeyException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateKey(DuplicateKeyException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(new ErrorResponse("DUPLICATE_REQUEST", "이미 처리된 요청"));
    }

    @ExceptionHandler(NoResourceFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(NoResourceFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("NOT_FOUND", "존재하지 않는 리소스"));
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<ErrorResponse> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
                .body(new ErrorResponse("METHOD_NOT_ALLOWED", "지원하지 않는 메서드"));
    }

    record ErrorResponse(String code, String message) {
    }
}

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

트랜잭션

 

11. UnexpectedRollbackException

내부 트랜잭션에서 발생한 예외를 중간 서비스가 catch해서 삼켰지만

트랜잭션은 이미 rollback-only로 마킹된 경우다.

바깥 트랜잭션이 커밋하는 순간 이 예외가 발생한다.

"분명히 예외를 잡았는데 왜 롤백되지"라는 미스터리의 정체다.

Transaction silently rolled back because it has been marked as rollback-only

같은 트랜잭션 안에서는 예외를 잡아도 롤백 마킹이 사라지지 않는다는 것

즉 try~catch가 트랜잭션 전파보다 우선하지 않는다는 것을 알려주는 예외다.

 

 

 

 


 

 

 

 

스레드·파싱

12. InterruptedException

sleep()이나 블로킹 호출 대기 중 인터럽트 신호가 오면 발생한다.

이걸 빈 catch로 삼키면 스레드 풀이 보내는 종료 신호가 무시된다.

catch했다면 Thread.currentThread().interrupt()로 인터럽트 상태를 복원하는 것이 규칙이다.

위 RetryExecutor 코드의 sleep() 처리가 그 예다.

 

 

 

 

13. JsonProcessingException

ObjectMapper로 외부 응답이나 메시지를 직접 파싱할 때 발생한다.

외부에서 들어오는 데이터는 언제든 깨질 수 있으므로

파싱 실패 시 기본값을 쓸지 실패 응답을 줄지 방침을 미리 정해둔다.

 

 

 

 


 

 

 

핵심만 나열

  • 예외를 잡는 기준은 IDE의 경고가 아니라 "복구 가능한가, 응답이 달라지는가"다.
  • 개별 try~catch는 재시도나 대체 동작이 있는 복구 지점에만 두고, 나머지는 @RestControllerAdvice로 모은다.
  • 빈 catch와 e.printStackTrace()는 금지다. 잡았다면 로그와 의도를 남긴다.
  • 외부와 닿는 모든 지점(HTTP, DB, 메시지 파싱)에는 타임아웃과 실패 시나리오를 먼저 정해둔다.

 

 

 

이전 편: 네트워크 타임아웃 예외가 발생하는 원리

 

네트워크 타임아웃 예외가 발생하는 원리 (feat. ConnectException, SocketTimeoutException)

주제 정의 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션| 김영한 - 인프런 강의현재 평점 5.0점 수강생 7,020명인 강의를 만나보세요. I/O, 네트워크, 리플렉션, 애노테이션을 기초부터 실

yurizzy.tistory.com

 

반응형