프로젝트

[예외 처리] 회원가입 Custom Exception을 해보자

ummchicken 2023. 1. 19. 20:24

회원가입을 할 때 생길 수 있는 예외들에는 뭐가 있을까?

 

Custom Exception을 적용시키지 않았던 이 편에 이어서...

 

기존에 작성했던 예외 처리 코드를 조금 바꿔보았다.

Custom Exception을 해보았다.

 

 

음... 어찌저찌 테스트 통과하긴 하는데, 

문제는, 내가 짠 테스트코드가 틀린 로직일 수도 있다ㅋㅋㅠ

 

근데 일단 통과는 함...

 

원래 통과 못했는데, 이것저것 바꾸다보니 됐음.

 

 

암튼 

회원 이름이 겹치는 중복 회원가입 상황에 대한 예외처리를 해보았다.

 

 

 

 

 

exception 패키지를 생성한다.

그리고 그 안에 RuntimeException을 상속받는 AppException 클래스를 생성한다.

[AppException.java]

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class AppException extends RuntimeException {

    private ErrorCode errorCode; // Enum Type, Custom ErrorCode이다.
    private String message;

}

 

 

 

그 다음 

Enum 타입 ErrorCode를 생성한다.

[ErrorCode.java]

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
@Getter
public enum ErrorCode {

    // 회원 이름 중복
    MEMBERNAME_DUPLICATED(HttpStatus.CONFLICT, "");

    private HttpStatus httpStatus;
    private String message;

}

→ 회원 이름 중복 시 http status가 conflict로 처리한다.

 

 

 

다음은 @RestControllerAdvice를 선언할 ExceptionManager 클래스를 생성한다.

[ExceptionManager.java]

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ExceptionManager {

    @ExceptionHandler(AppException.class)
    public ResponseEntity<?> appExceptionHandler(AppException e) {
        return ResponseEntity.status(e.getErrorCode().getHttpStatus())
                .body(e.getErrorCode().name() + " " + e.getMessage()); // String으로 처리
    }

    // 특정 Exception을 받아서 처리할 수 있음
    // ? : 뭐든지 받을 수 있음
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> runtimeExceptionHandler(RuntimeException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(e.getMessage());
    }

}

→ @ExceptionHandler로 RuntimeException과 AppException(특정 Exception)을 받는다.

 

 

 

그리고 중복 검사를 하는 MemberService를 수정한다.

[MemberService.java]

private void validateDuplicateMember(Member member) {
    Member findMember = memberRepository.findByEmail(member.getEmail());
    if(findMember != null) {
        throw new AppException(ErrorCode.MEMBERNAME_DUPLICATED, findMember.getName() + "은 이미 있습니다.");
    }
}

 

 

 

그 다음 테스트 코드를 작성한다.

[MemberServiceTest]

@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMemberTest(){
    Member member1 = createMember();
    Member member2 = createMember();
    memberService.saveMember(member1);
    Throwable e = assertThrows(IllegalStateException.class, () -> {
        memberService.saveMember(member2);});
    assertEquals("이미 가입된 회원입니다.", e.getMessage());
}

→ 테스트 코드는 전에 했던 코드를 그대로 썼더니, 

테스트 코드가 실패했다.

 

 

 

fail한 이유를 보니, 

org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <java.lang.IllegalStateException> but was: <com.myrecipe.exception.AppException>

이런 에러가 났다.

 

 

IllegalStateException을 기대했는데, AppException가 발생했다는 문장인 것 같다.

 

 

따라서 Test 코드를 수정한다.

[MemberServiceTest 수정 후]

@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMemberTest(){
    Member member1 = createMember();
    Member member2 = createMember();
    memberService.saveMember(member1);
    Throwable e = assertThrows(AppException.class, () -> {
        memberService.saveMember(member2);});
    assertEquals("김길동은 이미 있습니다.", e.getMessage());
}

 

 

 

결과는 

통과한다.

 

 

 

참고로 내가 짠 테스트코드 불신해서(ㅋ) 한번 에러 메세지를 바꿔보았다.

assertEquals("김길동은dkdk 이미 있습니다.", e.getMessage());

이렇게 말이다.

 

 

 

그랬더니

이렇게 결과가 나온다.

 

 

다행히 코드를 이상하게 짜진 않았나보다.

 

 

 


 

@ControllerAdvice란?

간단하게 말하자면 @ExceptionHandler, @ModelAttribute, @InitBinder 가 적용된 메서드들을
AOP를 적용해 컨트롤러 단에 적용하기 위해 고안된 애너테이션 입니다.

클래스에 선언하면 되며, 모든 @Controller에 대한,
전역적으로 발생할 수 있는 예외를 잡아서 처리할 수 있다.

 

 

@RestControllerAdvice란?

@ResponseBody + @ControllerAdvice => @RestControllerAdvice 

@ControllerAdvice와 동일한 역할을 수행하고,
추가적으로 @ResponseBody를 통해 객체를 리턴할 수도 있다.

따라서 단순히 예외만 처리하고 싶다면 @ControllerAdvice를 적용하면 되고,
응답으로 객체를 리턴해야 한다면 @RestControllerAdvice를 적용하면 된다.

 

 

 

@ExceptionHandler란?

위에서 @ControllerAdvice에 대해서 이야기할 때 언급한 어노테이션이다.
이 어노테이션을 메서드에 선언하고 특정 예외 클래스를 지정해주면
해당 예외가 발생했을 때 메서드에 정의한 로직으로 처리할 수 있다. 
@ControllerAdvice 또는 @RestControllerAdvice에 정의된 메서드가 아닌
일반 컨트롤러 단에 존재하는 메서드에 선언할 경우, 해당 Controller에만 적용된다.

Controller, RestController에만 적용이 가능하다(@Service 등의 빈에서는 안된다.)

 

출처 : https://javachoi.tistory.com/253

https://velog.io/@banjjoknim/RestControllerAdvice