저는 디미페이 v2 프로젝트를 시작하면서 백엔드를 처음부터 다시 개발하기로 결정했어요. 이 글에선 왜 이런 선택을 하게 되었고, 타입스크립트 백엔드를 개발하기 위해 어떤 도구들을 선택했는지 공유해보려 해요.
이전 백엔드는 어땠는대요?
디미페이 v1 백엔드는 TypeScript와 Expressjs 바탕의 디미고인 v3 백엔드 코드 베이스를 기반으로 개발되었어요. 디미고인(교내 인트라넷)과 다른 점은 데이터 모델 도구로 Prisma와 Postgresql을 사용했다는 점이 있고, 나머진 매우 비슷한 모습이에요.
디미고인 백엔드 아키텍처는 간단한 CRUD 서버를 만들기에 간결하고 체계적인 구조를 가지고 있어요. 디미페이도 처음엔 이 구조에 잘 적응했어요. 하지만 복잡한 로직을 처리하는 일이 늘어나면서 점점 CURD 코드베이스의 한계가 느껴지기 시작했어요.
"이번에도 컨트롤러 하나 만들면 되겠지."
"완전 처음부터 구현하고있는데, 아무래도 이건 아닌 것 같아."
처음엔 릴리즈 일정을 맞추려고 너무 빨리 만들다 보니 엉망이 되지 않았나 생각했어요. 그런데 이 코드베이스에선 아무리 여유를 가지고 개발한다 하더라도 이 정도 규모의 백엔드 코드를 깔끔하게 다룰 수 없다는 걸 깨달았어요. 제가 알아낸 이유는 규칙이 없고 즉흥적으로 만들고 있다는 것이었어요. 여러 책임을 가지고 있는 큰 함수를 작은 함수로 분리하더라도, 그 분리의 기준이 명확하지 않았었어요. 지금 옛날 코드를 둘러보면 리펙터링이라고 해놓은 게 입력 유효성 검사와 비즈니스 로직을 함께 처리하고 있는 게 꽤 있더라고요.
사실 express는 개발자가 따라야 할 엄격한 규칙이 없어(unopinionated) 개발자가 자유롭게 사용하도록 만들어진 프레임워크예요. 그러나 개발 경험이 많지 않았던 저에겐 오히려 단점이었어요. 저는 이 제자리걸음 리펙터링을 잠시 접어두고 리펙터링을 자꾸 실패하는 원인이 무엇이고, 큰 코드베이스는 어떻게 코드를 관리하고 있는지 알아내고 싶었어요.
DDD & 헥사고날 아키텍처
여러 오픈소스를 둘러보다 우연히 Domain-Driven Hexagon이라는 리포지토리를 찾게 되었어요. 그리고 이건 앞으로 저의 백엔드 여정을 완전히 바꿔놓았죠. 이 리포지토리는 DDD(도메인 주도 설계)와 헥사고날 아키텍처를 구현한 예제 소스코드예요. 예제 코드지만, 아무것도 모르는 제가 보더라도 적절히 분리된 책임과 깔끔한 폴더 구조는 제가 해결하고 싶었던 모든 것을 해결해 줄 수 있다는 걸 느낄 수 있었어요.
결과적으로 바로 이게 제가 찾던 거였어요. 위 그림은 이 소스코드에서 구현하고 있는 헥사고날 아키텍처의 다이어그램이에요. 이 그림을 모두 이해하는 데만 정말 많은 시간이 들었어요. 모든 걸 여기서 설명하기엔 내용이 너무 길어지니, 지금은 도메인 주도 설계와 헥사고날 아키텍처가 도대체 뭐길래 대단한지 간단히 알아보도록 해요.
DDD(Domain-Driven Design) Nutshell!
큰 의미로 도메인은 조직이 하는 일과 그 조직 안의 세계를 뜻해요. DDD에선 도메인을 기업의 핵심 서비스인 비즈니스 도메인과 이와 함께 동작하는 하위 도메인으로 나누고, 이들의 경계를 적절히 설정하여 일관되고 예측 가능한 동작을 설계할 수 있도록 도와줘요. 그리고 이를 코드상에 엔티티(Entity)와 애그리겟(aggregate) 등의 도메인 빌딩 블록으로 구현해요.
이렇게 도메인 모델을 구현하면, 잘 사용하는 방법도 중요해요. 결제 승인 로직이 있다면, 이 로직을 사용자가 직접 호출할 순 없잖아요? 사용자 입력을 검증하고, 트랜잭션을 시작하고, 결제 완료 알림을 보내는 등 해야 할게 많으니까요. 그런데 만약 이런 것까지 도메인 로직에 전부 집어넣는다면 더 이상 도메인 모델이라고 부를 수 없을 뿐만 아니라 코드를 유지하기도 정말 어려워질 거예요. 왜냐면 디미페이 v1 백엔드가 그랬거든요:)
이런 문제를 예방하고 정돈된 코드베이스를 유지하기 위한 방법을 아키텍처 패턴라고 불러요. DDD에서 사용할 수 있는 대표적인 아키텍처에는 계층형 아키텍처(layered architecture), 클린 아키텍처(clean architecture), 어니언 아키텍처(onion architecture), 헥사고날(또는 포트-어댑터) 아키텍처(hexagonal/port-adapter architecture) 등이 있어요. 많아 보이지만, 사실 계층형 아키텍처에서 파생되고 파생된 것들이라 압도되지 않아도 괜찮아요.
계층형 아키텍처는 인터페이스, 애플리케이션, 도메인, 인프라의 네 가지 계층으로 나누고, 상위 계층만 하위 계층에 의존할 수 있다는 규칙이 있어요.
인터페이스 계층: UI와 API 엔트포인트 등 사용자와 사용작용을 처리
애플리케이션 계층: 인터페이스와 도메인 계층을 연결하며, 필요한 리소스를 불러오고 도매인 로직을 호출
도메인 계층: 엔티티와 비즈니스 로직을 관리
인프라 계층: 리파지토리 등의 기술적 지원을 담당
전통적인 계층형 아키텍처는 인프라 계층이 가장 아래에 있어요. 근데 이 규칙 때문에 영속성을 담당하는 리파지토리가 도메인 계층을 참조할 수 없는 문제가 발생해요. 이를 해결하려면 도메인 계층에서 정의한 리파지토리 인터페이스를 애플리케이션 계층에서 구현하도록 만들 수 있어요. 하지만 더 좋은 방법이 있는데, 바로 의존성 역행의 원리(DIP)를 이용하여 인프라 계층은 제일 위로, 도메인 계층은 가장 아래로 만드는 거예요.
이 규칙이 적용된 아키텍처가 클린 아키텍처, 어니언 아키텍처, 헥사고날 아키텍처예요. DIP는 로버트 마틴이 제안했고, 하위 모듈이 상위 모듈에서 정의한 인터페이스에 의존한다는 규칙을 가지고 있어요. 즉, 인프라는 애플리케이션이나 도메인 계층에서 정의된 인터페이스를 의존할 수 있어요. 이 모습이 그 유명한 클린 아키텍처의 모습이에요.
이제 헥사고날 아키텍처를 설명할 수 있어요. 헥사고날은 앨리스테어 콕번이 재시한 아키텍처로, 포트라는 개념을 명시한 아키텍처예요. 포트의 역할 중 하나는 애플리케이션의 내부와 외부의 경계를 만들어 주는 거예요. 여기서 "내부"는 도메인 모델과 애플리케이션을 뜻하고, "외부"는 API, DB, 메시징과 같은 말 그대로 외부(인프라와 비슷함)를 뜻해요. 포트를 구현하는 어댑터를 구현하면 애플리케이션 내부와 호환되기 때문에 새로운 어뎁터를 만들고 테스트가 용이해지는 장점이 있어요.
"왜 이름이 헥사고날이에요?" 콕번의 글에서도 나와있듯이, 육각형에 큰 의미가 있는 게 아니라, 육각형은 포트와 어댑터 그림을 넣을 자리가 많아서 사용했다고 해요. 그래서 포트와 어댑터 아키텍처로 이름이 바뀌긴 했지만, 여전히 많은 사람들이 헥사고날이라고 부르기 때문에 두 이름을 가지게 되었어요. 하지만 포트 덕분에 여러 애플리케이션이 쉽게 연결될 수 있다는 점이 벌집과 비슷해서 육각형이라는 해석도 있는데, 이 말도 그럴싸하지 않나요?