본문 바로가기
📙/Study

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

by kiwi_wiki 2024. 10. 17.

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

코드의 의존성이 올바른 방향을 가리키게 하기 위해서 유스케이스와 어댑터를 그냥 필요할 때 인스턴스화하면 안된다.

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

 

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

 

설정 컴포넌트(configuration component)

아키텍처에 대해 중립적이고 인스턴스 생성을 위해 모든 클래스에 대한 의존성을 가지는 컴포넌트

 

역할

- 웹 어댑터 인스턴스 생성

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

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

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

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

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

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

 

 

커맨드라인 파라미터 등과 같은 설정 파라미터의 소스에도 접근할 수 있어야 한다.

책임이 굉장히 많지만 애플리케이션의 나머지 부분을 깔끔하게 유지하고 싶다면 이처럼 구성요소들을 연결하는 바깥쪽 컴포넌트가 필요하다. 이 컴포넌트는 작동하는 애플리케이션으로 조립하기 위해 애플리케이션을 구성하는 모든 움직이는 부품을 알아야 한다.

평범한 코드로 조립하기

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

package copyeditor.configuration;

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

		SendMoneyController sendMoneyController = new SendMoneyController(sendMoneyUseCase);

        startProcessingWebRequests(sendMoneyController);
    }
}

main 메서드 안에서 웹 컨트롤러부터 영속성 어댑터까지 필요한 모든 클래스의 인스턴스를 생성한 후 함께 연결한다.

 

단점

- 웹 컨트롤러, 유스케이스, 영속성 어댑터가 복수개 존재하는 애플리케이션을 실행하기 위해서는 위와 같은 코드를 많이 만들어야 한다.

- 각 클래스가 속한 패키지 외부에서 인스턴스를 생성하기 때문에 전부 public 클래스여야 한다. 이러면 유스케이스가 영속성 어댑터에 직접 접근하는 것을 막지 못하며 원치 않은 의존성을 피할 수 없다.

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

스프링 프레임워크를 이용해서 애플리케이션을 조립한 결과물을 application context 라고 한다. 애플리케이션 컨텍스트는 애플리케이션을 구성하는 모든 객체(bean)을 포함한다.

@RequiredArgsConstructor // 필요한 모든 인자를 받는 생성자를 가지고 있어야 한다
@Component // 클래스패스 스캐닝으로 @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) {
		// ... 
	}
}

클래스패스 스캐닝 방식을 이용하면 적절한 곳에 @Component 애너테이션을 붙이고 생성자만 만들어두면 된다.

 

직접 메타 애너테이션을 만들수도 있다.

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

@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(); 
    }
}

@EnabledJpaRepositories 를 설정 클래스뿐만 아니라 메인 애플리케이션에도 붙일 수 있지만 애플리케이션을 시작할 때마다 JPA를 활성화해서 영속성이 실질적으로 필요 없는 테스트에서도 활성화 된다. 그러므로 기능 애너테이션을 별도의 설정 모듈로 옮기는 편이 애플리케이션을 더 유연하게 만들고, 항상 모든 것을 한꺼번에 시작할 필요 없게 해준다.

 

특정 모듈만 포함하고, 그 외의 다른 모듈의 빈은 모킹해서 애플리케이션 컨텍스트를 만들 수 있다. 이렇게 하면 테스트에 큰 유연성이 생긴다. 클래스패스 스캐닝 방식과 달리 @Component 애너테이션을 코드 여기저기에 붙이도록 강제하지 않기 때문에 애플리케이션 계층을 프레임워크에 대한 의존성 없이 깔끔하게 유지할 수 있다.

 

단점

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

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

스프링과 스프링 부트는 애플리케이션을 조립하는 것을 편하게 도와준다.

클래스패스 스캐닝은 스프링에게 패키지만 알려주면 거기서 찾은 클래스로 애플리케이션을 조립한다.

하지만 코드의 규모가 커지면 어떤 빈이 애플리케이션 컨텍스트에 올라오는지 정확히 알 수 없게 된다. 또 테스트에서 애플리케이션 컨텍스트의 일부만 독립적으로 띄우기 어려워진다.

설정 컴포넌트를 만들면 애플리케이션이 책임으로부터 자유로워진다. 유지보수하는 데 시간이 추가적으로 들지만 서로 다른 모듈로부터 독립되어 응집성 높은 모듈을 만들 수 있다.

728x90
반응형