🌱 들어가기 전
이번 포스팅에서는, 테스트에서 집중해야 하는 부분과 테스트 리팩토링 사항에 대해 살펴보자.
- 김우근님의 'Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트'를 공부하고 정리한 글입니다. ✏️
지난 포스팅과 이어집니다 !
https://deeper-dev.tistory.com/19
[테스트 코드와 설계] 레이어 아키텍처 개선하기 - SOLID를 준수하고 Testability 높이기
🌱 들어가기 전이번 포스팅에서는 레이어 아키텍처의 문제점을 알아보고, SOLID 준수하고 Testability를 높이는 방향으로의 아키텍처 개선 과정을 살펴보자. - 김우근님의 'Java/Spring 테스트를 추가
deeper-dev.tistory.com
🌱 테스트 범위
💭 집중해야 하는 테스트
집중해야 하는 테스트는 ServiceImpl, Domain이다.
💭 Controller, RepositlryImpl은 왜 테스트 범위로 잡지 않을까?
프레임워크와 라이브러리들이 알아서 잘해주고 있을 것이다.
- Controller: 역할이 매우 단순하다.
1. 핸들러가 Request Body를 받아서
2. Service 호출
3. 응답 내려준다.
몇 가지 처리 과정이 있을 수 있지만, 주 역할은 요청을 받아 응답을 내리는 것이다. 그리고 이런 부분은 Spring팀에서 알아서 잘 테스트하고 있을 것이다.
- RepositoryImpl: 엔티티를 통해 데이터를 CRUD하는 것은 JPA, Hibernate팀에서 훨씬 잘해주고 있을 것이다.
그럼 커버리지가 낮아지지 않을까?
💭 집중해야 하는 테스트는 본질이다.
Domain이 애플리케이션의 핵심이다.
ServiceImpl와 Domain이 잘 만들어져있어야 애플리케이션 내적 품질이 향상된다. 여기를 테스트하는 것이 전체 애플리케이션을 테스트하기 위한 최소 조건인 것이다.
🌱 낮은 커버리지 걱정
- 걱정: Jpa, Spring쪽 테스트를 하지 않으면 커버리지가 너무 낮게 나온다
↓
- 도메인: 도메인이 그만큼 빈약하다는 의미이다. 즉, CRUD를 제외하면 도메인이 없다는 의미이다.
↓
- 경쟁력: 서비스의 경쟁력을 의심해봐야 한다.
🌱 실습시 주의사항 (리팩토링하는 테스트)
💭 의존성 역전 원리를 이용하여 외부를 다룬다.
class PostRepositoryImpl implements PostRepository {
private final PostJpaRepository postJpaRepository; // 구현체는 JpaRepository를 멤버변수로 들고있다.
@Override
public Post getById(long id) {
return postJpaRepository.findById(id) // JpaRepository를 호출하여 영속성 객체를 받는다.
.orElseThrow(NotFoundException::new)
.toDomain(); // 호출 결과를 Domain 엔티티로 변환하여 Service로 내려준다.
}
// ...
}
이 원리는 모든 외부 호출에 똑같이 적용된다.
- 의존성 역전: 의존 관계를 약화시킨다.
↓
- 독립: 호출체와 구현체를 독립적으로 구성한다.
↓
- 테스트 가능성: Testable
💭 필요한 경우에는 mock으로 치환하여 테스트한다.
이런식으로 처리하여 일관된 테스트를 얻을 수 있도록 한다.
따라서, mokito같은 프레임워크나 H2를 사용하지 않는다.
같은 기능을 어떻게 테스트 하냐에 따라, 테스트의 크기가 결정된다.
이전에 H2를 이용하여 테스트하는 것은 중형 테스트였다. 이를 앞에서 살펴본 의존성 역전을 활용하여 Fake로 대체하여 테스트 할 수 있다.
그럼 중형 테스트였던 것이 소형 테스트로 변신하는 것이다. (소형 테스트가 전체의 80%가 되도록, 대부분을 소형 테스트로 구성하는게 좋다.)
즉, 서비스 구현체마저도 소형테스트로 만들 수 있다!
💭 리팩토링a. 패키지 관리
가장먼저 패키지 구조부터 개선한다.
레이어 아키텍처에 따라서 레이어 하나에 모든 컴포넌트를 몰아넣고 있다.
장점
- 구조가 단순하고 사용하기 편하다.
단점
- 도메인이 눈에 보이지 않는다.
- 동시 작업이 불가능하다.
- 의존성이 관리되지 않는다.
도메인을 드러내기 위해 도메인을 패키지 제일 바깥으로 빼자. 그 아래 하위 구조는 이전과 똑같이 유지한다.
장점
- 어떤 도메인을 다루는 시스템인지 눈에 보인다. ex) user를 관리하고, post를 관리하는 서비스구나.
- 필요에 따라 MSA로 시스템 확장이 가능하다. 즉 시대 요구에 발맞추어 필요에 따라 도메인을 분리하여 y축 확장이 가능해진다.
ex) user만 떼어내서 별도의 시스템으로 확장할 수 있다.
프로젝트 구조 Before
프로젝트 구조 After
참고로, repository였던 패키지 이름을 infrastructure로 수정한다.
외부 연동도 의존성 역전의 원리에 따라 구조화 될건데, 외부 연동이 repository에 들어가기 애매해지기 때문이다.
💭 리팩토링a. 패키지 관리 + 의존성 역전
Repository 의존성 역전
Service 의존성 역전
💭 리팩토링b. 패키지 의존성 : 순환 참조가 생기는지 의식하며 개발해야 한다.
게시글의 작성자 조회기능이 있다고 생각해보자. post가 user를 의존할 것이다.
그런데 여기서 user가 post를 의존하면? 순환참조가 발생한다.
- 순환참조: 컴포넌트나 클래스, 넓게는 패키지까지도 참조관계가 순환하는 그림이다. 이는 소프트웨어 공학에서 피해야하는 해악이다!
패키지 의존성까지도 한 번씩 확인하며 개발하자.
💭 리팩토링c. Jpa 엔티티와 도메인 모델을 분리한다.
엔티티인 이유 = RDB의 '엔티티'이기 때문이다.
만약 mongoDB를 사용했다면, UserDocument로 지었을 것이다.
💭 리팩토링d. setter를 없애고 domain / vo으로 로직을 이동시킨다.
이런 서비스에 있던 로직들을 가능하면 도메인 모델로 몰아넣는다.
그리고 도메인 모델을 테스트한다. 이렇게하면 테스트가 쉬워진다.
💭 리팩토링e. CQRS(Command and Query Responsibility Segregation).
명령과 질의의 책임을 분리한다. : 메소드를 명령과 질의로 나누자. (더 넓게는 클래스까지도 나누자)
명령(Command)
객체의 상태를 바꾸는 메소드 / 일을 시키는 메소드
특징
명령 메소드는 void타입이어야한다. 즉, 메소드 호출로 객체의 상태를 변경하면 return 타입이 void가 되어야한다.- 편의상 명령 메소드가 종종 return this하는 경우도 있는데, 이렇게 해서도 안된다.
질의(Query)
상태를 물어보는 메소드
특징
질의 메소드는 상태를 변경해서는 안된다. 즉 return 타입이 void가 아니라면 객체의 상태를 변경해서는 안된다.
"하나의 메소드는 명령이나 쿼리여야하며, 두 가지 기능을 모두 가져서는 안된다. 명령은 객체의 상태를 변경할 수 있지만 값을 반환하지 않는다. 쿼리는 값을 반환하지만 객체를 변경하지 않는다."
Repository 대신 Reader/Writer
Repository에 CQRS를 적용해서 reader와 writer로 구분하고 readonly 객체와 editable 객체로 분리한다.
Repository | Object | Type |
UserReader | UserReadonly | VO |
UserWriter | UserEditable | Editable object |
'Back-End > Test Code' 카테고리의 다른 글
[테스트 코드와 설계] 레이어 아키텍처 개선하기 - SOLID를 준수하고 Testability 높이기 (0) | 2024.09.24 |
---|---|
[트러블 슈팅 - 테스트 코드] 테스트 데이터 삽입 시 발생하는 auto_increment 컬럼값 충돌 문제 해결 (0) | 2024.09.09 |
[트러블 슈팅 - 테스트 코드] 테스트 간 데이터 간섭과 트랜잭션 문제 해결 - @SpringBootTest 테스트 격리시키기 (2) | 2024.09.08 |
[테스트 코드와 설계] 프로젝트 Test Code 작성과 문제 해결 (0) | 2024.09.04 |
[테스트 코드와 설계] 빌더 패턴과 엔티티, 그리고 테스트에 대한 조언 (0) | 2024.09.04 |