본문 바로가기
Spring Security

예외 디버깅 - java.lang.AssertionError: Expecting code to raise a throwable. 테스트코드 분석 및 리펙토링

by 개발자 JACOB 2023. 6. 24.

1. 예외가 발생한 이유 분석하기


테스트 코드를 작성했는데 다음과 같은 에러 메세지가 떴습니다.

Expecting code to raise a throwable.
java.lang.AssertionError: 
Expecting code to raise a throwable.
at co m.readingbooks.web.service.member.MemberServiceTest.register_fail_present_email(MemberServiceTest.java:83)

결론적으로 예외가 던져져야 하는데 던져지지 않아 생긴 예외입니다.

이것만 보면 별로 블로그 포스팅할 내용은 없지만, 예외 디버깅하는 과정에서 배운 점이 있어 공유하고자 포스팅을 작성했습니다.

@Test
void register_fail_present_email(){
    //given
    RegisterRequest registerMember = createRequest("test@example.com", "test1234", "test", "1999", "01012341234", Gender.SECRET);
    memberService.register(registerMember);

    RegisterRequest request = createRequest("test@example.com", "test1234", "test", "1999", "01012341234", Gender.MEN);

    assertThatThrownBy(() -> memberService.register(request))
            .isInstanceOf(MemberPresentException.class)
            .hasMessageContaining("이미 가입된 이메일입니다.");
}

private RegisterRequest createRequest(String email, String password, String name, String birthYear, String phoneNo, Gender gender){
    return new RegisterRequest(email, password, name, birthYear, phoneNo, gender);
}

제가 작성한 코드입니다. 위의 테스트 코드는 이미 가입한 이메일을 가지고 가입을 시도할 때 예외가 터져야 하는 상황입니다.  또 RegisterRequest는 나중에 컨트롤러에서 @ModelAttribute 어노테이션을 통해 데이터바인딩을 한 이후 서비스 계층으로 전달될 DTO 클래스입니다.

스프링 ModelAttribute 데이터 바인딩 과정 이해하기
https://jaykaybaek.tistory.com/15

아무튼 이 과정에서 문제가 생겨 예외가 터졌으므로 우선 로직을 확인해보겠습니다.

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public boolean register(RegisterRequest request){
        validatePresentEmail(request);
        validateForm(request);

        Member member = new Member();
        member.createMember(request);
        memberRepository.save(member);

        return true;
    }
    
    private void validatePresentEmail(RegisterRequest request) {
    String email = request.getEmail();
    Optional<Member> findMember = memberRepository.findByEmail(email);

    if(findMember.isPresent()){
        throw new MemberPresentException("이미 가입된 이메일입니다.");
    }
    
    ...
}

register 메소드는 두 개의 유효성 검사 이후에 회원가입을 진행하는 함수입니다.

@Entity
@Getter
public class Member extends BaseEntity {
	...

    public Member createMember(RegisterRequest request){
        Member member = new Member();
        member.email = request.getEmail();
        member.password = request.getPassword();
        member.name = request.getName();
        member.birthYear = request.getBirthYear();
        member.phoneNo = request.getPhoneNo();
        member.gender = request.getGender();
        return member;
    }
}

Member 클래스를 보면 createMember 메소드는 dto를 받아 Member 객체를 반환하는 메소드입니다. 코드만 봐서는 무엇이 문제인지 몰랐습니다. 그래서 디버깅 모드로 확인해봤습니다.

register 메소드와 Member 클래스의 createMember 메소드에 브레이크 포인트를 지정했습니다.

(디버깅 속도가 너무 느려 왜 그런가 알아보니 메소드단에 브레이크 포인트를 지정하면 속도가 많이 느려집니다. createMember처럼 브레이크 포인트를 지정해주세요)

맨 처음 register 메소드를 확인해봤습니다. 우선 다행히도 dto는 잘 들어왔습니다.

처음 회원가입 했을 때에는 test@example.com 이메일로 가입된 회원이 없습니다. 따라서 validate 메소드는 모두 통과해야 하며, 모두 통과했습니다.

createMember 메소드입니다. 앞서서와 같이 dto는 정상적으로 들어왔네요. RegisterREquest@12079로 동일한 객체가 들어왔습니다.

Member 객체의 초기화도 완료되었습니다. 이제 다시 createMember를 호출한 register 메소드를 확인해보겠습니다.

이때부터 뭔가 느낌이 쌔했습니다. 그리고 바로 예외의 원인을 찾았습니다.

Member 클래스의 createMember 메소드는 dto 값으로 초기화된 Member 객체를 반환합니다. 하지만 Member Service의 register 메소드는 초기화된 Member 객체를 리포지토리에 저장한 것이 아니라, 초기화 되지 않은 null 상태의 Member 객체를 저장했습니다.

이에 따라 validatePresentEmail 메소드에서 문제가 생겼던 것입니다.

해당 메소드에서는 데이터베이스에서 값을 가져오기 위해 리포지토리를 호출했습니다. 리포지토리는 DB에 SELECT * FROM member WHERE email = test@example.com와 같이 WHERE 절의 email이 동일한 조건을 만족하는 SELECT 문을 보냈습니다.

하지만 현재 데이터베이스에 저장되어 있는 이메일은 값은 null 뿐입니다. 따라서 리포지토리는 test@example.com이란 이메일의 컬럼을 찾을 수 없었던 것입니다. 이것은 validatePresentEmail 메소드가 작동되지 않았던 이유로 귀결됩니다.

그리고 제 실수도 찾았습니다. createMember 메소드는 정적 팩토리 메소드였는데 코드의 실수가 있었습니다. 그래서 다음과 같이 코드를 수정했습니다.

2. 코드 리펙토링


public static Member createMember(RegisterRequest request){
    Member member = new Member();
    member.email = request.getEmail();
    member.password = request.getPassword();
    member.name = request.getName();
    member.birthYear = request.getBirthYear();
    member.phoneNo = request.getPhoneNo();
    member.gender = request.getGender();
    return member;
}

일단 Member 클래스의 createMember 메소드를 static 메소드로 선언해, 객체를 생성하고 메소드를 호출할 필요 없이 바로 생성 없이 호출할 수 있도록 했습니다.

public boolean register(RegisterRequest request){
    validatePresentEmail(request);
    validateForm(request);

    Member member = Member.createMember(request);
    memberRepository.save(member);

    return true;
}

register 메소드입니다. 이전 코드와는 달리 객체의 생성 없이 바로 메소드를 호출해 Member 객체를 생성했습니다. 이제 다시 디버그 모드로 돌려보겠습니다.

성공적으로 Member 객체가 초기화 된 모습입니다.

기분 좋은 파란 체크까지 확인했습니다.