헥사고날-아키텍쳐로-구현하는-작은-스프링-부트-토이-프로젝트-우아한스터디-09
만들면서 배우는 클린 아키텍쳐 9장 정리
애플리케이션 조립하기
유스케이스, 웹 어댑터, 영속성 어댑터를 구현한 다음은 이것들 조립할 무언가가 필요합니다.
애플리케이션이 시작될 때 클래스를 인스턴스화하소 의존성 주입을 이 조립기가 해줍니다.
이 조립의 방법으로 순수 자바, 스프링, 스프링 부트 프레임워크를 사용할 수 있습니다.
왜 조립까지 신경 써야할까?
필요할 때마다 유스케이스나 어댑터를 그냥 인스턴스하면 안되는 이유는 코드의 의존성 방향때문입니다.
모든 의존성은 안쪽으로, 애플리케이션의 도메인 코드 방향으로 향해야만 바깥 계층의 코드가 변경되더라도 도메인 코드는 변경할 필요가 없습니다.
만약에 그렇지 않고 유스케이스가 영속성 어댑터를 직접 인스턴스화한다면 코드 의존성이 잘못된 방향으로 흐르고 있다는 것입니다.
이를 방지 하기 위해 아웃고잉 포트 인터페이스가 있습니다.
유스케이스는 인터페이스만 알고(인터페이스에만 의존하고), 그 구현체는 몰라야(의존하지 않아야) 하며, 런타임에 이 인터페이스의 구현체를 제공받아 그 구현체가 무엇인지는 모르지만 그냥 실행시키면 됩니다.
이렇게 하면 코드를 테스트하기 훨씬 더 쉽습니다.
한 클래스가 필요로 하는 모든 객체를 생성자로 전달(의존성 주입)할 수 있다면 실제 객체 대신에 목(mock)을 전달할 수 있고, 이로 인해 격리된 단위 테스트를 만들기 쉬워집니다.
설정 컴포넌트(configuration component)
설정 컴포넌트는 아키텍처에 대해 중립적이고 인스턴스 생성을 위해 모든 클래스에 대해 의존성을 가지는데 이를 통해 다른 계층들이 의존성 규칙을 어기지 않게 대신해서 객체들의 인스턴스를 생성하여 의존성을 주입해주는 조립 역할을 합니다.
따라서 설정 컴포넌트는 육각형 아키텍처의 가장 바깥쪽에 위치하며 모든 내부 계층에 접근할 수 있습니다.
설정컴포넌트는 다음과 같은 역할을 수행합니다.
- 웹 어댑터 인스턴스 생성
- HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장
- 유스케이스 인스턴스 생성
- 웹 어댑터에 유스케이스 인스턴스 제공
- 영속성 어댑터 인스턴스 생성
- 유스케이스에 영속성 어댑터 인스턴스 제공
- 영속성 어댑터가 실제로 데이터베이스에 접근할 수 있도록 보장
이와 더불어 설정 컴포넌트는 설정 파일이나 커맨드라인 파라미터 등과 같은 설정 파라미터의 소스에도 접근해야 합니다.
설정 컴포넌트가 애플리케이션을 조립하는 동안 이러한 파라미터를 애플리케이션 컴포넌트에 제공해 어떤 데이터베이스에 접근하고 어떤 서버를 메일 전송에 사용할지 등의 행동 양식를 제어합니다.
이처럼 설정 컴포넌트는 책임(변경할 이유)이 굉장히 많습니다.
이것은 단일 책임 원칙을 위반하는 것이지만 설정 컴포넌트의 희생으로 애플리케이션의 나머지 부분은 단일책임으로 깔끔하게 유지할 수 있습니다.
그래서 설정 컴포넌트는 애플리케이션을 구성하는 모든 부품을 알아야만 성공적으로 작동하는 애플리케이션을 조립할 수 있습니다.
평범한 코드로 조립하기
이 설정 컴포넌트를 구현하는 방법 중에 의존성을 주입해주는 스프링 프레임워크의 도움 없이 애플리케이션을 만들수도 있습니다.
단점
첫째, 간단한 아키텍처야 스프링 프레임워크의 도움없이 순수 자바 코드로 의존성을 주입해주는 조립기를 만들 수 있으나 완전한 엔터프라이즈 애플리케이션을 실행하기 위해서는 배보다 배꼽이 더 커지는 상황이 발생합니다.
이 조립기를 만들다가 시간이나 에너지를 다 소모하여 정작 진짜 만들어야 하는 애플리케이션을 못 만듭니다.
둘째, 각 클래스가 속한 패키지 외부에서 인스턴스를 생성해야 하기 때문에 이 클래스들은 전부 public으로 설정해야 합니다.
이렇게 되면 예를 들어 유스케이스가 영속성 어탭터에 직접 접근하는 것을 코드로 막지 못합니다.
그러나 스프링 프레임워크를 사용하면 package-private 접근 제한자를 이용해 이러한 워니 않은 접근을 막을 수 있고, 개발자들은 조립하는데 신경쓰지 말고, 진짜 만들어야 하는 애플리케이션에 집중할 수 있습니다.
애플리케이션 컨텍스트(application context)
스프링 프레임워크를 이용해 애플리케이션을 조립한 결과물을 애플리케이션 컨텍스트라고 합니다.
애플리케이션 컨텍스트는 애플리케이션을 구성하는 모든 객체인 빈(bean)을 포함합니다.
클래스패스 스캐닝(classpath scanning)으로 조립하기
애플리케이션 컨텍스트를 조립하는 방법 중에 가장 편리한 방버이 바로 클래스패스 스캐닝입니다.
다른 말로 하면 컴포넌트 스캔(component scan)입니다.
즉, 스프링이 Component 애너테이션이 붙은 클래스를 찾아서 객체를 대신 생성해주는데 이 때 객체를 생성하는데 필요한 모든 필드(또 다른 객체가 될 수도 있음)에 대해 의존성 주입을 대신해줍니다.
대신에 개발자는 Component 애너테이션이 붙은 클래스가 필요로 하는 모든 필드를 매개변수로 가지는 생성자를 정의해야 합니다.
이 생성자를 개발자가 직접 코드로 작성할 수도 있짐난 Lombok의 RequiredArgsContructor 애너테이션을 이용하면 final이 붙은 모든 필드를 인자로 받는 생성자를 자동으로 생성해줍니다.
스프링은 이 생성자를 찾아서 생성자의 인자로 사용된 Component가 붙은 클래스들을 찾고 이 클래스들을 인스턴스화하여 애플리케이션 컨텍스트에 추가합니다.(스프링 컨테이너에 빈으로 등록합니다.)
이렇게 생성자의 인자로 사용될 클래스들을 모두 생성해서 애플리케이션 컨텍스트에 추가하고 나면 생성자를 이용해 해당 클래스를 인스턴스화하여 이 인스턴스도 애플리케이션 컨텍스트에 추가합니다.
클래스패스 스캐닝 방식은 적절한 곳에 Component 애너테이션만 붙여 주면 되기 때문에 아주 간편합니다.
메타-애네테이션 방식으로 커스터마이징된 애너테이션 만들기
이 Component 애너테이션을 활용해 메타-애네테이션 방법으로 커스터마이징된 애너테이션을 만들어서 스프링이 인식하게 만들어 줄 수 있습니다.
이렇게 커스터마이징화된 애너테이션은 코드를 읽는 사람으로 하여금 아키텍처를 더 쉽게 파악할 수 있도록 도움을 줄 수 있습니다.
단점
첫째 클래스패스 스캐닝 방식을 사용하면 클래스에 Component 애너테이션을 붙여야 하기 때문에 해당 클래스가 스프링 프레임워크에 의존하게 만듭니다.
그래서 강경한 클린아키텍처파는 도메인 및 유스케이스 코드가 스프링 프레임워크와 의존하는 것을 반대합니다.
일반적인 애플리케이션 개발에서는 이것이 큰 문제는 되지 않지만 다른 개발자들이 사용할 라이브러리나 프레임워크를 만들때는 지양해야 하는 방법입니다.
왜냐하면 이렇게 스프링 의존적인 코드로 라이브러리를 만들면 해당 라이브러리를 사용하는 개발자도 스프링 프레임워크의 의존성에 엮이기 때문입니다.
둘째 클래스패스 스캐닝 방식을 사용하다 보면 자칫 잘못하면 예상치 못한 부수효과가 발생할 수 있는데 이 때 이 원인을 찾기가 쉽지 않아서 많은 시간을 낭비할 수 있습니다.
클래스패스 스캐닝 방식은 아무래도 자동으로 손쉽게 조립하다보니 세세하게 커스터마이징하기 쉽지 않은데 프로그램이 복잡해질수록 의도치 않게 불필요한 클래스가 애플리케이션 컨텍스트에 등록이 되어 문제가 발생할 수도 있는데 이를 발견하는 것이 쉽지 않습니다.
자바 컨피그(Java Config)로 조립하기
자바 컨피그를 사용하면 클래스패스 스캐닝보다 훨씬 더 정교하게 애플리케이션을 조립할 수 있습니다.
스프링을 사용하지 않고 조립기를 만드는 방식과 비슷하지만 스프링을 사용하기 때문에 모든 것을 직접 코딩할 필요가 없어서 자바 코드로 직접 조립기를 만드는 것보다는 훨씬 쉽습니다.
조립기 역할을 하는 설정 클래스를 만들어서 여기서 애플리케이션 컨텍스트에 추가할 빈을 생성합니다.
Configuration 애너테이션
클래스패스 스캐닝에서 이 애너테이션이 붙은 클래스는 설정 클래스로 인식합니다.
클래스패스 스캐닝은 여전히 사용되고 있지만 그 범위가 좁고, 나머지는 설정 클래스에서 처리하기 때문에 아까처럼 모든 클래스를 클래스패스 스캐닝이 처리하는 것보다는 에러가 발생할 확률이 낮습니다.
Configuration 애너테이션이 붙은 설정 클래스 내부에 Bean 애너테이션이 붙은 클래스들을 스프링이 자동으로 생성하여 애플리케이션 컨텍스트에 등록합니다.
EnableJpaRepository
리포지토리들을 애플리케이션 컨텍스트에 등록하려면 원래는 설정 클래스에 직접 리포지토리들을 Bean 애너테이션으로 붙여서 일일이 직접 repository들을 애플리케이션 컨텍스트에 등록해야 하지만 EnableJpaRepository 애너테이션을 사용하면 스프링이 직접 우리가 정의한 모든 스프링 데이터 repository들을 생성해서 빈에 등록합니다.
EnableJpaRepository 애너테이션은 설정 클래스뿐만 아니라 main 애플리케이션에도 붙일 수 있지만 그렇게 되면 애플리케이션을 실행할 때마다 JPA를 활성화해서 영속성이 실질직으로 필요없는 단위 테스트에서 애플리케이션을 실행할 때도 JPA 리포지토리들을 활성화시키기 때문에 main 애플리케이션에는 붙이지 않는 것이 좋습니다.
따라서 EnableJpaRepository과 같은 기능 애너테이션은 별도의 설정 클래스에 두는 것이 애플리케이션을 더 유연하게 만들어 주고 항상 모든 것을 한꺼번에 시작할 필요없이 효율적으로 만들어줍니다.
장점
이렇게 설정 클래스를 이용하면 특정 모듈만 포함하고, 다른 모듈의 빈은 모킹해서 애플리케이션 컨텍스트를 만들 수 있기 때문에 유연하게 테스트를 할 수 있습니다.
심지어 리팩토링을 적게 하여도 각 모듈의 코드 자체를 코드베이스, 자체 패키지, 자체 JAR 파일로 밀어 넣을 수 있습니다.
또한 이 방법을 사용하면 Component 애너테이션을 여러 클래스에 붙이지 않아도 되기 때문에 도메인이나 애플리케이견 계층을 스프링 프레임워크에 대한 의존성 없이 깔끔하게 유지할 수 있습니다.
단점
설정 클래스와 같은 패키지에 존재하지 않는 빈을 설정 클래스에서 빈으로 등록하는 경우 이 빈들의 제한자를 다른 패키지에 있는 설정클래스에서도 접근할 수 있게 하기 위해 public으로 설정해야합니다.
패키지 모듈 경계를 사용해 각 패키지 안에 전용 설정 클래스를 만들수는 있지만 이렇게 되면 하위패키지를 사 용할 수 없습니다.(무슨 말인지 잘모르겠는데 10장에 이야기한다고함)
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
스프링과 스프링 부트를 사용하면 애플리케이션 조립을 손쉽게 할 수 있습니다.
클래스패스 스캐닝을 이용하면 편해서 빠르게 개발을 할 수 있지만 코드의 규모가 커지면 투명성이 현저하게 낮아집니다.
어떤 클래스가 빈으로 등록되는지 명확하게 알 수 없고, 테스트에서 애플리케이션 컨텍스트의 일부만 독립적으로 띄어서 테스트하기 어렵습니다.
반면에 설정 컴포넌트를 만들어서 애플리케이션을 조립하면 애플리케이션에서 코드를 변경해야 할 이유를 줄일 수 있습니다.
예를 들어 현재 인메모리 방식으로 영속성 포트를 구현하고 있는데 이를 바꾸려면 여러 애플리케이션 코드에 손을 대야하지만 설정클래스를 이용하면 설정클래스에서 코드만 한 줄 수정하면 바로 바꿀 수 있습니다.
하지만 별도로 설정클래스를 만들어주어야 하기 때문에 클래스패스 스캐닝을 이용하는 방식보다 별도의 시간이 듭니다.
댓글남기기