
아키텍처 경계를 강제한다는 것은 의존성이 올바른 방향을 향하도록 강제하는 것을 의미한다. 아키텍처에서 허용되지 않은 의존성을 점선 화살표로 표시했다.
육각형 아키텍처의 요소들이, 포괄적인 클린 아키텍처 방식과 유사한 4개의 계층에 어떻게 흩어져 있는지 보여준다.
가장 안쪽의 계층에는 도메인 엔티티가 있다. 애플리케이션 계층은 애플리케이션 서비스 안에 유스케이스를 구현하기 위해 도메인 엔티티에 접근한다. 어댑터는 인커밍 포트를 통해 서비스에 접근하고, 반대로 서비스는 아웃고잉 포트를 통해 어댑터에 접근한다. 마지막으로 설정 계층은 어댑터와 서비스 객체를 생성할 팩터리를 포함하고 있고, 의존성 주입 메커니즘을 제공한다.
그림에서 아키텍처의 경계는 꽤 명확하다. 각 계층 사이, 안쪽 인접 계층과 바깥쪽 인접 계층 사이에 경계가 있다. 의존성 규칙에 따르면 계층 경계를 넘는 의존성은 항상 안쪽 방향으로 향해야 한다.
package-private 제한자는 자바 패키지를 통해 클래스들을 응집적인 ‘모듈’로 만들어 주기 때문에 중요한 제한자이다. 이런 모듈 내에 있는 클래스들은 서로 접근 가능하지만, 패키지 바깥에서는 접근할 수 없다. 그럼 모듈의 진입점으로 활용될 클래스들만 골라 public으로 만들면 된다. 이렇게 하면 의존성이 잘못된 방향을 가리켜서 의존성 규칙을 위반할 위험이 줄어든다.

접근 제한자가 추가된 패키지 구조
persistence 패키지에 있는 클래스들은 외부에서 접근할 필요가 없기 때문에 package-private(o)으로 만들 수 있다. 영속성 어댑터는 자신이 구현하는 출력 포트를 통해 접근된다. 같은 이유로 SendMoneyService를 package-private으로 만들 수 있다. 의존성 주입 메커니즘은 일반적으로 리플렉션을 이용해 클래스를 인스턴스로 만들기 때문에 package-private이라도 여전히 인스턴스를 만들 수 있다.
이 방법을 스프링에서 사용하려면 클래스패스 스캐닝을 이용해야만 한다. 다른 방법에서는 객체의 인스턴스들을 직접 생성해야 하기 때문에 public 제한자를 이용해야 한다.
나머지 클래스들은 아키텍처의 정의에 의해 public(+)이어야 한다. domain 패키지는 다른 계층에서 접근할 수 있어야 하고, application 계층은 web 어댑터와 persistence 어댑터에서 접근 가능해야 한다.
package-private 제한자는 몇 개 정도의 클래스로만 이뤄진 작은 모듈에서 가장 효과적이다. 그러나 패키지 내의 클래스가 특정 개수를 넘어가기 시작하면 하나의 패키지에 너무 많은 클래스를 포함하는 것이 혼란스러워지게 된다. 이렇게 되면 코드를 쉽게 찾을 수 있도록 하위 패키지를 만드는 방법을 선호한다. 이렇게 하면 자바는 하위 패키지를 다른 패키지로 취급하기 때문에 하위 패키지의 package-private 멤버에 접근할 수 없게 된다. 그래서 하위 패키지의 멤버는 public으로 만들어 바깥에 노출시켜야 하기 대문에 의존성 규칙이 깨질 수 있는 환경이 만들어진다.
클래스에 public 제한자를 쓰면 아키텍처 상의 의존성 방향이 잘못되더라도 컴파일러는 다른 클래스들이 이 클래스를 사용하도록 허용한다. 이런 경우 컴파일러가 전혀 도움이 되지 않기 때문에 의존성 규칙을 위반했는지 확인할 다른 수단을 찾아야 한다.
한 가지 방법은 컴파일 후 체크(post-compile check)를 도입하는 것이다. 다시 말해, 코드가 컴파일된 후에 런타임에 체크한다는 뜻이다. 런타임 체크는 지속적인 통합 빌드 환경에서 자동화된 테스트 과정에서 가장 잘 동작한다.
자바에서는 ArchUnit이라는 도구가 있다. 의존성 방향이 기대한 대로 잘 설정되어 있는지 체크할 수 있는 API를 제공한다. 의존성 규칙 위반을 발견하면 예외를 던진다. ArchUnit은 JUnit 같은 단위 테스트 프레임워크 기반에서 가장 잘 동작하며 의존성 규칙을 위반할 경우 테스트를 실패시킨다.
이전에 정의한 패키지 구조대로 각 계층이 전용 패키지를 가지고 있다고 가정하면 ArchUnit으로 계층 간의 의존성을 체크할 수 있다. 예를 들어, 도메인 계층에서 바깥쪽의 애플리케이션 계층으로 향하는 의존성이 없다는 것을 체크할 수 있다.
class DependencyRuleTests {
@Test
void domainLayerDoesNotDependOnApplicationLayer() {
noClasses()
.that()
.resideInAPackage("buckpal.domain..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("buckpal.application..")
.check(new ClassFileImporter()
.importPackages("buckpal.."));
}
}