기존 구조와 문제점
현재 내 프로젝트에서, 백엔드 구조는 다음과 같다.
현재 Graphql 을 사용하고 있고, 외부에서 요청이 들어오면 이를 Resolver에서 받아서 알맞는 Service 로직을 호출한다. (REST API 에서 Controller의 역할을 한다) 이후에 Service 로직에서는 비즈니스 로직을 수행하고 필요한 데이터를 얻기 위해 Repository에 접근하며, Repository에서는 DB에 접근한다.
지금은 수정된지 오래되어 꽤 오래전 코드지만, 처음에는 이렇게 구현되어 있었다.
UserService에서는 의존하고 있는 UserRepository를 그냥 Import 해서 Static 으로 사용한다(당시에는 TypeORM의 Data Mapper 대신 Active Record 패턴을 사용하고 있었다)
만약 이 상태에서 UserRepository(MySQL을 사용하는)가 모종의 이유로 In-Memeory User Repository가 되어야 한다면 어떻게 될까? MemoryDB를 다루는 UserRepository를 MemoryUser 라고 정한다면 다음과 같이 코드가 전부 바뀌어야 할 것이다.
물론 Import 할때 as User 를 사용해서 코드의 변화를 좀 줄일 순 있겠지만, 근본적으로 User의 변화 하나때문에 다른 의존관계에 있는 모든 클래스에 들어가서 코드를 다 바꾸어야 한다는 단점이 있다. 이는 유지보수하기에 매우 치명적인 단점이며, 객체지향 프로그래밍의 중요한 5가지 원칙 중 DIP(Dependency Inversion Principle), OCP(Open-Closed Principle)를 위반한다.
첫번째로, 의존관계 주입(DI)
여기서 의존관계 주입(Dependency Injection) 패턴을 사용하면 이 변화를 줄일 수 있다.
이를 실현하기 위해 먼저 내 코드에서 Active Record 패턴을 모두 Data Mapper 패턴으로 바꾸는 작업과 정적으로 사용하던 메서드들을 동적으로 사용하게 하는 작업이 필요했다. 그러나 이 포스트에서 핵심적인 내용은 아니므로 생략한다.
이런 식으로 Service 로직에서는 UserRepository라는 인터페이스에 의존시킨다. 그리고 이 클래스를 사용하는 곳에서 생성자를 통해 구현체를 주입받는 것이다. 이러면 UserService는 구체적인 구현체보다 추상적인 인터페이스에 의존하게 된다. DIP를 어느정도 지켰다고 할 수 있는 것이다.
사실 여기까지가 DI의 끝이다. DI는 말은 거창하지만, 사실 클래스 생성할때 인자를 넘겨주는 것만으로 충분하다고 생각한다. DI 가 조금 더 특별한 이유는 UserService 클래스의 인터페이스를 만들 때 알 수 있다. 상단 코드에서는 하지 않았지만 UserService 라는 인터페이스를 만든다면, UserRepository에 관한 정보를 전혀 담지 않을 수 있다. '추상화를 해치지 않고 의존성을 넘겨주는 방법'이 DI인 것이다.
그런데 아직도 문제가 남아있다. UserService는 바꾸지 않아도 되지만, UserService를 생성하는 과정에서 결국 사용할 Repository를 생성자에서 넘겨주어야 한다. Repository를 바꾼다면 UserService의 생성자의 인자를 바꾸어야 하는 것이다.
두번째로, IoC Container(TypeDI) 도입
여기에 사실 문제가 하나 더 있는데, 해당 UserSerivce 구현체를 주입해줄 곳이 없다는 점이다. 만약 필요하다고 해서 아무데서나 구현 객체를 생성하면 이는 SRP 위반이 된다. 이를 해결하기 위해 구현객체를 자동으로 생성 및 연결해주는 IoC Container로써 TypeDI를 설치했다.
자동으로 객체를 생성할 클래스 위에 @Service(name) 데코레이터를 붙이면 TypeDI가 자동으로 해당 객체를 생성해준다. 이후에 Container.get<T>(name) 메서드를 통해 이를 주입해줄 수 있다. 이로써 원하는 곳에서 구현체를 주입받아 사용할 수 있게 되었다.
세번째로, 관심사의 분리
이제 진짜로 문제를 해결해보려고 한다. 아직 내 코드에서는 MySQLUserRepository에서 MemoryUserRepository로 바꾸기 위해 Container.get<T>(name) 에서 name 부분을 바꿔야 한다. 아직 OCP는 만족하지 못하고 있는 것이다. 이를 어떻게 해결해야 할지 생각해보고 다른 프레임워크도 찾아봤는데, Spring에서는 AppConfig 를 통해 의존관계를 어떻게 주입할지 설정해준다고 한다. 그래서 이를 모방하여 나는 프로젝트 src 폴더에 InjectionConfig.ts 파일을 생성하고 거기에서 관리해주려고 했다.
먼저 UserService 구현체에서는 (Memory or MySQL 대신) UserRepository를 가져오게 한다. 그리고 InjectionConfig.ts에서는 다음과 같이 작성한다.
이렇게 실제로 주입받을 의존성을 따로 InjectionConfig.ts에 전부 작성하고, 주입받을 때는 get과 함께 인터페이스의 이름을 사용하려고 했다..
이를 통해 만약 MemoryUserRepository 를 사용해야 할 경우 InjectionConfig.ts에서 getCustomRepository 를 new MemoryUserRepository로 바꾸어 주기만 하면 될 터였다..
하지만..
컨테이너 함수 호출 관련 에러에 봉착했다. 추측으로는 Entity들 사이 관계 때문에 컨테이너 등록 순서가 꼬여서 이런 에러가 발생한 것 같은데.. 몇시간 삽질했지만 결국 해결할 수 없어서 결국 실패로 끝났다 ㅠㅠ
관련해서 TypeDI Repository에 질문이나 이슈를 남기려고 했지만..
결론
NestJs의 도입이 시급하다
그 뒤에 다른 내용에 대해 검색하다가 다음과 같은 이슈를 확인할 수 있었다.
나와 같은 에러였는데, 이러한 답변이 있었다.
혹시나 하고 0.8.0으로 다운그레이드 해보았더니... 잘 된다!!!!
Reference
https://medium.com/sjk5766/dependency-injection-ioc-dip-ioc-container%EC%A0%95%EB%A6%AC-310885cca412
'Projects > NPE' 카테고리의 다른 글
빠른 대응을 위한 실시간 모니터링/알림 툴 도입 (0) | 2022.01.07 |
---|---|
[GraphQL] Typescript+GraphQl+TypeORM 프로젝트에 TypeGraphQL 적용하기 (0) | 2021.11.08 |
[TypeORM] 쿼리빌더를 통해 데이터 가져오기 (0) | 2021.11.07 |
댓글