들어가면서
테스트 코드를 작성하던 중 이해가 되지않는 신기한 오류를 만났는데 오류의 원인이 @Transcational 을 사용한 롤백이었습니다. @Transactional을 사용하면 간편하게 테스트에 사용한 데이터를 제거할 수 있고 삭제를 위한 코드를 제거할 수 있다는 장점 때문에 애용했지만 이번에 오류를 만나서 주의할 점을 알게 되었습니다.
에러 상황
에러가 발생한 테스트는 회원가입 API를 테스트할 때, 중복된 유저이름을 사용한 요청이 있다면 거부 응답을 반환하도록 하는 API를 테스트하는 테스트 코드에서 @Transactional 을 테스트 코드에 적용하면 테스트가 실패하지만 테스트 데이터를 @AfterEach 를 통해서 초기화하는 코드를 작성하는 경우에는 성공하는 상황
원인
테스트 코드에 적용된 @Transactional 으로 인해서 서비스에서 쿼리를 커밋하는 시점이 변경되어서 데이터 베이스의 unique한 속성을 인지하지 못했기 때문입니다.
설명
다음은 에러가 발생한 테스트 코드입니다.
@DisplayName("회원가입 실패 : 중복된 유저이름 사용시")
@Test
void signupFailWhenDuplicatedUsername() throws Exception {
// given
var username = "testuser";
var password = "testT12345";
var request = new SignUpRequest(username, password);
saveCustomer(username, password);
// when // then
mockMvc.perform(post("/auth/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request))
).andExpectAll(
status().isBadRequest(),
jsonPath("$.status").value("error"),
jsonPath("$.message").value("중복된 username 입니다.")
);
}
saveCustomer
를 사용해서 고객을 저장하고 저장된 고객의 이름과 비밀번호와 동일한 회원가입 요청을 서버로 요청하는 테스트입니다.
// service 코드
public void signUp(SignUpRequest request) {
customerRepository.save(Customer.builder()
.password(request.password())
.username(request.username())
.authority(List.of(new Authority(UserRole.USER)))
.build());
}
API 요청은 다음 서비스의 로직을 통해서 회원가입을 진행합니다. 이때 고객 Entity에서 username 속성에 unique 옵션을 활성화하여 같은 username을 가지도록 고객을 저장하면 에러를 반환하도록 작동하도록 코드를 작성하였습니다.
@Column(unique = true)
private String username;
따라서 구현한 로직에 의하면 중복된 유저이름을 가지는 요청이 처리가 될 때, 에러를 발생시켜서 발생한 에러를 핸들링하고 그 결과를 API에서 반환하도록 하는 것이 이번 테스트의 목적입니다.
하지만 원인에서 설명한 이유로 인해서 회원가입 요청이 성공되었다는 메시지 응답을 서버로 부터 응답 받는 기묘한 현상을 마주할 수 있었습니다.
쿼리가 실행되는 시점?
unique한 값인지 판별하기 위해서는 쿼리가 실행이 되어야합니다. 하지만 테스트 코드가 정상적으로 처리가 되지 않은 이유는 쿼리가 실행이 되지 않기 때문이었습니다.@Transactional
을 적용하므로서 커밋의 실행 시점이 테스트 코드가 끝나는 시점으로 변경이 되었고 서비스 로직의 save() 메서드의 저장 쿼리는 테스트 코드가 끝나는 이후에 진행되게 됩니다.
그래서 테스트의 API 요청을 처리할 때, 데이터 베이스로 부터 username의 유니크한지를 판별하지 못하게 되었던 것입니다.
해결
- 해결 방법으로는 @Transactional을 통해서 테스트 케이스의 데이터를 초기화하지 않고 @AfterEach를 통해서 직접 테스트에 사용한 데이터를 삭제하는 방식을 사용하는 방식이 있습니다.
- 다른 방식은 unique 속성을 통해서 중복된 필드 값을 확인하는 것이 아니라 검증 로직을 분리하여 코드를 작성하는 방법을 사용하는 것입니다.
주의점
Spring data JPA
를 이용해서 쉽게 데이터 베이스를 다룰 수 있지만 그 속에 담겨진 동작 방식을 이해하는 것이 필요하다는 것을 느낄 수 있었던 오류였습니다.
@Transactional
을 테스트 코드에서 사용하게 되면 편리하게 테스트에서 입력한 데이터의 사이드 이펙트를 제거할 수 있지만 커밋 시점을 테스트 코드가 완료된 이후로 설정하기 때문에 커밋에 의존적인 기능이 존재한다면 오류의 원인이 될 수 있다는 것을 인지해야 합니다.
'spring' 카테고리의 다른 글
[Spring] RedisCacheManager에 대해서 (1) | 2024.01.13 |
---|---|
스프링 AOP를 적용하는 방법 (1) | 2024.01.10 |
Spring 메일 전송기능 구현하기 (3) | 2023.12.01 |
커스텀 Validation annotation 만들기 (1) | 2023.11.13 |
Springdoc을 이용해서 Swagger API 만들기 (0) | 2023.11.03 |