서론

지난 한 달 동안 앱을 기획하고 개발해서, 구글 플레이 스토어 출시를 앞두고 있다. 이 과정에서 '기획' 업무를 경험하며 느낀 점을 정리해보려고 한다. (서비스명은 모밋(mommit)으로,  일상을 기록하고 원하는 경우 친구, 가족과 공유할 수 있고, N년전 오늘, 지금 내 위치에서의 지난 기록을 되돌아볼 수 있다.) 


본론

세부사항이 누락되었다

현업에서 백엔드 개발자로 일하면서, 기획안이 나오고 나면 종종 이런 생각을 했다.

  • 이런 케이스는 왜 기획서에 미리 정의가 안되어있지?

위의 케이스들은 물론 기획 리뷰 또는 슬랙을 통해서 공수 산정 이전에 해결했었다. 지금 와서 생각해보면 부끄럽지만, 당시에는 기획팀의 일을 개발자가 대신 해준다는 느낌으로 생각했던 것 같다.

 

새 서비스를 기획하며

pain point 를 정의하고, 내 서비스가 어떻게 이를 해결할 수 있는지를 정했다. 그 다음으로 요구사항을 정리했다. 이 과정을 진행하면서, 특별히 집중하고 고민했던 것은 구현 상세가 아니었다.

  • 무엇을 만들고, 무엇을 만들지 않을 것인가
    • 서비스 이용을 방해하는 멤버를 그룹에서 분리하는 메커니즘이 필요함.
    • 독단적 강퇴는 없음
  • 핵심 정책
    • 리더가 강퇴투표를 만들 수 있음 
    • 투표를 만든 리더 제외 멤버의 과반수 동의해야 강퇴됨
    • 투표 만료 시점이 있음
  • 이 기능이 서비스의 목표에 부합한가? 이게 사용자에게 가치가 있나?

구현하다 보니

요구사항을 정의하고 나서 백엔드를 구현하다보니 비로소 엣지 케이스가 보이기 시작했다.

  • 강퇴 투표중인 멤버가 자발적으로 그룹을 탈퇴하면 투표의 상태는?
  • 리더가 권한을 위임하지 않고 그룹을 탈퇴하면 그 이후에는 강퇴투표는 어떻게 진행하지?

깨달은 점은, 이 엣지 케이스들이 구현 단계에서야 보인 것이 잘못이 아니라는 사실이다. 추상 단계에서는 끌어낼 수 없는, 만들어 봐야 비로소 보이는 종류의 디테일이 있다. 기획서를 더 꼼꼼히 적었다고 메워질 빈칸이 아니라, 코드로 구현된 강퇴투표가 눈앞에 있어야 "그럼 투표 도중에 도망가면?"이 떠오르는 종류의 빈칸이다.


결론

기획자가 세부사항을 다 잡아오지 못하는 것은 게으름이 아니다. 큰 그림과 핵심 정책에 제한된 리소스를 집중한 결과다. 세부사항은 처음부터 잡을 수 있는 종류의 것이 아니라, 협업으로 채워가는 영역이다. 개발팀은 여러 엣지 케이스를 잡아내서 처리 방안을 리스트업하고, 기획팀은 큰 그림을 지키면서 각 케이스에 대해 이 문제를 풀것인가, 풀지 않을것인가, 푼다면 어떤 정책을 가질 것인가와 같은 결정을 내릴 수 있을 것이다. 실제로 팀 내에서 기획 - 개발간 논의를 업무 프로세스의 한 단계로 자리 잡는 것에 대해 이야기를 나누기도 했었다.

 

논의를 어떻게 진행하면 좋을지 방법론도 가볍게 생각해보았는데, DDD 를 중심으로 해서 아래의 것들이 있을 것 같다. 

  • 데이터 모델링 해보기 : 정책의 빈칸은 자연어 기획서에서는 잘 안 보이지만, ERD나 도메인 모델 스케치에서는 시각적으로 드러난다. "리더는 1명이다"가 정책일 때, Group.leaderId의 nullable 여부를 결정해야 할 때 "리더 부재 상태가 가능한가?"라는 질문이 강제된다.
  • 유비쿼터스 언어로 같은 모델 공유하기 : 강퇴투표의 엣지 케이스가 빠진 진짜 이유는, "강퇴(kick)"와 "탈퇴(leave)"가 같은 상태 변화인지 다른 사건인지, "리더"가 역할인지 책임인지, 부재가 가능한 개념인지가 도메인 언어로 합의되지 않았기 때문이다.

 

'기록 > 회고' 카테고리의 다른 글

2025년 회고: 중니어가 되어가는 과정  (2) 2026.01.16

서론

EDA 스터디를 진행하면서 개인적으로도, 또 공통적이고 반복적으로 나온 의견이 다음과 같았다.

  • DDD 를 같이 공부해야 하나?
    • 이벤트 스토밍 중 Aggregate 용어가 나와서 찾아보니 DDD(Domain Driven Design) 용어임.
    • 경계 컨텍스트를 나누는 작업은 도메인에서 시작함.
  • 추상적인 이야기라서 와닿지 않는다. 코드레벨에서 구현이 어떻게 되어있는지 궁금하다.

DDD와 헥사고날이 패턴이 각각 어떤 질문에 어떻게 답하는지를 조사하고, 이 결과를 얼마 전 진행한 EDA 를 적용하여 API 응답 지연 개선 작업 회고에 적용해보았다.

 


DDD

도메인을 어떻게 모델링하고, 경계를 어떻게 나눌 것인가?

 

EDA에서 다루는 용어와 설계 단위가 DDD에서 비롯된다고 느껴져서 DDD의 철학과 주요 개념을 간단히 정리해보았다. 

 

Eric Evans는 DDD에서 복잡한 소프트웨어의 진짜 복잡성은 기술이 아니라 도메인에 있다고 말한다. 코드의 구조를 비즈니스의 구조와 일치시키면, 비즈니스가 변할 때 코드도 해당 부분만 변경하면 되므로 복잡성이 줄어든다.

 

용어

  • 도메인(Domain): 소프트웨어가 해결하려는 비즈니스 문제 영역 그 자체
    • 채용 플랫폼이면 "채용 공고, 지원, 기업 매칭"이 도메인이고, 쇼핑몰이면 "상품, 주문, 결제"가 도메인
    • DDD는 이 도메인의 언어와 규칙을 코드에 그대로 반영하자는 설계 철학
  • Entity: 고유 식별자(ID)가 있는 객체.
    • 속성이 바뀌어도 같은 객체임.
  • Value Object:
    • 식별자가 없고, 값 자체가 정체성인 객체.
    • 불변(immutable)
  • Aggregate: 함께 변경되어야 하는 Entity와 Value Object의 묶음.
    • Aggregate Root가 유일한 진입점이고, 외부에서 내부 객체를 직접 수정하면 안 됨
    • Repository는 Aggregate Root 단위로만 존재
  • 경계 컨텍스트: 같은 단어가 다른 의미를 가지는 경계.
    • EDA에서 이벤트가 흐르는 단위
  • Ubiquitous Language: 개발자와 비즈니스 담당자가 같은 용어를 사용하자는 원칙

 

경계컨텍스트 예시

일상에서 사용하는 서비스들을 분석해보았다.

오늘의 집 서비스에서 상품이라는 단어는 컨텍스트에 따라 다른 모델을 가진다.

콘텐츠 컨텍스트   → 사진 속 태그 (이름, 이미지)
스토어 컨텍스트   → 구매 대상 (가격, 옵션, 재고, 배송, 리뷰)
판매자 컨텍스트   → 관리 대상 (등록폼, 심사상태, 수수료, 정산)
광고 컨텍스트    → 광고 소재 (노출수, 클릭률, 캠페인기간)

당근마켓에서 게시글이라는 단어는 컨텍스트에 따라 다른 모델을 가진다

중고거래 컨텍스트   → 사진, 가격, 카테고리, 판매상태, 끌어올리기
동네생활 컨텍스트   → 주제태그, 공감수, 댓글, 동네 범위
알바 컨텍스트      → 시급, 근무시간, 근무요일, 사업장
부동산 컨텍스트    → 매매/전세/월세, 면적, 보증금, 입주일
광고 컨텍스트      → 노출수, 클릭률, 지역타겟, 과금방식

 

Hexagonal 아키텍처 (aka. Adapter and Port 패턴)

코드를 어디에 배치하고, 의존성 방향을 어떻게 잡을 것인가?

 

레퍼런스 코드를 찾다보니 반복적으로 언급되는 Hexagonal 아키텍처를 알게 되었다. 

 

전통적으로 비즈니스 로직과 외부 기술을 분리하기 위해 UI 쪽에는 MVC, DB 쪽에는 DAO 패턴이 사용되어왔다. 그런데 Cockburn은 이 둘이 사실 같은 문제, 즉 비즈니스 로직과 외부 기술의 얽힘라는 점에 주목하여 비즈니스 로직을 외부 기술로부터 완전히 분리하는 Adapter and Port 패턴을 설계했다. Hexagonal은 육각형과는 관련이 없으며, UI - 애플리케이션 - DB 의 일렬로 이루어진 구조가 개발자들로 하여금 마치 UI 와 DB 를 완전히 다른 것으로 인식하게 하여 서로 다른 접근과 전략을 사용하도록 해온 것과 대비되기 위함이라고 한다.

Alistair Cockburn, hexagonal-architecture

 

이 아키텍처는 크게 세 영역으로 이루어져 있다.

  • Domain (=핵심 비즈니스 로직): 가장 안쪽 레이어로, 순수한 비즈니스 규칙만 존재. 외부 기술에 대한 의존성이 전혀 없음.
    • Entity, Value Object, Domain Service 등
  • Port (=인터페이스): 도메인과 외부 사이 인터페이스
    • Inbound Port (Driving Port): 외부에서 도메인으로 들어오는 요청을 정의
    • Outbound Port (Driven Port): 도메인이 외부 인프라에 요청하는 것을 정의
  • Adapter (=구현체): Port의 실제 구현
    • Inbound Adapter: REST Controller, GraphQL Resolver, CLI 등
    • Outbound Adapter: Prisma Repository 구현체, HTTP 클라이언트, SQS Publisher 등

의존성 방향은 항상 바깥 → 안쪽이다. 즉 도메인은 절대 Adapter를 직접 참조하지 않고, Port(인터페이스)만 알고 있다.

 

장점으로는

 

  • DB를 Aurora에서 다른 걸로 바꿔도 Adapter만 교체하면 도메인 로직은 그대로
  • 테스트 시 Adapter를 Mock으로 쉽게 교체 가능
  • 비즈니스 로직이 프레임워크나 인프라에 종속되지 않음

 


 

Conclusion

정리해보면 DDD, EDA와 헥사고날 구조는 현실세계를 모델링하고 SW로 구현하는데 필요한 개념이라고 이해했다.

  • 모델링
    • 명사 (무엇이 있는가) → DDD (Entity, Value Object, Aggregate)
    • 규칙 (무엇을 해도/하면 안 되는가) → DDD (도메인 메서드, 상태 전이)
    • 동사-동기적 (무엇을 한다) → DDD (Aggregate 메서드)
    • 동사-비동기적 (그 결과 뭐가 일어난다) → EDA (도메인 이벤트, 구독)
  • 코드 구조 → 헥사고날 (Port, Adapter)

Discussion

만약 서비스를 처음 개발할때 부터 DDD, 헥사고날 구조, EDA를 적용했으면 문제가 전혀 없었을 것이라고 생각하지는 않는다.

처음부터 완벽한 아키텍처를 짜는 게 아니라, 단순하게 시작하고 복잡성이 생기는 지점에서 구조를 개선하는 진화적 설계(Evolutionary Design) 개념이 비즈니스 최적화 측면에서는 최선의 선택일 수 있다. 

또 Evans 에 따르면 모델은 반복적으로 정제된다고 한다. 처음부터 완벽한 도메인 모델을 짜는 게 아니라, 비즈니스를 운영하면서 이해가 깊어질 때 오히려 모델이 더 정교하게 고도화 될 수 있다고 생각한다.

이는 YAGNI(You Ain't Gonna Need It)와도 비슷한 맥락이다. 복잡성을 예측해서 미리 대비하는 것보다, 복잡성이 드러나는 시점을 잘 감지해서 적시에 구조를 개선하는 것이 현실적이라고 생각한다.

 

참고

https://alistair.cockburn.us/hexagonal-architecture

 

hexagonal-architecture

hexagonal-architecture

alistair.cockburn.us

 

 

 

포스트를 작성하면서, 이전에 진행했던 API 응답 지연을 Transactional Outbox 패턴으로 해결한 작업에 더 개선 포인트를 찾았다. 해당 내용은 별도로 다루겠다.

서론

백엔드 팀 내 Event Driven Architecture 스터디를 진행 중에 있다.

이론 학습과 실습을 병행했는데, 첫 실습은 경계 콘텍스트를 나눠보자는 것이었다.

경계콘텍스트는 용어의 의미가 변하거나, 관심사가 완전히 달라지는 구역으로 EDA의 '이벤트'를 정의하는데 사용된다.

 

하지만 책에 서술된 내용은 모호하게 느껴졌고, 이렇게 나누는 것이 과연 맞는건지에 대한 의문도 있었다.

비즈니스의 전체 흐름을 시각화한 뒤 자연스럽게 경계를 찾기 위해 이벤트 스토밍을 진행했다. 

관련된 자료를 리서치하고 실습을 진행하며 아쉬웠던 점을 복기하면서 "그래서 왜 EDA가 필요한가"에 대한 의문까지 조금 더 다가가게 되었다.

 

 

과정

miro 를 사용하여 각자 이벤트 포스트잇을 생성하고 시간순에 따라 정렬하며 액터, 커맨드, 외부시스템, 정책 등을 정해진 규칙(색상, 위치)에 따라 추가했다. 이벤트 까지는 비교적 순조롭게 진행이 되었지만, 외부시스템에 내부의 다른 서버, 외부 솔루션의 어떤 범위까지 포함해야 하는지, 액터에 유저는 기업회원, 일반 회원을 분리할 것인지 여부에 대한 논의가 명확하게 결론지어지지 않았다.

아쉬웠던 점

정책 포스트잇을 붙이는 데 어려움을 겪음. 코드로만 유추 가능

백엔드 코드, 용어에 아직 갇혀있다는 느낌을 받음. 사용자 여정 관점에서 분석하기 어려움

 

그래서 PM, 기획팀, 마케팅 팀도 같이 하면 좋겠다는 생각이 듬.

그러면 그 분들의 시간을 쓰는건데, 어떻게 설득하면 좋을지 고민함.

이 때 EDA의 필요성을 개발팀 뿐 아니라 전사적으로 생각하게 바뀌게 됨.

 

Discussion

EDA, DDD, MSA, 왜 필요한가?

결국엔 KPI 인 것 같음.

개발팀은 CQRS를 통해 성능 이슈를 해결하고 시스템의 복잡도를 낮추어(분할정복 알고리즘 관점) 이득.

기획팀은 언어가 통일되어 프로젝트 진행 속도가 빨라져서 이득

마케팅 팀은 이벤트로 구성된 OLAP을 활용하여 개발팀 공수 없이 다양한 데이터를 자유롭게 활용하게 되어 이득

 

더 나아가서..

목적 조직 필요성을 체감함. 모든 팀원이 같은 목적을 가지고 비슷한 비즈니스 이해도를 가지면 서비스 운영이 원활할 것 같음.

콘웨이의 법칙과도 결이 맞음.

 

DO NOT:

  • 기업 회원 서비스
  • 일반 회원 서비스
  • 관리자 서비스

 

DO:

  • 공고
  • 광고
  • 지원
  • 회원

 

참고

https://www.youtube.com/watch?v=DY3sUeGu74M

https://www.youtube.com/watch?v=sLG5n_pXWK0

https://www.youtube.com/watch?v=gihxS6eE1DM

https://helloworld.kurly.com/blog/event-storming/

 

Database Driven Development에서 진짜 DDD로의 선회, 이벤트 스토밍 -2-

DDD를 위한 첫걸음. Event Storming

helloworld.kurly.com

 

 

서론

우리 팀에서는 검색 기능에 Elasticsearch를 사용한다. 배포 중 새 인덱스가 추가되거나 맵핑 변경이 포함된 경우에는 MySQL 데이터와의 동기화하는 작업이 필요한데, 시간이 오래 걸리는 문제가 있었다. 기존 작업은 새 인덱스를 만든 다음 모든 공고에 대해서 각 MySQL을 조회하여 특정 개수의 데이터를 모은 뒤 Elasticsearch 서버에 bulk API를 요청하고 있다. 마지막에 고정된 인덱스 별칭만 바꿔치기 한다.

 

주된 병목 원인인 MySQL 조회 쿼리 N+1 문제를 해결하여 실행시간을 33분에서 13분대로 개선했는데, Elasticsearch 서버의 지표를 모니터링하던 중 특이 사항이 있었다.

CPU 스로틀

Cgroup CFS Throttle Count가 벌크 인덱스 실행시간 내내 7~8을 유지했다.

  • cgroup(리소스 제한 정책)에 의해 CPU 사용이 강제로 차단된 횟수

예를 들어 이번 주기(100ms)가 시작된 지 겨우 30ms(물리적 시간) 만에, 모든 코어가 열일해서 할당된 Quota(200ms, CPU가 일한 시간, 여러 코어의 Sum)를 다 써버리면 커널은 이 컨테이너는 이번 주기 할당량을 다 썼다고 판단하고, 남은 70ms 동안 해당 컨테이너의 모든 프로세스를 Ready Queue에서 빼버린다. (할당량을 관리하는 것은 다른 컨테이너가 영향을 받지 않도록 하는 것 같다)

데이터 저장인데 왜 CPU가 문제일까 ?

Bulk 요청이 들어오면 CPU는 다음의 일을 한다.

  • JSON 파싱 및 역직렬화
  • 텍스트를 토큰화하고, 형태소 분석을 하고, 역색인구조를 만듬
  • 세그먼트 파일로 저장하기 위해 데이터를 압축

이 때, bulk 옵션으로 refresh(즉시 검색 결과에 반영)를 설정하면 아래 부하가 추가된다.

  • CPU는 메모리에 임시로 모아둔 데이터(In-memory buffer)를 꺼내어 리눅스 시스템의 파일 형태인 Lucene Segment로 만듬
  • 이 과정에서 데이터 구조를 재배열하고, 검색에 최적화된 포맷으로 변경하는 연산이 필요

해결 방법

기존 bulk API 요청마다 refresh를 하고 있었지만 굳이 그럴 필요가 없었기에, 비활성화 설정한 후 모든 bulk가 완료되고 나서 1회 Refresh 및 설정을 원상복구했다. 이로써 인덱싱 진행 중 CPU 스로틀은 1 정도로 감소했으며, CPU사용량도 100% 에서 74%로 줄었다.

 

 

 

참고

https://www.elastic.co/docs/manage-data/data-store/near-real-time-search

 

Near real-time search | Elastic Docs

When a document is stored in Elasticsearch, it is indexed and fully searchable in near real-time--within 1 second. What defines near real-time search?...

www.elastic.co

 

https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/indexing-speed

 

Tune for indexing speed | Elastic Docs

Elasticsearch offers a wide range of indexing performance optimizations, which are especially useful for high-throughput ingestion workloads. This page...

www.elastic.co

 

https://www.elastic.co/docs/reference/elasticsearch/index-settings/merge

 

Merge settings | Reference

A shard in Elasticsearch is a Lucene index, and a Lucene index is broken down into segments. Segments are internal storage elements in the index where...

www.elastic.co

 

https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-refresh

 

Refresh an index | Elasticsearch API documentation

All methods and paths for this operation: POST /_refresh GET /_refresh POST ...

www.elastic.co

Refreshes are resource-intensive. To ensure good cluster performance, it's recommended to wait for Elasticsearch's periodic refresh rather than performing an explicit refresh when possible.

'SW Engineering > Database' 카테고리의 다른 글

Elasticsearch 로 기업 검색 구현하기  (2) 2025.11.06

우리 팀에서는 DB 스키마 정의 파일(schema.prisma)을 기반으로 타입을 생성하고 쿼리를 실행해주는 Prisma 를 ORM 으로 사용하고 있다.

개발을 하면서 경험했던 불편한 점과 Prisma 가 제공하는 기본 cli의 한계를 정리하며 느낀 점을 토대로 보조 툴을 개발하여 생산성을 높여보고자 한다.

 

전제조건

DB 스키마 변경사항이 있는 경우 schema.prisma 를 수정한 다음 로컬 DB에 연결한 상태에서 prisma migrate dev [--create-only]를 커맨드를 사용하여 마이그레이션 파일을 생성한다.

 

migrate dev 의 동작

공식 문서에 따르면 migrate dev 실행 시 아래의 작업이 수행된다.

 

  1. 스키마 드리프트를 감지하기 위해 쉐도우 데이터베이스에서 마이그레이션 히스토리, 즉 _prisma_migration 테이블에서 applied로 마킹된 항목을 로컬의 prisma/migrations 에서 찾아 순서대로 실행함
    • 실행 완료 후 쉐도우 데이터베이스와 연결된 데이터베이스의 스키마가 다르면(Drift) 오류가 발생함
      • (적용되었다고 마킹된) 마이그레이션 파일을 수정(*)하거나 삭제(**)한 경우, 스키마를 수동으로 변경(***)한 경우 발생 가능함
    • 마이그레이션 파일이 비정상적이라면 실행 중에 오류가 발생할 수도 있음(****)
  2. 보류 중인 마이그레이션을 쉐도우 데이터베이스에 적용
  3. schema.prisma의 모든 변경 사항으로부터 새로운 마이그레이션을 생성
  4. 적용되지 않은 모든 마이그레이션을 개발 데이터베이스에 적용하고 _prisma_migrations 테이블을 업데이트함

마이그레이션 파일을 수정(*)한 경우 발생하는 오류


직면한 문제와 해결 시도

migrate dev를 실행하면 (경고 / 확인 메시지에 yes를 선택할 경우) database reset이 필요하여 로컬 데이터가 모두 삭제된다.

Case 1: 여러 작업을 병렬로 진행해야 함

이 상황은 다른 프로젝트에도 보편적으로 발생 가능한 조건이다. main 브랜치에서 딴 브랜치 A에서 Post 테이블을 추가하는 스키마 변경을 로컬 DB에 적용하여 기능을 테스트 하다가 (상용배포 X), 우선 순위가 높은 다른 작업을 요청받아 동일한 상태의 main 브랜치에서 생성한 B 브랜치에서 스키마 변경을 하면서 migrate dev를 실행하면 쉐도우 DB와 로컬 DB 간 차이, 즉 drift가 감지된다. migrate dev 동작 문단의 (**)에 해당하는 케이스이다.

쉐도우 데이터베이스와 로컬 데이터베이스와의 스키마가 불일치하고 마이그레이션 히스토리가 불일치한다는 두 가지 오류가 발생한다.

에러메시지 해석

  • 예상 스키마(쉐도우데이터베이스의 스키마)가 실제 스키마가 되기 위해 변화가 필요하다
  • [+] Added tables: Post 테이블이 실제 DB에(만) 추가되어 있다
  • _prisma_migrations에 applied로 마킹되어있는 마이그레이션이 로컬 prisma/migrations 폴더에 존재하지 않는다

Solution 1: db push (제한이 있음) 또는 migrate diff 를 활용

  • db push

위의 문제 상황에서, main브랜치에서 checkout한 직후의 브랜치 B에서 npx prisma db push 를 실행하면 현재 schema.prisma 와 로컬 DB가 동기화되어 A 브랜치에서 적용된 변경사항과 연관 데이터가 제거된다. 그러나 _prisma_migration 테이블의 기록은 그대로기에 오류가 발생한다.

스키마 불일치 오류는 없어졌지만 여전히 마이그레이션 히스토리가 불일치한다는 오류는 발생한다.

 

이 오류는 브랜치 A에서 migrate dev를 실행할 때 --create-only를 설정하여 새 마이그레이션 파일을 생성하되 _prisma_migrations 테이블에는 적용하지 않고 db push 를 통해 스키마를 반영하고 테스트 한다면 해결 가능하다. 다만  직접 작성한 DML이 포함된 마이그레이션이라면 db push 를 사용할 수 없다는 예외가 있다. 

 

참고로, db push 에 --force-reset 옵션을 활성화하면 _prisma_migrations 테이블의 모든 로우가 삭제되므로 적절하지 않다.

  • migrate diff

두 스키마 파일만을 비교하여 마이그레이션 파일을 생성하는 migrate diff 커맨드도 있다(이외에도 from 과 to 에 다양한 옵션이 가능하다). 이 기능을 사용하면 운영환경에 배포 후 롤백할 때 실행할 Down 마이그레이션 파일도 자동생성 가능하다.

npx prisma migrate diff \
  --from-schema-datamodel ./prisma/schema-old.prisma \
  --to-schema-datamodel ./prisma/schema.prisma \
  --script > migration_name.sql

Case 2: 부적절한 마이그레이션 파일이 기존에 운영 환경에 배포된 상태

이는 우리 프로젝트에서 발생하는 특수한 상황이다. 한 테이블 C 에 데이터를 생성하는 DML이 포함된 마이그레이션 파일이 있는데, 다른 테이블 D 의 컬럼을 외래키로 참조하는 컬럼을 포함하여 생성하므로 테이블 D에 해당 로우가 존재하지 않으면 DB 엔진에서 외래키 제약 오류가 발생한다. 이 때, 쉐도우 DB에는 기본적으로 데이터가 없으므로 오류가 발생한다. 운영환경에서는 데이터가 존재하기에 문제가 발생하지 않았지만 스키마와 무관한, 데이터의 유무에 따라 오류가 발생하는 SQL은 마이그레이션에 포함하기에 부적절하다고 판단하고 있다.

 

쉐도우 데이터베이스에 기존 마이그레이션 파일을 실행하는 도중 발생한다.

 

Solution 2: 마이그레이션 파일을 임의로 수정

테이블 D에 데이터를 생성하는 Insert문을 추가하거나, 테이블 C에 데이터를 생성하는 Insert문을 주석처리하는 임시방편이 있다. _prisma_migration 테이블의 로우가 비어있어야 가능하며, 운영환경의 데이터를 덤프했다면 체크섬 불일치로 오류가 발생할 수 있다. 이는 migrate dev동작 문단의 (*) 에 해당하는 케이스이다.


로컬 DB 관리에 대한 고민

문제를 경험하면서, 처음에는 어떻게 하면 스키마 drift를 피할 수 있을지 위주로 고민했다. 그러던 중 다른 팀에서는 로컬 DB를 어떻게 관리하는지가 궁금해졌고, 관련해서 조사하던 중 12 factors app 의 운영-개발 간 동등성 을 읽어보았다 주 내용은 운영환경에서의 예상치 못한 치명적인 버그를 방지하기 위해서는 로컬 개발의 편의성보다 운영환경과 동일한 환경을 선택해야 한다는 것인데, main 브랜치의 마이그레이션 파일과의 차이가 있다면, 이것을 베이스로 하여 로컬 DB를 reset 하는 prisma migrate dev 커맨드의 동작이 이 원칙을 지키기 위한 목적으로 설계된 것일 수도 있겠다는 생각이 들었다.


 

오픈소스 활동

이러한 고민을 바탕으로 개발 생산성을 높일 수 있는 도구를 만들어보고자 한다.

prisma-migrator (개발 중단)

prisma migrate dev로 자동 실행된 마이그레이션 파일이 아니라 수동으로 생성한 파일은 실행 시 SQL 오류가 발생할 수도 있다. 이 때, 파일에 여러 SQL문이 있다면, 이 오류를 수정하여 다시 마이그레이션을 실행하려면 오류가 발생하기 전까지 정상 실행된 SQL 문을 수동으로 되돌려야 하는 점이 불편하여 롤백 파일이 있으면 마이그레이션이 실패했을 때 롤백 파일이 실행되도록 하는 라이브러리를 만들어 npm 에 배포해보았다. migrate dev는 스크립트로 실행되지 않도록 하는 제한이 있어 deploy를 대체로 사용하는 등 한계가 있고 롤백 파일도 직접 만들어야 하는 등 완성도가 떨어진다고 판단하여, 개발을 중단하고 새 프로젝트를 시작할 예정이다

lazy prisma 기여

lazy prisma는 lazy git, lazy docker 와 유사한 터미널 ui로 prisma의 커맨드를 편리하게 사용하고 마이그레이션 파일을 확인하기 좋은 오픈소스 프로젝트이다. 이 곳에 down 마이그레이션 파일 생성 기능을 요청하고 가능하다면 개발해보려고 한다.

 

checkout db (가칭)  - 개발 브랜치마다 별도의 데이터베이스를 관리

일련의 조사와 고민을 통해 prisma를 사용하면서 로컬(개발) 데이터베이스를 관리하는 이상적인 방법은 작업 브랜치마다 별도의 데이터베이스를 관리하는 것이라는 결론을 내렸다. 이 아이디어를 토대로 git checkout -b {새 브랜치} 를 실행했을때 아래의 작업을 자동화하는 도구를 개발해보려고 한다.

  1. 데이터베이스를 생성
  2. 현재 코드를 기반으로 스키마 반영과 시딩
    • prisma seed 파일이 있다면 반영
    • 아니라면 스냅랫으로 생성
  3. env 파일의 데이터베이스 연결 파라미터에 생성된 데이터베이스 반영
  4. 브랜치가 삭제되면 데이터베이스도 삭제

2025년을 되돌아보고, 정리하며 잘한점은 더욱 디벨롭하고, 부족한 점은 개선하려고 한다.

 

메인 이벤트들

Node.JS 

7월~10월 간 참여한 Node.JS 컨트리뷰션 아카데미. Node.JS 의 아키텍처를 이해하며 직접 기여를 하는 것이 너무 가슴뛰는 일이었다. 이 활동을 기점으로 내 개발 결과물에 대한 자신감이 많이 생겼다.

https://9436188.tistory.com/39

 

회사 프로젝트

기존의 자기소개서, 채용 공고, 멘토 게시글, 합격후기, 채팅을 기업이라는 주제로 묶어 제공하는 큰 규모의 프로젝트를 진행했다.

이를 진행하면서, 개발 일정이 두 번이나 밀렸고 배포 과정에서의 운영 이슈도 발생했다. 스스로의 신뢰도에 대해서 되돌아보았고, 한편으로는 휴먼 에러에 강한 배포 시스템도 고민해보았다. 화면 위주의 기획서로부터 백엔드 작업을 리스트업하는 전략도 고민했다.

 

Elasticsearch를 활용한 검색 품질 개선 업무를 진행하고, Nest.JS 마이그레이션과 커스텀 데코레이터를 개발하면서 깊이 있는 공부를 할 수 있었다.

https://9436188.tistory.com/42

 

 

AWS SAA 준비 (진행 중)

AWS 서비스를 활용해서 서비스를 운영하면서 경험을 통해서 배운 지식도 많았지만, 모든 서비스를 훑어보고 싶다는 생각이 들어 준비하게 되었다. 가용성과 확장성, 응답 시간, 규정준수, 재해복구 수준과 같은 다양한 요구사항과 운영 오버헤드, 비용, 네트워크 대역폭 등의 제한조건에 맞게 AWS의 서비스를 적절히 구성하는 케이스를 스터디하는 중이다.

https://9436188.tistory.com/59

 

이벤트 드리븐 아키텍처 스터디 (진행 중)

동기식 마이크로서비스 아키텍처 서비스를 운영하면서 여러 어려움이 있던 와중에, CTO님의 제안으로 팀 스터디를 진행하고 있다. 아직은 분산로그, 통신구조와 같은 소프트웨어 공학의 기초 지식을 쌓으며 우리 서비스에 어떻게 적용할 수 있을지 대략적인 구상만 하고 있다. 스터디에서 의견을 나누며 메시지큐나 서비스 메시와 같은 다른 대안들도 논의되었다.

https://9436188.tistory.com/60

 

AI 활용

클로드코드, 제미나이(대화, 코드 리뷰 봇), 크롬 개발자도구의 ai assistant를 사용하여 생산성을 높일 수 있었다.

개인 프로젝트의 플러터 개발은 클로드코드를 사용해서 진행하는데, claude.md 를 사용하여 컨텍스트가 늘어나도 잃어버리면 안되는 공통 요구사항을 관리했다.

제미나이를 쓸 때에는 ~ 이렇게 하는거 맞지? 보다는 이렇게 하는 것의 기술적 타당성과 비즈니스 관점에서의 장, 단점과 가능한 대안을 출처와 함께 요청하고, 방금 네 답변을 반박한다면? 과 같은 형태로 내 주장을 확인받는 것이 아니라 논리를 발전 시키는 데 활용했다.

이 밖에 google sheet에서도 ai 어시스턴트를 활용하여 기획팀과의 협업 시 데이터 변환 업무를 손쉽게 수행할 수 있었다.

 


그래서 앞으로..

잘한점, 앞으로도 이렇게 하자

런타임과 프레임워크, 프로그래밍 패턴과 아키텍처를 스터디하고, 업무에 적용해보자

AI를 능동적으로 활용하자

개발 생산성을 높이는 방안(깃헙액션, 공통로직 데코레이터 구현, 운영업무 스크립트화) 고민하기

 

못한점

이렇게는 하지말자 (DO NOT)

기술적 완성도가 가장 중요하다

배포 후 문제 없으면 끝

저 사람(주로 상사)이 맞겠지

 

이렇게 해보자 (DO)

기술은 비즈니스를 하기 위한 도구임. 현재 우선순위와 팀의 KPI를 파악

배포 후 모니터링 지표를 시각화 자료로 공유하며 운영 안정성을 모두가 확인 => 개발팀의 신뢰도와 투명성을 높임

이 방법을 적용했을 때 이슈가 없을까? 더 좋은 방법은 뭘까? 그 근거는? 혹은 의견에 동의하는 피드백을 전달


결론

필요한 기능을 개발하고 운영 이슈의 원인을 파악하여 타당한 방법으로 해결할 수 있는 단계를 지나서, 기술적 깊이와 탐구에 대한 목표를 달성한 1년이었다고 생각한다. 이제는 서비스의 안정성 측면에서의 더 나은 대안을 찾고, 업무 프로세스와 리스크 관리를 고민하는 과정을 통해 주니어에서 중니어로 넘어가보려고 노력해보려고 한다. 여러 내용을 썼지만 결국에는 비판적 사고 + 논리적 근거로 귀결되는 것 같기도 하다.

회사에서 Event Driven Architecture를 우리 팀의 서비스에 적용하는 것을 목표로 이 책을 스터디하게 되었다.

 

Building Event-Driven Microservices

Organizations today often struggle to balance business requirements with ever-increasing volumes of data. Additionally, the demand for leveraging large-scale, real-time data is... - Selection from Building Event-Driven Microservices [Book]

www.oreilly.com

 

이 책의 1~5장을 읽으며 한 번에 이해되지 않거나 헷갈리는 개념과 핵심 키워드을 정리해보았다.

  • 1장: 왜 이벤트 기반 마이크로서비스인가?
  • 2장: 이벤트 기반 마이크로서비스 기초
  • 3장: 통신 및 데이터 규약
  • 4장: 기존 시스템에 이벤트 기반 아키텍처 통합
  • 5장: 이벤트 처리 기본

도메인과 모델

  • 도메인: 비즈니스가 다루는 실제 세계의 영역
  • 모델: 도메인을 특저안 목적에 맞게 추상화한 결과물

예: 공고라는 도메인을 경계 콘텍스트에 따라 아래의 다양한 모델로 설계할 수 있다.

  • 정보제공 콘텍스트: 세부정보(채용인원, 근무지, 연봉정보 등)
  • 관리자 콘텍스트: 심사(현재상태, 승인/반려 이력, 반려 사유 등)
  • 광고 콘텍스트: 광고상품 (지면, 단가, 성과 등)
  • 인터렉션 콘텍스트: 유저활동(스크랩, 조회, 공유 등)

각 모델은 DB레벨에서 외래키로 관리하기보다는 ID로 연결하는 것이 권장된다.

경계콘텍스트

특정한 용어와 규칙이 일관되게 유지되는 논리적 경계, 위의 예시에서 정보제공, 관리자, 광고, 인터렉션에 해당한다.

 

통신구조

레벨에 따라

  • 비즈니스 통신 예
    • 지원팀 > 운영팀 > 기술팀 간 통신
    • 조직이나 서비스 간의 의사결정과 협업
  • 구현 통신
    • 웹 서버 > 앱서버 > DB
    • 시스템 간 물리적/논리적 연결 방식과 기술적 아키텍처
  • 데이터 통신
    • 프로토콜, 파일 형식 등
    • 데이터의 구조와 규격

 

중앙 통제 / P2P 에 따라

  • 애드혹 통신 (P2P) : 서로에 특화된 방법, 초기구축이 빠르고 단순
  • 정규 통신 (중앙통제): 정규화된 방법, 유지보수/확장성에 유리
  애드혹 정규
비즈니스  단발적, 즉흥적 표준 워크플로우
구현 api 호출, 웹훅 이벤트브로커, 버스, 펍-섭
데이터 가변적 스키마 사용 고정적 스키마

 

토폴로지

위상수학에서 시작된 개념으로, 구성요소들이 서로 연결되어 있는 물리적/논리적 배치 형태

  • 단일 마이크로서비스의 토폴로지
    • 예: 이벤트스트림을 받아서 필터링/변환한 뒤 DB에 저장하고 새 이벤트 스트림으로 내보내는 구조
  • 비즈니스 토폴로지
    • 복잡한 비즈니스 로직을 처리하는 마이크로서비스, 이벤트스트림, API 로 이루어짐
      • 마이크로서비스: 비즈니스 경계콘텍스트를 구현
      • 이벤트 스트림: 전체 콘텍스트의 도메인 데이터를 공유하기 위한 수단
  • 이벤트 브로커와 메시지 브로커
    • 이벤트 브로커는 메시지 브로커를 대체할수 있지만, 반대는 그렇지 않다.
    • 메시지 브로커는 메시지를 큐 단위로만 처리하므로 여러 컨슈머가 큐에서 같은 이벤트데이터를 소비하거나 메시지로부터 전체 데이터를 얻을 수 없음
    • 이벤트 브로커는 거대한 장부에 순서대로 적힌 레코드에 인덱스를 통한 개별 액세스를 관리함 (+ 컨슈머는 각자 오프셋을 관리하여 독립적으로 소비함)

그 외

  • 테이블 - 스트림 이원화: (로그)스트림을 테이블로, 테이블을 스트림으로 구체화 가능하다는 개념
    • 이벤트브로커가 스트림을 컴팩션하여 크기를 줄이기도 함
  • 스키마화: 컨슈머가 프로듀서에 묻지 않고도 이벤트 콘텐츠의 의미를 해석
    • 진화 프레임워크
    • 정형 클래스 생성
  • 마이크로서비스 단일 작성자 원칙: 이벤트 스트림은 정확히 하나의 생산 마이크로서비스를 가진다는 원칙
  • 이벤트 브로커
    • 이벤트를 받아 큐 또는 파티션된 이벤트 스트림에 저장하여 다른 프로세스가 사용할 수 있도록 제공
    • 대규모 이벤트 기반 체계를 운영하는 데 장점 제공, 마이크로서비스를 강화
      • 확장성, 보존성, 고가용성, 고성능
    • 브로커가 사용하는 기반(underlying) 데이터스토리지의 요건
      • 파티셔닝, 순서보장, 불변성, 인덱싱, 무기한 보존, 재연성

서론

요구사항은 다음과 같다.

  • 어떤 엔티티를 일반 사용자, 관리자 모두 생성할 수 있지만, 관리자만 이 데이터의 검증 여부를 설정하여 생성할 수 있다. 
// Graphql schema 예
createEntityInput {
  name: String! // 필수값
  isVerified: Boolean // 선택값, 관리자만 설정가능, 기본값은 false
}

 

일반 유저가 검증여부를 설정한 경우 서비스 로직에서 조용히 무시하는 방법은 직관적이지 않고 디버깅의 어려움을 유발할 수 있기에, 이 경우에 오류를 던지는 방향으로 설계를 시작했다.

 

그러러면 요청의 특정 인풋 필드가 유효한 값을 가지는 경우 유저의 권한을 체크해야 하는데, 이 로직을 리졸버(GraphQL의 컨트롤러와 비슷하다) 또는 서비스에 직접 구현하기에는 비슷한 코드가 중복되는 분산으로 인하여 관리가 어렵고 비효율적이라고 판단했다. 따라서 Nest.js의 Enhancer을 활용하여 AOP(Aspect Oriented Programming) 를 적용해보았다.


 

작업하기

이를 구현하는 여러 방법 중 한 가지를 선택할 때의 근거는 과도한 패치보다는 제공되는 기능의 본 목적에 부합하게 사용하는 것이었다. 그리고 두 곳 이상에서 설정(예: DTO / 리졸버 등)할 필요가 없도록 하여 간단하게 사용가능하게 만들어서 휴먼 에러를 줄이려고 했다.

 

Nest.js에는 예외필터, 파이프, 가드, 인터셉터, 커스텀 데코레이터를 포함하는 Enhancer 기능을 제공한다.

예외가 발생하기 전 사전 처리이기에, Exception Filter는 제외했다.

권한을 체크하기 위해서는 요청 컨텍스트에서 유저 정보를 추출해야 했기에, guard 와 interceptor(before 단계)가 적합하다고 판단했다.

API 는 일반 유저도 요청은 가능하지만, 인풋 필드 일부에 대해서 보다 세밀하게 권한을 제어하는 AOP 를 구현하기 위해서 guard보다는  interceptor(before단계)가 적합하다고 판단했다.

Nest.js 요청의 수명주기

 

인터셉터는 정적으로 정의가능 할 경우 미리 정의된 하나의 클래스로 구현할 수도 있다. 이번에는 인터셉터에 커스텀 데이터를 전달하기 위해 팩토리 함수를 정의하고 이 함수(AdminOnlyInputFields)가 호출될 때마다 인터셉터 클래스가 만들어지도록 했다. 단 mixin을 통해 함수가 아닌 클래스를 반환함으로써 Nest.js의 DI 를 추후에 활용가능하도록 했다.

  • 일반적인 케이스
// cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}


// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // ....
  }
}
  • 런타임(리졸버가 로드되는 시점)에 결정된 파라미터를 받는 Interceptor
// foo.resolver.ts
  
@UseInterceptors(AdminOnlyInputFields(['isVerified'])) // 이 부분!
@Mutation(() => fooPayloadDto)
async foo(
@Args('input') input: fooInputDto,
): Promise<fooPayloadDto> {
 return await this.barService.baz(input);
}
  
// admin-only-input.interceptor.ts
  
import { CallHandler, ExecutionContext, Injectable, mixin, NestInterceptor, Type } from '@nestjs/common'
import { GqlExecutionContext } from '@nestjs/graphql'
import { GraphQLError } from 'graphql'
import { Observable } from 'rxjs'

export const AdminOnlyInputFields = (
  fields: string[],
): Type<NestInterceptor> => {
  @Injectable()
  class AdminFieldSecurityInterceptor implements NestInterceptor {
    async intercept(
      context: ExecutionContext,
      next: CallHandler,
    ): Promise<Observable<any>> {
      const gqlContext = GqlExecutionContext.create(context)
      const { input } = gqlContext.getArgs()
      const user = gqlContext.getContext().currentUser

      if (!input) return next.handle()

      if (fields.length > 0 && !user.isAdmin) {
        for (const field of fields) {
          if (input[field] !== undefined && input[field] !== null) {
            throw new GraphQLError(
              `'${field}' 필드는 관리자만 설정할 수 있습니다.`,
            )
          }
        }
      }

      return next.handle()
    }
  }

  return mixin(AdminFieldSecurityInterceptor)
}

한계

필드명을 문자열로 넘겨주기 때문에, 오타가 있으면 의도대로 동작하지 않는다.

 

다른 시도들

 

실패한 방법과 그 이유도 정리해보았다. 인풋 DTO 클래스 필드에 데코레이터를 달아 관리자전용 필드임을 나타내는 메타데이터를 설정하고, 인터셉터에서 해당 메타데이터가 설정된 필드를 찾아서 권한을 체크하는 방식을 시도했었다. 다음은 코드 스니펫이다.

// foo-input.dto.ts
import { Field, InputType } from '@nestjs/graphql'

@InputType('fooInput')
export class fooDto {
  @Field(() => String)
  name: string

  @AdminOnlyInput() // 이 데코레이터를 사용하여 관리자가 아닌 유저가 isVerified를 요청하면 오류 발생
  @Field(() => Boolean, {
    nullable: true,
    defaultValue: false,
  })
  isVerified: boolean
}

// admin-only-input.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ADMIN_ONLY_FIELDS_KEY = 'admin_only_fields';

export function AdminOnly() {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata(ADMIN_ONLY_FIELDS_KEY, true, target, propertyKey);
  };
}

// admin-only-input.interceptor.ts
@Injectable()
export class AdminOnlyInterceptor implements NestInterceptor {
  constructor(private readonly reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = GqlExecutionContext.create(context);
    const args = ctx.getArgs(); // 실제 유저가 보낸 데이터 { name: "...", isVerified: true }
    const inputData = args.input || args; 
    const adminOnlyFields = Reflect.getMetadata(
      ADMIN_ONLY_FIELDS_KEY, 
      Object.getPrototypeOf(inputData)
    );

    console.log(typeof inputData); // Object
    console.log(adminOnlyFields); // [](빈 배열)
    
    // 따라서 이 블록은 절대 실행되지 않는다.
    if (adminOnlyFields) {
      // ... 권한 체크 로직 ...
    }

    return next.handle();
  }
}

 

이 코드는 의도대로 동작하지 않았는데, 근본적인 이유는 adminOnlyFields가 항상 빈 배열이기 때문이다. typeof로 inputData 의  타입을 확인해보면 fooInputDto 가 아니라 Object 타입이었다. 즉 데코레이터를 달아놓은 fooDto 클래스와의 연결고리가 없기 때문에 Reflect.getMetadata는 를 통해 메타데이터를 얻을 수 없었다. 참고했던 작업은 rediscacheable 데코레이터인데, 이 때는 메소드에 메타데이터를 설정했기에, 런타임에도 동작할 수 있었다.

 

Object 타입인 이유는 인터셉터의 before 단계는 class-transformer의 PlainToClass가 실행되어 객체가 DTO 인스턴스로 변환되는 시점인 Pipe(파이프) 단계의 이전이기 때문이다. 

 

참고

요청 라이프사이클

https://docs.nestjs.com/faq/request-lifecycle

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

Nest.js 데코레이터

https://docs.nestjs.com/custom-decorators

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

Nest.js 인터셉터

https://docs.nestjs.com/interceptors

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

리플렉션과 메타데이터

https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

작업 배경

우리 팀의 서버 개발 히스토리와 상황은 다음과 같다.

  • 최초 express.js로 개발 -> Nest.js 도입하여 express는 라우트 연결하여 유지, 새 기능은 Nest.js로 개발
    • express.js: shecma-first(graphQL 스키마를 직접정의), Seqeulize
    • Nest.js: code-first(graphQL 스키마를 코드로 정의한 뒤 생성), prisma
  • Apollo GraphQL 기반
  • 이전에 개발된 express 의 공고 스크랩 API
  • 최근에 개발된 Nest.js 의 관심기업 설정 API

작업

전략

요구사항은 공고를 스크랩할 때 해당 공고의 기업에 관심설정을 하도록 하는 것이다. 아래에 채택되지 않은 방안과 제약을 정리해보았다.

  • 클라이언트에서 두 API를 각각 호출: 구버전 앱에서 호환되지 않음
  • express 로직에서 Nest.js API를 호출: 한 서버 프로세스에서 불필요한 네트워크 호출
  • express 코드를 모두 Nest.js로 작성: 공고달력, 팔로우 등 많은 기능과 얽혀있기에 검증이 어려워 서비스에 영향을 줄 수 있음

결론적으로는 위 두 방안이 아닌 Nest.js 기반의 엔드포인트와 인풋/아웃풋 타입을 정의하고, 내부에서 express 서비스 함수를 호출하고도록 하였다. 이 전략은 다음과 같은 장점과 한계를 가진다.

장점

  • Apollo express 서버는 요청 context 에 서비스 클래스를 포함한다는 점을 활용하여 쉽게 구현할 수 있다
  • 코드 기반을 Nest.js 로 구성하여 Nest.js의 가드, 미들웨어, 인터셉트와 같은 인프라 기능을 활용할 수 있다
  • GraphQL 스키마를 코드로 관리하여 스키마 관리가 용이

단점

  • express 코드에서는 sequelize 를 사용하고 Nest.js 코드에서 prisma 를 사용하고 있기에, 트랜잭션으로 처리하기 위하여 별도의 처리가 필요하다.

참고한 글

코드 샘플

  • 기존 공고 스크랩 API
// src/resolvers/mutation.js
const resolvers = {
  Mutation: {
      activityScrap: async (_, { input }, context) => {
        // 1. Apollo Context에서 서비스 추출
        const { userScrapService } = context;

        // 2. 공고 스크랩
        const result = await userScrapService.updateUserScrap(input);

        return result;
      }
    }
 }
  • 새 공고 스크랩 API
// src/scraps/resolvers/user-scraps.resolver.ts
@Resolver(() => UserScrapDto)
export class UserScrapsResolver {
  constructor(private scrapsService: ScrapsService) {}

  @Mutation(() => ScrapPayloadDto, { nullable: true })
  async activityScrap(
    @Context() context: RequestContext,
    @Args('input') input: ScrapInputDto,
  ) {
    return await this.scrapsService.updateActivityScrap(context, input)
  }
}

// src/scraps/services/user-scraps.service.ts
public async updateActivityScrap(
    context: RequestContext,
    input: ScrapUpdateInputDto,
  ): Promise<ActivityScrapUpdatePayloadDto | null> {
    // express 서비스 함수 
    const { userScrapService } = context

    const result = await userScrapService.updateUserScrap(input);

    // Nest.js 서비스 함수
    await this.companyInterestService.setCompanyInterestedIn(
      context,
      compnayId
    );

    return result
  }

향후 계획

순수 비즈니스 로직 또한 모듈 단위, 함수 단위로 나누어 NestJS Service로 하나씩 추출할 예정이다.

 

 

+ Recent posts