스프링 전역 예외 처리(`@ControllerAdvice`)로 간결하고 일관된 예외 처리 구현하기

023·2024년 10월 12일
0

스프링 전역 예외 처리(@ControllerAdvice)로 간결하고 일관된 예외 처리 구현하기

애플리케이션을 개발하다 보면 예외 처리는 필수적인 부분입니다. 예외가 발생할 경우 사용자에게 적절한 메시지를 전달하고, 시스템 내에서 문제가 발생한 위치를 추적할 수 있도록 로그를 기록하는 것이 중요합니다. 특히, 여러 컨트롤러에서 반복적으로 예외 처리를 해야 할 때, 코드가 중복되고 복잡해질 수 있습니다. 이를 해결하기 위해 스프링 프레임워크에서는 전역 예외 처리를 제공하며, 이를 통해 예외 처리를 효율적으로 관리할 수 있습니다.

이 글에서는 @ControllerAdvice를 활용한 전역 예외 처리 방법을 살펴보고, 예외를 처리하는 일관된 방식으로 코드를 개선하는 방법을 설명합니다.


1. 기존의 개별 예외 처리 방식

먼저, 개별 컨트롤러에서 직접 try-catch 블록을 사용하여 예외를 처리하는 전통적인 방식을 살펴보겠습니다. 이 방식에서는 각 컨트롤러 메서드에서 발생할 수 있는 예외를 모두 감싸야 하고, 예외마다 적절한 메시지와 상태 코드를 반환하는 작업이 반복됩니다.

예시: 기존 컨트롤러에서 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("서버 에러가 발생했습니다.");
        }
    }
}

문제점:

  • 중복 코드: 각 컨트롤러 메서드마다 try-catch 블록을 반복해서 사용해야 하므로, 코드가 중복되고 관리가 어렵습니다.
  • 일관성 부족: 예외가 발생할 때 반환하는 응답 형식이나 로그 처리 방식이 컨트롤러마다 달라질 수 있어, 예외 처리 방식의 일관성이 떨어집니다.

2. 전역 예외 처리(@ControllerAdvice)란?

스프링의 전역 예외 처리 기능을 사용하면, 컨트롤러에서 발생하는 예외를 중앙에서 관리할 수 있습니다. 이를 통해 각 컨트롤러마다 개별적으로 예외 처리를 하지 않아도 되고, 예외 처리 로직을 한 곳에서 관리할 수 있습니다.

@ControllerAdvice@ExceptionHandler

  • @ControllerAdvice: 애플리케이션 전체의 컨트롤러에서 발생하는 예외를 처리하는 전역 예외 처리기를 선언하는 어노테이션입니다.
  • @ExceptionHandler: 특정 예외 유형에 대한 처리 방법을 정의하는 어노테이션입니다. 전역 예외 처리기에서 각 예외에 맞는 처리 방식을 설정할 수 있습니다.

3. @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("서버 에러가 발생했습니다.");
    }
}

코드 설명:

  1. @ControllerAdvice: 이 어노테이션을 사용하면 모든 컨트롤러에서 발생하는 예외를 중앙에서 처리할 수 있습니다.
  2. @ExceptionHandler: 예외 타입별로 처리할 방법을 정의합니다. 예를 들어, IllegalArgumentException400 Bad Request로 처리하고, 그 외의 예외는 500 Internal Server Error로 처리합니다.
  3. 로그 기록: log.error()를 사용하여 예외 발생 시 로그에 기록합니다. 이렇게 하면 서버에서 발생한 문제를 추적할 수 있습니다.

4. 컨트롤러 코드의 간소화

이제 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("회원가입이 성공적으로 완료되었습니다.");
    }
}

개선된 코드 설명:

  1. try-catch 제거: 더 이상 개별 메서드에서 예외를 처리할 필요가 없습니다. 예외가 발생하면 전역 예외 처리기가 자동으로 처리합니다.
  2. 입력값 검증: BindingResult를 사용하여 유효성 검사를 수행하고, 오류가 있을 경우 400 Bad Request를 반환합니다.
  3. 간결한 코드: 예외 처리가 전역적으로 관리되므로 컨트롤러 메서드는 핵심 로직에만 집중할 수 있습니다.

5. 전역 예외 처리 적용의 장점

1. 중복 코드 제거

  • 여러 컨트롤러에서 반복적으로 발생하는 예외 처리 코드를 전역에서 한 번만 정의하여 중복을 제거할 수 있습니다.

2. 일관된 에러 응답

  • 애플리케이션 전반에 걸쳐 일관된 방식으로 예외 응답을 처리할 수 있습니다. 모든 컨트롤러에서 동일한 에러 형식과 상태 코드를 반환할 수 있어 사용자 경험을 개선합니다.

3. 가독성과 유지보수성 향상

  • 컨트롤러 코드가 예외 처리 로직으로부터 분리되어 가독성이 높아지고, 예외 처리 로직을 중앙에서 관리할 수 있어 유지보수성이 향상됩니다.

6. 결론

@ControllerAdvice@ExceptionHandler를 활용한 전역 예외 처리는 스프링 애플리케이션에서 예외를 관리하는 효율적인 방법입니다. 이를 통해 중복 코드를 줄이고, 컨트롤러는 핵심 비즈니스 로직에 집중할 수 있으며, 예외 발생 시 일관된 응답을 제공할 수 있습니다.

이 글에서 설명한 방법을 적용하면, 애플리케이션에서 발생하는 모든 예외를 한 곳에서 관리하고, 더욱 안정적이고 유지보수하기 쉬운 시스템을 만들 수 있습니다.

profile
Get your hands dirty

0개의 댓글