9장. 애플리케이션 조립하기
의존성 주입 이용하기
- 앞에서 유스케이스, 웹 어뎁터, 영속성 어뎁터를 구현했으니, 이제 이것들을 동작하는 애플리케이션으로 조립할 차례
- 3장에서 이야기했듯이 애플리케이션이 시작될 때 클래스를 인스턴스화하고 묶기 위해서 의존성 주입 메커니즘을 이용
왜 조립까지 신경 써야 할까?
- 왜 유스케이스와 어탭터를 그냥 필요할 때 인스턴스화하면 안 되는 걸까?
- 코드 의존성이 올바른 방향을 가리키게 하기 위해서입니다(안쪽으로 향하도록)
- 만약 유스케이스가 영속성 어탭터를 호출해야 하고, 스스로 인스턴스화한다면 의존성의 방향이 잘못된것이다.
- 해당 이유가 바로 아웃고잉 포트 인터페이스를 구현한 이유입니다.
- 유스케이스는 인터페이스만 알아야 하고, 런타임에 이 인터페이스의 구현을 제공받아야 한다.
- 이러한 스타일의 부수효과로는 한 클래스가 필요로 하는 모든 객체를 생성자로 전달할 수 있다면 실제 객체 대신 목으로 전달가능 하며 격리된 단위 테스트를 생성하여 코드 테스트가 쉬워집니다.
설정 컴포넌트
그럼 우리의 객체 인스턴스를 생성할 책임은 누구에게 있는지, 그리고 어떻게 의존성 규칙을 어기지 않으면서 그렇게 할 수 있을지에 대해 알아보겠습니다.
해답은 그림 9.1 처럼 아키텍처에 대해 중립적이고 인스턴스 생성을 위해 모든 클래스에 대한 의존성을 가지는 설정 컴포넌트가 있어야 합니다.
그림과 같이 중립적인 설정 컴포넌트는 인스턴스 생성을 위해 모든 클래스에 접근할 수 있습니다
2장에서 소개한 클린 아키텍처에서 이 설정 컴포넌트는 의존성 규칙에 정의된 대로 모든 내부 계층에 접근할 수 있는 원의 가장 바깥쪽에 위치합니다.
그리고 설정 컴포넌트는 우리가 제공한 조각들로 애플리케이션을 조립하는 것을 책임집니다.
이 컴포넌트는 다음과 같은 역할을 수행해야 합니다.
- 웹 어뎁터 인스턴스 생성
- HTTP 요청이 실제로 웹 어뎁터로 전달되도록 보장
- 유스케이스 인스턴스 생성
- 웹 어댑터에 유스케이스 인스턴스 제공
- 영속성 어댑터 인스턴스 생성
- 유스케이스에 영속성 어댑터 인스턴스 제공
- 영속성 어댑터가 실제로 데이터베이스에 접근할 수 있도록 보장
이처럼 설정 컴포넌트의 역할은 책임이 굉장히 많습니다.
이것은 어찌보면 단일 책임 원칙을 위배하는 것이기도 합니다.
그러나 나머지 애플리케이션 부분을 깔끔하게 유지하기 싶다면 이처럼 구성요소들을 연결하는 바깥쪽 컴포넌트가 필요합니다.
평범한 코드로 조립하기
애플리케이션을 조립할 책임이 있는 설정 컴포넌트를 구현하는 방법은 여러가지가 있습니다.
만약 의존성 주입 프레임워크의 도움 없이 애플리케이션을 만들고 있다면 평범한 코드로 다음과 같은 컴포넌트를 만들 수 있습니다.
package com.book.cleanarchitecture.buckpal;
import com.book.cleanarchitecture.buckpal.account.adapter.in.web.SendMoneyController;
import com.book.cleanarchitecture.buckpal.account.adapter.out.persistence.AccountPersistenceAdapter;
import com.book.cleanarchitecture.buckpal.account.application.port.in.SendMoneyUseCase;
import com.book.cleanarchitecture.buckpal.account.application.service.SendMoneyService;
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입니다.
스프링의 클래스패스 스캐닝으로 조립하기
스프링 프레임워크를 이용해서 애플리케이션을 조립한 결과물을 애플리케이션 컨택스트라고 합니다. 애플리케이션 컨텍스트는 애플리케이션을 구성하는 모든 객체(자바 용어로 bean)을 포함합니다.
스프링은 클래스패스 스캐닝으로 클래스패스에서 접근 가능한 모든 클래스를 확인해서 @Component 에너테이션이 붙은 클래스를 찾습니다.
스프링이 인식할 수 있는 커스텀 애노테이션도 생성 가능합니다.
package com.book.cleanarchitecture.buckpal.shared;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface UseCase {
@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을 통해 스캐닝을 가지고 찾아야 하는 설정 클래스임을 표시합니다.
- 이건 모든 빈을 가져오는 대신 설정 클래스만 선택하기 때문에, 마법이 일어날 일은 적습니다. (위에서 말한 발견하기 어려운 에러)
- 특정 모듈만 포함하고, 그 외의 다른 모듈의 빈은 모킹해서 애플리케이션 컨텍스트를 만들 수 있습니다. (테스트에 큰 유연성)
- @Component 에너테이션을 붙이는 것을 강제하지 않아 애플리케이션 계층을 스프링 프레임워크에 대한 의존성 없이 유지가 가능합니다.
단점
- 설정 클래스와 같은 패키지에 넣어놓지 않는 경우에 public으로 해야 합니다.
- 패키지를 모듈 경계로 사용하고 각 패키지 안에 전용 클래스를 만들 수 있지만, 하위 패키지를 사용할 순 없습니다(10장)
iOS 의존성 주입
SwInject
Swinject는 의존성 주입 프레임워크입니다. (pure, needle 등 다른 의존성 프레임워크도 존재)
Swinject는 앱을 느슨하게 결합된 구성 요소로 분할하여 더 쉽게 개발, 테스트 및 유지 관리할 수 있도록 도와줍니다.
Swinject는 Swift 일반 유형 시스템과 앱의 종속성을 간단하고 유창하게 정의하는 일급 함수로 구동됩니다.
Swift에서도 마찬가지로 의존성 주입은 Class 외부에서 해야합니다.
보통 앱의 설정을 관리해주는 파일(AppDelegate와 같은)에서 의존성 주입을 실행합니다.
개념은 쉽게 다음과 같습니다.
1. 의존성을 주입해주는 큰 틀인 컨테이너를 생성
2. register를 이용해 해당 컨테이너에 주입시켜줄 의존성의 서비스를 등록
3. resolve를 이용해 사용할 서비스의 인스턴스를 가져온다
import Foundation
import Swinject //의존성 주입 라이브러리
struct AppContainer {
private init() {}
static let shared: Container = {
let container = Container()
container.register(FirstViewModel.self) { r in FirstViewModel() }
container.register(SecondViewModel.self) { r in SecondViewModel() }
container.register(FirstViewController.self) { r in
let controller = FirstViewController()
controller.viewModel = r.resolve(FirstViewModel.self)
return controller
}
container.register(SecondViewController.self) { r in
let controller = SecondViewController()
controller.viewModel = r.resolve(SecondViewModel.self)
return controller
}
}
}
코드에서 register를 이용해 container에 firstViewModel, secondViewModel 서비스를 등록합니다.
그리고 FirstViewController에서 resolve를 활용해 사용할 서비스인 FirstViewModel의 서비스를 가져옵니다.
이런식으로 하나의 컨테이너를 만들고 여러 의존성을 주입시키고 사용함으로써 앱 전체의 의존성을 편리하게 관리할 수 있습니다.
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
클래스패스 스캐닝은 아주 편리한 기능입니다.
스프링에게 패키지만 알려주면 거기서 찾은 클래스로 애플리케이션을 조립하기 때문에 애플리케이션 전체를 고민하지 않고도 빠르게 개발할 수 있습니다.
하지만 코드의 규모가 커지면 금방 투명성이 낮아집니다. 어떤 빈이 애플리케이션 컨텍스트에 올라오는지 정확히 알 수 없게 되고, 또 테스트에서 애플리케이션 컨텍스트의 일부만 독립적으로 띄우기가 어려워집니다.
반면 애플리케이션 조립을 책임지는 전용 설정 컴포넌트를 만들면 애플리케이션이 이러한 책임으로부터 자유로워 집니다.
이 방식을 이용하면 서로 다른 모듈로부터 독립되어 코드 상에서 손쉽게 옯겨 다닐 수 있는 응집도가 매우 높은 모듈을 만들 수 있습니다.
하지만 늘 그렇듯이 설정 컴포넌트를 유지보수하는데 약간의 시간을 추가로 들여야 합니다.
-> 상황에 맞는 방법을 선택해라
'Clean Architecture' 카테고리의 다른 글
[Clean Architecture] iOS Repository Pattern이란? (0) | 2023.10.04 |
---|---|
[Clean Architecture] iOS 클린 아키텍처 실제 프로젝트 3: DI(의존성 주입) (0) | 2023.06.12 |
Coodinater Pattern 정의 및 예제(1) (0) | 2023.05.08 |
클린 아키텍쳐 정리 및 예제 (0) | 2023.04.27 |