@ControllerAdvice
)로 간결하고 일관된 예외 처리 구현하기애플리케이션을 개발하다 보면 예외 처리는 필수적인 부분입니다. 예외가 발생할 경우 사용자에게 적절한 메시지를 전달하고, 시스템 내에서 문제가 발생한 위치를 추적할 수 있도록 로그를 기록하는 것이 중요합니다. 특히, 여러 컨트롤러에서 반복적으로 예외 처리를 해야 할 때, 코드가 중복되고 복잡해질 수 있습니다. 이를 해결하기 위해 스프링 프레임워크에서는 전역 예외 처리를 제공하며, 이를 통해 예외 처리를 효율적으로 관리할 수 있습니다.
이 글에서는 @ControllerAdvice
를 활용한 전역 예외 처리 방법을 살펴보고, 예외를 처리하는 일관된 방식으로 코드를 개선하는 방법을 설명합니다.
먼저, 개별 컨트롤러에서 직접 try-catch 블록을 사용하여 예외를 처리하는 전통적인 방식을 살펴보겠습니다. 이 방식에서는 각 컨트롤러 메서드에서 발생할 수 있는 예외를 모두 감싸야 하고, 예외마다 적절한 메시지와 상태 코드를 반환하는 작업이 반복됩니다.
package com.example.controller;
import com.example.dto.UserSignupDto;
import com.example.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/join")
public ResponseEntity<?> join(@Valid @ModelAttribute UserSignupDto userSignupDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(bindingResult.getAllErrors().toString());
}
try {
userService.join(userSignupDto);
return ResponseEntity.ok("회원가입이 성공적으로 완료되었습니다.");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 에러가 발생했습니다.");
}
}
}
@ControllerAdvice
)란?스프링의 전역 예외 처리 기능을 사용하면, 컨트롤러에서 발생하는 예외를 중앙에서 관리할 수 있습니다. 이를 통해 각 컨트롤러마다 개별적으로 예외 처리를 하지 않아도 되고, 예외 처리 로직을 한 곳에서 관리할 수 있습니다.
@ControllerAdvice
와 @ExceptionHandler
@ControllerAdvice
: 애플리케이션 전체의 컨트롤러에서 발생하는 예외를 처리하는 전역 예외 처리기를 선언하는 어노테이션입니다.@ExceptionHandler
: 특정 예외 유형에 대한 처리 방법을 정의하는 어노테이션입니다. 전역 예외 처리기에서 각 예외에 맞는 처리 방식을 설정할 수 있습니다.@ControllerAdvice
를 사용한 전역 예외 처리이제 전역 예외 처리를 구현한 코드를 살펴보겠습니다. GlobalExceptionHandler
라는 클래스를 만들어 모든 컨트롤러에서 발생하는 예외를 처리하고, 컨트롤러에서는 try-catch 블록을 제거하여 코드의 가독성을 높일 수 있습니다.
package com.example.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
/**
* IllegalArgumentException 예외 처리
* @param e 발생한 예외
* @return 에러 메시지와 상태 코드 반환
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("IllegalArgumentException 발생: {}", e.getMessage(), e); // 예외 로그 기록
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
/**
* 그 외 일반적인 예외 처리
* @param e 발생한 예외
* @return 에러 메시지와 상태 코드 반환
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
log.error("Exception 발생: {}", e.getMessage(), e); // 예외 로그 기록
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 에러가 발생했습니다.");
}
}
@ControllerAdvice
: 이 어노테이션을 사용하면 모든 컨트롤러에서 발생하는 예외를 중앙에서 처리할 수 있습니다.@ExceptionHandler
: 예외 타입별로 처리할 방법을 정의합니다. 예를 들어, IllegalArgumentException
은 400 Bad Request로 처리하고, 그 외의 예외는 500 Internal Server Error로 처리합니다.log.error()
를 사용하여 예외 발생 시 로그에 기록합니다. 이렇게 하면 서버에서 발생한 문제를 추적할 수 있습니다.이제 UserController
에서 try-catch 블록을 제거하고, 예외가 발생하면 전역 예외 처리기가 처리하도록 코드를 간소화할 수 있습니다.
UserController
코드package com.example.controller;
import com.example.dto.UserSignupDto;
import com.example.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 회원가입 요청 처리
* @param userSignupDto 회원가입 요청 데이터
* @param bindingResult 입력값 검증 결과
* @return 성공 또는 에러 응답
*/
@PostMapping("/join")
public ResponseEntity<String> join(@Valid @ModelAttribute UserSignupDto userSignupDto,
BindingResult bindingResult) {
// 입력값 검증 오류 처리
if (bindingResult.hasErrors()) {
log.error("입력값 검증 오류: {}", bindingResult.getAllErrors());
return ResponseEntity.badRequest().body(bindingResult.getAllErrors().toString());
}
// 회원가입 로직 실행
userService.join(userSignupDto);
// 성공 응답
return ResponseEntity.ok("회원가입이 성공적으로 완료되었습니다.");
}
}
try-catch
제거: 더 이상 개별 메서드에서 예외를 처리할 필요가 없습니다. 예외가 발생하면 전역 예외 처리기가 자동으로 처리합니다.BindingResult
를 사용하여 유효성 검사를 수행하고, 오류가 있을 경우 400 Bad Request를 반환합니다.@ControllerAdvice
와 @ExceptionHandler
를 활용한 전역 예외 처리는 스프링 애플리케이션에서 예외를 관리하는 효율적인 방법입니다. 이를 통해 중복 코드를 줄이고, 컨트롤러는 핵심 비즈니스 로직에 집중할 수 있으며, 예외 발생 시 일관된 응답을 제공할 수 있습니다.
이 글에서 설명한 방법을 적용하면, 애플리케이션에서 발생하는 모든 예외를 한 곳에서 관리하고, 더욱 안정적이고 유지보수하기 쉬운 시스템을 만들 수 있습니다.