더 테스트하기 쉬운 코드 (아키텍처 개선 경험)
회사에서 테스트코드를 작성하다 굉장한 피로감을 느끼고 아키텍처 개선으로 더 테스트하기 쉬운 구조로 개선한 경험에 대한 포스트입니다.
문제점
문제를 인지한것은 테스트코드를 도입하며 테스트코드를 짜며 기존 코드들을 개선해 나가던 중
단위테스트를 작성 할려 했지만 완성된 테스트를 보면 단위테스트보다는 통합테스트에 더 가까운 모습으로 보였다.
가장 간단해야할 테스트인 단위테스트를 하기위해 DB모킹을 해서 수많은 데이터를 셋팅하고 그 데이터를 셋팅한거에따라 결과를 예측해야합니다.
무엇보다 테스트의 실패시 내가 데이터를 잘못넣은건지 실제로 구현로직이 잘못된건지 구분할수없어 확인해야합니다.
아키텍처 개선
// 서비스계층에서 DB를 관리하는 계층을 직접 주입받는경우
public class MemberService {
@Autowired
MemberMapper memberMapper;
// 서비스에서 마이바티스에 종속되는 맵퍼를 의존하는 형태 또는
// jpa를 사용하는 repository를 직접 의존하는 경우
}
만약 여러분의 코드에서 다음과 같은 모습을 보이고 해당 코드에 테스트코드를 붙히거나 혹은 해당형태로 개발을 하면서 테스트코드를 작성을 한다면 위에서 말한 문제점을 겪게 될 확률이 매우 높다.
이런 경우 DB계층에서 DTO나 VO를 받아 서비스 계층에서 작업한다. 자연스럽게 모든 계산 및 비즈니스로직은 서비스계층에서 처리된다.
다음은 서비스 계층에서 모든 과정을 처리하는 서비스 메서드를 테스트하는 코드이다.
@DisplayName("패키지 해지 테스트")
@Test
void terminationTest() throws Exception {
//given
List<MembershipVO> membershipList = getMembershipList();
List<ClientMembershipDTO> devData = getDevClientMembershipData();
given(clientMemberShipMapper.getMembership())
.willReturn(membershipList);
given(clientMemberShipMapper.selectPaymentTarget(any()))
.willReturn(devData);
ArgumentCaptor<ClientMembershipDTO> param = ArgumentCaptor.forClass(ClientMembershipDTO.class);
//when
BatchSubscriptionService batchSubscriptionService = new BatchSubscriptionService(clientMemberShipMapper,paymentService, messageService);
assertAll(
() -> assertDoesNotThrow(batchSubscriptionService::batchPayment)
);
//then
verify(paymentService, times(0)).paymentProcess(any(),any());
verify(paymentService, times(0)).refundPackage(any(),any(),any());
verify(clientMemberShipMapper, times(1)).updateClientMembership(param.capture());
assertEquals(MembershipCode.FREE.getMembershipCode(),param.getValue().getMembership_code());
}
아키텍처적으로 객체지향적이지 않은 문제와 SRP원칙을 지키지 않아서 생긴 대참사의 모습이다… 모든 단위테스트를 이렇게 해야한다면 테스트에 대한 피로도가 너무 증가한다.
개선안
- domain 계층의 신설
- DB계층 고립화 및 domain에 대한 접근은 DB계층으로만 가능
예시 패키지 구조
-domain
- Member.Class
-repository
- MemberRepository // 최상위 인터페이스
- MemberJpaRepository
- MemberMapper // jpa의 엔티티를 domain으로 변경하요 맵핑하는 클래스
- MemberRdmsRepository // jpa나 쿼리Dsl을 조합하여 설제 db접근을 하는 구현클래스
- MemberEntity // jpa용 엔티티
- domain.Member는 setter가 없어야한다.
- domain.Member 에 대한 접근은 DB계층을 통해서만 가능해야하고 Repository는 항상 domain을 리턴해야합니다.
- DB에 대한 접근도 domain을 repository계층에 넘기는걸로 시작해야한다.
- 모든 계산 및 변경은 domain에서 처리합니다.
- 서비스 계층은 이제 MemberRepository같은 db의 최상위 인터페이스만 의존합니다.
개선으로 얻는 장점
- 단위테스트가 쉬워집니다.
- 단위테스트의 주 대상이 서비스에서 도메인 테스트로 변경됩니다. 도메인에서 처리하는 가장 작은부분들만 따로 처리하면 됩니다. 테스트할때 필요한 데이터가 적어집니다. 해당 메서드의 동작이 필요한 데이터가 아니면 셋팅할 이유가 없습니다.
- 서비스 레이어에서는 도메인이 계산 처리하는 메서드를 묶고 트랜잭션을 관리하는 역할을 주로 맡게됩니다. 이 효과로 서비스 레이어의 테스트는 이제 통합테스트의 성격을 띄게 됩니다. (물론 서비스테스트도 단위테스트 작성이 필요합니다.)
개선후 테스트코드의 모습
기존 코드 예제
요구사항 : 멤버십 리셋일자에 유료회원이면 결제를 시키고 무료회원이라면 결제를 하지않는다.
public void changeMembership(MemberDTO member){
if(member.getStatus().equals("FREE")){
// 무료회 멤버십 충전
}else{
// 결제후 멤버십 충전
}
}
개선후
// 리펙토링 예제
// 서비스 로직
public void changeMembership(Member member){
if(member.isFreeUser()){
// 무료회 멤버십 충전
}else{
// 결제후 멤버십 충전
}
}
public class Member(){
private String status;
public boolean isFreeUser(){
return member.getStatus().equals("FREE");
}
}
member 도메인에서 조건문을 직접 처리합니다.
도메인테스트에서 이제 조건문에 대한 상세한 테스트를 더 적은량의 데이터 셋팅만으로 가능합니다.
서비스의 경우도 이제 도메인과 다른 서비스를 묶는 형태를 보여주며 단위테스트가 훨씬 간단해집니다.
// 도메인 테스트 예제
@Test
void freeChangeMembership() throws Exception{
//given
Member member = Member.builder()
.stauts("FREE")
.build();
//when
boolean isFreeUser = member.isFreeUser();
//then
assertThat(isFree).isTrue();
}
서비스계층은 이제 도메인의 행동을 묶고 결합시키는 행위를 합니다.
단위테스트는 이제 이름에 걸맞게 가장 작은 단위로 쪼개져 테스트하기만 하면됩니다.
아마 단위테스트는 주로 도메인에서 담당하게 될것이고 통합테스트는 서비스테스트,컨트롤러테스트에서 더 많이 담당하게 하는 구조로 가능합니다.
구조를 변경하고 느낀점은 기존에 테스트코드작성과 달라지니 테스트에 대한 피로도가 굉장히 감소하였습니다.
자연스럽게 더 TDD에 가깝게 코딩스타일을 적응 할 수 있엇습니다.
댓글남기기