만들면서 배우는 클린 아키텍처 06-07

inseo24·2024년 7월 9일

clean-architecture

목록 보기
3/5

06 영속성 어댑터 구현하기

의존성 역전

  • application.서비스 → port.out ← persistence adapter
  • 영속성 어댑터는 애플리케이션에 의해 호출될 뿐, 애플리케이션을 호출하지 않는다.
    • 포트 계층은 영속성 계층에 대한 코드 의존성을 없애기 위한 간접 계층
    • 포트가 계약을 만족하는 한, 코어에 영향을 미치지 않으면서 영속성 코드를 마음껏 수정할 수 있다.

영속성 어댑터의 책임

  1. 입력을 받는다.
  2. 입력을 데이터베이스 포맷으로 매핑한다.
  3. 입력을 데이터베이스로 보낸다.
  4. 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다.
  5. 출력을 반환한다.

포트 인터페이스 나누기

  • 인터페이스 분리 원칙(ISP)에 따라 클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야 한다.
  • ex) SendMoneyService → LoadAccountPort / RegisterAccountService → CreateAccountPort
    ㄴ→ UpdateAccountPort
  • 각 서비스는 실제로 필요한 메서드에만 의존한다. ← 이렇게 좁은 포트를 만드는 것은 코딩을 plug-and-play 경험으로 만든다.
    • 서비스 코드를 짤 때는 필요한 포트에 그저 ‘꽂기만’ 하면 된다.

영속성 어댑터 나누기

  • 도메인 코드는 영속성 포트에 의해 정의된 명세를 어떤 클래스가 충족시키는지 관심이 없다. 모든 포트가 구현되어 있기만 한다면 영속성 계층에서 하고 싶은 어떤 작업이든 해도 된다.
  • DDD에 따르면 애그리거트당 하나의 영속성 어댑터를 만들어도 된다. 그건 선택사항.

데이터베이스 트랜잭션은 어떻게 해야할까?

  • 트랜잭션은 하나의 특정한 유스케이스에 대해 일어나는 모든 쓰기 작업에 걸쳐 있어야 한다. 그래야 그중 하나라도 실패할 경우 다 같이 롤백될 수 있기 때문이다.
  • 영속성 어댑터는 어떤 데이터베이스 연산이 같은 유스케이스에 포함되는지 알지 못하기 때문에 언제 트랜잭션을 열고 닫을지 결정할 수 없다. 이 책임은 영속성 어댑터 호출을 관장하는 서비스에 위임해야 한다.
  • 자바/스프링에서 가장 간단한 방법은 @Transactional + public
    • 아니면 AspectJ를 이용해 AOP로 트랜잭션 경계를 코드에 위빙(weaving) 할 수 있다.

07 아키텍처 요소 테스트하기

테스트 피라미드

  • 테스트 피라미드에 따르면 비용이 많이 드는 테스트는 지양하고 비용이 적게 드는 테스트를 많이 만들어야 한다.
  • 테스트의 기본 전제는 만드는 비용이 적고, 유지보수하기 쉽고, 빨리 실행되고, 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다는 것이다.
    • 이 테스트는 하나의 ‘단위’가 제대로 동작하는지 확인할 수 있는 단위 테스트들이다.
  • ‘단위 테스트’, ‘통합 테스트’, ‘시스템 테스트’의 정의는 맥락에 따라 다르다. 프로젝트마다 다른 의미를 가질 수 있다. 아래 정의는 이번 장에서만 사용하는 의미다.
    • 단위 테스트: 일반적으로 하나의 클래스를 인스턴스화하고 해당 클래스의 인터페이스를 통해 기능들을 테스트한다.
      • 의존하는 클래스들은 인스턴스화하지 않고 mocking
    • 통합 테스트: 연결된 여러 유닛을 인스턴스화하고 시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후 유닛들의 네트워크가 기대한대로 잘 동작하는지 검증
    • 시스템 테스트: 애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는지 검증, ex) UI를 포함한 end-to-end 테스트

단위 테스트로 유스케이스 테스트하기

  • given/when/then(BDD) 활용
    • given: 인스턴스 생성 및 적절한 상태로 세팅
    • when: 유스케이스 실행을 위한 메서드 호출
    • then: 예상 결과 및 호출 검증
  • 테스트 중인 유스케이스 서비스는 상태가 없으므로 then 에서 상태를 검증할 수 없다. 대신 테스트는 서비스가 (모킹된) 의존 대상의 특정 메서드를 호출했는지 검증하는 식으로 이뤄진다. 이는 테스트가 코드의 행동 변경뿐만 아니라 코드의 구조 변경에도 취약해진다는 의미가 된다. 코드가 리팩터링되면 테스트도 변경할 확률이 높아진다.
  • 테스트에서 어떤 상호작용을 검증하고 싶은지 신중하게 생각해야 한다.
    모든 동작을 검증하는 대신 중요한 핵심만 골라 집중해서 테스트하는 것이 좋다.
    - 만약 모든 동작을 검증하려고 하면 클래스가 조금이라도 바뀔 때마다 테스트를 변경해야 한다.

통합 테스트로 웹 어댑터 테스트하기

  • @WebMvcTest 가 포함되면 통합 테스트인가 -ㅅ-
    • 스프링이 특정 요청 경로, 자바와 JSON 간의 매핑, HTTP 입력 검증 등에 필요한 전체 객체 네트워크를 인스턴스화하도록 만듦
    • 테스트에서는 웹 컨트롤러가 이 네트워크의 일부로서 잘 동작하는지 검증한다.
  • 웹 컨트롤러가 스프링 프레임워크에 강하게 묶여 있기 때문에 격리된 상태로 테스트하기보다는 이 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.

통합 테스트로 영속성 어댑터 테스트하기

  • 비슷한 이유로 영속성 어댑터의 테스트도 단위 테스트보다 통합 테스트를 적용하는 것이 합리적이다. 단순히 어댑터 로직만 검증하는 것이 아니라 데이터베이스 매핑도 검증하고 싶기 때문이다.
    • @DataJpaTest 활용
  • 이 테스트에선 데이터베이스를 모킹하지 않는다. 테스트가 실제로 데이터베이스에 접근한다.
    • DB마다 고유 SQL 문법이 있어서 문제가 생길 가능성이 크다.
  • 영속성 어댑터 테스트는 실제 데이터베이스를 대상으로 진행해야 한다. Testcontainers 같은 라이브러리는 필요한 데이터베이스를 도커 컨테이너에 띄울 수 있어 유용하다.

시스템 테스트로 주요 경로 테스트하기

  • @SpringBootTest
  • 전체 애플리케이션을 띄우고 API를 통해 요청을 보내고, 모든 계층이 정상 작동 하는지 검증
  • 시스템 테스트라 하더라도 언제나 서드파티 시스템을 실행해서 테스트할 수 있는 것은 아니므로 모킹을 해야 할 때도 있음 ← 출력 포트 모킹해서 사용
  • 단위 테스트나 통합 테스트만으로 알기 어려운 계층 간 매핑 버그 등을 확인 가능
  • 시스템 테스트는 여러 개의 유스케이스를 결합해 시나리오를 만들 때 더 효과적이다.
    • 각 시나리오는 사용자가 애플리케이션을 사용하면서 거쳐갈 특정 경로를 의미한다.
    • 시스템 테스트를 통해 중요한 시나리오들이 커버된다면 최신 변경사항들이 애플리케이션을 망가뜨리지 않았음을 가정할 수 있고, 배포될 준비가 됐다는 확신을 할 수 있다.

얼마만큼의 테스트가 충분할까?

  • 얼마나 마음 편하게 소프트웨어를 배포할 수 있느냐를 테스트의 성공 기준으로 삼으면 된다고 생각한다. 더 자주 배포할수록 테스트를 더 신뢰할 수 있다.
  • 프로덕션의 버그를 수정하고 이로부터 배우는 것을 우선순위로 삼으면 제대로 가고 있는 것이다.
  • 헥사고날 아키텍처에서 사용하는 전략
    • 도메인 엔티티를 구현할 때는 단위 테스트로 커버
    • 유스케이스를 구현할 때는 단위 테스트로 커버
    • 어댑터를 구현할 때는 통합 테스트로 커버
    • 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버
  • 새로운 필드를 추가할 때마다 테스트를 고치는데 한 시간을 써야 한다면 뭔가 잘못된 것이다.
    • 테스트가 코드의 구조적 변경에 너무 취약할 것이므로 어떻게 개선할지 살펴봐야 한다.
  • 리팩토링할 때마다 테스트 코드도 변경해야 한다면 테스트는 테스트로서의 가치를 잃는다.
profile
나 개발자

0개의 댓글