개발은 아름다워

[ Spring ] customException API를 만들어보자(feat. 체크예외와 언체크예외 이해하기) 본문

스프링

[ Spring ] customException API를 만들어보자(feat. 체크예외와 언체크예외 이해하기)

do_it_zero 2024. 10. 12. 11:22

예외 상황은 늘 존재해

차비와 시간을 아낄겸(집에서 회사까지 4km) 출퇴근용으로 로드 바이크를 당근에서 구매했다. 쌩쌩 잘나가는 자전거를 타고 매일 재밌게 출퇴근을 했다. 어느 날 자전거를 타고 퇴근 중 가볍게 차와 부딪히게 되었다. 그 이후 나는 자전거를 더 조심히 타게 되었다. 이렇게 살다보면 예외 상황을 맞이하곤 한다. 프로그램도 마찬가지이다. 프로그램이 진행되다 보면 예외 상황이 발생할 수 있다는 것이다.

 

자바가 처리하는 예외

자바는 예외 처리를 두 가지 유형으로 처리한다. 두 가지 유형의 차이는 컴파일 시점에 강제로 처리하느냐 이다.

 

1. 체크 예외 - 컴파일 시점에 강제로 처리해야 함
2. 언체크 예외 - 컴파일러가 강제로 예외 처리를 요구하지 않으며 런타임(프로그램 실행시점) 시점에 발생함

즉, 컴파일러가 예외를 처리를 강제적으로 요구하느냐의 차이로도 볼 수 있다.

 

예외에 대한 자세한 내용을 공부하고 싶다면, 김영한님의 강의를 추천!

이번 글에서는 컴파일러가 강제로 예외 처리를 요구하지 않는 언체크 예외에 대해서 다룰 것이다.

 

스프링이 제공하는 언체크 예외

스프링은 다양한 RuntimeException을 제공한다. 자주 만나는 NullPointerException이 대표적이며, 예외 상황에 따른 다양한 예외 처리들을 만들어서 제공해준다.

왜 cutomException을 만드는거지?

이미 스프링에서 만든 예외 처리들이 존재하는데 왜 customException을 만드는걸까 하고 생각할 수 있다. 이유는 간단하다. 개발자 간의 소통을 정확하게 하기 위해서이다. 이 때 정확하다는 의미는 개발자끼리 상의하여 만들어 놓은 API 스펙대로 한다는 의미이다. API 스펙을 만들어서 예외 상황을 커스텀 함으로 정확한 소통이 가능하다고 생각한다.

또한 발생할 법한 예외인데, 따로 스프링에 없는 경우 직접 예외를 직접 만들 수 있기 때문이다. 

 

예외 처리 스펙

프론트엔드 개발자와 상의 하여 예외 처리 API 스펙은 다음과 같이 하기로 결정했다.(상상)

{
   "status": 400,
    "message": "이름은 반드시 입력해야 합니다."
}

이제 예외가 발생하면 위와 같은 응답값을 내려주는 API를 만들어야한다.

 

데이터 값이 정해진 객체들을 쓰려면? Enum!

이번 글에서의 예외 처리는 두 가지 경우에 검증 후 예외 처리하는 것을 만들것이다.

 

1. 회원 가입 시, 이전에 가입 한 name을 쓸 경우

2. 회원 가입 시, name에 공백을 하는 경우

 

예외 처리 스펙은 이미 정해졌으므로, 스펙에 맞춰 내용을 정하면 된다. 각 예외는 객체이며 내용은 각각 다르지만 변하지 않는다. 이럴 때 사용하면 되는 것은 뭘까? 바로 enum 이다. enum 클래스에는 값이 정해진 객체들을 나열해서 쓸 수 때문이다. 이 덕분에 객체가 가지고 있는 데이터의 타입 안정성도 확보 될 수 있다.

예외 처리의 내용은 정해져 status와 messgae로 정해져있다. status와 message를 데이터를 가진 객체들을 enum 클래스에 넣어 놓으면 되는 것이다.
코드로 봐보자.

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

    EXISTING_MEMBER(422,"이미 존재하는 회원입니다."),

    private final int status;
    private final String message;
}

 

customException 만들기

예외 발생 시 status와 message가 담긴 객체들을 errorCode enum 클래스에 모아두었다. 이제 RuntimeException을 상속 받는 customException을 만들면 된다.

@Getter
public class CustomException extends RuntimeException {

    private final ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public int getStatus(){
        return errorCode.getStatus();
    }

    public String getMessage(){
        return errorCode.getMessage();
    }
}

 

첫번째 예외) 회원 가입 시, 이전에 가입 한 name을 쓸 경우

이제 회원 가입 시에 동일한 이름이 있는 경우 예외가 발생하도록 만들어주자.

    @Transactional
    public Long join(Member member) {

        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()) {
            throw new CustomException(ErrorCode.EXISTING_MEMBER);
        }
    }

중요한 것은 이 코드이다. 같은 이름이 존재한다면 예외를 발생시키는 것이다.
CustomException 예외를 발생시킬 것이며, 그 안의 내용은 errorCode에서 미리 만들어 놓은 EXISTIN_MEMBER 객체를 쓸 것이다.

if (!findMembers.isEmpty()) {
            throw new CustomException(ErrorCode.EXISTING_MEMBER);
        }

 

두번째 예외) 회원 가입 시, name을 입력하지 않은 경우

이 때는 스프링 validation을 사용한다. MemberRequestDto로 클라이언트로부터 name을 받을 때 @NotEmpty를 사용하여 공백인지 검증한다. 공백인 경우 MethodArgumentNotValidException이 발생한다.

    @PostMapping("/api/members")
    public MemberResponseDto saveMember(@RequestBody @Valid MemberRequestDto request){
        Member member = new Member();
        member.setName(request.getName());

        Long id = memberService.join(member);
        return new MemberResponseDto(id);
    }

    @Data
    static class MemberRequestDto{
        @NotEmpty(message = "이름은 반드시 입력해야 합니다.")
        private String name;

    }

 

발생한 예외를 처리하여 클라이언트에게 전달해 줄 handler!

@RestControllerAdvice
public class GlobalExceptionHandler {

    // customException 처리기
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> customExceptionHandler(CustomException e){
        ErrorResponse responseError = new ErrorResponse(e.getStatus(),e.getMessage());
        return new ResponseEntity<>(responseError, HttpStatusCode.valueOf(e.getStatus()));
    }


    // 검증예외 처리기
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> methodArgValidHandler(MethodArgumentNotValidException e) {
        ErrorResponse responseError = new ErrorResponse(e.getStatusCode().value(),e.getFieldError().getDefaultMessage());
        return new ResponseEntity<>(responseError,e.getStatusCode());

    }

    @Getter
    @AllArgsConstructor
    private static class ErrorResponse{
        private final int status;
        private final String message;
    }

}

 

테스트

같은 이름으로 보낼 시, 두번째 때 예외 처리 내용을 받아 볼 수 있다.

 

 

공백을 보낼 시 예외 처리 내용을 받아 볼 수 있다.

 

마무리

스프링에서 예외를 처리하는 방법을 공부함으로써 enum 사용 방법, 예외 흐름을 만들어서 처리하는 방법을 익힐 수 있었다. 특히 @Valid로 예외 발생시 예외 메세지를 어떻게 처리하지에 대한 방법도 배울 수 있었다.

 

 

참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard