인프런 - Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 / 섹션4 강의
레이어드 아키텍쳐에서 (직관적인 형태) 서로 역의존성을 주입한 상태로 바꾸기 위해 개선된 아키텍쳐로 변경하는 작업을 끝내고 -추상화작업- 그에 맞춰 테스트에도 새로운 방식을 적용하게 되었다.
우선 뜯어고치는 단계에 대해 설명해주시는데 그 부분 덕에
깃허브 코드들 종종 보면 그놈의 Impl 클래스가 왤케 많냐~~~~~~ 싶었던 부분이 해소됐다.
이게 의식의 흐름대로 구조를 직관적으로 짜게 되면 나오는 아키텍쳐다.
실제로 내가 첫 프로젝트를 생성할 때도 그렇게 진행을 했었다.
그런데 이처럼 구조를 만들게 되면 테스트를 하게 될 때 DB를 자꾸 연결해서 테스트 할 수 밖에 없고
테스트하고 싶은건 Repository인데 Service를 주입받아야한다거나
그러기 위해서 또 Service구현을 해줘야 한다거나 등등의 번거로운 점이 생긴다.
이렇게 되면 소형테스트가 되지 않는 것이 문제다.
해서 테스트용을 위해 상속되어야 할 로직은 interface로 생성하고
그를 상속한 클래스를 별도로 만들어 사용하는 것이 좋다. 이렇게 되면 역의존성 주입이 가능해 진다.
이는 Repository에도 적용이 된다.
사실 처음엔 이 부분이 상당히 어려웠다. DB없이 테스트를 할 수 있는 구조를 만들기 위해 여러 클래스, 인터페이스를 만들어서 상속에 상속을 꼬리 물어서 실제로 Service단에서 구현할 때도 헷갈렸음ㅠㅠ
Repository의 상속 구조
JpaRepository를 직접적으로 상속하는 UserJpaRepository 인터페이스
- 이는 차후 Jpa를 이용한 것이 아닌 Mybatis 등 다른 DB Connection방식을 쓸 때를 위한(확장성) 절차라고 봐도 좋다.
public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
다른 클래스에서 끌어다 쓸 UserRepository 인터페이스
- 위에서 말한 Jpa가 아닌 Mybatis를 쓰더라도 변경되지 않을 인터페이스다.
public interface UserRepository {
Optional<User> findByIdAndStatus(long id, UserStatus userStatus);
Optional<User> findByEmailAndStatus(String email, UserStatus userStatus);
User save(User user);
Optional<User> findById(long id);
}
UserRepository 인터페이스의 구현부 클래스
- JpaRepository를 쓴다면 UserRepository를 빈등록하여 쓰면 되고,
Mybatis를 쓴다면 그에 맞는 Repository 인터페이스를 빈 등록하여 쓸 수 있다.
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository{
private final UserJpaRepository userJpaRepository;
//UserRepository구현
이런 상속 구조를 가지고 있다.
그리고 이렇게 되면 테스트에서는
@DataJpaTest
@Sql("/sql/user-repository-test-data.sql")
class UserJpaRepositoryTest {
@Autowired
private UserJpaRepository userRepository;
@Test
void findByIdAndStatus_데이터가있으면_가져오는지확인(){
//g
//w
Optional<UserEntity> res = userRepository.findByIdAndStatus(1, UserStatus.ACTIVE);
//t
Assertions.assertThat(res.isPresent()).isTrue();
}
이렇게 UserJpaRepository만 생성해서 제대로 동작하는 지 확인할 수 있다. 근데 솔직한 말로 이건 JPA내에서 쿼리를 알아서 짜주는 거라 테스트가 큰 의미가 있나 라는 생각이 사실 좀 든다 ^^..
그런만큼 이정도까지 상속을 나눌 필요도 있나라는 의구심도 살짝 들고..?
이번 프로젝트 리팩토링 과정 & 테스트 변경작업 중에 겪은 상황
@Test
void userUpdateDto를_이용하여_유저수정(){
UserUpdateDto updateDto = UserUpdateDto.builder()
.address("Incheon")
.nickname("test-I")
.build();
userService.update(3, updateDto);
User userEntity = userService.getById(3);
Assertions.assertThat(userEntity.getId()).isNotNull();
Assertions.assertThat(userEntity.getAddress()).isEqualTo("Incheon");
Assertions.assertThat(userEntity.getNickname()).isEqualTo("test-I");
UserSerivce테스트 중,
업데이트 후 nickname 비교에서 널값이 떨어져서 update수정하는 중에 문제가 생겼나 하고 봤더니
최소 UserSerivce.update()함수에는 문제가 없어보인다.
@Transactional
public User update(long id, UserUpdateDto userUpdateDto) {
User user = getById(id);
user = user.update(userUpdateDto);
user = userRepository.save(user);
return user;
}
그러면 User.update()쪽이 문제가 있어보이는데
public User update(UserUpdateDto userUpdateDto){
return User.builder()
.id(id)
.email(email)
.nickname(userUpdateDto.getNickname())
.address(userUpdateDto.getAddress())
.certificationCode(certificationCode)
.status(status)
.lastLoginAt(lastLoginAt)
.build();
}
여기도 딱히 빠진 요소 없이 잘 들어가 있는 것 같다. 그럼 대체 어디인가..?
차선으로는 userRepository.save(user)여기인가?
그럼 create도 문제가 됐어야 하지 않을까?!
@Override
public User save(User user) {
return userJpaRepository.save(UserEntity.fromModel(user)).toModel();
}
혹시나해서 체크하러 갔다.
너무 단순한 함수구조인데 이제 슬슬 결말에 다 다른 것 같다.
UserEntity.fromMode(user)여기까지 흘러들어갔다.
그리고 여기서 userEntity.nickname = user.getNickname(); 줄이 빠져있는 것을 발견하고 추가했다.
이런 것이 테스트의 진정한 순기능이 아닌가 한다.
물론 사람이 하다보면 실수 하기 마련이라 왜 꼼꼼한 테스트가 중요한지.
잘 짜둔 테스트가 이런 사소한 실수를 잡아낼 수 있는지를 느낄 수 있는 기회였다.