개발 낙서장

[TIL] 내일배움캠프 47일차 - 커스텀 예외 처리 본문

Java/Sparta

[TIL] 내일배움캠프 47일차 - 커스텀 예외 처리

권승준 2024. 3. 4. 21:33

 

 

오늘의 학습 키워드📚

커스텀 Exception

주로 사용하는 Exception들은 많다. 보통 일반적으로 어떠한 상황에서 이런 예외가 발생하더라 해서 보편적으로 만들어진 예외들이다.
(NullPointerException, IllegerArgumentException, NoSuchElementException ...)

이미 있는 예외들만 사용해도 충분히 서비스를 개발하는데 문제가 있지는 않지만 각 예외마다 어떠한 상황인지 정확히 구분되어야 할 경우에서는 표준 예외로는 확인이 어려울 수 있다.

예를 들어 POST, GET 등에서 데이터를 찾을 수 없다면 NoSuchElementException이나 NullPointerException을 사용할 것이다.
근데 POST, GET에서 예외 발생 시 처리되는 로직이 다르다면? 표준 예외를 사용할 경우 내부에서 구분 지어 로직을 처리해주어야 하기 때문에 불필요한 코드도 생기고 유지/보수에 힘들어질 수 있다.
그래서 커스텀 Exception을 사용하면 PostNoSuchElementException, GetNoSuchElementException 이런 식으로 예외를 나누어 각 예외마다 처리할 수 있게 된다.

또 다른 예를 들어보자. IndexOutOfBoundsException은 배열의 범위를 벗어났을 때 가장 많이 사용하는 예외이다. Index로 음수 값을 입력하거나 크기를 벗어나는 값이 입력됐을 경우 발생하게 되는데 뭔가 다양한 예외 상황을 부여하고 싶다면?
0 미만인 값이 입력되는 경우를 구분하기 위한 커스텀 Exception, 해당 Collection의 Size를 message에 반환해서 범위에서 얼마나 벗어났는지를 알려주는 Exception 등 여러 사용처가 있을 것이다.

물론 웬만하면 표준 Exception을 사용하는 것이 옳지만 상황에 맞게 커스텀 Exception을 활용한다면 훨씬 가독성 좋고 유지보수가 편한 코드가 될 수 있다.
하지만 반대로 불필요한 커스텀 Exception은 아무런 이점을 가지지 않으며 가독성은 물론 프로젝트 구조를 이해하는데 어렵게 할 것이다.

만드는 방법은 간단하다. Exception 혹은 RuntimeException을 상속하는 클래스를 만들어주면 된다.
둘의 차이는 컴파일 단계에서 처리가 되냐 안 되냐의 차이이다. Exception은 컴파일러 체킹이 가능한 예외이고 RuntimeException은 실행 단계에서 발생하는 예외이다.

이런 예외를 사용하는 건 적절치 않지만, 테스트를 위해 회원가입 시 이미 회원이 존재할 경우 따로 처리할 로직이 있다고 가정하고 커스텀 예외를 발생시켜 보도록 하자.

public class SignUpUserExistsException extends RuntimeException {

    public SignUpUserExistsException() {

    }

    public SignUpUserExistsException(String message) {
        super(message);
    }
}

회원가입할 때 발생하는 예외는 실행 단계에서 발생하는 예외이므로 RuntimeException을 상속한다.
여기서 커스텀 Exception을 만들 때 주의 사항이 있는데

1. 클래스의 네이밍은 Exception으로 끝나도록 한다.
2. 기본 생성자와 message를 받는 생성자 두 개의 생성자는 필수로 구현한다.

이 외에도 여러 가지 규칙이나 방법들이 있지만 두 가지는 반드시 지켜줘야 한다.

    @Transactional
    public UserResponseDto signup(SignUpRequestDto signUpRequestDto) {
        String username = signUpRequestDto.getUsername();
        String password = passwordEncoder.encode(signUpRequestDto.getPassword());

        if(userRepository.findByUsername(username).isPresent()) {
            throw new SignUpUserExistsException("이미 존재하는 회원입니다.");
        }
		
        ...
    }

이제 해당 예외 처리를 방금 만든 예외로 발생시키면 된다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(SignUpUserExistsException.class)
    public ResponseEntity<ResponseMessage<Void>> handleSignUpUserExistsException(SignUpUserExistsException ex) {
        System.out.println(ex.getMessage());

        // 로직 처리 ...

        return ResponseEntity.status(HttpStatus.NOT_FOUND.value())
            .body(ResponseMessage.<Void>builder().httpCode(HttpStatus.NOT_FOUND.value())
                .msg(ex.getMessage()).build());
    }
}

그리고 발생한 예외들을 전역으로 처리해 주는 예외 핸들러 클래스에서 해당 예외를 처리해 주면 된다.

    @Test
    @DisplayName("회원 가입 실패(유저 이미 존재)")
    void signUpUserExists() {
        // given
        String username = "abc123";
        String password = "abc12345";

        User user = new User();
        user.setId(100L);
        user.setUsername(username);
        user.setPassword(password);
        user.setRole(UserRoleEnum.USER);
        userRepository.save(user);

        SignUpRequestDto signUpRequestDto = SignUpRequestDto.builder().username(username)
            .password(password).build();

        UserService userService = new UserService(userRepository, passwordEncoder);

        // when - then
        try {
            UserResponseDto userResponseDto = userService.signup(signUpRequestDto);
        } catch (SignUpUserExistsException ex) {
            assertEquals(ex.getMessage(), "이미 존재하는 회원입니다.");
        }
    }

대충 가짜 유저 객체를 만들어 저장해 주고 해당 예외가 발생하는지 테스트해 보면?

성공적이다!
실제로 예외 처리가 잘 되는지 테스트하기 위해 PostMan으로도 실행해 보면?

실제로 예외도 잘 뱉어냈고 내부 처리 로직인 메세지 출력도 잘 실행됐다.


오늘의 회고💬

기존에 했던 프로젝트를 살짝 살펴보고 JPA 심화 주차로 넘어왔다. 근데 강의 스타일이 나와 매우 안 맞아서 상당히 힘든 길이 예상된다....

 

내일의 계획📜

내일부터 개인 과제 발제가 있어서 아마 강의를 계속 열심히 들어야 하지 않을까 싶다.

Comments