@ControllerAdvice, @RestControllerAdvice - 중앙집중 예외처리

2023. 8. 7. 20:58·Spring/Spring 기초 지식
반응형

@ControllerAdvice를 사용한 중앙집중 예외처리

0. 패키지 구조

 

1.  ErrorCode를 정의한다.

  • Enum 타입으로 ErrorCode 클래스를 정의했다. 이렇게 관리하면 커스텀한 에러처리를 할수가 있다.
  • 우선 나는 status, code, message를 필드로 선언했고 이 모든것들을 생성자로 받도록 설정했다.
  • status에는 우리가 자주보는 400번대 500에러가 있고 code에는 내가 직접 지정한 커스텀 에러코드를 넣어줬다. 마지막으로 message에는 알아보기 쉽게 한글에러 메시지를 넣어줬다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

/**
 * 커스텀한 에러코드를 작성한다.
 */
@Getter
@AllArgsConstructor
public enum ErrorCode {

    USER_ALREADY_EXIST(409, "1001", "유저가 이미 존재합니다."),
    USER_NOT_FOUND(404, "1002", "유저를 찾을수 없습니다."),
    INVALID_PASSWORD(401, "1003", "패스워드가 틀렸습니다."),
    DUPLICATED_USER_NAME(409, "1004", "중복된 아이디입니다."),
    INVALID_TOKEN(401, "1005", "토큰이 유효하지 않습니다."),
    INVALID_PERMISSION(401, "1006", "접근권한이 없습니다."),
    INTERNAL_SERVER_ERROR(500, "1008", "서버에 에러가 발생했습니다.")
    ;

    private int status;
    private String code;
    private String message;

}

2.  커스텀 예외 클래스를 생성한다.

  • 나는 UserApplicationException이라는 클래스명으로 내 프로젝트에 적용시킬 예외 클래스를 만들어 줬다.
  • 이때 클래스에서 RuntimeException을 상속받는걸 잊지말자 -> [extends RuntimeException]
  • 내부에는 ErrorCode 필드만 선언한다.
    • 추후 예외처리는 이 객체 내부에 접근해서 GlobalControllerAdvice에서 처리한다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

/**
 * User 커스텀 예외처리 클래스
 */
@Getter
@AllArgsConstructor
public class UserApplicationException extends RuntimeException {

    private ErrorCode errorCode;

}

3. GlobalControllerAdvice 클래스를 생성한다.

  • @ControllerAdvice 어노테이션을 사용하여 전역 예외 처리 클래스를 생성한다. 이 클래스는 모든 컨트롤러에서 발생하는 예외를 처리하는 역할을 한다. @RestController를 사용하는지 아니면 @Controller를 사용하는지에따라 상단의 ControllerAdivce 어노테이션 코드가 달라진다.

  • 코드를 작성하기전에 이걸 꼭 확인해라 -> @ExceptionHandler() 이 괄호안에는 본인이 만든 예외클래스.class를 넣어줘야하고 메서드의 매게변수로도 꼭 본인이 만든 예외클래스를 넣어줘라 (커스텀 예외 중앙처리 ExceptionHandler에만 해당함)

  • @RestController를 사용할때 -> @RestControllerAdvice 적용
    • 내용을 확인해보자면 내가 직접 만든 커스텀한 예외클래스인 UserApplicationException이 터졌을때 @ExceptionHandler의 코드가 동작해서 예외를 처리해 준다. 당연히 api전용인 @RestController를 사용했기때문에 return도 ResponseEntity<>로 처리한다.

    • 나머지 RuntimeException이 터졌을때에는 하단의 RuntimeException전용 에러처리가 동작해서 NPE같은 처리를 원래대로 해준다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * 중앙 집중 예외처리를 위한 GlobalControllerAdvice 선언
 * RestController를 사용중이니 Advice도 @RestControllerAdvice를 사용한다.
 */
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {

    /**
     * 에러 중앙처리 로직 작성완료
     */
    @ExceptionHandler(UserApplicationException.class)
    public ResponseEntity<?> applicationHandler(UserApplicationException e) {
        log.error("Error occurs {}", e.toString());

        // 에러 응답을 세팅한다.
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("code", e.getErrorCode().getCode());
        errorResponse.put("message", e.getErrorCode().getMessage());

        return ResponseEntity.status(e.getErrorCode().getStatus())
                .body(errorResponse);
    }


    /**
     * 이 방식을 적용하면 조금 더 상세하게 에러 정보를 볼수가 있다. (NPE같은 특정 예외처리의 세부정보를 제공한다.)
     * 하지만 이 방법은 예외 메시지가 직접 노출되므로 주의해야 한다.
     * 최근에는 이렇게 사용하는게 조금 더 선호되는것 같다.(더 많은 정보를 제공하고, 필터링으로 보안문제를 해결할 수 있고, 유연하다.)
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> applicationHandler(RuntimeException e) {
        log.error("Error occurs {}", e.toString());

        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("code", ErrorCode.INTERNAL_SERVER_ERROR.getCode());
        errorResponse.put("message", e.getMessage());

        return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
                .body(errorResponse);
    }

}
  • @Controller를 사용할 때 -> @ControllerAdvice 적용
    • 위의 @RestControllerAdvice와 다르게 이녀석은 타임리프를 사용하기에 return이 ModelAndView인것을 볼 수 있다.

    • 그리고 api는 response에 에러코드를 담아줬지만 여기서는 error페이지로 이동시키면서 ModelAndView안에 "errorMessage"를 바로 담아줘서 error.html안에서 모델에 접근해서 가져다 사용할수가 있다.

    • NPE를 잡아주는 RuntimeException은 여기에도 따로 적용시켜줬다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

/**
 * 중앙 집중 예외처리를 위한 GlobalControllerAdvice 작성
 */
@Slf4j
@ControllerAdvice
public class GlobalControllerAdvice {

    /**
     * UserApplicationException 발생했을 때 호출되는 메서드
     * 에러 페이지를 보여주고, 에러 메시지를 모델에 추가한다.
     * <p th:text="${errorMessage}"></p> 이런식으로 타임리프에 에러페이지를 만들고 가져다 사용한다.
     */
    @ExceptionHandler(UserApplicationException.class)
    public ModelAndView handleProfileApplicationException(UserApplicationException e) {
        log.error("Error occurs {}", e.toString());

        // Spring MVC는 알아서 'error'라는 이름의 뷰를 찾아서 렌더링한다.
        ModelAndView mav = new ModelAndView("error"); // 에러 페이지의 뷰 이름
        mav.addObject("errorMessage", e.getErrorCode().getMessage()); // 에러 메시지를 모델에 추가
        return mav;
    }

    /**
     * RuntimeException이 발생했을 때 호출되는 메서드
     * 에러 페이지를 보여주고, 에러 메시지를 모델에 추가한다.
     */
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView handleRuntimeException(RuntimeException e) {
        log.error("Error occurs {}", e.toString());

        // Spring MVC는 알아서 'error'라는 이름의 뷰를 찾아서 렌더링한다.
        ModelAndView mav = new ModelAndView("error"); // 에러 페이지의 뷰 이름
        mav.addObject("errorMessage", e.getMessage()); // 에러 메시지를 모델에 추가
        return mav;
    }
}

4. 커스텀 예외를  코드에 적용시키는 예시

  • 아래 코드는 userId를 통해 User 엔티티를 찾는 Service레이어의 코드다.

  • 코드에 있는 userRepository.findById()는 Jpa에서 자체적으로 지원하는 코드이고 이건 Optional<T>을 반환한다. 즉 Optional을 반환하니까 에러가 나면 orElseThrow()로 Optional에서 객체를 꺼내면서 동시에 커스텀한 예외처리를 해줄수가 있다는 것이다.

  • 그래서 아래와 같이 Optional안에 값이 없다면(empty) 내가만든 예외인 UserApplicationException을 던지면서 그 안에 매게변수로 ErrorCode에 직접 선언해준 USER_NOT_FOUND를 넣어서 던져줬다.

  • 이러면 userId로인한 조회의 오류가 생기면 내가만든 예외인 USER_NOT_FOUND의 정보가 전달될 것이다.
import com.diningtalk.user.domain.User;
import com.diningtalk.user.dto.UserDto;
import com.diningtalk.user.exception.ErrorCode;
import com.diningtalk.user.exception.UserApplicationException;
import com.diningtalk.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    public UserDto findUser(Long userId) {
        return userRepository.findById(userId)
                .map(UserDto::fromEntity)
                .orElseThrow(() -> new UserApplicationException(ErrorCode.USER_NOT_FOUND));
    }
}

 

반응형

'Spring > Spring 기초 지식' 카테고리의 다른 글

스프링에서의 데이터베이스 접근 방법: DAO, Mapper, 그리고 @Mapper 어노테이션 사용법  (0) 2023.08.09
스프링에서 데이터 전달의 핵심: VO와 DTO의 이해 및 활용  (0) 2023.08.09
Spring Boot에서 REST 컨트롤러 활용하기: @RestController 어노테이션 이해하기  (0) 2023.08.08
Spring Boot 심화: 커스텀 어노테이션 만들기 (2편)  (0) 2023.08.08
Spring Boot 기초: 어노테이션 활용하기 (1편)  (0) 2023.08.08
'Spring/Spring 기초 지식' 카테고리의 다른 글
  • 스프링에서 데이터 전달의 핵심: VO와 DTO의 이해 및 활용
  • Spring Boot에서 REST 컨트롤러 활용하기: @RestController 어노테이션 이해하기
  • Spring Boot 심화: 커스텀 어노테이션 만들기 (2편)
  • Spring Boot 기초: 어노테이션 활용하기 (1편)
Stark97
Stark97
문의사항 또는 커피챗 요청은 링크드인 메신저를 보내주세요! : https://www.linkedin.com/in/writedev/
  • Stark97
    오늘도 개발중입니다
    Stark97
  • 전체
    오늘
    어제
    • 분류 전체보기 (247) N
      • 개발지식 (20)
        • 스레드(Thread) (8)
        • WEB, DB, GIT (3)
        • 디자인패턴 (8)
      • JAVA (21)
      • Spring (88)
        • Spring 기초 지식 (35)
        • Spring 설정 (6)
        • JPA (7)
        • Spring Security (17)
        • Spring에서 Java 활용하기 (8)
        • 테스트 코드 (15)
      • 아키텍처 (6)
      • MSA (15)
      • DDD (11)
      • gRPC (9)
      • Apache Kafka (18)
      • DevOps (23)
        • nGrinder (4)
        • Docker (1)
        • k8s (1)
        • 테라폼(Terraform) (12)
      • AWS (32)
        • ECS, ECR (14)
        • EC2 (2)
        • CodePipeline, CICD (8)
        • SNS, SQS (5)
        • RDS (2)
      • notion&obsidian (3)
      • AI 탐험대 (1) N
      • 팀 Pulse (0)
  • 링크

    • notion기록
    • 깃허브
    • 링크드인
  • hELLO· Designed By정상우.v4.10.0
Stark97
@ControllerAdvice, @RestControllerAdvice - 중앙집중 예외처리
상단으로

티스토리툴바