728x90

만들면서 배우는 클린 아키텍처 책을 읽고 정리하며 소감을 적는 포스트입니다.

 

서론


유스케이스, 웹 어댑터, 영속성 어댑터를 구현해봤으니, 이것들을 동작하는 애플리케이션으로 조립해보자. 클래스를 인스턴스화하고 묶기 위해서 의존성 주입 메커니즘을 이용한다. 평범한 자바, 스프링, 스프링 부트 프레임워크에서는 이를 어떻게 하는지 살펴보자

 

왜 조립까지 신경 써야 할까?


왜 유스케이스와 어댑터를 그냥 필요할 떄 인스턴스화 하면 안되는 걸까? 그것은 코드 의존성이 올바른 방향을 가리키게 하기 위해서다. 모든 의존성은 안쪽으로, 애플리케이션의 도메인 코드 방향으로 향해야 도메인 코드가 바깥 계층의 변경으로부터 안전하다는 점을 기억하자.

 

유스케이스가 영속성 어댑터를 호출하고 스스로 인스턴스화한다면 코드 의존성이 잘못된 방향으로 만들어진 것이다. 이것이 바로 아웃고잉 포트 인터페이스를 생성한 이유다. 유스페이스는 인터페이스만 알아야 하고, 런타임에 이 인터페이스의 구현을 제공받아야 한다.

 

이 프로그래밍 스타일의 유익한 부수효과 중 하나는 코드를 훨씬 더 테스트하기 쉽다는 것이다. 한 클래스가 필요로 하는 모든 객체를 생성자로 전달할 수 있다면 실제 객체 대신 목으로 전달할 수 있고, 이렇게 되면 격리된 단위 테스트를 생성하기가 쉬워진다.

 

그럼 우리의 객체 인스턴스를 생성할 책임은 누구에게 있을까? 그리고 어떻게 의존성 규칙을 어기지 않으면서 그렇게 할 수 있을까?

그것은 아키텍처에 대해 중립적이고 인스턴스 생성을 위해 모든 클래스에 대한 의존성을 가지는 설정 컴포넌트가 있어야 한다는 것이다.

 

  • 중립적인 설정 컴포넌트는 인스턴스 생성을 위해 모든 클래스에 접근할 수 있다.
  • 클린 아키텍처에서 이 설정 컴포넌트는 의존성 규칙에 정의한 대로 모든 내부 계층에 접근할 수 있는 원의 가장 바깥쪽에 위치한다.

 

설정 컴포넌트는 우리가 제공한 조각들로 애플리케이션을 조립하는 것을 책임지며 다음과 같은 역할을 수행해야한다.

  • 웹 어댑터 인스턴스 생성
  • HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장
  • 유스케이스 인스턴스 생성
  • 웹 어댑터에 유스케이스 인스턴스 제공
  • 영속성 어댑터 인스턴스 생성
  • 유스케이스에 영속성 어댑터 인스턴스 제공
  • 영속성 어댑터가 실제로 데이터베이스에 접근할 수 있도록 보장

 

더불어 설정 컴포넌트는 설정 파일이나 커맨드라인 파라미터 등과 같은 설정 파라미터의 소스에도 접근할 수 있어야 한다. 애플리케이션이 조립되는 동안 설정 컴포넌트는 이러한 파라미터를 애플리케이션 컴포넌트에 제공해서 어떤 데이터베이스에 접근하고 어떤 서버를 메일 전송에 사용할지 드으이 행동 양식을 제어한다.

 

보다시피 책임이 굉장히 많다. 이것은 단일 책임 원칙을 위반하는게 아닐까? 위반하는게 맞다. 그러나 애플리케이션의 나머지 부분을 깜끌하게 유지하고 싶다면 이처럼 구성요소들을 연결하는 바깥쪽 컴포넌트가 필요하다. 그리고 이 컴포넌트는, 작동하는 애플리케이션으로 조립하기 위해 애플리케이션을 구성하는 모든 구성요소를 알아야 한다.

 

평범한 코드로 조립하기


애플리케이션을 조립할 책임이 있는 설정 컴포넌트를 구현하는 방법은 여러 가지다. 의존성 주입 프레임워크의 도움 없이 애플리케이션을 만들고 있다면 평범한 코드로 이러한 컴포넌트를 만들 수 있다.

package copyeditor.configuration;

public class Application {
    public static void main(String[] args) {
    
        AccountRepository accountRepository = new AccountRepository();
        ActivityRepository activityRepository = new ActivityRepository();

        AccountPersistenceAdapter accountPersistenceAdapter = new AccountPersistenceAdapter(accountRepository, activityRepository);

        SendMoneyUseCase sendMoneyUseCase = new SendMoneyService(
                accountPersistenceAdapter,
                accountPersistenceAdapter
        );

        SendMoneyController sendMoneyController = new SendMoneyController(sendMoneyUseCase);
        
        startProcessingWebRequests(sendMoneyController);
    }
}
  • 설정 컴포넌트의 모습을 관략하게 보여주는 예시
  • main 메서드 안에서 웹 컨트롤러부터 영속성 어댑터까지, 필요한 모든 클래스의 인스턴스로 생성한 후 함께 연결

 

단점

  • 앞의 코드는 웹 컨트롤러, 유스케이스, 영속성 어댑터가 단 하나씩만 있는 애플리케이션 예시
    • 완전한 애플리케이션 실행하기 위해서는 이러한 코드를 엄청나게 많이 만들어야 한다.
  • 각 클래스가 속한 패키지 외부에서 인스턴스를 생성하기 때문에 이 클래스들은 전부 public 이어야 한다.
    • 유스케이스가 영속성 어댑터에 직접 접근하는 것을 막지 못한다.
    • package-private 이용이 가능하다면 좋았을 것이다.

 

이러한 단점을 해결해주고 대신해주는 의존성 주입 프레임워크인 spring framewrok가 있다.

 

스프링의 클래스패스 스캐닝으로 조립


스프링 프레임워크를 이용해서 애플리케이션을 조립한 결과물을 애플리케이션 컨텍스트라고 한다. 애플리케이션 컨텍스트는 애플리케이션을 구성하는 모든 객체(빈)을 포함한다. 스프링은 애플리케이션 컨텍스트를 조립하기 위한 몇 가지 방법을 제공하는데, 각각 장단점이 있다.

 

일단 클래스패스 스캐닝을 살펴보자.

 

클래스패스 스캐닝

  • 클래스패스에서 접근 가능한 모든 클래스를 확인해서 @Component 애너테이션이 붙은 클래스를 찾는다.
  • 해당 애너테이션이 붙은 클래스이 객체를 생성한다.
  • 이 때 클래스는 필요한 모든 필드를 인자로 받는 생성자를 가지고 있어야 한다.
@RequiredArgsConstructor
@PersistenceAdapter
class AccountPersistenceAdapter implements
        LoadAccountPort,
        UpdateAccountStatePort {

    private final AccountRepository accountRepository;
    private final ActivityRepository activityRepository;
    private final AccountMapper accountMapper;

    @Override
    public Account loadAccount(
                    AccountId accountId,
                    LocalDateTime baselineDate) {
        // ...
      }

    @Override
    public void updateActivities(Account account) {
        // ...
    }
}

 

혹은 스프링이 인식할 수 있는 애너테이션을 직접 만들수도 있다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface PersistenceAdapter {

    @AliasFor(annotation = Component.class)
    String value() default "";
}
  • 코드를 읽는 사람들은 아키텍처를 더 쉽게 파악할 수 있다.

 

단점

  • 클래스에 프레임워크에 특화된 애너테이션을 붙여야 한다는 점에서 침투적이다.
    • 강경한 클린 아키텍처파는 이런 방식이 코드를 특정한 프레임워크와 결합시킨다고 할 것이다.
    • 애플리케이션 개발이 아닌 다른 개발자들이 사용할 라이브러리나 프레임워크를 만드는 입장에서는 사용하지 말아야 할 방법이다.
  • 애플리케이션에 존재하는 모든 클래스 중 컨텍스트에 실제로 올라가지 않았으면 하는 클래스가 올라가는 등 실수가 유발될 수 있다.

 

스프링의 자바 컨피그로 조립하기


자바 컨피그를 이용하는 방법은 클래스패스 스캐닝보다 좀 더 세밀하게 설정할 수 있다. 애플리케이션 컨텍스트에 추가할 빈을 생성하는 설정 클래스를 만든다.

@Configuration
@EnableJpaRepositories
class PersistenceAdapterConfiguration {

    @Bean
    AccountPersistenceAdapter accountPersistenceAdapter(
            AccountRepository accountRepository,
            ActivityRepository activityRepository,
            AccountMapper accountMapper
    ) {
        return new AccountPersistenceAdapter(
                accountRepository,
                activityRepository,
                accountMapper
        );
    }

    @Bean
    AccountMapper accountMapper() {
        return new AccountMapper();
    }
}
  • @Configuration을 통해 스캐닝을 가지고 찾아야 하는 설정 클래스임을 표시
    • 클래스패스 스캐닝을 사용하지만 설정 클래스만 선택하기 때문에 발견하기 어려운 에러가 일어날 확률이 줄어든다.
  • 리포지토리 객체들은 @EnableJpaRepositories 애너테이션으로 인해 스프링이 직접 생성해서 제공
  • @EnableJpaRepositories와 같은 기능 애너테이션을 별도의 설정 모듈로 옮기는 편이 애플리케이션을 더 유연하게 만들고, 효율적이다.
    • 다른 모듈은 모킹해서 애플리케이션 컨텍스트를 만들 수 있으므로 테스트에도 큰 유연성이 생긴다.

 

단점

  • 설정 클래스가 생성하는 빈이 설정 클래스와 같은 패키지에 존재하지 않는 다면 이 빈들을 public으로 만들어야 한다.
  • 가시성을 제한하기 위해 패키지를 모듈 경계로 사용하고 각 패키지 안에 전용 클래스를 만들 수 있지만, 하위 패키지를 사용할 순 없다.(10장)

 

요약


클래스패스 스캐닝은 아주 편리한 기능이다. 하지만 코드의 규모가 커지면 금방 투명성이 낮아진다. 어떤 빈이 애플리케이션 컨텍스트에 올라오는지 정확히 알 수 없게 된다. 또, 테스트에서 애플리케이션 컨텍스트의 일부만 독립적으로 띄우가기 어려워진다.

 

반면, 애플리케이션 조립을 책임지는 전용 설정 컴포넌트를 만들면 애플리케이션이 이러한 책임으로부터 자유로워진다. 이 방식을 이용하면 서로 다른 모듈로부터 독립되어 코드 상에서 손쉽게 옮겨 다닐 수 있는 응집도가 매우 높은 모듈을 만들 수 있다.

728x90

+ Recent posts