[Spring Boot 3.x] 6. @ControllerAdvice를 활용한 Global Exception Handling 전역 예외 처리

2025. 3. 3. 22:44·Spring Boot/Sprng Boot Sample Code

기존에 서비스 로직에 예외처리가 되어있어 예외 상황 발생시 처리는 되지만 대부분의 경우 status가 403이 출력된다.

 

예를들어 아래와 같이 중복된 사용자에 대해 예외를 발생시키는 로직을 만들어두고 요청을 보내면

// 회원가입
@Transactional
public void signUp(SignUpReq req) {
    String username = req.getUsername();
    String password = passwordEncoder.encode(req.getPassword());
    UserRoleEnum role  = req.getRole();

    // 회원 중복 확인
    Optional<User> checkUsername = userRepository.findByUsername(username);
    if (checkUsername.isPresent()) {
        throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
    }

    if(role == null) {
        role = UserRoleEnum.USER;
    } else if(role == UserRoleEnum.ADMIN) {
        if(req.getAdminToken() == null || !req.getAdminToken().equals(ADMIN_TOKEN)) {
            throw new IllegalArgumentException("유효한 관리자 토큰이 필요합니다.");
        }
    }

    // 사용자 등록
    User user = User.builder()
        .username(username)
        .password(password)
        .role(role)
        .build();

    userRepository.save(user);
}

 

콘솔에 IllegalArgumentException 예외와 함께 중복된 사용자가 존재한다는 메시지가 출력되지만 

2025-03-03T21:44:27.463+09:00 ERROR 13936 --- [springboot3template] [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalArgumentException: 중복된 사용자가 존재합니다.] with root cause

java.lang.IllegalArgumentException: 중복된 사용자가 존재합니다.

 

포스트맨에 status는 403으로 나온다.

 

그나마 403은 괜찮지만 어떤 예외의 경우에는 서버 에러인 status 500을 반환할 수 있고 서버 에러는 단순 예외가 아닌 서버에 문제가 있다는 경고 이기 때문에 실제 서버에 문제가 있는 경우가 아니라면 반환 되어서는 안된다.

 

이를 해결하기 위해 전역 예외 처리를 담당하는 GlobalExceptionHandler 클래스를 만들어 상태를 관리해야 한다. 

 

1. 코드

1.GlobalExceptionHandler

 

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 커스텀 예외 처리
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorRes> handleCustomException(CustomException ex) {
        return ResponseEntity.status(ex.getHttpStatus()).body(ErrorRes.of(ex));
    }
    // 특정 예외 처리 (IllegalArgumentException)
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorRes> handleIllegalArgumentException(IllegalArgumentException ex) {
        ErrorRes response = new ErrorRes(4000, ex.getMessage(), HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.value(), LocalDateTime.now());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

    // 일반적인 예외 처리 (NullPointerException 등 포함)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorRes> handleGeneralException(Exception ex) {
        ErrorRes response = new ErrorRes(9999, ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value(), LocalDateTime.now());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}

 

2. CustomException

 

@Getter
public class CustomException extends RuntimeException {
    private final int code;
    private final String message;
    private final HttpStatus httpStatus;
    private final int status;

    public CustomException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
        this.httpStatus = errorCode.getHttpStatus();
        this.status = errorCode.getHttpStatus().value();
    }
}

 

3. ErrorRes

 

@Getter
@AllArgsConstructor
public class ErrorRes {
    private final int code;        // 커스텀 상태 코드 (예: 1000, 2000)
    private final String message;  // 오류 메시지
    private final HttpStatus httpStatus;  // HTTP 상태 코드
    private final int status; // 상태 코드 숫자
    private final LocalDateTime timestamp;

    public static ErrorRes of(CustomException ex) {
        return new ErrorRes(ex.getCode(), ex.getMessage(), ex.getHttpStatus(), ex.getHttpStatus().value(), LocalDateTime.now());
    }
}

 

4. ErrorCode

 

@Getter
@AllArgsConstructor
public enum ErrorCode {

    // 유저 관련 에러 (1000번대)
    DUPLICATE_USERNAME(1000, "이미 존재하는 아이디입니다.", HttpStatus.CONFLICT), // 409
    INVALID_TOKEN(1001, "유효하지 않은 토큰입니다.", HttpStatus.FORBIDDEN), // 403
    LOGIN_FAIL_USERNAME(1002, "존재하지 않는 아이디 입니다.", HttpStatus.BAD_REQUEST), // 400
    LOGIN_FAIL_PASSWORD(1003, "비밀번호를 틀렸습니다.", HttpStatus.BAD_REQUEST), // 400
    USER_NOT_FOUND(1004, "사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), // 404
    UNAUTHORIZED_ACCESS(1005, "권한이 없습니다.", HttpStatus.FORBIDDEN); //403

    private final int code;
    private final String message;
    private final HttpStatus httpStatus;
}

 

5. AuthService

@Service
@RequiredArgsConstructor
public class AuthService {

    private final JwtProvider jwtProvider;
    private static final String ADMIN_TOKEN = "adminToken";

    private final UserRepository userRepository;

    private final PasswordEncoder passwordEncoder;

    // 회원가입
    @Transactional
    public void signUp(SignUpReq req) {
        String username = req.getUsername();
        String password = passwordEncoder.encode(req.getPassword());
        UserRoleEnum role  = req.getRole();

        // 회원 중복 확인
        Optional<User> checkUsername = userRepository.findByUsername(username);
        if (checkUsername.isPresent()) {
            throw new CustomException(ErrorCode.DUPLICATE_USERNAME);
        }

        if(role == null) {
            role = UserRoleEnum.USER;
        } else if(role == UserRoleEnum.ADMIN) {
            if(req.getAdminToken() == null || !req.getAdminToken().equals(ADMIN_TOKEN)) {
                throw new CustomException(ErrorCode.INVALID_TOKEN);
            }
        }

        // 사용자 등록
        User user = User.builder()
            .username(username)
            .password(password)
            .role(role)
            .build();

        userRepository.save(user);
    }

    // 로그인
    @Transactional
    public void login(LoginReq req, HttpServletResponse res) {
        String username = req.getUsername();
        String password = req.getPassword();

        // 사용자 확인
        User user = userRepository.findByUsername(username).orElseThrow(
            () -> new CustomException(ErrorCode.LOGIN_FAIL_USERNAME)
        );

        // 비밀번호 확인
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new CustomException(ErrorCode.LOGIN_FAIL_PASSWORD);
        }

        // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
        String token = jwtProvider.createToken(user.getUsername(), user.getRole());
        jwtProvider.addJwtToCookie(token, res);
    }

}

 

2. 결과

이미 존재하는 아이디로 요청을 보내면

POST
http://localhost:8080/api/v1/auth/sign-up
application/json

{
    "username" : "test00",
    "password" : "1234",
    "role" : "ADMIN",
    "adminToken" : "adminToke"
}

 

아래와 같이 설정한 code, message, status, timestamp를 반환하고 실제로 status 값도 중복에 대한 예외인 409를 반환한다.

{
    "code": 1000,
    "message": "이미 존재하는 아이디입니다.",
    "httpStatus": "CONFLICT",
    "status": 409,
    "timestamp": "2025-03-03T22:53:45.5997527"
}

 

저작자표시 비영리 변경금지 (새창열림)

'Spring Boot > Sprng Boot Sample Code' 카테고리의 다른 글

[Spring Boot 3.x] accessToken & refreshToken with Redis  (0) 2025.03.28
[Spring Boot 3.x] 5. @Valid 어노테이션을 통한 DTO validation 설정  (0) 2025.03.03
Spring Boot 에서 네이버 클라우드 Object Storage 에 파일 업로드  (1) 2025.03.03
[SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정  (0) 2025.03.03
[Spring Boot 3.x] 3. JWT 토큰 인증(filter) & API 테스트  (0) 2025.03.01
'Spring Boot/Sprng Boot Sample Code' 카테고리의 다른 글
  • [Spring Boot 3.x] accessToken & refreshToken with Redis
  • [Spring Boot 3.x] 5. @Valid 어노테이션을 통한 DTO validation 설정
  • Spring Boot 에서 네이버 클라우드 Object Storage 에 파일 업로드
  • [SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정
Yun-seul
Yun-seul
재능이 없으면 열심히라도 하자
  • Yun-seul
    윤슬
    Yun-seul
  • 전체
    오늘
    어제
    • 분류 전체보기 (22)
      • Java (1)
        • Java 기본 (1)
      • Spring Boot (9)
        • Sprng Boot Sample Code (9)
      • Docker (1)
      • Kubernetes (7)
        • Common (3)
        • GKE(Google Kubernetes Engin.. (4)
        • EKS(Elastic Kubernetes Serv.. (0)
      • Redis (0)
      • AWS (0)
      • Git (0)
      • Reflection (1)
      • Troubleshooting (3)
      • Performance Tuning (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    어세스토큰
    onceperrequestfilter
    SpringBoot
    커스텀익셉션
    kubernetes
    methodargumentnotvalidexception
    GKE
    필터2번
    전역예외처리
    jwt토큰 #로그인 #회원가입 #쿠키 #보안설정
    rfc6750
    쿠버네티스
    재요청
    djava.io.tmpdir
    @value #null #어노테이션 #springboot #springioc #컨테이너
    임시디렉토리
    에러코드관리
    unable to create tempdir. java.io.tmpdir is set to
    docker
    globalexception
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Yun-seul
[Spring Boot 3.x] 6. @ControllerAdvice를 활용한 Global Exception Handling 전역 예외 처리
상단으로

티스토리툴바