의존성 주입과 관련해서
고민 도입
우리는 8월 14일에 의존성 주입과 관련해서 토론을 나누었었다. 그래서 결과적으로 서비스의 의존성은 컨트롤러에 주입하고 컨트롤러에 생길 수 있는 작은 비지니스 로직은 우리의 서비스 환경에서는 크게 문제될 정도가 되지 않을 것이기 때문에 괜찮을 것이라 생각하고 넘어갔었다.
하지만 오늘 우리 팀은 한 가지 문제에 봉착했다. Post 요청과 관련해서 transaction을 할 때 발생한 문제인데, 우리의 모듈 중 Booth 모듈의 경우에는 여러 서비스로부터 주입을 받아서 작동할 수 있도록 구현이 되어있다. 다음은 매우 간단한 booth controller의 의존성 주입 그림이다.
repository의 service 귀속화
우리 팀은 DB에 접근할 수 있도록 하는 통로를 단 하나만 만들기로 했고 그로 인해 repository 선언은 무조건 해당 service에만 해두었다. (entity당 단 한 번의 선언이 있는 것) 해당 문제에 관한 생각은 잠시 해보았는데, typeORM에서는 Entity를 마치 객체처럼 다루기 때문에 만약에 내가 repository를 동일한 entity에 대해서 2개를 생성한다는 것은 마치 복사본 두개를 들고 있는 것과 같은 불안정한 상황이 발생할 수 있기 때문에 하나만 선언하는 것이 올바르다고 생각을 했다. 일관성을 해칠 수 있기 때문이다. 마치 cpu에 여러개의 코어가 서로다른 캐시에 같은 데이터에 대한 정보를 서로 다르게 가지고 있어 발생하는 문제와 같은 상황 말이다. 구글링을 통해 시원한 답변이 나오지 않아 GPT에게 해당 질문을 물어보았고 적당한 답변을 받았다.
문제 상황
문제는 여기에서 발생했는데, transaction을 하기 위해서는 여러 entity에 접근할 수 있는 상황에서 코드를 작성할 수 있어야 하는데, 우리의 구조에서는 그것이 가능한 곳이 controller 밖에 없었다. 왜냐하면 service를 주입을 받아야 해당 module에 관련된 repository를 이용할 수 있게 되는데, service 끼리는 주입을 하지 않았고 컨트롤러에 만 service를 주입받았기 때문이다. 만약에 service끼리 주입을 하는 구조였다면 transaction을 할 때 다른 service로부터 주입을 받아 transaction을 처리할 수 있었을 것이다.
그래서 우리는 처음에는 controller에서 transaction 코드를 작성해보기 시작했다. 이렇게 되니 비지니스 로직이 controller에도 있고 service에도 있는 상황이 연출되었는데, 여기서 디자인 적으로 이런식으로 구현을 해도 되는지가 궁금해졌다. 그래서 몇 가지 자료를 찾아보았다.
MVC 패턴
https://m.blog.naver.com/jhc9639/220967034588
MVC패턴은 알고 있지 못한 개념이었지만 NestJs 프레임워크를 사용하면서 은연중에 친숙해진 개념이었다. 소프트웨어 디자인 패턴 중 하나인데, 모델, 뷰, 컨트롤러로 프로젝트의 구성요소를 나누는 것이다.
NestJs로 치면 controller는 그대로 controller이고 model은 service, view는 프론트 정도가 될 것 같다.
모델 - 어플리케이션의 정보, 데이터를 나타내고 데이터베이스에서 정보를 읽고 쓰는 등의 일 처리를 해준다. 몇 가지 규칙이 있는데 첫째, 사용자가 편집하길 원하는 모든 데이터를 가지고 있어야한다. 둘째, 뷰나 컨트롤러에 대해서는 어떤 정보도 알지 말아야 한다. 셋째, 변경이 일어나면 변경 통지에 대한 처리방법을 구현해야한다. 이 세번째의 경우가 잘 이해되지 않았는데, 너무 깊게 이해하진 않아도 될 것 같다.
뷰 - 사용자들이 볼 수 있는 화면. 모델과 특징은 거의 비슷하며 다른 부분은 첫째 부분으로, 모델이 가지고 있는 정보에 대해서 따로 저장하면 안된다는 것이다.
컨트롤러 - 데이터와 사용자인터페이스를 이어주는 다리. 모델과 뷰를 이어준다고 생각하면 편하다.
왜 MVC 패턴을 사용해야하는가?
사용자가 보는 페이지, 데이터처리, 그리고 이 2가지를 중간에서 제어하는 컨트롤, 이 3가지로 구성되는 하나의 애플리케이션을 만들면 각각 맡은 바에만 집중을 할 수 있게 된다. 공장에서도 하나의 역할들만 담당을 해서 처리를 해서 효율적이게 된다. 여기서도 마찬가지이다.
서로 분리되어 각자의 역할에 집중할 수 있게끔하여 개발을 하고 그렇게 애플리케이션을 만든다면, 유지보수성, 애플리케이션의 확장성, 그리고 유연성이 증가하고, 중복코딩이라는 문제점 또한 사라지게 된다.
NestJs 구조
Nest는 그렇다면 어떤 구조를 가지고 있을까?
Module - 기본적인 기능들의 덩어리인 Module이 존재한다. 예를들어 예약 기능을 모아둔 모듈, 주문 기능을 모아둔 모듈 이런식으로. 그래서 모듈은 기능의 집합 이라고 보면 좋다.
Controller - ‘Controllers are responsible for handling incoming requests and returning responses to the client.’ 클라이언트가 보낸 요청이 어떤 기능의 요청인지 확인하고 그에 해당하는 응답을 주어야 하는 것이 우리 서버의 역할이므로, 클라이언트의 요청을 해석하고 내가 원하는 길로 인도하는 것이 중요하다. 그것을 컨트롤러에서 해준다. 이후에 서버에서 연산이 완료된 응답 또한 컨트롤러에서 다시 올바른 길로 클라이언트에게 전달해준다. 라우팅을 해주는 것이다. 이 안에는 rest api를 기반으로한 handler들이 있고 이를 통해 클라이언트의 요청을 받는다.
Provider - nestjs에서 사용하는 개념. provider는 마치 service 와 동일한 개념으로 받아들여 질 수 있는데 조금 다르다. 공식 문서를 읽어본 결과 특정 기능과 로직을 가지고 있는 컴포넌트인데, 이를 주입할 수 있는 컴포넌트를 의미하는 것인듯 하다. 그래서 service 뿐만아니라 repository와 같은 주입이 가능한 객체들도 모두 provider가 될 수 있다. 핵심은 데이터나 기능, 로직을 가지고 “의존성 주입”이 가능하다는 것.
Service - service는 nestjs 뿐만 아니라 sw 개발 내 공통 개념이다. 데이터를 검사하거나 연산하는 등 DB와 함께 로직을 처리하는 부분을 의미한다.
스스로 이해
이제 학습을 했으니 적용을 해보자. 우리가 프로그래밍을 하면서 모듈화를 하는 이유는 유지보수의 용이성, 코드의 중복사용을 줄이고 재사용성 증대, 코드의 유연성 증가 등등이 있다. 이를 나는 어떻게 이해했냐면, 간단하게 “ 사실 Nest로 짜고 있는 우리의 모든 코드 - controller, service 등등 - 마치 알고리즘 공부할 때처럼 모든 로직을 한 파일안에 다 넣고 한 함수안에 다 넣어서 만들어도 기능이 달라질까? “ 라고 생각을 해보았을 때, 대답이 “ 아니다 “ 라고 나오는 것부터 시작했다. 내가 만약에 예약기능, 주문기능, 부스기능 등을 모두 한 파일안에 모아두고 main.ts라고 이름을 붙였다면, 예약기능에 오류가 발생했을 때 해당 코드를 모두 뒤져보아야 하는 상황이 발생한다. 물론 자주 이 코드를 본 사람이거나 코드를 직접 작성한 사람에 경우에는 상관이 없겠지만 한 코드의 유지보수를 한 팀이 영원히 한다는 보장은 없다. 이는 굉장한 피로감을 안겨줄 것이다. 하지만 모듈화가 되어 있다면 코드의 각 부분들이 자신의 기능에 따라 나누어져 있으므로 오류를 특정하고 대처하는 능력이 증가할 수밖에 없다. 그리고 단순한 로직이지만 자주 사용되는 로직에 경우 계속 작성하는 것보다 컴포넌트간의 주입을 통해서 같은 코드의 재사용성을 늘릴 수 있고, 이것은 코드 작성 능률을 올려줄 뿐만아니라 코드를 읽고 이해하는 데에도 좋은 영향을 준다.
이런 측면에 바라 보았을 때, 과연 transaction 코드를 controller에 적용하는 것이 맞냐라고 생각해본다면 나는 해당 구조에 회의적이고 지양해야하는 디자인이라고 생각했다. 그래서 서로 주입할 수 있는 서비스간의 주입이 nest에서 의도하고 있는 바라고 생각해서 코드를 리펙토링하자는 의견에 도달했는데, 우리는 조금 더 학습을 한 뒤 controller에 service를 주입하는 직관성을 챙기며 controller에 비지니스 로직이 생기지 않게 하는 절충안을 찾았다.
고민 해결
컨트롤러에 직접 모든 서비스를 주입하는 것이 아니라, 컨트롤러 아래에 서비스를 주입받는 서비스를 따로 두는 구조를 채택하기로 했다.