애플리케이션 구조, 어떻게 잡아야 할까? (도메인, 패키지, MSA, DB)
서비스를 처음 만들 때 가장 먼저 부딪히는 벽은 바로 '구조 설계'다. 도메인은 뭘로 정의할지, 패키지는 어떻게 나눌지, 아키텍처는 모놀리식으로 갈지 MSA로 갈지...
서버 설계시 성능, 확장성(객체지향!!!!), 가용성, 유지보수성, 보안성, 비용 효율성을 모두 따져봐야 한다.
오늘은 애플리케이션을 지탱하는 구조 설계의 핵심 개념들을 정리해본다.
1. 도메인(Domain)이란?
도메인은 우리가 해결하고자 하는 영역 또는 만들 서비스의 주요 개념이다
- 쇼핑몰 → 상품(Product), 장바구니(Cart), 주문(Order), 회원(User) 등이 도메인이 된다.
- 비즈니스의 핵심 가치를 담고 있는 덩어리
2. 모듈(패키지)은 어떻게 나눌까?
코드를 작성할 때 폴더(패키지) 구조를 잡는 방법은 크게 두 가지가 있다.
① 기술 기반 패턴 (Layered Architecture)
가장 고전적이고 익숙한 방식이다. 역할(Layer)에 따라 나눈다.
- 구조: Controller, Service, Repository, Domain 별로 패키지를 생성.
- 장점: 초기 구현이 매우 직관적이고 간단하다.
- 단점: 프로젝트가 커지면 지옥이 시작된다. 예를 들어 User 관련 기능을 수정하려면 Controller 패키지 갔다가, Service 패키지 갔다가, Repository 패키지를 다 뒤져야 한다. 관련된 코드가 여기저기 흩어져 있어 유지보수가 힘들어진다.
- 예시
📁 src
├── 📁 main
│ ├── 📁 java (또는 python, ts 등 언어별 루트)
│ │ ├── 📁 com.company.app
│ │ │ ├── 📁 controller // 🌐 Presentation/Controller 레이어
│ │ │ │ ├── 📄 UserController.java
│ │ │ │ ├── 📄 ProductController.java
│ │ │ │ └── 📄 OrderController.java
│ │ │ ├── 📁 service // ⚙️ Business Logic/Service 레이어
│ │ │ │ ├── 📄 UserService.java
│ │ │ │ ├── 📄 ProductService.java
│ │ │ │ └── 📄 OrderService.java
│ │ │ ├── 📁 repository // 💾 Data Access/Repository 레이어
│ │ │ │ ├── 📄 UserRepository.java
│ │ │ │ ├── 📄 ProductRepository.java
│ │ │ │ └── 📄 OrderRepository.java
│ │ │ ├── 📁 model // 🗃️ Domain Model/Entity
│ │ │ │ ├── 📄 User.java
│ │ │ │ ├── 📄 Product.java
│ │ │ │ └── 📄 Order.java
│ │ │ └── 📁 config // 환경 설정 및 공통 기능
│ │ │ └── 📄 SecurityConfig.java
│ └── 📄 Application.java② 도메인 기반 패턴 (DDD 스타일)
요즘 많이 선호하는 방식이다. 관련된 업무(도메인)끼리 뭉친다.
- 구조: User, Order, Product 패키지를 만들고, 그 안에 각각의 Controller, Service, Repository를 넣는다.
- 특징: 각 패키지마다 비슷한 CRUD 구조가 반복되는 것처럼 보일 수 있다. 하지만 이게 장점이다.
- 장점:
- 응집도: User 도메인을 수정해야 하면 User 패키지 안에서만 놀면 된다. 유지보수와 확장성이 뛰어나다.
- AI 친화적(?): 요즘처럼 코딩 어시스턴트(AI)를 많이 쓰는 시대에, 관련 코드가 한곳에 모여 있으면 AI가 컨텍스트를 이해하기 훨씬 쉽다. (이건 꽤 큰 장점이다)
- 예시
📁 src
├── 📁 main
│ ├── 📁 java (또는 python, ts 등 언어별 루트)
│ │ ├── 📁 com.company.app
│ │ │ ├── 📁 user // 👨💼 User 도메인
│ │ │ │ ├── 📄 UserController.java
│ │ │ │ ├── 📄 UserService.java
│ │ │ │ ├── 📄 UserRepository.java
│ │ │ │ └── 📄 User.java (Domain Model/Entity)
│ │ │ ├── 📁 product // 📦 Product 도메인
│ │ │ │ ├── 📄 ProductController.java
│ │ │ │ ├── 📄 ProductService.java
│ │ │ │ ├── 📄 ProductRepository.java
│ │ │ │ └── 📄 Product.java (Domain Model/Entity)
│ │ │ ├── 📁 order // 🛒 Order 도메인
│ │ │ │ ├── 📄 OrderController.java
│ │ │ │ ├── 📄 OrderService.java
│ │ │ │ ├── 📄 OrderRepository.java
│ │ │ │ └── 📄 Order.java (Domain Model/Entity)
│ │ │ └── 📁 shared // 공통 유틸리티/설정
│ │ │ ├── 📁 util
│ │ │ ├── 📁 config
│ │ │ └── 📁 exception
│ └── 📄 Application.java3. 아키텍처 스타일: Monolithic vs MSA
서버를 어떻게 배포하고 관리할 것인가에 대한 문제다.
① 모놀리식 (Monolithic)
- 특징: 모든 기능이 하나의 애플리케이션 안에 다 들어있다. 한 통에 다 넣고 끓이는 전골 같은 느낌. 보통 대부분의 토이 프로젝트는 이 아키텍처 스타일을 따를 것이라 생각한다.
- 장점: 배포가 단순하고, 하나의 DB를 쓰기 때문에 데이터 일관성(Transaction) 관리가 쉽다.
- 단점:
- 작은 기능 하나만 수정해도 전체 애플리케이션을 다시 빌드하고 배포해야 한다.
- 특정 기능만 트래픽이 몰려도 전체 서버를 스케일 아웃해야 해서 비효율적일 수 있다.
② MSA (Microservices Architecture)
이 아키텍처는 직접 설계해본 적이 없어서 직접 와닫지는 않는다.
- 특징: 애플리케이션을 여러 개의 독립적인 서비스로 잘게 쪼갠다. (주문 서버, 회원 서버, 배송 서버 등)
- 통신: 각 서비스끼리는 HTTP/HTTPS나 메시지 큐(Kafka 등)를 통해 대화한다.
- 장점: 각 서비스별로 독립적인 배포와 확장이 가능하다. 장애가 나도 전체 서비스가 죽지 않고 해당 기능만 죽는다.
- 단점:
- 복잡도 폭발: 운영 비용이 증가하고 관리가 어렵다.
- 데이터 일관성: DB가 쪼개져 있으니 JOIN도 안 되고 트랜잭션 관리(2PC, Saga 패턴 등)가 헬게이트다.
4. API 설계와 게이트웨이
API는 클라이언트와 서버가 소통하는 통로이다. API를 잘 설계하면 개발자들이 헷갈리지 않고 기능을 빠르게 구현할 수 있다.
또한 swagger나 restdoc 같은 라이브러리를 통해 문서화를 하는 것도 큰 도움이 된다.
운영 환경, 특히 MSA 환경에서는 클라이언트가 각 마이크로서비스에 직접 요청을 보내지 않는다. 중간에 문지기를 하나 세우는데, 이게 API Gateway다.
API Gateway의 역할
- 모든 요청을 받아 적절한 서비스로 라우팅해준다.
- 인증/인가, 로깅, 속도 제한(Rate Limiting), 오류 처리 등 공통된 기능을 중앙에서 제어한다.
Q. 잠깐, 이거 스프링의 Dispatcher Servlet 아닌가?
A. 아니다! 비슷해 보이지만 노는 물이 다르다.
- Dispatcher Servlet:
- 위치: 하나의 Spring 애플리케이션 내부에 존재.
- 역할: 들어온 HTTP 요청을 어떤 Controller가 처리할지 결정하는 앱 내부의 교통정리 담당.
- API Gateway:
- 위치: 서버(인프라)의 최전방에 존재. (별도의 서버거나 Nginx, Cloud Gateway 등)
- 역할: 클라이언트의 요청을 어떤 서버(서비스)로 보낼지 결정하는 시스템 전체의 교통정리 담당.
즉, 요청 흐름은 (스프링이라면) Client → API Gateway → 개별 서비스(Spring App) → Dispatcher Servlet → Controller 순서가 된다.
5. 데이터베이스 설계: 샤딩과 레플리케이션
서비스가 커지면 DB 하나로는 버티기 힘들다. 이때 사용하는 기법들이다.
샤딩 (Sharding)
- 개념: 데이터가 너무 많을 때 여러 서버나 테이블에 나누어 저장하는 기술. (수평 분할)
- 예시: 사용자 ID가 홀수면 A서버, 짝수면 B서버에 저장. 쓰기 성능을 분산시킬 때 필수적이다.
레플리케이션 (Replication)
- 개념: 데이터를 다른 서버에 복제해두는 기술. (Master-Slave 구조)
- 목적:
- 장애 대응: 본진(Master)이 터지면 복제본(Slave)을 승격시켜 서비스를 유지한다.
- 성능 향상: 쓰기는 Master에, 읽기는 Slave에서 수행하여 부하를 분산한다(Read-only 복제).
마치며
완벽한 아키텍처는 없다. 작은 프로젝트에 굳이 MSA와 샤딩을 도입할 필요는 없고, 거대한 서비스에 모놀리식을 고집하면 확장이 불가능하다. 현재 팀의 규모와 서비스의 복잡도에 맞는 구조를 선택하는 것이 설계의 핵심이다.
참고. Gemini Pro3가 나왔대서 미리 작성해둔 초안을 바탕으로 포스팅해달라고 했습니당. 짱이당
'항해 Lite' 카테고리의 다른 글
| 서버 구조 설계(3) - 콘서트 예약 서비스 만들기 리뷰 (1) (0) | 2025.11.20 |
|---|---|
| 서버 구조 설계(2) - 인프라 설계 요소 (0) | 2025.11.19 |
| TDD(3) - 완성 (1) | 2025.10.22 |
| TDD(2) - 첫 작성해보기 (0) | 2025.10.16 |
| TDD (1) - TDD의 정의와 중요성 (0) | 2025.10.15 |