헥사고날-아키텍쳐로-구현하는-작은-스프링-부트-토이-프로젝트-우아한스터디-10
만들면서 배우는 클린 아키텍쳐 10장 정리
아키텍처 경계 강제하기
일정 규모 이상의 모든 프로젝트에서는 시간이 지남에 따라 아키텍처가 서서히 무너지기 시작합니다.
이렇게 되면 점점 더 계층 간의 경계가 약화되고, 테스트하기 어려워지고, 새로운 기능을 구현하기 위해 더 많은 시간이 소모됩니다.
이번 장에서는 이를 막기 위해서 아키텍처 내의 경계를 강제하는 방법에 대해 알아봅니다.
경계와 의존성
아키텍처의 경계는 각 계층 사이, 안쪽 입접 계층(포트)과 바깥쪽 인접 계층(어댑터) 사이에 있습니다.
의존성의 규칙에 따르면 계층 경계를 넘는 의존성은 항상 안쪽 방향으로 향하게 해야 합니다.
그래서 이 안쪽 방향의 의존성을 어기는 경우를 강제로 막기 위한 방법에 대해 알아보도록 합니다.
접근 제한자(visibility modifier)
경계를 강제하기 위해 자바에서 제공라는 가장 기본적인 도구가 바로 접근 제한자입니다.
사람들이 public, protected, private 제한자는 잘알고 있지만 package-private(혹은 ‘default’)제한자를 잘모릅니다.
private-package는 자바 패키지를 통해 같은 패키지 내에 있는 클래스들은 서로 접근할 수 있지만 패키지 바깥에서는 접근할 수 없게 됩니다.
이 것을 활용하여 계층의 진입점으로 활용되는 포트 인터페이스의 경우 접근제한자를 public으로 설정하고, 이를 구현하는 application의 서비스 같은 경우는 private-package를 이용하고, 어댑터의 경우도 private-package로 설정하여 잘못된 방향으로 접근이 이뤄지지 않도록 강제로 막을 수 있습니다.
유의사항
다만 이 방법을 이용하려면 자바 컨피그 방식대신에 클래스패스 스캐닝 방식을 이용해야만 합니다.
왜냐하면 자바 컨피그 방식에서는 객체의 빈 등록을 설정 클래스에서 직접 해야하는데 그러기 위해서는 public제한자여야만 설정 클래스에서 접근하여 빈 등록을 해줄 수 있기 때문입니다.
또한 package-private 제한자는 한 패키지에 몇 개 정도의 클래스로만 이루어진 경우에 가장 효율적입니다.
왜냐하면 하나의 패키지에 클래스 개수가 너무 많아지면 코드를 쉽게 찾을 수 있도록 한 패키지 아래에 의미적으로 비슷한 것끼리 덩어리로 합치는 등 하위 패키지를 만들어 깔끔하게 정리하고 싶은 욕구가 생길 수 있습니다.
하지만 이렇게 하위패키지를 만드는 순간 자바는 하위 패키지를 다른 패키지로 취급하기 때문에 상위 패키지나 상위패키지 아래의 하위패키지가 같은 상위패키지의 또다른 하위 패키지의 package-private에 멤버에 접근할 수 없게 됩니다.
그래서 만약에 이를 접근하게 하기 위해서는 결국에 public으로 접근 제어자를 바꿔야하는데 그렇게 되면 접근제어자로 잘못된 방향으로 못가게 강제하는 방법은 와해되어 버립니다.
컴파일 후 체크(post-compile check)
클래스에 public 제한자를 사용하면 아키텍처 상의 의존성 방향이 잘못되더라도 컴파일러는 다른 클래스들이 이 클래스를 사용하는 것을 전혀 막지 못합니다.
이럴 경우 코드가 컴파일 된 후에 런타임에서 체크하는 컴파일 후 체크 방법을 사용할 수 있습니다.
이러한 런타임 체크는 지속적인 통합 빌드 환경에서 자동화된 테스트 과정에서 가장 잘 작동합니다.
이것을 도와주는 ArchUnit이라는 자바 도구가 있습니다.
ArchUnit은 의존성 방향이 기대한 대로 잘 설정되어 있는지 체크하여 위반을 발견하면 예외를 던지는 API를 제공합니다.
ArchUnit은 JUnit과 같은 단위 테스트 프레임워크에서 잘 작동하며 의존성 규칙을 위한반 경우 테스트를 실패시킵니다.
ArchUnit API를 이용하면 적은 작업 만으로도 육각형 아키텍처 내에서 관련된 모든 패키지를 명시할 수 있는 일종의 도메인 특화 언어(DSL)를 만들 수 있고, 패키지 사이의 의존성 방향이 올바른지 자동으로 체크할 수 있습니다.
바운디드 컨텍스트(Bounded Context)
바운디드 컨텍스트는 모델의 경계를 결정하며 한 개의 바운디드 컨텍스트는 논리적으로 하나의 모델을 가집니다.
도메인 주도 개발에서는 프로그램이 복잡해지고 규모가 커지면 맥락의 경계를 명확하게 정의하라고 제안합니다.
왜냐하면 같은 용어도 맥락에 따라 다르게 쓰이기 때문입니다.
하지만 프로그램 내부에서는 어떤 단어가 어떤 맥락에서 쓰이는 말인지 구분하려면 세심한 노력이 필요합니다.
프로그램 내부에서는 어떤 단어가 어떤 패키지나 클래스에서 쓰이느냐에 따라 맥락을 상황에 맞춰 파악해야 합니다.
그런 구분을 위한 단위가 바로 바운디드 컨텍스트입니다.
책에서는 먼저 바운디드 컨텍스트의 부모 패키지를 지정합니다.
단일 바운디드 컨텍스트라면 애플리케이션 전체에 해당합니다.
그 다음 도메인, 어댑터, 애플리케이션, 설정 계층에 해당하는 하위 패키지들을 지정합니다.
드 다음 패키지 의존성이 의존성 규칙을 따라 유효하게 설정되었는지 테스트 코드를 통해 검증합니다.
유의사항
이렇게 잘못된 의존성을 바로잡는 데 컴파일 후 체크가 큰 도움이 되긴 하지만, 단점으로는 실패에 안전(fail-safe)하지 않습니다.
예들 들어 패키지 이름에 오타를 내면 테스트가 어떤 클래스도 찾지 못하기 때문에 의존성 규칙 위반 사례를 발견하지 못합니다.
즉, 패키지 이름에 오타가 하나라도 나거나 패키지 이름을 리팩터링해버리면 테스트 전체가 무의미해질 수 있습니다.
이것을 방지하기 위해 클래스를 하나도 찾지 못했을 때 실패하는 테스트를 추가해야 합니다.
그렇게 하더라도 여전히 리팩터링에는 취약할 수 밖에 없습니다.
그래서 컴파일 후 체크는 언제나 코드와 함께 유지보수해야 합니다.
빌드 아티팩트(build artifact)
빌드 아티팩트는 빌드 도구 프로세스의 결과물입니다.
자베에서 가장 인기 있는 빌드 도구는 메이븐(Maven)과 그레이들(Gradle)입니다.
빌드 도구의 주요한 기능 중 하나가 바로 의존성 해결입니다.
어떤 코드베이스를 빌드 아티팩트로 변환하기 위해 빌드 도구가 가장 먼저 할 일은 코드베이스가 의존하고 있는 모든 아티팩트가 사용 가능한지 확인하는 것입니다.
만약에 사용할 수 없는 것이 있다면 아티팩트 리포지토리로부터 가져오려고 시도합니다.
이것이 안되면 코드를 컴파일하기 전에 에러와 함께 빌드를 실패합니다.
이를 활용하여 모듈과 아키텍쳐의 계층 간의 의존성을 강제할 수 있습니다.
각 모듈의 빌드 스크립트에서는 아키텍처에서 허용하는 의존성만 지정합니다.
클래스들이 클래스페스에 존재하지 않아 컴파일에서 어락 발생하기 때문에 개발자들이 더이상 실수로 잘못된 의존성을 만들 수 없습니다.
빌드 모듈로 아키텍처 경계를 구분하는 것은 패키지로 구분하는 방식과 비교했을 때 몇가지 장점이 있습니다.
첫째, 빌드 도구를 이용하면 순환 의존성을 막을 수 있고, 이로 인해 빌드 모듈 간 순환 의존성이 없음을 확신할 수 있습니다.
반면에 자바 컴파일러는 두 개 또는 그 이상의 패키지에서 순환 의존성이 있든지 없든지 신경쓰지 않습니다.
(순환 의존성은 하나의 모듈에서 일어나는 변경이 잠재적으로 순환 고리에 포함된 다른 모듈을 변경하게 만들어서 단일 책임 원칙을 위반하게 만들어서 좋지 않습니다.)
둘째, 빌드 모듈 방식에서는 다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한 채로 변경할 수 있습니다.
일시적으로 특정 어댑터가 컴파일 에러가 생기는 애플리케이션 계층을 리팩터링하는 경우 어댑터와 애플리케이션 계층이 같은 빌드 모듈에 있다면 어댑터가 컴파일되지 않더라도 애플리케이션 계층의 테스트를 실행할 수 있더라도 대부분의 IDE는 태스트를 실행하려면 어댑터의 컴파일 에러를 모두 고쳐야 테스트를 실행할 수 있습니다.
그러나 만약에 애플리케이션 계층이 독립된 빌드 모듈이라면 IDE가 어댑터에 신경 쓰지 않을 것이기 때문에 이와 상관없이 애플리케이션 계층의 테스트를 마음대로 실행할 수 있습니다.
이것은 메이븐이나 그레이들로 빌드 프로세스를 실행할 때도 마찬가지입니다.
그러므로 여러 개의 빌드 모듈은 각 모듈을 격리한 채로 변경할 수 있게 해줍니다.
심지어 각 모듈울 자체 코드 리포지토리에 넣어 서로 다른 팀이 서로 다른 모듈을 유지보수하게 할 수도 있습니다.
셋째, 모듈 간 의존성이 빌드 스크립트에 분명히 선언되어 있기 때문에 새로 의존성을 추가하는 일은 우연이 아닌 의식적인 행동이 됩니다.
어떤 개발자가 당장은 접근할 수 없는 특정 클래스에 접근해야 할 일이 생기면 빌드 스크립트에 의존성을 추가해야 하기 때문에 정말로 이 의존성이 필요한지 한 번 더 고민해보게 만듭니다.
하지만 이러한 장점을 가진 빌드 스크립트를 유지보수하는 비용이 들기 떄문에 아키텍처를 여러 개의 빌드 모듈로 나누기 전에 아키턱체가 어느 정도 안정된 상태여야만 합니다.
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
기본적으로 소프트웨어 아키텍처는 아키텍처 요소 간의 의존성을 관리하는 것이 전부입니다.
아키텍처를 잘 유지해나가고 싶다면 의존성이 올바른 방량으로 가리키고 있는지 지속적으로 체크해야 합니다.
새로운 코드 추가, 리팩터링 시 패키지 구조를 항상 염두에 둬야 하고, 가능하면 package-private 가시성을 이용해 피키지 바깥에서 접근하면 안 되는 클래스에 대해 접근할 수 없도록 막아야 합니다.
하나의 빌드 모듈 안에서 아키텍처 경계를 강제해야 하고, 패키지 구조 상으로 인해 package-private 제한자를 사용할 수 없을 경우 ArchUnit을 이용해 컴파일 후 체크 도구를 이용하여 잘못된 의존성 방향을 확인할 수 있습니다.
그리고 아키텍처가 충분히 안정적일 경우 아키텍처 요소룰 독립적인 빌드 모튤로 추출해서 의존성을 분명하게 제어해야 합니다.
시간이 지나도 아키텍처를 계속 유지하고 우지보수하기 좋은 코드를 만들기 위해서는 위에서 언급한 3가지 방법을 조합해서 사용할 수 있습니다.
댓글남기기