Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

csct3434

[만들면서 배우는 클린 아키텍처] 09. 애플리케이션 조립하기 본문

개발 서적/만들면서 배우는 클린 아키텍처

[만들면서 배우는 클린 아키텍처] 09. 애플리케이션 조립하기

csct3434 2024. 3. 3. 21:20

서론

자바, 스프링, 스프링부트 프레임워크에서의 의존성 주입 메커니즘에 대해 살펴본다.

(이미지 출처 : https://rudaks.tistory.com)


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

  • 모든 의존성이 안쪽으로, 애플라케이션의 도메인 코드 방향으로 향해야 도메인 코드가 바깥 계층의 변겨응로부터 안전하다
  • 한 클래스가 필요로 하는 모든 객체를 생성자로 전달할 수 있다면 실제 객체 대신 목을 전달할 수 있고, 이렇게 되면 격리된 단위 테스트를 생성하기가 쉬워진다
  • 우리의 객체 인스턴스를 생성할 책임은 누구에게 있을까? 그리고 어떻게 의존성 규칙을 어기지 않으면서 그렇게 할 수 있을까?

설정 컴포넌트

  • 아키텍처에 중립적이고 인스턴스 생성을 위해 모든 클래스에 의존성을 가지는 설정 컴포넌트가 그 책임을 갖는다
  • 설정 컴포넌트의 역할
  • 이는 단일 책임 원칙을 위반하지만, 애플리케이션의 나머지 부분을 깔끔하게 유지하고 싶다면 이처럼 구성요소들을 연결하는 바깥쪽 컴포넌트가 필요하다.

평범한 코드로 조립하기

package copyeditor.configuration;

class Application {
    public static void main(String [] args) {

        AccountRepository accountRepository = new AccountRepository();
        ActivityRepository acitivityRepository = new ActivityRepository();

        AccountPersistenceAdapter accountPersistenceAdapter =
          new AccountPersistenceAdapter(accountRepository, activityRepository);

        SendMoneyUseCase sendMoneyUseCase = 
					new SendMoneyUseService(
            accountPersistenceAdapter, // LoadAccountPort
            accountPersistenceAdapter); // UpdateAccountStatePort

       SendMoneyController sendMoneyController = 
         new SendMoneyController(sendMoneyUseCase);

       startProcessingWebRequests(sendMoneyController);

    }
}
  • 의존성 주입 프레임워크의 도움 없이 설정 컴포넌트를 구현하는 방법이다.
  • main 메서드 안에서 필요한 모든 클래스의 인스턴스를 생성한 후 함께 연결한다.
  • 마지막으로 웹 컨트롤러를 HTTP로 노출하는 커스텀 메서드인 startProcessingWebRequests()를 호출한다.
  • 단점
    • 작성해야 할 코드의 양이 많고 복잡하다.
    • 각 클래스가 속한 패키지 외부에서 인스턴스를 생성하기 때문에 이 클래스들은 전부 public이어야 한다.
      이렇게 되면 가령 유스케이스가 영속성 어댑터에 직접 접근하는 것을 막지 못한다.
  • 대안
    • 의존성 주입 프레임워크를 사용한다면 package-private 의존성을 유지할 수 있다.
    • 자바 세계에서는 그중 스프링 프레임워크가 가장 인기있다.
    • 무엇보다도 스프링은 웹과 데이터베이스 환경을 지원하기 때문에 신비한 startProcessingWebRequests() 메서드 같은 것을 직접 구현할 필요가 없다.

스프링 프레임워크로 조립하기

  • 애플리케이션 컨텍스트(application context) : 스프링 프레임워크를 이용해서 애플리케이션을 조립한 결과
  • 빈(bean) : 애플리케이션 컨텍스트에 포함된 애플리케이션을 구성하는 모든 객체
  • 스프링은 애플리케이션 컨텍스트를 조립하기 위한 몇가지 방법을 제공한다

클래스패스 스캐닝

@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) {
        // ...
    }
}
  • 스프링은 클래스패스 스캐닝으로 클래스패스에서 접근 가능한 모든 클래스를 확인해서 @Component 애너테이션이 붙은 클래스의 인스턴스를 생성하고 애플리케이션 컨텍스트에 등록한다.
  • 이때, 각 클래스는 필요한 모든 필드를 인자로 받는 생성자를 가지고 있어야 한다.
  • 스프링이 인식할 수 있는 애너테이션을 직접 만들 수도 있다.
  • @Target({ElementType.Type})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface PersisteneAdapter {
    
    	@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();
  }

}
  • 클래스패스 스캐닝이 애플리케이션 조립하기의 곤봉이라면 스프링의 자바 컨피그(Java Config)는 수술용 메스다
  • 프레임워크를 활용하므로 모든 것을 직접 코딩할 필요가 없는 방식이다.
  • PersistenceAdapterConfiguration 클래스를 이용해서 영속성 계층에서 필요로 하는 모든 객체를 인스턴스화하는 매우 한정적인 범위의 영속성 모듈을 만들었다.
    • @Configuration 애너테이션을 통해 이 클래스가 스프링의 클래스패스 스캐닝에서 발견해야 할 설정 클래스임을 명시한다.
    • 빈은 설정 클래스 내의 @Bean 애너테이션이 붙은 팩터리 메서드를 통해 생성된다.
    • @EnableJpaRepositories 애터네이션은 스프링 부트가 자동으로 우리가 정의한 모든 스프링 데이터 리포지토리 인터페이스의 구현체를 제공하도록 한다.
  • 장점
    • 클래스패스 스캐닝과 달리 @Component 애너테이션을 코드 여기 저기에 붙이도록 강제하지 않는다.
      따라서, 애플리케이션 계층을 스프링 프레임워크에 대한 의존성 없이 깔끔하게 유지할 수 있다.
  • 단점
    • 설정 클래스가 생성하는 빈이 설정 클래스와 같은 패키지에 존재하지 않는다면 이 빈들을 public으로 만들어야 한다.

정리

  • 클래스패스 스캐닝은 아주 편리한 기능이다.
    • 하지만, 코드의 규모가 커지면 금방 투명성이 낮아진다.
    • 테스트에서 애플리케이션 컨텍스트의 일부만 독립적으로 띄우기가 어려워진다.
  • 전용 설정 컴포넌트를 만들면 응집도가 매우 높은 애플리케이션 모듈을 만들 수 있다.
    • 하지만 설정 컴포넌트를 유지보수하는 데 약간의 시간을 추가로 들여야 한다.