[클린 아키텍처] 09. 애플리케이션 조립하기

Jimin Lim·2023년 6월 11일
0

Architecture

목록 보기
9/23
post-thumbnail

✅ 09. 애플리케이션 조립하기

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

조립까지 신경써야하는 이유는 코드 의존성이 올바른 방향을 가리키게 하기 위해서다. 모든 의존성은 안쪽으로 즉, 애플리케이션의 도메인 코드 방향으로 향해야 바깥 계층의 변경으로부터 안전하다.

객체 인스턴스를 생성할 책임은 설정 컴포넌트가 가진다. 설정 컴포넌트는 클린 아키텍처에서 가장 바깥쪽에 위치하며 우리가 제공한 조각들로 애플리케이션을 조립하는 것을 책임진다. 구체적으로 다음과 같은 역할을 수행해야 한다.

  1. 웹 어댑터 인스턴스 생성
  2. HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장
  3. 유스케이스 인스턴스 생성
  4. 웹 어댑터에 유스케이스 인스턴스 제공
  5. 영속성 어댑터 인스턴스 생성
  6. 유스케이스에 영속성 어댑터 인스턴스 제공
  7. 영속성 어댑터가 실제 DB에 접근할 수 있도록 보장

🔗 평범한 코드로 조립하기

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); //웹 어댑터를 HTTP로 노출시키는 데 필요한 애플리케이션 부트스트랩핑 로직이 들어갈 곳
    }
}

위 방식은 다음과 같은 단점이 존재한다.

  1. 컨트롤러, 유스케이스, 영속성 어댑터 등 여러 개가 존재한다면, 이러한 코드를 여러 번 반복해서 작성해야 함
  2. 각 클래스의 패키지 외부에서 인스턴스 생성하므로 모두 public 이어야 한다. 즉, 유스케이스가 영속성 어댑터에 직접 접근하는 것을 막지 못한다. (package-private 을 이용해 접근을 막을 수 있는데)

이런 의존성 주입을 해주는 스프링을 사용해보자

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

스프링을 이용해서 애플리케이션을 조립한 결과물을 애플리케이션 컨텍스트라고 한다. 애플리케이션 컨텍스트는 빈(bean)을 포함한다.

클래스패스 스캐닝은 @Component 애너테이션이 붙은 클래스를 찾아 그 클래스의 객체를 생성한다. 이는 필요한 모든 필드를 인자로 받는 생성자를 가지고 있어야 한다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface PersistenceAdapter {
  @AliasFor(annotation = Component.class)
  String value() default "";
}

위와 같이 커스텀 애너테이션으로 만들어 PersistenceAdapter가 애플리케이션 일부임을 나타낼 수 있다.

이 방식도 다음과 같은 단점이 존재한다.

  1. 클래스에 프레임워크에 특화된 애너테이션을 붙여야 한다. 코드를 특정 프레임워크와 결합시킨다.
  2. 스프링 전문가가 아니라면 모를 부수효과를 야기할 수도 있다. 클래스패스 스캐닝은 단순히 스프링에게 부모 패키지를 알려준 후, 이 패키지 안에 @Componenet가 붙은 클래스를 찾으라고 지시한다.

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

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

@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을 통해 클래스패스 스캐닝에서 발견해야 할 설정 클래스임을 표시해둔다. 여전히 클래스패스 스캐닝을 사용하는건 맞지만, 모든 빈을 가져오는 대신 설정 클래스만 선택하도록 한다.

@Bean을 통해 영속성 어댑터를 애플리케이션 컨텍스트에 추가한다. @EnableJpaRepositories을 통해 스프링 데이터 리포지토리 인터페이스의 구현체를 가져온다.

PersistenceAdapterConfiguration 클래스를 이용해 영속성 계층에서 필요한 모든 객체를 한정적인 범위에서 만들도록 하였다. 이를 통해 우리는 어떤 빈이 애플리케이션 컨텍스트에 등록될지 제어할 수 있게 된다. 또한 코드 전체에 @Component 애노테이션을 붙이지 않아도 돼서 스프링에 대한 종속성 없이 애플리케이션 계층을 깨끗하게 유지할 수 있다.

하지만 위 방식은 설정 클래스와 bean이 같은 패키지에 존재하지 않는다면 bean대상들도 public으로 만들어야 한다.

✅ 결론

클래스패스 스캐닝은 어떤 빈이 애플리케이션 컨텍스트에 올라오는지 정확히 알 수 없고, 테스트에서 일부만 독립적으로 띄우기 어려워진다.
전용 설정 컴포넌트를 만들면 애플리케이션이 책임(변경할 이유)로부터 자유로워지며 응집도가 높은 모듈을 만들 수 있다.

profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️

0개의 댓글