주 : 객체 지향 프레임워크에서 배울 점.
예전과 다르게 요즘은 마이크로서비스의 인기에 힘입어. 경량 프레임워크 상에서 개발을 지향하는 케이스가 점점 커지고 앞으로도 더 그렇게 될 것이라 생각한다. (serverless, fission, sam 등)
이런 프레임워크의 특징은 이미 있는 프레임워크(spring, nestjs)를 이용하기 보다는,
필요하면 그때그때 구축해서 사용하는 것을 지양하는데, 함수형이거나 클래스이거나 기능을 적절하게 나누는 것이 필요하다.
이를 단일 책임 원칙(Single Responsibility Principle)이라 하는데, 해당 클래스(혹은 함수 - 모듈)가 단 하나의 변경의 원인을 가져야 한다는 것이다.
더 나아가서 모듈이 잘 분리가 된 상태라면, 개발자가 기능을 추가하거나 변경함에 따라, 단 하나의 모듈만 수정하면 되어야 한다.
프론트엔드에서는 대놓고 컴포넌트(리액트 컴포넌트 등)라는 명을 사용하는데, 컴포넌트는 기본적으로 사용자가 수정이 불가능하지만, 그 자체로 완결성 있는 기능을 제공하는 모듈 단위다.
리액트 컴포넌트라면 마크업과 사용자 인터랙션 관점의 API를 제공하는게 목표가 되겠다.
스프링이나 nestjs가 제공하는 핵심 기능 중 하나는 Dependency Injection(DI)이다.
실제로 지금 참여중인 프로젝트에서는 Inversify.js를 이용해 DI를 구현한다.
DI의 장점은 패키지 단위의 의존성 관리가 가능하다는 점이다.
내 소스가 아닌 프레임워크로 의존성 관리를 넘겨버려, 내 코드에서는 의존성 사슬이 헥사고널(Hexagonal)한 모습이 된다.
마틴 파울러가 말하는 컴포넌트와 서비스
컴포넌트 : 컴포넌트 개발자가 제어할 수 없는 응용 프로그램에서 소스 수정 없이 사용하도록 의도된 소프트웨어 덩어리를 의미.
서비스 : 컴포넌트랑 비슷하지만 컴포넌트는 jar, dll, assembly, import 대상을 의미한다면, 서비스는 RPC, Socket, Web Service등 원격 인터페이스를 통해 호출되는 대상.
대부분의 사람들은 컴포넌트 = 라이브러리 = 패키지로 생각함.
서비스는 애플리케이션의 로직을 오케스트레이션 하는 계층. 혹은 웹 애플리케이션 전체로 생각함.
예제 : 인터페이스 도입
Lister와 Finder 객체가 등장함
Lister는 메모리에서 가져옴
Finder는 파일시스템에서 가져옴
// class MovieLister...
private MovieFinder finder;
public MovieLister() {
// 텍스트 파일에서 콜론으로 구분된 객체 정보를 가져옴.
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}
public interface MovieFinder {
List findAll();
}
다른 개발자가 DB, csv 등에서도 가져올 수 있게 기능 확장을 원함.
현재는 finder 객체는 코드에서 하나로 특정되어 있음.
finder 객체만 다른 걸로 바꿀 수 있으면, 나머지 메모리에서 처리하는 로직은 동일할 수 있지 않을까?
인터페이스 도입
해당 작업을 통해 컴파일 타임에 구현 클래스가 정해지는 것을 피할 수 있다.
다른 문제가 생겼다. 해당 구체 클래스를 어떻게 런타임에 연결할 것인가?
Inversion Of Control
IoC는 간단하게, "프레임워크가 대신 처리해준다." 라고 이해하면 된다.
예를 들어 UI프레임워크가 있다면,(ex 앵귤러) 우리는 앵귤러 코드를 작성한다.
그러면 실제 html, css, js에 관련한 복잡한 처리는 대부분 UI 프레임워크가 하게 된다.
IoC라는 용어는 상당히 일반적이다.
우리가 프레임워크 사용 시 필요한 IoC는 Dependency Injection이라 한다. 즉, Dependency Injection은 IoC의 일종이다.
(주, 마틴파울러와 동료들이 정의한 용어이다. 참고)
프레임워크가 필요한 의존성을 주입해주는 역할을 처리해주기 때문이다.
비슷한 IoC 방법 중 하나로 Service Locator Pattern이 있다.
정리하자면, DI와 Service Locator Pattern을 통해 프레임워크에 의존성 관리를 위임할 수 있다.
Dependency Injection의 형태
맨 처음 언급했다시피, 실제로 구체적인 클래스를 결정 및 주입하기 위해선, 해당 역할을 하는 클래스가 존재해야 한다.
이는 스프링이나 nestjs가 대신해주지만, bean을 등록하는 일은 어쨌든 어디선가 해야한다.
의존성을 주입하는 방법은 3가지가 있다.
type3이 Best Practice로 여겨지기 때문에, type3만 알아둬도 된다.
type1은 프레임워크 없이 의존성 주입 테스트를 할 수 없다.
type2는 런타임에 구현체가 바뀔 수 있다. 객체는 불변으로 유지하는 것이 좋다. (사이드이펙트 방지)
type 1 IoC (interface injection)
type 2 IoC (setter injection)
type 3 IoC (constructor injection).
DI : type 3
마틴파울러는 자기들이 만들었으나 지금 아무도 모르는 PicoContainer라는 예시로 설명한다.
class MovieLister
해당 클래스는 컨테이너에 의해 관리된다.
해당 클래스를 등록해준다.
컨테이너는 해당 클래스에 어떤 의존성을 넣을 지 결정한다.
// class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder
물론 어떤 파인더 클래스를 넣을지 선언해줘야 컨테이너가 알 수 있다.
아래 클래스도 같이 등록해준다.
// class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
아래와 같이 컨테이너에 각 인터페이스에 대한 구현 클래스를 등록해준다.
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
생성자 주입과 AppConfig, Annotation 기반 빈 등록은 스프링 사용자들은 너무도 잘 알고있다.
DI : type 2
아래 글 참조.
Spring Setter Dependency Injection Example - amitph
DI : type 1
마틴파울러는 망하고 없는 Apache의 Avalon 이라는 프레임워크로 해당 기능을 설명한다.
1. 의존성 주입을 위한 인터페이스를 선언한다.
2. 해당 인터페이스를 호출해 의존성을 주입하기 위한 인터페이스를 선언한다.
3. 컨테이너에서 사용할 클래스(빈)는 1,2를 구현한다.
1. 의존성 주입을 위한 인터페이스 선언.
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
2. 인터페이스 구현 클래스 생성
class MovieLister implements InjectFinder
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
Lister 외에 Finder에도 비슷한 작업을 해줌.
public interface InjectFinderFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename...
public void injectFilename(String filename) {
this.filename = filename;
}
3. 인터페이스를 구현한 클래스들을 이용해 등록하는 방법.
// class Tester...
private Container container;
private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
// 인터페이스를 구현한 클래스들
// 조회키를 통해 컴포넌트 등록
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
// 클래스에 의존성 주입.
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
4. 실제로 의존성 주입 인터페이스를 호출해 줄 인터페이스를 선언
public interface Injector {
public void inject(Object target);
}
class ColonMovieFinder implements Injector...
public void inject(Object target) {
((InjectFinder) target).injectFinder(this);
}
class Tester...
public static class FinderFilenameInjector implements Injector {
public void inject(Object target) {
((InjectFinderFilename)target).injectFilename("movies1.txt");
}
}
5. 실제 테스트.
class Tester…
public void testIface() {
configureContainer();
MovieLister lister = (MovieLister)container.lookup("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
즉, 위 방법은 컨테이너가 injection 인터페이스를 통해 종속성을 파악 및 주입하는 것이다.
Service Locator
의존성 주입의 주요 이점은 구체 클래스에 대한 종속성을 내 코드에서 없앨 수 있다는 것이다.
다른 사람들이 잘 모르는 방법은 Service Locator 패턴이 있다.
Service Locator는 애플리케이션에 필요한 모든 서비스에 접근하는 방법을 알고 있다.
따라서 다른 모든 서비스들은 해당 컴포넌트만 알면 된다. redux와 비슷한 개념이다.
생성자 주입 등의 방법 대신, ServiceLocator를 싱글턴 레지스트리로 활용한다.
// class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
// class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
ServiceLocator에서 실제 클래스를 주입해준다.
// class Tester...
private void configure() {
ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));
}
// class ServiceLocator...
public static void load(ServiceLocator arg) {
soleInstance = arg;
}
public ServiceLocator(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// class Tester...
// 실제 테스트 코드
public void testSimple() {
configure();
MovieLister lister = new MovieLister();
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
ServiceLocator.load 메서드를 통해 가짜 객체를 이용, 얼마든지 테스트할 수 있다.
ServiceLocator는 하나의 서비스 레지스트리이기 때문에 몇개고 다양하게 선언하여 사용할 수 있다.
(React Context를 생각하면 된다)
// 인터페이스 기반 롤 리듀싱도 가능.
public interface MovieFinderLocator {
public MovieFinder movieFinder();
}
MovieFinderLocator locator = ServiceLocator.locator();
MovieFinder finder = locator.movieFinder();
결론 :
해당 글을 정독한 이유는, react의 의존성 관리를 의해 DI를 사용하는 케이스를 찾아보기 위해서였다.
DI를 하려면 컨테이너가 필요한데, 여러 구루들의 프로젝트와 npm에서 di 컨테이너를 적극적으로 활용하며 리액트를 개발하는 경우는 발견하지 못했다.
DI는 의존성 관리에 있어서는 정말 괜찮은 도구라고 생각하기에, 다른 방법이 없나 찾아보다가,
Service Locator를 Context로 생각하면, 꽤 괜찮은 해법이 되지 않을까 해서 여기에 남겨둔다.
참고 : Inversion of Control Containers and the Dependency Injection pattern (martinfowler.com)
'BackEnd' 카테고리의 다른 글
비즈니스 이벤트(도메인 이벤트)를 통해 도메인 이해하기 (0) | 2022.03.17 |
---|---|
공유 모델의 중요성(The Importance of a Shared Model) (0) | 2022.03.17 |
JS OOP 시리즈 3 : 자바스크립트 데코레이터 이해하기 (0) | 2022.01.17 |
JS OOP 시리즈 2 : 프록시를 이용한 vue3 반응형 동작 원리 살펴보며 AOP 이해하기. (0) | 2022.01.17 |
JS OOP 시리즈 1 : 메타 프로그래밍과 Proxy, Reflect 간단하게 알아보기 (0) | 2022.01.17 |