사건의 시작은 현재 사용하고 개발 및 운영하고 있는 우리 서비스인 DMS(기숙사 관리 시스템)를 AWS로 마이그레이션하는 것이었다.
이 아키텍처를 나는 AWS로 마이그레이션해야 했다.

위 이미지처럼 Public Subnet과 Private Subnet으로 나누고, 각 서버마다 모니터링 시스템을 넣고, NAT Gateway를 사용할 돈은 없으니 앞단에 Public Subnet에 있는 서버를 사용해서 외부 인터넷에 접근하고, 이런 식으로 인프라 아키텍처를 다 설계한 뒤 이걸 Terraform으로 옮기고, CI/CD 파이프라인을 구축하면서 든 생각이 있다. (참고로 이 작업을 대략 2주 동안 했다...)
MSA는 분명 장점이 있는 아키텍처이다. 서비스별 독립 배포가 가능하고, 특정 서비스에만 트래픽이 몰릴 경우 개별적으로 확장할 수 있으며, 장애가 발생하더라도 다른 서비스에 영향을 최소화할 수 있다.
하지만 이러한 장점은 충분한 규모의 서비스에서 비로소 빛을 발한다. 수백만 명의 사용자를 대상으로 하거나, 여러 팀이 동시에 개발을 진행하거나, 서비스별로 다른 기술 스택을 사용해야 하는 상황이라면 MSA는 강력한 선택지가 될 수 있다.
하지만 우리 서비스는 그렇지 않았다. 사용자는 고작 200명뿐이고, 트래픽이 몰려서 서버가 터진 경험도 없다. 심지어 우리 DMS를 개발하는 Backend Developer는 2~3명이다. 그리고 MSA는 서버를 나눔으로써 비용이 Monolithic Architecture보다 많이 든다. 그런데도 불구하고 굳이 MSA를 유지해서 얻는 장점은 무엇일까?
Notification Server가 죽을 때 Main Server가 죽지 않는 것밖에 존재하지 않는다. 물론 이 장점은 매우 크지만, 우리 서비스에서는 그렇게 큰 의미가 없었다.
그래서 난 결심했다! 이 MSA 아키텍처를 갈아엎고 Monolithic Architecture로 돌아가기로 말이다.
일단 Monolithic Architecture로 합치기 전에 먼저 불필요한 코드를 제거하기로 했다. 사용되지 않는 기능까지 유지하면 이후 병합 과정에서 더 힘들어질 것 같았기 때문이다.
코드를 쭉 읽어본 결과 자습실 자리 예약, 봉사 신청, 외출 신청 같은 기능들은 사용하지 않는 것을 확인하고 과감하게 삭제했다. (어차피 나중에 필요하면 Version Control로 다시 가져오면 되니까)
Notification Server를 Main Server로 흡수하는 과정은 생각보다 쉽지 않았다.
가장 먼저 고민했던 부분은 알림 발송 방식이었다.
기존 구조에서는 Main Service와 Notification Service가 분리되어 있었기 때문에 Outbox Pattern과 RabbitMQ를 사용하고 있었다. 사용자의 행동으로 인해 알림이 발생하면 Outbox 테이블에 이벤트를 저장하고, 이후 RabbitMQ를 통해 Notification Service가 해당 이벤트를 소비하는 구조였다.
덕분에 서비스 간 통신 과정에서 발생할 수 있는 메시지 유실 문제를 줄일 수 있었지만, Monolithic Architecture로 전환하는 지금도 과연 이 구조를 사용하는 것이 맞는지에 대한 의문이 들었다.
현재 DMS의 사용자는 약 200명 수준이고, 알림이 몇 초 늦게 전송되거나 아주 드물게 유실되는 것이 서비스 전체에 치명적인 문제를 일으키지 않는다. 반면 RabbitMQ를 유지하기 위해서는 별도의 컨테이너를 운영해야 하고, 버그가 발생했을 때 확인해야 할 부분도 늘어난다.
나는 신뢰성과 복잡도라는 두 가지 속성 중에서 하나를 선택해야 하는 트레이드오프에 놓였다. 나는 복잡도를 낮추는 것이 더 이득이 많을 것이라고 판단했다.
그래서 RabbitMQ와 Outbox Pattern을 제거하고, 대신 @TransactionalEventListener(phase = AFTER_COMMIT)와 @Async를 이용해 알림을 비동기적으로 처리하도록 변경했다.
이렇게 하면 데이터베이스 트랜잭션이 성공적으로 커밋된 이후에만 알림 발송 로직이 실행되므로, 트랜잭션이 롤백되었는데 알림만 전송되는 문제는 방지할 수 있다.
물론 Outbox Pattern만큼 강한 신뢰성을 보장하지는 않는다. 서버가 알림 발송 직전에 종료되면 메시지가 유실될 수도 있다.
하지만 현재 DMS의 규모와 운영 환경을 고려했을 때, 그 정도의 리스크는 감수할 수 있다고 판단했다. 얻을 수 있는 안정성보다 유지해야 하는 복잡도가 더 크다고 생각했기 때문이다.
결과적으로 Notification Server, RabbitMQ, 관련 인프라를 모두 제거할 수 있었고 아키텍처는 훨씬 단순해졌다.
기존 DMS는 Gateway Server가 모든 요청을 먼저 받은 뒤 JWT를 이용해 인증/인가를 수행하고, 인증에 성공하면 요청 헤더에 passport라는 긴 무작위 문자열을 추가하여 다른 서버로 요청을 전달하는 구조였다.
이 구조는 서비스가 분리되어 있을 때는 나름의 장점이 있었다. Gateway Server가 인증이라는 책임을 전담하고, 내부 서비스들은 passport만 검증하면 되었기 때문이다.
하지만 Notification Server를 Main Server로 흡수하면서 상황이 달라졌다.
더 이상 요청을 라우팅해야 할 서버가 존재하지 않게 되었고, 결국 Gateway Server의 역할은 인증/인가 처리만 남게 되었다.
하지만 인증/인가 처리만을 위해 별도의 서버를 운영하는 것은 오히려 과도한 비용이라고 생각했다. 서버 한 대를 더 관리해야 하고, 배포 파이프라인도 추가로 관리해야 하며, 요청이 한 번 더 네트워크를 거쳐야 하기 때문이다.
그래서 Gateway Server가 담당하던 JWT 인증/인가 로직을 Main Server로 옮기기로 했다.
기존에는 다음과 같은 요청 흐름을 사용했다.
Client
↓
Gateway Server
↓
Main Server
하지만 변경 후에는 다음과 같이 단순화되었다.
Client
↓
Main Server
덕분에 관리해야 하는 서버 수가 하나 줄어들었고, 네트워크 홉도 감소했다. 또한 인증 로직과 비즈니스 로직이 하나의 애플리케이션 안에서 동작하게 되면서 배포 과정 역시 훨씬 단순해졌다.
이로써 DMS는 Gateway Server와 Notification Server를 제거하고 Monolithic Architecture를 구성하게 되었다.

이렇게 새로운 t3.medium 크기의 EC2를 생성한 docker를 설치하고 다음 ECR에 이미지를 Push하고, EC2에서 최신 이미지를 Pull 받아온 다음 docker-compose.yml파일로 컨테이너를 띄웠다. 또한 nginx를 통해 리버스 프록시를 구성했다.

이전에는 Gateway Server, Main Server, Notification Server 3개의 서버가 존재했기 때문에 배포 과정도 복잡했다.
서비스마다 별도의 Docker 이미지를 빌드해야 했고, 각 이미지를 ECR에 업로드한 뒤 EC2에서 각각 배포해야 했다. 또한 특정 서비스만 수정하더라도 관련된 파이프라인과 배포 과정을 함께 관리해야 했다.
하지만 Monolithic Architecture로 전환하면서 상황이 완전히 달라졌다.
이제 배포해야 할 애플리케이션은 단 하나뿐이다.
현재 CI/CD 파이프라인은 다음과 같은 순서로 동작한다.
이전에는 여러 서비스의 배포 상태를 각각 확인해야 했지만, 이제는 하나의 파이프라인만 관리하면 된다.
덕분에 배포 과정이 단순해졌고, 장애가 발생했을 때 원인을 파악하는 것도 훨씬 쉬워졌다.
기존의 복잡한 MSA 아키텍처를 Monolithic Architecture로 전환하면서 DMS의 운영 복잡도는 크게 감소했다.
Gateway Server, Notification Server, RabbitMQ를 제거하면서 관리해야 하는 서버 수가 줄어들었고, 서비스 간 통신을 위한 여러 설정들도 함께 사라졌다. 또한 배포 파이프라인 역시 하나로 통합되면서 운영 부담이 훨씬 줄어들었다.
그리고 기존에는 t3.micro 1개, t3.small 4개를 사용할 예정이었다. 이렇게 해서 소모되는 금액은 약 $78.36/월이었다. 한화로는 약 115,000원이다.
| 리소스 | 월 비용 |
|---|---|
| Gateway Server (t3.micro) | $7.59 |
| Main Server (t3.small) | $15.18 |
| Notification Server (t3.small) | $15.18 |
| Infra Server (t3.small) | $15.18 |
| Monitoring Server (t3.small) | $15.18 |
| EBS Storage (70GB) | $6.38 |
| Elastic IP | $3.65 |
| 총 비용 | 약 $78.36 / 월 |
| 항목 | 기존 MSA 구조 | Monolithic Architecture |
|---|---|---|
| EC2 Compute | $68.33 | $30.37 |
| EBS | $6.38 (70GB) | $2.74 (30GB) |
| Elastic IP | $3.65 | $3.65 |
| 월 합계 | $78.36 | $36.75 |
이렇게 Monolithic Architecture로 바꾸게 되면 약 $36.75/월 가격으로, 한화 약 54,000원 수준으로 약 53%의 비용 절감에 성공했다!
나는 그동안 "MSA가 더 발전된 아키텍처"라고 생각하고 있었다. 그래서 서비스 규모와 상관없이 MSA를 사용하면 좋은 서비스라고 생각했다. 하지만 이번 일을 계기로 반성하게 되었다.

중요한 것은 현재 서비스 규모와 팀이 감당할 수 있는 복잡도에 맞는 구조를 선택하는 것이었다.
수십 명의 개발자가 함께 개발하고, 수많은 사용자가 이용하는 서비스라면 MSA는 좋은 선택이 될 수 있다. 하지만 개발자 수가 적고 사용자 규모도 크지 않은 DMS에는 오히려 불필요한 복잡성을 만들어내고 있었다.
이번 경험을 통해 기술적 의사결정이라는 것을 배울 수 있었다.
언젠가 DMS가 더 성장해서 지금의 Monolithic Architecture가 한계에 도달한다면 MSA를 도입해야 할 수도 있다. 하지만 내가 생각하기에 지금은 아니다. 언젠가 팀원이 많아지고, 서비스가 복잡해지고, 트래픽이 많이 몰린다면 그때서야 도입하는 것이 좋다고 생각한다.
그래서 나는 이번에 한 작업을 한 줄로 표현하자면
세상에 최고의 아키텍처란 없다. 그저 최선의 아키텍처만 있을 뿐....
개인적으로 명언을 남기고 싶어서 이렇게 적어보았다...(좀 쪽팔리다)
고생하셨네요! 저도 예전에는 낭만파 개발자였지만, 요즘 들어서는 현실파 개발자가 되가는 것 같습니다. 한샘님도 이번 경험을 통해 큰 성장과 상황에 맞는 아키텍처, 인프라 선택을 할 수 있는 개발자가 되신 것 같습니다.