spring/게시판 api

Spring boot 게시판 API 서버 제작 (7) - 로그인 - Exception 처리

얼킴 2023. 2. 19. 18:07

이번 글에서는 잠시 쉬어가는 시간으로 Exception처리를 해보려고 합니다. 기존에 예외처리는 모두 RuntimeException으로 처리를 해서 오류가 났을때 어디서 났는지 확인하기가 어려웠는데요, 이번에 각 기능별로 Exception처리를 달리하여 오류가 발생했을 때 어디서 발생했는지 확인하기 쉽게 만들어보겠습니다.

 

Exception 클래스 생성

각 기능에 대해서 Exceptino 클래스를 생성하겠습니다. 현재까지 구현된 기능중에서 발생할 만한 Exception들은 다음과 같습니다.

LoginFailureException.java

public class LoginFailureException extends RuntimeException {
}

MemberEmailAlreadyExistsException

public class MemberEmailAlreadyExistsException extends RuntimeException {
    public MemberEmailAlreadyExistsException(String message) {
        super(message);
    }
}

MemberNicknameAlreadyExistsException

public class MemberNicknameAlreadyExistsException extends RuntimeException{
    public MemberNicknameAlreadyExistsException(String message) {
        super(message);
    }
}

MemberNotFoundException

public class MemberNotFoundException extends RuntimeException {
}

RoleNotFoundException

public class RoleNotFoundException extends RuntimeException {
}

Exception 교체

다음으로 기존의 Exception 처리를 이번에 만든 Exception들로 교체하겠습니다.

SignService.java

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SignService {
    private final MemberRepository memberRepository;
    private final RoleRepository roleRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenService tokenService;

    @Transactional
    public void signUp(SignUpRequest req){
        validateSignUpInfo(req);
        memberRepository.save(SignUpRequest.toEntity(
                req,
                //roleRepository.findByRoleType(RoleType.ROLE_NORMAL).orElseThrow(RuntimeException::new),
                roleRepository.findByRoleType(RoleType.ROLE_NORMAL).orElseThrow(RoleNotFoundException::new),
                passwordEncoder
        ));
    }
    private void validateSignUpInfo(SignUpRequest req) {
        if(memberRepository.existsByEmail(req.getEmail()))
            //throw new RuntimeException();
            throw new MemberEmailAlreadyExistsException(req.getEmail());
        if(memberRepository.existsByNickname(req.getNickname()))
            //throw new RuntimeException();
            throw new MemberNicknameAlreadyExistsException(req.getNickname());
    }

    public SignInResponse signIn(SignInRequest req){
        //Member member = memberRepository.findByEmail(req.getEmail()).orElseThrow(RuntimeException::new);
        Member member = memberRepository.findByEmail(req.getEmail()).orElseThrow(LoginFailureException::new);
        validatePassword(req, member);
        String subject = createSubject(member);
        String accessToken = tokenService.createAccessToken(subject);
        String refreshToken = tokenService.createRefreshToken(subject);
        return new SignInResponse(accessToken, refreshToken);
    }

    private void validatePassword(SignInRequest req, Member member) {
        if(!passwordEncoder.matches(req.getPassword(), member.getPassword())) {
            //throw new RuntimeException();
            throw new LoginFailureException();
        }
    }

    private String createSubject(Member member) {
        return String.valueOf(member.getId());
    }

}

ExceptionAdvice 생성

이제 Exception이 발생했을 때 처리하기 쉽도록 ExceptionAdvice를 만들어보겠습니다.

ExceptionAdvice는 특정 예외가 발생했을 때 미리 지정해둔 메서드를 실행시켜줍니다.

ExceptionAdvice.java

@RestControllerAdvice //@ControllerAdvice + @ResponseBody -> 예외 발생할 경우 자동으로 JSON 형식의 응답을 반환
@Slf4j // Lombok 라이브러리에서 제공하는 로깅 어노테이션
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class) //예외 처리 메서드 : 특정 예외가 발생하면 함수 실행
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Response exception(Exception e) {
        log.info("e = {}", e.getMessage());
        return Response.failure(-1000, "오류가 발생하였습니다.");
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Response methodArgumentNotValidException(MethodArgumentNotValidException e) {
        return Response.failure(-1003, e.getBindingResult().getFieldError().getDefaultMessage());
    }

    @ExceptionHandler(LoginFailureException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Response loginFailureException() {
        return Response.failure(-1004, "로그인에 실패하였습니다.");
    }

    @ExceptionHandler(MemberEmailAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Response memberEmailAlreadyExistsException(MemberEmailAlreadyExistsException e) {
        return Response.failure(-1005, e.getMessage() + "은 중복된 이메일 입니다.");
    }

    @ExceptionHandler(MemberNicknameAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Response memberNicknameAlreadyExistsException(MemberNicknameAlreadyExistsException e) {
        return Response.failure(-1006, e.getMessage() + "은 중복된 닉네임 입니다.");
    }

    @ExceptionHandler(MemberNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Response memberNotFoundException() {
        return Response.failure(-1007, "요청한 회원을 찾을 수 없습니다.");
    }

    @ExceptionHandler(RoleNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Response roleNotFoundException() {
        return Response.failure(-1008, "요청한 권한 등급을 찾을 수 없습니다.");
    }
}
  • @RestControllAdvice : @ControllerAdvice 어노테이션과 @ResponseBody 어노테이션이 합쳐진 것으로, 예외가 발생할 경우 자동으로 JSON 형식의 응답을 반환합니다.
  • @ExcentionHandler : 특정 예외 타입을 지정하여 해당 예외가 발생했을 때 실행될 메서드를 설정할 수 있습니다. 예외 타입을 지정하지 않으면 모든 예외에 대해 처리됩니다. 
  • @ResponseStatus : 해당 예외가 발생했을 때 반환되는 HTTP 응답 코드를 설정하고, Response 객체를 반환합니다.

ExceptionAdvice 테스트

ExceptionAdvice를 만들었으니 이번에는 테스트를 진행해 보겠습니다. ExceptionAdvice테스트는 이전 테스트들과 동일하게 진행되니 설명은 생략하겠습니다. 

더보기
@ExtendWith(MockitoExtension.class)
class ExceptionAdviceTest {
    @InjectMocks SignController signController;
    @Mock SignService signService;
    MockMvc mockMvc;
    ObjectMapper objectMapper = new ObjectMapper();

    @BeforeEach
    void beforeEach(){
        mockMvc = MockMvcBuilders.standaloneSetup(signController).setControllerAdvice(new ExceptionAdvice()).build();
    }

    @Test
    public void signInLoginFailureExceptionTest() throws Exception{
        //given
        SignInRequest req = new SignInRequest("email@email.com","123456a!");
        given(signService.signIn(any())).willThrow(LoginFailureException.class);

        //when //then
        mockMvc.perform(
                post("/api/sign-in")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void signInMethodArgumentNotValidExceptionTest() throws Exception {
        // given
        SignInRequest req = new SignInRequest("email", "1234567");

        // when, then
        mockMvc.perform(
                        post("/api/sign-in")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isBadRequest());
    }

    @Test
    void signUpMemberEmailAlreadyExistsExceptionTest() throws Exception {
        // given
        SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
        doThrow(MemberEmailAlreadyExistsException.class).when(signService).signUp(any());

        // when, then
        mockMvc.perform(
                        post("/api/sign-up")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isConflict());
    }

    @Test
    void signUpMemberNicknameAlreadyExistsExceptionTest() throws Exception {
        // given
        SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
        doThrow(MemberNicknameAlreadyExistsException.class).when(signService).signUp(any());

        // when, then
        mockMvc.perform(
                        post("/api/sign-up")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isConflict());
    }

    @Test
    void signUpRoleNotFoundExceptionTest() throws Exception {
        // given
        SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
        doThrow(RoleNotFoundException.class).when(signService).signUp(any());

        // when, then
        mockMvc.perform(
                        post("/api/sign-up")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isNotFound());
    }

    @Test
    void signUpMethodArgumentNotValidExceptionTest() throws Exception {
        // given
        SignUpRequest req = new SignUpRequest("", "", "", "");

        // when, then
        mockMvc.perform(
                        post("/api/sign-up")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(req)))
                .andExpect(status().isBadRequest());
    }

}

 

API 로 확인

테스트까지 완료하였으니 이번에는 Postman를 활용하여 API를 테스트를 진행해보겠습니다. 현재상태로 어플리케이션을 실행시키면 role에 등급이 저장되어 있지 않기때문에 정상적으로 api를 호출할 수 없습니다. 그래서 DB에 Role 관련 데이터들을 저장해주고, ROLE_NORMAL 권한 등급을 조회해서 회원가입시에 사용자에게 부여해주어야 합니다. 일단은 클래스를 하나 생성하여 실행과 동시에 데이터베이스에 값을 넣어주도록 하겠습니다.

InitDB.java

@Component // 해당 객체를 어플리케이션 내에서 사용할 수 있도록 Bean으로 관리하는 역할
@RequiredArgsConstructor
@Slf4j
@Profile("local") // InitDB 클래스가 local 프로파일에서만 실행되도록 설정하는 역할
public class InitDB {
    private final RoleRepository roleRepository;

    @PostConstruct //객체 생성 후 초기화를 수행하는 메소드를 정의
    @Transactional
    public void initDB(){
        log.info("initialize database");
        initRole();
    }

    private void initRole(){
        roleRepository.saveAll( // 모든 Role_Type role에 저장
                List.of(RoleType.values()).stream().map(roleType -> new Role(roleType)).collect(Collectors.toList())
        );
    }
}

/api/sign-up api 테스트

/api/sign-up

/api/sign-in api 테스트

/api/sign-in

postman으로 sign-up sign-in 모두 확인을 해보았습니다. 

Data base에서도 확인해보니 정상적으로 데이터가 들어와 있습니다.

 


궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.

github : https://github.com/jaeyeon423/spring_board_api