최원종의 개발 블로그

V4-2 에러 컨트롤러 및 커스텀 예외 처리 (@ControllerAdvice 활용) 본문

Spring boot 입문

V4-2 에러 컨트롤러 및 커스텀 예외 처리 (@ControllerAdvice 활용)

chl6698 2026. 5. 18. 10:24

@ControllerAdvice

1. 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리
2. 예외 처리 로직의 중앙 집중화
3. 코드 중복 제거 및 유지보수성 향상
4. 일관된 에러 응답 제공

 

 

개념 확인용 코드

// 전통적인 방식 (각 컨트롤러마다 try-catch)
@Controller
public class BoardController {
    
    @GetMapping("/board/{id}")
    public String detail(@PathVariable Long id) {
        try {
            Board board = boardRepository.findById(id);
            return "board/detail";
        } catch (RuntimeException e) {
            // 매번 이런 처리가 필요함
            request.setAttribute("msg", e.getMessage());
            return "err/404";
        }
    }
}

// @ControllerAdvice 방식 (중앙 집중 처리)
@Controller
public class BoardController {
    
    @GetMapping("/board/{id}")
    public String detail(@PathVariable Long id) {
        Board board = boardRepository.findById(id);  // 예외 발생 시 자동으로 핸들러가 처리
        return "board/detail";
    }
}

 


GlobalExceptionHandler 코드

package com.tenco.blog._core.errors;


import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

// 모든 컨트롤러에서 발생하는 예외를 이 클래스에서 처리 하겠다.
// RuntimeException 이 발생되면 해당 이 파일로 예외 처리가 오게 됨.
@Slf4j
@ControllerAdvice // IoC
public class GlobalExceptionHandler {


    @ExceptionHandler(Exception400.class)
    public String ex400(Exception400 e, HttpServletRequest request) {
        log.warn("=== 400 Bad Request 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", e.getMessage());
        return "err/400";
    }

    @ExceptionHandler(Exception401.class)
    public String ex401(Exception401 e, HttpServletRequest request) {
        log.warn("=== 401 Unauthorized 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", e.getMessage());
        return "err/401";
    }

    @ExceptionHandler(Exception403.class)
    public String ex403(Exception403 e, HttpServletRequest request) {
        log.warn("=== 403 Forbidden 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", e.getMessage());
        return "err/403";
    }

    @ExceptionHandler(Exception404.class)
    public String ex404(Exception404 e, HttpServletRequest request) {
        log.warn("=== 404 Not Found 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", e.getMessage());
        return "err/404";
    }

    @ExceptionHandler(Exception500.class)
    public String ex500(Exception500 e, HttpServletRequest request) {
        log.warn("=== 500 Internal Server Error 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", e.getMessage());
        return "err/500";
    }

    // 기타 모든 RuntimeException 처리 (최후의 보루)
    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(RuntimeException e, HttpServletRequest request) {
        log.warn("=== 예상치 못한 런타임 에러 발생 ===");
        log.warn("요청 URL: {}", request.getRequestURL());
        log.warn("에러메시지: {}", e.getMessage());

        request.setAttribute("msg", "시스템 오류가 발생했습니다. 관리자에게 문의해주세요");
        return "err/500";
    }
}

400 Bad Request ~ 500 코드

더보기

400

package com.tenco.blog._core.errors;

// 400 Bad Request
public class Exception400 extends RuntimeException {

    // 예외 메시지를 외부에서 받아서 내 부모클래스 RuntimeException 에게 생성자로 전달
    public Exception400(String msg) {
        super(msg); // 즉, 부모 클래스 메세지도 내가 직접 작성 부분으로 설정 됨.
    }
    // throw new Exception400("잘못된 요청"); 사용 예시
}

 

401

package com.tenco.blog._core.errors;

public class Exception401 extends RuntimeException {

    // 예외 메시지를 외부에서 받아서 내 부모클래스 RuntimeException 에게 생성자로 전달
    public Exception401(String msg) {
        super(msg); // 즉, 부모 클래스 메세지도 내가 직접 작성 부분으로 설정 됨.
    }
}

403

package com.tenco.blog._core.errors;

public class Exception403 extends RuntimeException {

    // 예외 메시지를 외부에서 받아서 내 부모클래스 RuntimeException 에게 생성자로 전달
    public Exception403(String msg) {
        super(msg); // 즉, 부모 클래스 메세지도 내가 직접 작성 부분으로 설정 됨.
    }
}

404

package com.tenco.blog._core.errors;

public class Exception404 extends RuntimeException {

    // 예외 메시지를 외부에서 받아서 내 부모클래스 RuntimeException 에게 생성자로 전달
    public Exception404(String msg) {
        super(msg); // 즉, 부모 클래스 메세지도 내가 직접 작성 부분으로 설정 됨.
    }
}

500

package com.tenco.blog._core.errors;

public class Exception500 extends RuntimeException {

    // 예외 메시지를 외부에서 받아서 내 부모클래스 RuntimeException 에게 생성자로 전달
    public Exception500(String msg) {
        super(msg); // 즉, 부모 클래스 메세지도 내가 직접 작성 부분으로 설정 됨.
    }
}

400.mustache ~ 500.mustache

더보기

400

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <h1>BadRequest 400</h1>
    <hr>
    <h4>{{msg}}</h4>
    <div class="mb-3">
        <a href="javascript:history.back()">이전 페이지</a>
        <a href="/">메인 페이지</a>
    </div>
</div>
{{> layout/footer}}

 

401

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <h1>Unauthorized 401</h1>
    <hr>
    <h4>{{msg}}</h4>
    <div class="mb-3">
        <a href="/login-form">로그인</a>
        <a href="/">메인 페이지</a>
    </div>
</div>
{{> layout/footer}}

403

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <h1>Forbidden 403</h1>
    <hr>
    <h4>{{msg}}</h4>
    <div class="mb-3">
        <a href="javascript:history.back()">이전 페이지</a>
        <a href="/" class="btn btn-primary">메인 페이지</a>
    </div>
</div>
{{> layout/footer}}

404

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <h1>Not Found 404</h1>
    <hr>
    <h4>{{msg}}</h4>
    <div class="mb-3">
        <a href="/" class="btn btn-primary">메인 페이지</a>
    </div>
</div>
{{> layout/footer}}

 

500

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <h1>Internal Server Error 500</h1>
    <hr>
    {{^msg}}
        <h4>서버관리자에게 문의</h4>
    {{/msg}}
    {{#msg}}
        <h4>{{msg}}</h4>
    {{/msg}}

    <div class="mb-3">
        <a href="/" class="btn btn-primary">메인 페이지</a>
        <a href="javascript:location.reload()">새로고침</a>
    </div>
</div>
{{> layout/footer}}

방식 사용 편의성 Spring 친화성 권장도 주요 용도
Model ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 일반적인 데이터 전달
ModelMap ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ Model과 유사한 용도
ModelAndView ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ 뷰와 데이터 통합 관리
Map ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ 간단한 데이터 전달
@ModelAttribute ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ 공통 데이터 전달
HttpServletRequest ⭐⭐ ⭐⭐ ⭐⭐ 레거시 코드
Session ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ 상태 유지 필요 시