본문 바로가기
language/java

Java/Spring 실무 예외 처리 패턴 완벽 이해하기

by 죄니안죄니 2026. 5. 29.
반응형

Java/Spring 실무 예외 처리 패턴 완벽 이해하기

지금까지 배운:

  • Exception 구조
  • Checked vs Unchecked
  • Custom Exception
  • 예외 로그 전략
  • try-catch-finally

를 실제 Spring 프로젝트에서는 어떻게 사용할까요?

많은 초급 개발자의 코드는 보통 이렇게 시작합니다.

 
try {

    ...

} catch(Exception e) {

    e.printStackTrace();

    return "ERROR";
}
 

하지만 실무에서는 거의 이런 방식으로 처리하지 않습니다.

대부분 다음 구조를 사용합니다.

Repository
    ↓
Service
    ↓
Controller
    ↓
@ControllerAdvice
 

예외는 위로 던지고

로그는 한 번만 남기고

응답은 전역에서 통일합니다.


1. 가장 안 좋은 예외 처리

예:

 
public User findUser(Long id) {

    try {

        return repository.findById(id);

    } catch(Exception e) {

        log.error("조회 실패");

        return null;
    }
}
 

2. 문제점

예외가 사라짐

실패했는데 성공처럼 보임
 

원인 추적 불가

StackTrace 유실
 

Null 지옥 시작

 
user.getName();
 

 
NullPointerException
 

발생.


3. 실무 기본 원칙

매우 중요.

예외는 가능한 한 빨리 잡지 않는다.
 

4. 왜?

실패 원인을 가장 잘 아는 곳은

보통 최상위 계층.


5. 권장 구조

Repository
 ↓
Service
 ↓
Controller
 ↓
ControllerAdvice
 

6. Repository

 
public User findById(Long id) {

    ...
}
 

예외 발생

그냥 던짐.


7. Service

 
public User findUser(Long id) {

    return repository.findById(id)
            .orElseThrow(
                UserNotFoundException::new
            );
}
 

8. Controller

 
@GetMapping("/{id}")
public User findUser(
        @PathVariable Long id) {

    return userService.findUser(id);
}
 

9. 여기까지 catch 없음

매우 중요.

예외를 잡지 않는다.
 

10. 최종 처리

 
@RestControllerAdvice
public class GlobalExceptionHandler {

}
 

11. 왜 좋을까?

예외 처리 위치 통일.


12. 대표 패턴

비즈니스 예외

 
public class UserNotFoundException
        extends RuntimeException {
}
 

Service

 
throw new UserNotFoundException();
 

13. ControllerAdvice

 
@ExceptionHandler(
    UserNotFoundException.class
)
public ResponseEntity<?> handle() {

    return ResponseEntity
            .status(404)
            .body("회원 없음");
}
 

14. 결과

예외 발생
 ↓
자동 전파
 ↓
404 반환
 

15. ErrorCode 패턴

실무 최다 사용.


Enum

 
public enum ErrorCode {

    USER_NOT_FOUND,
    DUPLICATE_USER,
    INVALID_REQUEST
}
 

16. Custom Exception

 
public class BusinessException
        extends RuntimeException {

    private final ErrorCode errorCode;

    public BusinessException(
            ErrorCode errorCode) {

        this.errorCode = errorCode;
    }
}
 

17. 사용

 
throw new BusinessException(
    ErrorCode.USER_NOT_FOUND
);
 

18. ControllerAdvice

 
@ExceptionHandler(
    BusinessException.class
)
public ResponseEntity<?> handle(
        BusinessException e) {

    return ResponseEntity
            .badRequest()
            .body(e.getErrorCode());
}
 

19. 장점

프론트는

USER_NOT_FOUND
 

만 보고 처리 가능.


20. Exception Translation

실무 핵심.


Repository

 
SQLException
 

발생.


Service까지 그대로 전달?

좋지 않음.


21. 변환

 
catch(SQLException e) {

    throw new UserRepositoryException(e);
}
 

22. 이유

하위 기술 의존 제거.


좋은 구조

Service는

SQLException

존재 자체를 모름
 

23. Spring도 사용

대표:

Spring DataAccessException


원래

 
SQLException
 

Spring

 
DataAccessException
 

변환.


24. 로그는 어디서 남길까?

매우 중요.


안 좋은 예

 
Repository
log.error()
 
 
Service
log.error()
 
 
Controller
log.error()
 

25. 결과

같은 예외
3번 출력
 

26. 좋은 패턴

최상위에서 1회
 

ControllerAdvice

 
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handle(
        Exception e) {

    log.error("시스템 오류", e);

    ...
}
 

27. 예외는 변환만 하고 로그는 안 남김

예:

 
catch(SQLException e) {

    throw new UserRepositoryException(e);
}
 

28. 왜?

최상위에서 이미 로그 남길 예정.


29. Validation 예외 처리

실무 매우 많음.


 
@NotBlank
private String name;
 

실패

 
MethodArgumentNotValidException
 

발생.


30. 전역 처리

 
@ExceptionHandler(
    MethodArgumentNotValidException.class
)
 

31. 응답 예시

 
{
  "code":"INVALID_REQUEST",
  "message":"이름은 필수입니다."
}
 

32. 표준 에러 응답 객체

실무 필수.


 
public record ErrorResponse(

    String code,
    String message

) {}
 

33. 응답 통일

 
{
  "code":"USER_NOT_FOUND",
  "message":"회원이 존재하지 않습니다."
}
 

 
{
  "code":"INVALID_REQUEST",
  "message":"입력값 오류"
}
 

항상 동일 구조.


34. 트랜잭션과 연결

매우 중요.


 
@Transactional
 

RuntimeException 발생

자동 Rollback


35. 예시

 
@Transactional
public void order() {

    saveOrder();

    throw new OrderException();
}
 

결과

전체 롤백
 

36. 예외를 삼키면?

 
try {

} catch(Exception e) {

}
 

결과

Rollback 안 됨
 

37. 실무 최악 패턴

 
catch(Exception e) {

    return false;
}
 

왜 위험할까?

실패를 숨김.


38. 실무 추천 구조

Controller
 ↓
Service
 ↓
Repository

예외 발생
 ↓
전파
 ↓
ControllerAdvice
 ↓
로그
 ↓
응답
 

39. 대규모 프로젝트 구조

RuntimeException
      ↑
BusinessException
      ↑
 ├─ UserException
 ├─ ProductException
 ├─ OrderException
 

ErrorCode

USER_NOT_FOUND
USER_DUPLICATED

PRODUCT_NOT_FOUND

ORDER_ALREADY_COMPLETED
 

40. 실무에서 자주 하는 실수

1)

 
catch(Exception e){}
 

예외 삼킴.


2)

모든 계층에서 log.error()

중복 로그.


3)

RuntimeException 대신

 
throws Exception
 

남발.


4)

에러 응답 포맷 제각각.


5)

예외 메시지로 분기.

 
if(message.contains("회원"))
 

절대 금지.


41. 실무 Spring 예외 처리 최종 구조

예외 발생
 ↓
Custom Exception
 ↓
전파
 ↓
@ControllerAdvice
 ↓
ErrorResponse 생성
 ↓
로그 1회 기록
 ↓
HTTP 응답 반환
 

42. 가장 중요한 핵심 한 줄

실무 예외 처리의 목표는
예외를 막는 것이 아니라
예외를 일관되게 전파하고,
한 곳에서 기록하고,
한 형태로 응답하는 것이다.
 

43. 면접 단골 질문

Q. Service에서 예외를 catch 해야 하나요?

대부분 No

전파 후 ControllerAdvice 처리
 

Q. 로그는 어디서 남기나요?

최상위 예외 처리 지점
 

Q. ErrorCode를 사용하는 이유는?

메시지 변경과 무관하게
클라이언트가 안정적으로 처리 가능
 

다음 글은 Spring @ExceptionHandler 입니다.

여기서부터는 지금까지 설명한 실무 예외 처리 패턴이 실제 Spring MVC 내부에서 어떻게 동작하는지 들어가게 됩니다. DispatcherServlet → ExceptionResolver → @ExceptionHandler → @ControllerAdvice 흐름까지 이해하면 Spring 예외 처리 구조가 완성됩니다.

반응형

댓글