기존에 서비스 로직에 예외처리가 되어있어 예외 상황 발생시 처리는 되지만 대부분의 경우 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 |