Spring Boot 테스트에서 PasswordEncoder.matches() 그리고 예외 처리 문제 해결 과정

이상민·2024년 9월 11일
0

오늘 Spring Boot 과제에서 테스트 코드를 작성하다가 몇 가지 문제를 겪었고, 이를 해결하는 과정을 기록해보았다. 특히 PasswordEncoder.matches() 메서드를 사용하는 부분에서 인자 순서 때문에 테스트가 실패하는 상황이 발생했다. 그리고 올바른 예외 처리 과정을 테스트 방식은 내가 의도한 상황에서 이를 명확하게 처리하는 것을 알게 되었다. 이 문제를 해결하면서 여러 가지 테스트 관련 문제를 다뤄야 했는데, 그 과정을 블로그에 작성해보았다.

1. PasswordEncoder.matches() 메서드 사용 시 인자 순서 문제

문제 상황:

처음에는 비밀번호를 암호화하고, 테스트에서 PasswordEncoder.matches() 메서드를 통해 평문 비밀번호와 인코딩된 비밀번호를 비교하려고 했다. 그런데, 예상과는 달리 테스트가 실패했다.

@Test
void matches_메서드가_정상적으로_동작한다() {
    // given
    String rawPassword = "testPassword";
    String encodedPassword = passwordEncoder.encode(rawPassword);

    // when
    boolean matches = passwordEncoder.matches(encodedPassword, rawPassword); // 인자 순서 문제

    // then
    assertTrue(matches);
}

이렇게 코드를 작성했는데, 테스트에서 실패했다. 왜 그럴까 고민하다가 발견한 건, matches() 메서드의 인자 순서가 잘못되었다는 것이다.

쉽게 설명하자면, 비밀번호를 비교할 때 "평문 비밀번호""암호화된 비밀번호"의 역할이 다르기 때문에 순서가 중요하다. 왜냐하면, 암호화된 비밀번호는 되돌릴 수 없기 때문이다.

암호화 과정:

  • 사용자가 입력한 평문 비밀번호는 암호화 과정을 거쳐서 보안이 강한 암호화된 비밀번호로 바뀐다.

  • 이때, 암호화된 비밀번호는 복호화(원래 상태로 되돌리는 것)가 불가능하다. 즉, 다시 평문으로 바꾸는 게 아니라, 평문 비밀번호를 다시 암호화해서 비교하는 방식으로 작동한다.

비밀번호 비교 과정:

1.평문 비밀번호: 사용자가 로그인할 때 입력하는 원래 비밀번호이다.

2.암호화된 비밀번호: 서버에 저장되어 있는 암호화된 비밀번호이다.

로그인할 때는, 입력한 평문 비밀번호를 다시 암호화해서, 서버에 저장된 암호화된 비밀번호와 같은지 비교한다.

왜 순서가 중요한가?

  • 올바른 순서:
    평문 비밀번호를 먼저 전달하고, 암호화된 비밀번호를 두 번째로 전달해야 한다. 이렇게 해야 평문을 암호화해서 둘을 비교할 수 있다.

  • 잘못된 순서로 하면,
    암호화된 비밀번호를 평문으로 변환할 수 없기 때문에 비교가 제대로 되지 않는다.

예를 들어:

  • 정상적인 비교: 평문 비밀번호("1234")를 다시 암호화한 후, 저장된 암호화된 비밀번호와 비교함.

  • 잘못된 비교:
    암호화된 비밀번호("2a$10...")를 다시 암호화하려고 시도하면, 두 비밀번호가 절대 같을 수 없기 때문에 비교가 실패함.

즉, 암호화된 비밀번호는 되돌릴 수 없어서 평문 → 암호화된 비밀번호 순서로 비교해야만 성공한다!

2. IllegalArgumentException이 발생하는 상황

IllegalArgumentException은 주로 잘못된 인자가 메서드에 전달되었을 때 발생한다. 메서드에서 특정 값을 기대하고 있는데, 그 값이 유효하지 않은 경우에 이 예외가 발생한다. 최근 프로젝트에서 이 예외를 여러 번 다루게 되었는데, 그중 몇 가지를 기록해보면:

- 인자 범위가 벗어난 경우: 나이를 설정할 때 음수나 비정상적으로 큰 값이 들어온 경우.

- 잘못된 형식의 값이 전달된 경우: 이메일 형식이 맞지 않을 때.

- 널 값이 전달된 경우:
필수 값으로 null이 들어오는 경우.

3. InvalidRequestException 처리

InvalidRequestException주로 요청이 잘못되었을 때 발생하는 예외다. 특히, 데이터베이스에서 해당 데이터를 찾지 못했거나, 요청이 예상과 다를 때 사용된다.

@Test
public void manager_목록_조회_시_Todo가_없다면_IRE_에러를_던진다() {
    // given
    long todoId = 1L;
    given(todoRepository.findById(anyLong())).willReturn(Optional.empty());

    // when & then
    InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> managerService.getManagers(todoId));
    assertEquals("Todo not found", exception.getMessage());
}

처음에는 "Manager not found"라는 메시지를 사용했는데, 사실 Todo가 없을 때 발생하는 예외니까 "Todo not found"가 더 적절했다. 이 부분을 수정하고 나니 더 직관적으로 이해할 수 있는 코드가 되었다.

4. commentService에서 예외 처리 문제

댓글 등록 과정에서 Todo가 없을 때 예외를 발생시키는 테스트도 해봤다. 처음엔 ServerException을 던지는 걸로 생각했는데, 이 경우는 잘못된 요청이므로 InvalidRequestException을 던져야 더 적절했다.

@Test
public void comment_등록_중_할일을_찾지_못해_에러가_발생한다() {
    // given
    long todoId = 1;
    CommentSaveRequest request = new CommentSaveRequest("contents");
    AuthUser authUser = new AuthUser(1L, "email", UserRole.USER);

    given(todoRepository.findById(anyLong())).willReturn(Optional.empty());

    // when
    InvalidRequestException exception = assertThrows(InvalidRequestException.class, () -> {
        commentService.saveComment(authUser, todoId, request);
    });

    // then
    assertEquals("Todo not found", exception.getMessage());
}

ServerException: 주로 서버 내부에서 예상치 못한 오류가 발생했을 때 사용된다. 하지만 여기서 발생한 문제는 서버 내부의 예기치 못한 문제가 아닌, 클라이언트가 존재하지 않는 Todo에 댓글을 달려고 한 상황이다. 따라서 ServerException은 이 상황에 적합하지 않다.

5. NullPointerException 대신 적절한 예외 던지기

마지막으로 NullPointerException 문제를 다뤄본다. Todousernull일 때, NullPointerException을 발생시키는 게 아니라, 명확하게 InvalidRequestException을 던지도록 수정했다

@Test
void todo의_user가_null인_경우_예외가_발생한다() {
    // given
    AuthUser authUser = new AuthUser(1L, "a@a.com", UserRole.USER);
    long todoId = 1L;
    long managerUserId = 2L;

    Todo todo = new Todo();
    ReflectionTestUtils.setField(todo, "user", null);  // user 필드를 null로 설정

    ManagerSaveRequest managerSaveRequest = new ManagerSaveRequest(managerUserId);

    given(todoRepository.findById(todoId)).willReturn(Optional.of(todo));

    // when & then
    InvalidRequestException exception = assertThrows(InvalidRequestException.class, () ->
        managerService.saveManager(authUser, todoId, managerSaveRequest)
    );

    // 예외 메시지 검증
    assertEquals("담당자를 등록하려고 하는 유저가 유효하지 않습니다.", exception.getMessage());
}

기존 테스트 코드에서 Null에러가 발생해서

public void saveManager(AuthUser authUser, long todoId, ManagerSaveRequest request) {
    Todo todo = todoRepository.findById(todoId)
        .orElseThrow(() -> new InvalidRequestException("Todo not found"));

    if (todo.getUser() == null) {
        throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 유효하지 않습니다.");
    }

    // 기타 로직
}
if (todo.getUser() == null) {
        throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 유효하지 않습니다.");
    }

이 부분을 서비스 로직에 추가해서 InvalidRequestException을 던지도록 수정했다

결론

테스트를 작성하는 과정에서 여러 가지 문제를 마주했지만, 하나씩 해결해 나가면서 느낀 점은 문제를 명확하게 정의하고 그에 맞는 적절한 예외를 던지는 것이 테스트 과정에서 중요하다는 것이다. 특히, 기본적인 메서드 사용 규칙(예: 인자 순서)을 지키는 것부터 시작해서, 사용자 정의 예외를 사용해 코드의 가독성과 유지보수성을 높이는 것이 필요하다.

profile
안녕하세요

0개의 댓글