9장 : 애플리케이션 조립하기

클래스들을 인스턴스화하고 묶기 위해 평범한 자바, 스프링, 스프링 부트 프레임워크에서 어떻게 의존성 주입 매커니즘을 이용하는지 살펴본다.

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

코드의 의존성이 올바른 방향을 가리키게 하기 위해서이다.

모든 의존성은 안쪽으로, 즉 애플리케이션의 도메인 코드 방향으로 향해야 한다.

  • 변경으로 부터 안전

  • 테스트 쉽다

의존성 규칙을 어기지 않고 객체 인스턴스를 생성할 책임을 부가하기 위해 설정 컴포넌트가 있어야 한다.

설정 컴포넌트(configuration component)의 역할

  • 웹 어댑터 인스턴스 생성

  • HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장

  • 유스케이스 인스턴스 생성

  • 웹 어댑터에 유스케이스 인스턴스 제공

  • 영속성 어댑터 인스턴스 생성

  • 유스케이스에 영속성 어댑터 인스턴스 제공

  • 영속성 어댑터가 실제로 데이터베이스에 접근할 수 있도록 보장

설정 컴포넌트는 설정 파일이나 커맨드라인 파라미터 등과 같은 설정 파라미터의 소스에도 접근할 수 있어야 한다.

설정 컴포넌트는 단일 책임 원칙을 위반하지만, 애플리케이션의 나머지 부분을 깔끔하게 유지하기 위해 필요한 컴포넌트다.

평범한 코드로 조립하기

의존성 주입 프레임워크의 도움 없이 애플리케이션을 만들고 있다면 평범한 코드로 설정 컴포넌트를 만들 수 있다.

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 SendMoneyUseService(
                accountPersistenceAdapter, // LoadAccountPort
                accountPersistenceAdapter); // UpdateAccountStatePort
        
        SendMoneyContoller sendMoneyController = 
            new SendMoneyContoller(sendMoneyUseCase);
            
        startProcessingWebRequests(sendMoneyController);
         
    }
}
  • main 메서드 안에서 웹 컨트롤러부터 영속성 어댑터까지, 필요한 모든 클래스의 인스턴스로 생성한 후 함께 연결한 후

  • 마지막에 웹 컨트롤러를 HTTP로 노출하는 메서드 startProcessingWebRequests()를 호출한다.

평범한 코드 방식의 단점

  • 엔터프라이즈 애플리케이션의 경우 설정 코드가 매우 많아진다.

  • 패키지 접근을 제한할 수 없다. 클래스가 속한 패키지 외부에서 인스턴스를 생성해야 하기 떄문에 클래스들은 전부 public이어야 한다.

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

애플리케이션 컨텍스트

  • 스프링 프레임워크를 이용해서 애플리케이션을 조립한 결과물

  • 애플리케이션을 구성하는 모든 객체(빈(bean))를 포함

클래스패스 스캐닝

  • 애플리케이션 컨텍스트를 조립하기 위해 스프링이 제공하는 방법

  • 스프링은 클래스패스 스캐닝으로 클래스패스에서 접근 가능한 모든 클래스를 확인해서 @Component 애너테이션이 붙은 클래스를 찾고,

  • 이 애너테이션이 붙은 각 클래스의 객체를 생성한다. 이때 클래스는 필요한 모든 필드를 인자로 받는 생성자를 가지고 있어야 한다. 이 코드에서는 생성자를 직접 만들지 않고 lombok 라이브러리의 @RequiredArgsConstructor 애너테이션을 이용해 모든 final 필드를 인자로 받는 생성자를 자동으로 생성했다. 스프링은 이 생성자를 찾아서 생성자의 인자로 사용된 @Component가 붙은 클래스들을 찾고, 이 클래스들의 인스턴스를 만들어 애플리케이션 컨텍스트에 추가한다.

  • 필요한 객체들이 모두 생성되면 AccountPersistenceAdapter의 생성자를 호출하고 생성된 객체도 애플리케이션 컨텍스트에 추가한다.

@RequiredArgsConstructor
@Component
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 "";
}

메타-애너테이션으로 @Component를 포함하고 있어서 스프링이 클래스패스 스캐닝을 할 때 인스턴스를 생성할 수 있도록 한다. @Component 대신 @PersistenceAdapter를 이용해 영속성 어댑터 클래스들이 애플리케이션의 일부임을 표시할 수 있다.

장점

  • 편리하다.

단점

  • 라이브러리 사용자가 스프링 프레임워크의 의존성에 엮이게 된다.

  • 원인을 찾는 데 수일이 걸릴 수 있는 숨겨진 부수효과를 야기할 수도 있다.

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

애플리케이션 컨텍스트에 추가할 빈을 생성하는 설정 클래스를 만든다.

여기서는 모든 영속성 어댑터들의 인스턴스 생성을 담당하는 설정 클래스 생성한다.

@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();
    }
}
  • @Compnent 애너테이션을 통해 이 클래스가 설정 클래스임을 표시한다.

  • 설정 클래스 내의 @Bean 애너테이션이 붙은 팩터리 메서드를 통해 빈이 생성된다. 스프링은 생성자 입력에 대해 자동으로 팩터리 메서드에 대한 입력으로 제공한다.

  • 리포지토리 객체들이 만약 또 다른 설정 클래스의 팩터리 메서드에서 수동으로 생성됐다면, 스프링이 자동으로 팩터리 메서드의 파라미터로 제공할 것이지만, 이 예제에서는 @EnabledJpaRepositories 애너테이션으로 인해 스프링이 직접 생성해서 제공한다.

@EnableJpaRepositories를 메인 애플리케이션에 붙일 수도 있다. 가능은 하지만 이렇게 하면 애플리케이션을 시작할 때마다 JPA를 활성화해서 영속성이 실질적으로 필요 없는 테스트에서 애플리케이션을 실행할 때도 JPA 리포지토리들을 활성화할 것이다. 그러므로 이러한 ‘기능 애네테이션’을 별도의 설정 ‘모듈’로 옮기는 편이 애프리케이션을 더 유연하게 만드록, 항상 모든 것을 한꺼번에 시작할 필요 없게 해준다.

장점

  • 어떤 빈이 애플리케이션 컨텍스트에 등록될지 제어할 수 있게 된다.

  • 특정 모듈만 포함하는 한정적인 범위의 모듈을 만들 수 있다**.**

    • ****그 외의 다른 모듈의 빈은 모킹해서 애플리케이션 컨텍스트를 만들 수 있다. 이렇게 하면 ****테스트에 큰 유연성이 생기고, 리팩터링을 많이 하지 않고도 각 모듈의 코드를 다체 코드베이스, 자체 패키지, 자체 JAR 파일로 밀어넣을 수 있다.

  • 애플리케이션 계층을 스프링 프레임워크에 대한 의존성 없이 깔끔하게 유지할 수 있다.

문제점

  • 설정 클래스가 생성하는 빈이 설정 클래스와 같은 패키지에 존재하지 않는다면 이 빈들을 public으로 만들어야 한다.

유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까 ?

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

애플리케이션 조립을 책임지는 전용 설정 컴포넌트를 만들면 애플리케이션이 변경할 책임으로부터 자유로워진다. 서로 다른 모듈로부터 독립되고, 응집도가 높은 모듈을 만들 수 있다.

DISCUSSION

Last updated