Nest.js로 Domain과 Design 주도 API 설계하기

goddoro
9 min readMay 7, 2023

--

안녕하세요 개발자 doro입니다.

Photo by Anders Jildén on Unsplash

초기 Duckie API서버를 구축할 때 가장 많이했던 고민은 변화에 흔들리지 않으면서도, 어떻게 하면 재사용에 용이한 클린 아키텍쳐를 적용할 수 있을까 였습니다.

MVP는 기능을 빠르게 추가할 수 있어야 하면서도 이전에 개발해놓은 API는 영향을 받지 않아야 하기 때문에, 서버 아키텍쳐가 각 레이어별로 관리가 되어야만 했고, 이는 여러 클라이언트에서 사용할 수 있는 서버의 역할을 수행할 수 있게 해주었습니다.

Clean Architecture

특히 클라이언트와 맞닿아있는 Controllers와 Application layer의 역할을 분명하게 나눠주고 적절하게 Domain layer의 컴포넌트들을 가져다 쓰는 구조는 관리포인트를 줄여줄 수 있었고 코드 몇자의 수정만으로 기획에 걸려있는 동일한 피쳐에 대응할 수 있었습니다.

Single Response Controller

그렇다면 먼저, Controller는 어떻게 정의했을까요?

Controller는 클라이언트의 플랫폼, 버전에 따라서 각각 다른 도메인 로직으로 분기 처리를 해주는 API 서버의 Gateway로 정의했습니다. 또한 엔드포인트마다 내려주는 응답 형태를 정제해주는 곳이며 비즈니스 로직이 존재하지 않습니다.

예를 들어, 카카오 로그인은 web과 mobile에서 처리해주는 비즈니스 로직이 다릅니다. web은 login_code 를 전달하고 mobile은 access_token 을 넘겨주기 때문인데요. 각각의 카카오 API를 이용해 받은 데이터를 가지고 Duckie내부에서 공통 로직을 처리해줍니다.

Duckie API Layer

만약, 이후에 구글 로그인을 구현한다는 기획에 대응을 할 때, Application Layer 안에 새로운 Service만 추가해준다면 다른 레이어의 컴포넌트들은 크게 수정할 필요가 없는 설계입니다. Controller가 중간에 버퍼 역할을 해주며 클라이언트별로 비즈니스 로직을 분기 처리해주고 Domain Layer로 필요한 데이터만 넘겨주고 있기 때문입니다.

그렇다면 Controller의 역할이 더 늘어나는 경우는 어떤 경우일까요? 가장 흔한 상황은 클라이언트의 버전별로 응답 형태를 다르게 나눠줘야 하는 경우입니다. 또는 같은 클라이언트지만 UI가 다른 경우에도 해당되는데요. (Duckie는 해당하지 않지만) Android TV나 tvOS같은 경우에도 device 정보를 받아서 응답값을 분기처리 해줘야 합니다.

위의 예시는 Android, iOS 모두 특정 버전별(Android는 2.0.3, iOS는 1.0.4)로 응답값을 분기해줘야할 수도 있기 때문에, Controller에는 도메인의 비즈니스 로직이 들어가면 객체지향의 SRP원칙을 위배하게 되는셈이죠.

이렇듯 controller는 어느 로직으로 태울지 방향만 정해주기에도 바쁜 컴포넌트이므로 해당 역할에 집중할 수 있게 로직을 분산해서 관리해줘야 합니다.

Domain Driven Application Service

Application Layer의 Application Service는 어떤 역할을 가지고 있을까요?

Application Service는 사용가능한 하나의 usecase입니다. 유저 입장에서 서비스내에서 행해지는 로직의 단위로 구분되어지고 다른 usecase에서도 재활용이 가능해야 하는데요. Application Service를 설계할 때는 도메인을 잘 이해해야만 관리 포인트를 줄이는 코드를 작성할 수 있습니다.

예시를 하나 들겠습니다.

제출하기 버튼은 되돌릴 수 없어요

유저가 덕력고사를 모두 응시한 후에 제출(채점)을 하려고 할 때, API서버에서는 어떤 작업들을 처리해줘야 할까요?

  • 덕력고사 시험의 상태 채점 완료됨으로 변경
  • 덕력고사 문제 채점
  • 유저의 덕력 올려주기
  • 덕력고사의 응시 수 올려주기
  • 출제자에게 FCM 이벤트 보내주기

입니다. 총 5가지의 작업을 처리해줘야 하는데요. 이런 작업들은 하나의 transaction으로 묶여야합니다. 비즈니스 로직 중간에 Throw가 돼서 덕력고사는 채점되었는데 덕력고사의 응시 수는 반영이 안되면 문제니깐요.

덕력고사 채점하는 Application Service

위의 usecase는 구성요소와 비즈니스 로직이 명확합니다. 그 이유는 이미 존재하는 기능들을 가지고 채점하는 usecase를 만들었기 때문인데요. 설계부터 재활용을 했기 때문에 깔끔한 구조와 코드를 작성할 수 있었습니다. 다른 예는 어떨까요?

덕력고사 만들기입니다.

문제만들기 플로우

덕력고사를 만들기 위해서는 시험 자체의 메타데이터(제목, 태그, 썸네일 등)와 덕력고사 문제들이 필요합니다. 먼저 시험 객체를 만들어주고 이후에 문제 객체를 만들고 외래키로 관계 형성을 해주는 식이죠. 문제 만들기 Application Service의 구조는 어떻게 될까요?

초기의 ExamProbelmService

초기에는 CreateProblemsService를 만들어 유저에게 입력받은 문제를 하나의 service에서 반복문을 이용해 만들어주는 또 하나의 application service를 만들어서 처리해주었습니다.

그런데 덕력고사에는 초기 생성 이후에도 덕력고사에 문제를 추가할 수 있어야하고, 덕퀴즈가 생김으로써 문제 생성이 또 하나의 usecase로 존재해야만 했습니다.

New ExamCreateService

CreateProblemService로 리팩토링을 해줌으로써 단일 문제를 생성할 수 있게 usecase로 만들어줬고 다른 Application Service에서도 재사용할 수 있었습니다. 도메인 지식을 더 잘 녹여낸 구조라고 할 수 있죠.

Design Originated Response

Duckie의 API 구조는 Domain layer와 Application layer의 코드를 완전히 분리 했기 때문에, 비즈니스 로직에서 사용되는 객체와 User-level에서 사용되는 객체는 DTO를 이용해 서로 연결해 아키텍쳐를 구성하고 있는데요.

이 때 단순히 하나의 Mapper Class에 의존하게 되면 다음과 같은 문제가 발생합니다.

프로필 화면 / 명예의 전당 화면 / 홈 화면

모두 User 객체가 필요한 화면들인데요.

Duckie는 SNS 성격이 강한 서비스 이므로 유저의 정보를 노출해줘야 하는 피쳐가 많습니다. 프로필에서 유저의 정보를 확인할 수 있으며, 덕력고사 출제자의 닉네임을 표시 해줘야하고, 명예의 전당에 등록도 해줘야 합니다.

이를 하나의 UserResponse 객체로 클라이언트에게 응답을 내려준다면 위의 세 화면에 대해서 User 엔티티에 걸려있는 모든 relations 을 매 API마다 호출 해줘야 합니다. 이는 API 성능상 매우 떨어질 수 밖에 없고, 또한 클라이언트 개발자 입장에서도 필요없는 필드들이 응답으로 내려가게 되므로 deprecated field 를 잘못 사용하게 되어 하위호환의 문제가 발생할 수도 있습니다.

가장 큰 문제는 dependency가 매우 많아지게 되므로 조그마한 변화에도 에러를 발생시킬 수 있는 여지가 매우 크다는 것입니다.

Design Dependency DTO

그래서 고민끝에 내린 결론은

디자인 컴포넌트 기준으로 API서버의 response를 만들어 클라이언트에게 응답해주자

였습니다. 초기 설계는 데이터 관점에서 빈틈없는 로직을 설계해나가자 였는데요. 하지만 Application Layer는 도메인 관점에서 설계하더라도 클라이언트가 직접 사용하는 응답 객체는 디자인 관점으로 기준을 잡는게 훨씬 명확한 기준이었습니다.

User Response 종류

위의 객체들은 서로 다른 디자인을 가지고 있는 유저 객체들에 대한 다른 응답 객체인데요. 이는 다른 도메인에서의 변경점에서부터 서로가 독립적으로 관리될 수 있음을 의미합니다.

예를 들어,

랭킹 페이지에 있는 유저 닉네임을 빼자

라는 새로운 기획이 생긴다면 RankingUserResponse만 수정하면 되고 유저 객체들은 전혀 영향을 받지 않는 구조인것 이죠. 또한 같은 디자인에 대한 응답 객체이므로 기획의 변화에도 빠르게 대응할 수 있습니다.

위의 두 페이지는 서로 다른 화면이지만 그 안에서 사용되는 유저 객체는 같은 디자인을 사용하고 있습니다. 이런 경우 FollowUserResponse 하나만 수정을 해도 저 두 개의 화면에 모두 대응할 수 있다는 장점도 있습니다. 이는 객체지향의 개방폐쇄원칙을 정확하게 지키고 있음을 알 수 있습니다.

마치며

아키텍쳐링에서 가장 중요한 것은 도메인 지식을 어떻게 서비스에 녹여내며 오버 엔지니어링을 피할 수 있냐를 고민하는 것입니다. 컴포넌트의 역할을 너무 작게 나누다보면 컴포넌트간 응집도가 너무 떨어져 관리 포인트가 많아지게 되고 한 클래스에 많은 역할을 넘겨주다보면 결합도가 높아져 변화에 대응하기 힘들어지기 마련이죠.

서비스에 녹여내야하는 도메인이 추가될 때마다, 현재의 아키텍쳐를 의심하고 리팩토링을 고려하며 코드 퀄리티를 신경써야합니다. 이런 점들을 고려하며 아키텍쳐링을 신경쓰며 MVP를 구현했고 점차적으로 기능 확장에 따른 새로운 구조나 마이크로 서비스를 고려하고 있습니다.

감사합니다.

--

--

goddoro
goddoro

Written by goddoro

TVING에서 동영상 플레이어를 개발하고 있습니다

No responses yet