10. 아키텍처 경계 강제하기
일정 규모 이상의 프로젝트라면 계층 간의 경계가 약화되고, 코드는 점점 테스트하기 어려워질 것이다.
아키텍처의 경계를 강제한다는 것은 각 계층간의 의존성이 올바른 방향을 향하도록 강제하는 것을 말한다. 바깥쪽에서 안쪽으로, 어댑터 → 애플리케이션 → 도메인 으로 의존성이 흘러가야 한다.
의존성 규칙을 강제하는 가장 기본적인 방법은 접근 제한자를 사용하는 것이다.
public, private, protected는 많이 알고 있지만 default에 대해서 깊게 생각하는 신입 개발자는 많지 않다.
자바 패키지를 통해 클래스들을 응집적인 모듈로 만들어주기 때문에 default 제한자는 중요하다.
모듈 내의 클래스들은 서로 접근이 가능하지만 패키지 바깥에선 접근할 수 없다.
그럼 모듈의 진입점으로 활용할 클래스들만 골라서 public으로 만들어준다면 의존성이 잘못된 방향을 가리키게 될 확률이 줄어든다.
각 계층의 어댑터, 서비스 클래스는 외부에서 접근할 필요가 없으니 default 제한자로 생성하고 다른 계층에서 접근해야 하는 domain 패키지는 public으로 만들 수 있다.
마찬가지로 웹어댑터와 영속성 어댑터에서 접근 가능해야 하는 애플리케이션 계층의 인커밍,아웃고잉 포트 역시 public으로 구현해야 한다.
default 제한자는 몇 개 정도의 클래스로만 이뤄진 작은 모듈에서 가장 효과적이다.
패키지 내 클래스가 일정 수준을 넘어서게 되면 보통 하위 패키지를 만드는 방법을 선호하는데 이렇게 하면 자바는 하위 패키지를 다른 패키지로 취급하여 하위 패키지의 default 멤버에 접근할 수 없게 된다.
이렇게 되면 하위 패키지의 멤버는 public으로 만들어서 바깥으로 노출시켜야 하는데 의존성 규칙이 깨질 수 있는 환경이 될 수 있다.
클래스에 public 제한자를 사용하면 아키텍처의 의존성 방향이 잘못되더라도 컴파일러에서 이를 확인할 수 없다.
이때 컴파일 후 런타임에서 체크하는 방법을 도입할 수 있는데 ArchUnit은 Junit과 같은 단위 테스트 프레임워크 기반에서 아키텍처의 의존성 규칙 위반을 발견하면 예외를 던질 수 있는 API를 제공한다.
ArchUnit API를 이용하면 헥사고날 아키텍처 내에서 관련된 모든 패키지를 명시할 수 있는 일종의 도메인 특화 언어(DSL)을 만들 수 있고 패키지 사이의 의존성 방향이 올바른지 자동으로 체크할 수 있다.
네이버에서 유닛테스트에 해당 라이브러리를 도입한 사례는 나중에 참고해보자 https://d2.naver.com/helloworld/9222129
Maven이나 Gradle은 자바에서 많이 사용하는 빌드 도구인데 주요한 기능 중 하나는 의존성 해결이다.
잘못된 의존성을 막기 위해 아키텍처를 여러 개의 빌드 아티팩트로 만들 수 있다.
각 모듈 혹은 계층에 대해 전용 코드베이스와 빌드 아티팩트로 분리된 빌드 모듈(JAR 파일)을 만들고 각 모듈의 빌트 스크립트에서는 아키텍처에서 허용하는 의존성만 지정하게 되면 클래스들이 클래스패스에 존재하지도 않아 컴파일 에러가 발생하여 잘못된 의존성을 만들수조차 없게 될 것이다.
또한 빌드 모듈로 아키텍처 경계를 구분하는 것을 패키지로 구분하는 방식에 비해 몇가지 장점이 더 있는데
- 빌드 도구는 순환 의존성(circular dependency)를 허용하지 않는다. 의존성을 해결하는 과정에서 무한 루프에 빠지기 때문에 빌드 도구를 사용하면 순환의존성이 없음을 확신할 수 있다.
- 빌드 모듈 방식은 다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한 채로 변경할 수 있다.
특정 어댑터에서 컴파일 에러가 발생하는 애플리케이션 계층을 리팩토링 할 경우 같은 빌드 모듈에 있다면 컴파일 에러를 고쳐야 애플리케이션 계층의 테스트가 실행되겠지만 서로 다른 빌드 모듈에 독립적으로 존재할 경우 어댑터의 컴파일 에러와 상관없이 애플리케이션 계층을 테스트할 수 있을 것이다. - 모듈 간 의존성이 빌드 스크립트에 분명하게 선언돼 있기 때문에 새로운 의존성을 추가할 때 의식적으로 한번 더 생각해볼 수 있다.