설계 원칙을 운영 가능한 코드로 잇기
Clean Architecture, DDD, Twelve-Factor App이 각각 코드 구조, 도메인 모델링, 배포와 운영 철학을 어떻게 나누어 담당하는지 설명한다. 이어서 SOLID, ADR, NestJS 기준 안티패턴과 장애 징후를 통해 설계 원칙이 실제 코드와 운영에서 드러나는 방식을 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
백엔드 시스템에서 설계 원칙이 중요한 이유는 요구사항이 바뀔 때 코드도 함께 바뀌기 때문이다. 핵심 질문은 바꾸느냐 마느냐가 아니라, 얼마나 쉽게 바꿀 수 있느냐다. Clean Architecture가 없으면 DB를 MySQL에서 PostgreSQL로 바꿀 때 비즈니스 로직까지 흔들릴 수 있다. DDD가 없으면 팀마다 주문이라는 말을 다르게 이해해 커뮤니케이션 오류가 생긴다. Twelve-Factor App이 없으면 로컬에서는 되던 앱이 ECS나 K8s에 올라간 뒤 환경설정 오류로 깨질 수 있다.
- 02
이 문서에서 세 축은 서로 경쟁하는 개념이 아니라 보완하는 개념이다. Clean Architecture는 코드 구조를 다루고, DDD는 도메인 모델링을 다루며, Twelve-Factor App은 배포와 운영 철학을 다룬다. 다만 이 문서는 세 주제를 깊게 다시 설명하기보다 설계 원칙을 둘러싼 메타 영역에 집중한다. 의존성 규칙, Bounded Context, 12원칙 같은 세부 내용은 별도 문서로 분리하고, 여기서는 SOLID, ADR, 안티패턴, 트러블슈팅을 중심으로 본다.
- 03
실무에서 설계 원칙 위반은 먼저 코드의 위치 이상으로 드러난다. Fat Controller는 Controller가 DB 조회, 비즈니스 규칙 판단, 이메일 발송까지 모두 떠안는 형태다. 신규 프로젝트나 빠른 프로토타이핑 때 흔하지만, 시간이 지나면 변경 이유가 한곳에 몰린다. Anemic Domain Model은 DDD를 흉내냈지만 Entity에 비즈니스 로직이 없고 getter와 setter만 남은 상태다. 이때 비즈니스 규칙은 Service 레이어 곳곳에 흩어지고, 도메인 객체는 이름만 도메인인 데이터 묶음이 된다.
- 04
SOLID는 Robert C. Martin이 정리한 다섯 가지 객체지향 설계 원칙이며, Clean Architecture와 DDD를 떠받치는 기초로 제시된다. 단일 책임 원칙은 한 클래스가 변경되어야 하는 이유를 하나로 제한한다. 개방-폐쇄 원칙은 기존 코드를 고치기보다 확장으로 대응하라는 방향을 준다. 리스코프 치환 원칙은 자식 클래스가 부모 클래스의 계약을 깨지 않고 대체되어야 한다고 말한다. 인터페이스 분리 원칙은 쓰지 않는 메서드에 의존하지 않게 하며, 의존성 역전 원칙은 고수준 모듈과 저수준 모듈이 모두 추상화에 의존하게 만든다.
- 05
NestJS에서는 SOLID가 추상 원칙으로만 남지 않고 구체적인 패턴으로 나타난다. 단일 책임 원칙은 Controller, Service, Repository 분리와 이벤트 기반 부수 효과 분리로 이어진다. 개방-폐쇄 원칙은 인터페이스와 useClass DI를 통해 결제나 알림 같은 전략을 확장하는 방식으로 연결된다. 리스코프 치환 원칙과 인터페이스 분리 원칙은 Repository의 읽기와 쓰기 인터페이스 분리, CQRS의 Command와 Query 분리와 맞닿아 있다. 의존성 역전 원칙은 @Inject와 추상화 토큰 DI를 통해 Clean Architecture의 의존성 규칙을 구현하는 쪽으로 이어진다.
- 06
ADR은 아키텍처 결정에도 왜 이 선택을 했는가라는 히스토리가 필요하다는 문제의식에서 나온다. 코드에 PR 리뷰 히스토리가 남듯, 아키텍처 결정에도 맥락과 결과를 남기는 파일이 필요하다는 비유다. ADR은 팀이 내린 중요한 아키텍처 결정을 문서화하는 경량 실천법이며, Michael Nygard가 제안한 템플릿이 사실상 표준으로 언급된다. 한 결정은 별도의 Markdown 파일로 관리하고, Title, Status, Context, Decision, Consequences를 남긴다. 핵심은 결정 자체뿐 아니라 배경과 장단점까지 함께 보존하는 것이다.
- 07
트러블슈팅 관점에서 첫 번째 실패 모드는 순환 의존이다. OrdersModule에서 PaymentsModule로, 다시 OrdersModule로 이어지면 Circular dependency detected 같은 오류가 나타날 수 있다. Order가 Payment를 알고 Payment가 다시 Order를 아는 구조도 같은 문제를 만든다. 해결 실마리는 결합을 직접 참조에서 Domain Event로 낮추거나, OrderId와 OrderStatus 같은 공유 타입을 Shared Kernel로 분리하는 것이다. NestJS에서는 forwardRef로 지연 해결할 수도 있지만, 그것만으로 도메인 결합 문제가 사라지는 것은 아니다.
- 08
두 번째 실패 모드는 환경설정과 실행 환경의 차이다. 로컬에서는 정상 동작하지만 ECS 프로덕션에서 Cannot read property url of undefined나 ECONNREFUSED 127.0.0.1:5432가 발생할 수 있다. 원인은 DB_HOST가 없을 때 localhost로 폴백하는 식의 코드일 수 있다. 문서는 DATABASE_URL 같은 필수 환경변수가 없으면 시작 시 명시적으로 에러를 내는 빠른 실패를 제시한다. ECS Task Definition에는 DATABASE_URL, REDIS_URL, NODE_ENV 같은 값이 실제로 들어 있는지 확인해야 하며, Twelve-Factor App 관점에서는 설정을 환경변수로 분리하는 일이 핵심이다.
- 09
세 번째 실패 모드는 도메인과 인프라의 혼합이다. 도메인 레이어의 Order 클래스에 TypeORM의 @Entity나 @Column 같은 데코레이터가 붙으면, 도메인 로직 테스트에 TypeORM 연결이 필요해진다. 이때 Clean Architecture인데 왜 도메인에 ORM이 있느냐는 리뷰가 자연스럽게 나온다. 해결 방향은 도메인 엔티티를 순수 비즈니스 로직으로 두고, ORM 스키마를 infrastructure와 persistence 쪽에 분리하는 것이다. Repository는 OrderSchema를 Order로, Order를 OrderSchema로 변환하는 경계 역할을 맡는다.
- 10
마지막으로 운영 중 실패는 종료 과정에서도 드러난다. ECS 롤링 업데이트 중 502 Bad Gateway나 socket hang up이 발생하면 Graceful Shutdown을 확인해야 한다. NestJS에서는 enableShutdownHooks를 활성화하고, OnModuleDestroy에서 DB 커넥션 풀 같은 자원을 정리하는 흐름이 제시된다. ECS의 stopTimeout이 null이면 기본 30초로 동작한다. 요청 처리 시간이 30초를 넘거나 종료 정리가 오래 걸리면 ECS가 강제 종료할 수 있으므로, 요청을 더 짧게 만들거나 작업을 큐에 재시도 가능하게 반환해야 한다. 정리하면 Clean Architecture는 핵심 비즈니스 로직을 외부 환경으로부터 보호하고, DDD는 팀이 공통된 언어로 복잡한 도메인을 모델링하며, Twelve-Factor App은 클라우드 환경에서 앱이 안정적으로 동작하도록 만드는 방법을 다룬다.
같은 레이어
L9에서 이어 듣기
- Clean Architecture의 의존성 규칙 길이 미정
- DDD 기본기: 도메인 언어와 경계 설계 길이 미정
- Twelve-Factor App 운영 원칙 길이 미정
- CAP과 일관성으로 보는 분산 시스템 선택 길이 미정
- MSA 패턴, 분리의 이득과 운영 비용 길이 미정
- Saga Pattern: 로컬 커밋과 역순 보상 길이 미정
- CQRS와 이벤트 소싱의 운영 경계 길이 미정
- TDD와 테스트 피라미드로 설계하는 테스트 전략 길이 미정
- 대규모 웹 크롤러의 큐, 정중함, 중복 제거 길이 미정
- API 계약으로 안전하게 서비스 경계를 진화시키기 길이 미정
- URL Shortener와 Rate Limiter로 보는 시스템 디자인 길이 미정