경제를 강제한다: 의존성이 올바른 방향을 향하도록 강제하는 것
자바 패키지를 통해 클래스들을 응집적인 모듈로 만들 수 있는데, 이러한 모듈 내에 있는 클래스들은 package-private
으로 설정하면 서로 접근 가능하고 바깥에서 접근할 수 없다. (의존성 주입 메커니즘은 리플렉션을 사용하므로 여전히 인스턴스를 만들 수 있음) 특정 클래스를 public
으로 만들면 진입점으로 사용할 수 있다.
persistence 패키지의 클래스는 외부에서 접근할 필요가 없기에 (포트 타야지!) package-private으로 만들 수 있다.
application 패키지의 port들은 web, persistence에서 접근해야 하므로 public이고, 구현체인 SendMoneyService는 package-private으로 만들 수 있다.
@Test
void validateRegistrationContextArchitecture() {
HexagonalArchitecture.boundedContext("io.reflectoring.buckpal.account")
.withDomainLayer("domain")
.withAdaptersLayer("adapter")
.incoming("in.web")
.outgoing("out.persistence")
.and()
.withApplicationLayer("application")
.services("service")
.incomingPorts("port.in")
.outgoingPorts("port.out")
.and()
.withConfiguration("configuration")
.check(new ClassFileImporter()
.importPackages("io.reflectoring.buckpal.."));
}
ArchUnit
을 이용해 DSL로 표현할 수 있고, 런타임에 의존성 방향을 체크할 수 있다.
패키지가 아닌 빌드 아티팩트를 이용해 각 모듈/계층에 대해 분리된 빌드 모듈을 만들어, 계층 간의 의존성을 강제할 수 있다.
이 방식의 장점은 다음과 같다.
순환 의존성을 막는다.
순환 의존성은 하나의 모듈에서 일어나는 변경이 순환 고리에 포함된 다른 모든 모듈을 변경하게 만들며, 단일 책임 원칙을 위배한다.
다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한 채로 변경할 수 있다.
모듈 간 의존성이 빌드 스크립트에 선언되어 있어, 새로 의존성을 추가하는 일은 의식적인 행동이 된다.
설정 -> 어댑터 -> 애플리케이션
어댑터 모듈은 영속성 어댑터, 웹 어댑터를 포함하고 있다. (즉 둘은 의존성을 막지 않는다는 것) 영속성 계층과 웹 계층이 서로 영향을 미치는 것을 바라진 않으므로 어댑터끼리는 분리하는 것이 좋다.
애플리케이션 모듈이 포트, 엔티티, 서비스 모두를 포함한다.
매핑하지 않기 전략을 허용하지 않는다면, 포트 인터페이스만 포함하는 API 모듈을 분리해서 빼낼 수 있다. 어댑터/애플리케이션 모듈은 API 모듈에 접근할 수 있지만 반대는 불가능하다.
in/ out 포트를 분리하고, 서비스/엔티티도 분리한다.
아키텍처를 잘 유지해나가고 싶다면 의존성이 올바른 방향을 가리키는지 확인할 필요가 있다. 새로운 코드를 추가하거나 리팩터링할 때 패키지 구조는 항상 염두에 두고, 가능하다면 package-private 가시성을 이용해 바깥에서 접근하면 안되는 클래스에 대한 의존성을 피해야 한다.
아키텍처가 충분히 안정적이라면, 독립적인 빌드 모듈로 추출할 수 있다.