[클린 아키텍처] Ch 10. 아키텍처 경계 강제하기

정미·2022년 5월 19일
0

아키텍처: 코드를 어떻게 작성하고 어디에 위치시켜야 하는가

일정 규모 이상이 되면 아키텍처는 서서히 무너지고, 계층 간 경계가 약화되고, 테스트하기 어려워지고, 새로운 기능을 구현하는 데 더 많은 시간이 든다.

아키텍처 내의 경계를 강제하는 방법

  1. 접근 제한자
  2. 컴파일 후 체크
  3. 빌드 아티팩트

10-1. 경계와 의존성

아키텍처 경계를 강제한다

= 의존성이 올바른(안쪽) 방향을 향하도록 강제한다.

= 점선(허용되지 않은 의존성)을 없앤다.

10-2. 접근 제한자(Visibility Modifier)

자바에서 제공하는 가장 기보적인 도구

  1. public
  2. protected
  3. private
  4. package-private (default)

package-private

  • 자바 패키지를 통해 클래스들을 응집적인 ‘모듈’로 만들어준다.
    • 패키지 내에서는 서로 접근 가능하지만, 바깥 패키지에서는 접근 불가능하다.
    • 모듈의 진입점으로 사용할 클래스만 public으로 연다.
  • 9-3장 classpath scanning 방법으로만 애플리케이션 컨텍스트 등록이 가능하다. 9-4장의 Java Config로 해당 클래스들을 bean으로 직접 생성하여 등록하려면 public으로 열어두어야하기 때문.
  • 몇 개 정도의 클래스로만 이루어진 작은 모듈에서 효과적이다. 클래스가 많아지면 하위 패키지를 만든다.
    • 이때 다른 하위 패키지에서는 접근 불가능하게 되어 public으로 열어두어야한다.

패키지 구조

payment
    ㄴ account
        ㄴ domain
            ㄴ + Account
		        ㄴ + Activity
        ㄴ application
            ㄴ o SendMoneyService
            ㄴ port
                ㄴ in
                    ㄴ + SendMoneyUseCase
                ㄴ out
                    ㄴ + LoadAccountPort
                    ㄴ + UpdateAccountStatePort
        ㄴ adapter
            ㄴ in
                ㄴ web
                    ㄴ o AccountController
            ㄴ out
                ㄴ persistence
                    ㄴ o AccountPersistenceAdapter
                    ㄴ o SpringDataAccountRepository
  • o : package-private, 포트를 통해 제공되기 때문에 외부에서 접근할 필요가 없다.
    • reflection을 이용한 의존성 주입 메커니즘
  • + : public, 다른 계층에서 domain 패키지 접근, web/persistence 어댑터에서 application 계층 접근

10-3. 컴파일 후 체크(Post-Compile Check)

코드가 컴파일된 후 런타임에 체크한다.

지속적인 통합 빌드 환경에서 자동화된 테스트 과정에서 가장 잘 동작한다.

ArchUnit

  • 런타임 체크를 도와주는 자바용 도구
  • JUnit 같은 단위 테스트 프레임워크 기반에서 가장 잘 동작한다.
  • 의존성 방향이 기대한 대로 잘 설정되어 있는지 체크할 수 있는 API 제공
    public class DependencyRuleTests {
        @Test
        void domainLayerDoesNotDependOnApplicationLayer() {
    						noClasses()
                    .that()
                    .resideInAPackage("com.woowa.cleanarchitecture.account.domain..")
                    .should()
                    .dependOnClassesThat()
                    .resideInAnyPackage("com.woowa.cleanarchitecture.account.application..")
                    .check(new ClassFileImporter()
                            .importPackages("com.woowa.cleanarchitecture.account.."));
        }
    }
  • 육각형 아키텍처 내 관련 모든 패키지를 명시할 수 있는 일종의 도메인 특화 언어(Domain specific language, DSL)를 만들 수 있다.

단점

  • 실패에 안전(fail-safe)하지 않다
    • 패키지 이름에 오타를 내면 어떤 클래스도 찾지 못하기 때문에 잘못된 의존성을 찾지 못한다
    • 같은 맥락에서 리팩토링에 취약하다.
    • 클래스를 하나도 찾지 못했을 때 실패하는 테스트를 추가해야 한다.

10-4. 빌드 아티팩트

패키지로 아키텍처 경계를 구분하던 이제까지는 모든 코드가 monolithic(하나로 된 거대한) build artifact의 일부였다.

용어

  • 빌드: 빌드 도구를 호출해서 코드 컴파일, 테스트, 하나의 JAR 파일로 패키징
    • 코드베이스 → 빌드 아티팩트로 변환
  • 빌드 도구: Maven, Gradle, …
  • 빌드 스크립트
  • (빌드) 아티팩트: (아마도 자동화된) 빌드 프로세스의 결과물, 빌드 모듈, JAR 파일

빌드 도구의 의존성 해결(dependency resolution)

코드베이스를 빌드 아티팩트로 변환 전, 이 코드베이스가 의존하고 있는 모든 아티팩트가 사용 가능한지 확인한다. 사용 불가능하다면 아티팩트 리포지토리부터 가져온다. 이것도 실패한다면 컴파일도 전에 에러를 발생시킨다.

의존성(경계) 강제 방법

  1. 각 모듈/계층별로 전용 코드베이스와 빌드 아티팩트를 분리하여 빌드 모듈을 만든다.
  2. 각 모듈의 빌드 스크립트에 허용하는 의존성만 지정한다.


1. 기본적인 3개의 모듈 빌드 방식

  • 설정 모듈은 암시적이고 전이적인 의존성 때문에 애플리케이션 모듈에도 접근 가능하다
  • 어댑터끼리의 의존성도 가능하다. 서드파티 API의 세부사항이 다른 어댑터로 새어나갈 수도 있다.
  1. 어댑터당 하나의 모듈
  2. API 모듈(인커밍, 아웃고잉 포트) 분리
    • 도메인 엔티티가 port에서 전송 객체(transfer object)로 사용되지 않는 경우만(완전 매핑)
      • api -/> application 모듈 접근 불가능
    • 의존성 역전 원칙 적용
    • 어댑터는 직접 엔티티와 서비스에 접근 불가
  3. 인커밍 포트와 아웃고잉 포트 분리
    • 어댑터의 역할을 명확하게 정의한다
  4. 서비스와 도메인 엔티티 모듈 분리

장점 (vs 패키지 구분 방식)

  • 모듈을 세분화할수록 모듈 간 의존성을 더 잘 제어할 수 있다.
  • 빌드 도구는 순환 의존성(circular dependency)을 허용하지 않는다.
    • 자바 컴파일러는 순환 의존성을 신경쓰지 않는다.
  • 특정 모듈의 코드를 격리한 채로 변경 가능하다.
    • a 계층에서 컴파일 에러가 났다. 문제가 생긴 A 클래스와 정상 B 클래스가 둘 다 a 계층에 있을 때 B를 테스트(혹인 빌드)하고 싶다면 A의 에러를 다 고쳐야 한다. 하지만 두 클래스를 다른 계층에 위치시키고 독립된 모듈로 빌드한다면 IDE가 신경쓰지 않을 것이고 마음껏 테스트를 할 수 있다.
  • 모듈 간 의존성이 빌드 스크립트에 분명하게 선언되어 있으므로 의존성을 추가하는 일은 의식적인 행동이 된다.

단점

  • 모듈 간 매핑을 더 많이 수행해야 한다. (8장 매핑하기 전략)
  • 빌드스크립트 유지보수 비용 포함
    • 아키텍처가 안정된 상태여야지 여러 개의 빌드 모듈로 나누기 수월하다.

10-5. 유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

아키텍처를 잘 유지해나가고 싶다면 의존성이 올바른 방향을 가리키는지 지속적으로 확인하자.

  1. package-private 가시성: 패키지 바깥에서접근하면 안 되는 클래스
  2. 패키지 구조가 허용하지 않아 public으로 열어두어야 한다면, ArchUnit(컴파일 후 체크) 도구 이용
  3. 아키텍처가 안정적이게 된다면 독립적인 빌드 모듈로 추출

질문 & 논의할 점

  1. 10-3 ArchUnit 테스트 예제 코드 / HexagonalArchitecture ↔ Adapters/ApplicationLayer 서로를 필드로 가지고 있음
  2. 10-4 빌드 아티팩트 / 빌드 스크립트까지 예시로 줬으면 좋았을 것 같다.
  3. 10-4 빌드 아티팩트 p129 빌드 모듈의 장점2 / 어댑터가 컴파일되지 않더라도 애플리케이션 계층의 테스트를 실행할 수 있다.
    • 빌드 스크립트를 따로 짜서 모듈을 나눠두면 다른 모듈에서 빨간 줄(컴파일 에러)이 떠도 해당 모듈은 테스트 통과가 가능한 건가?

참고

0개의 댓글