Skip to content

Commit 8c923bf

Browse files
authored
Merge pull request #127 from woowacourse-checkmo/feat/2/DB_Migration
Spring Modulith 전환 작업 완료 및 DB Migration 완료
2 parents 3ad8e1a + 6ad3d66 commit 8c923bf

File tree

416 files changed

+11871
-14071
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

416 files changed

+11871
-14071
lines changed

README.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,47 @@
1-
# 💻 Backend Members
1+
## 🏛️ Architecture
2+
3+
이 프로젝트는 **Spring Modulith**를 활용하여 모놀리식 환경에서 도메인 모듈 분리를 구현했습니다.
4+
5+
기존에는 Facade 패턴을 통해 도메인 간 경계를 나누었지만, 이는 코드로 강제할 수 없는 팀 간 약속에 불과했고 연관관계 생성을 위해 예외적으로 도메인 간의 참조를 허용하는 등 허점이 존재했습니다.
6+
하지만 Spring Modulith를 도입해서 **패키지 구조 자체가 모듈 경계를 강제**하고, **자동화된 검증 테스트가 위반 사항을 검사**하는 구조로 변경했습니다.
27

3-
| [**모두까기** / **임경표**](https://github.com/MODUGGAGI) | [**채이** / **이채은**](https://github.com/chaechaen) | [**송글송글** / **신지윤**](https://github.com/Yoon0221) | [**지니** / **정효정**](https://github.com/zjhj0814) |
4-
|:----------------------:|:----------------------:|:----------------------:|:--------------------:|
5-
| 팀장 🧑🏻‍💻 | 팀원 👩🏻‍💻 | 팀원 👩🏻‍💻 | 팀원 👩🏻‍💻 |
8+
각 도메인 모듈은 명확히 정의된 **Public API****이벤트**를 통해서만 통신하며, 외부 도메인 모듈은 내부 구현에 대해서 알지 못하게 추상화, 캡슐화 시켰습니다.
9+
10+
프로젝트의 아키텍처 변화 과정과 현재 구조에 대한 상세한 내용은 아래 문서들을 참고해주세요.
11+
12+
* **[🏛️ Facade 패턴 회고: 시도와 한계](./docs/01_facade_legacy.md)**
13+
* **[🏛️ Spring Modulith 아키텍처 (현재)](./docs/02_spring_modulith.md)**
614

715
---
8-
## 🏛️ Architecture
916

10-
우리 프로젝트는 MSA의 설계 사상을 모방한 모놀리식 아키텍처를 지향합니다. 각 도메인은 **Facade 패턴**을 통해 명확히 분리되며, 이는 코드의 유지보수성과 확장성을 극대화하는 핵심적인 설계 원칙입니다.
17+
## 📈 Module Dependency Graph
1118

12-
프로젝트의 상세한 설계 철학과 규칙은 아래 문서들을 참고해주세요.
19+
아래의 각 모듈간의 의존성 UML은 Spring Modulith의 [Documenting Application Modules](https://docs.spring.io/spring-modulith/reference/documentation.html)를 참고하여 문서화한 결과입니다.
1320

14-
* **[🏛️ Facade 패턴 설계 원칙](./docs/01_facade_pattern.md)**
15-
* **[🏛️ 아키텍처 관련 FAQ](./docs/02_facade_faq.md)**
16-
* **[🏛️ 객체 생성 시 Entity 관계 설계](./docs/03_entity_relations.md)**
21+
* [각 모듈들의 의존성 확인하기](./docs/module_graph.md)
1722

1823
---
24+
1925
## 🖥️ Server Architecture
26+
2027
<div align="center">
2128
<a href="./docs/images/Checkmo_Server_Architecture.png">
2229
<img src="./docs/images/Checkmo_Server_Architecture.png" alt="서버 아키텍처" width="800"/>
2330
</a>
2431
</div>
2532

2633
---
34+
2735
## 🚀 Deployment Architecture
36+
2837
<div align="center">
2938
<a href="./docs/images/Checkmo_AWS_Deploy_Architecture.png">
3039
<img src="./docs/images/Checkmo_AWS_Deploy_Architecture.png" alt="서버 아키텍처" width="800"/>
3140
</a>
3241
</div>
3342

3443
---
44+
3545
## 🛠️ Tech Stacks
3646

3747
<div align="center">

build.gradle

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ dependencies {
5050
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
5151

5252
// ============= 환경 변수 관리 =============
53-
//implementation 'me.paulschwarz:spring-dotenv:4.0.0'
53+
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
5454

5555
// ============= JWT 인증 =============
5656
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
@@ -76,6 +76,22 @@ dependencies {
7676
// ============ AWS 클라우드 서비스 =============
7777
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.4.0")
7878
implementation "io.awspring.cloud:spring-cloud-aws-starter-s3"
79+
80+
// ============ Spring Modulith =============
81+
implementation 'org.springframework.modulith:spring-modulith-starter-core'
82+
implementation 'org.springframework.modulith:spring-modulith-starter-jpa'
83+
implementation 'org.springframework.modulith:spring-modulith-events-api'
84+
testImplementation 'org.springframework.modulith:spring-modulith-starter-test'
85+
86+
// ============ flyway =============
87+
implementation 'org.flywaydb:flyway-core'
88+
implementation 'org.flywaydb:flyway-mysql'
89+
}
90+
91+
dependencyManagement {
92+
imports {
93+
mavenBom "org.springframework.modulith:spring-modulith-bom:1.4.4"
94+
}
7995
}
8096

8197
tasks.named('test') {

compose-dev.yml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
services:
2-
# mysql:
3-
# image: 'mysql:8.0'
4-
# environment:
5-
# MYSQL_DATABASE: checkmo_db
6-
# MYSQL_ROOT_PASSWORD: checkmo123
7-
# ports:
8-
# - 3306:3306
9-
# volumes:
10-
# - mysql_data:/var/lib/mysql
11-
# networks:
12-
# - checkmo-network
13-
# healthcheck:
14-
# test: [ "CMD", "mysqladmin", "ping" ]
15-
# interval: 5s
16-
# retries: 10
2+
mysql:
3+
image: 'mysql:8.0'
4+
environment:
5+
MYSQL_DATABASE: checkmo_db
6+
MYSQL_ROOT_PASSWORD: checkmo123
7+
ports:
8+
- 3306:3306
9+
volumes:
10+
- mysql_data:/var/lib/mysql
11+
networks:
12+
- checkmo-network
13+
healthcheck:
14+
test: [ "CMD", "mysqladmin", "ping" ]
15+
interval: 5s
16+
retries: 10
1717

1818
redis:
1919
image: 'redis:7.2-alpine'
@@ -25,7 +25,7 @@ services:
2525
networks:
2626
- checkmo-network
2727
healthcheck:
28-
test: ["CMD", "redis-cli", "-a", "checkmo123", "ping"]
28+
test: [ "CMD", "redis-cli", "-a", "checkmo123", "ping" ]
2929
interval: 5s
3030
timeout: 3s
3131
retries: 10
@@ -35,5 +35,5 @@ networks:
3535
driver: bridge
3636

3737
volumes:
38-
# mysql_data:
38+
mysql_data:
3939
redis_data:

docs/01_facade_legacy.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# 🏛️ Facade 패턴 회고: 시도와 한계
2+
3+
---
4+
5+
## 1. 왜 Facade 패턴을 도입했는가?
6+
7+
저희는 기존에 단일 서버 환경의 **모놀리식(Monolithic) 아키텍처**로 프로젝트를 개발했습니다.
8+
9+
프로젝트를 진행하면서 **마이크로서비스 아키텍처(MSA)** 라는 개념을 처음 접하게 되었고, 각 도메인이 물리적으로 분리된 서버에서 독립적으로 동작하는 구조에 큰 영감을 받았습니다. 특히 도메인 간의 명확한 경계와 독립성이 인상 깊었습니다.
10+
11+
"모놀리식 환경이지만, 도메인을 논리적으로 분리해서 마치 별도의 서버처럼 독립적으로 동작하게 만들 수는 없을까?"
12+
13+
이런 고민 끝에 **Facade 패턴**을 도입하여 각 도메인이 오직 Facade를 통해서만 소통하도록 설계했습니다. MSA의 도메인 분리 철학을 모놀리식 환경에서 구현해보는 시도였습니다.
14+
15+
### 패키지 구조의 변경
16+
17+
기존에 진행한 프로젝트에서는 레이어드 아키텍처(`service`, `entity`, `repository` 계층 분리)를 사용했지만
18+
도메인간의 낮은 결합도를 구현하기 위해 **도메인(Domain) 중심 패키지 구조**로 전환했습니다.
19+
20+
가장 중요한 규칙:
21+
> **각 도메인은 자신의 경계 밖을 절대 직접 참조할 수 없다**
22+
23+
다른 도메인의 정보나 기능이 필요할 경우, 반드시 해당 도메인의 **Facade**를 통해서만 접근하도록 팀 규칙을 정했습니다.
24+
25+
---
26+
27+
## 2. 우리가 정한 Facade 설계 원칙 3가지
28+
29+
### 원칙 1: 도메인 간 직접 접근 금지
30+
31+
> 어떤 도메인도 다른 도메인의 서비스, 리포지토리, 엔티티를 직접 참조하거나 호출할 수 없다.
32+
33+
```java
34+
// ❌ 잘못된 방식: 다른 도메인의 Repository 직접 주입
35+
@Service
36+
@RequiredArgsConstructor
37+
public class BookStoryCommandServiceImpl {
38+
private final BookStoryRepository bookStoryRepository;
39+
private final MemberRepository memberRepository; // ❌ 외부 도메인 Repository 직접 참조
40+
41+
public Long createBookStory(String memberId, BookStoryCreateDTO dto) {
42+
Member member = memberRepository.findById(memberId);
43+
// ...
44+
}
45+
}
46+
47+
// ✅ 올바른 방식: Facade를 통한 접근
48+
@Service
49+
@RequiredArgsConstructor
50+
public class BookStoryCommandServiceImpl {
51+
private final BookStoryRepository bookStoryRepository;
52+
private final MemberQueryFacade memberQueryFacade; // ✅ Facade 사용
53+
54+
public Long createBookStory(String memberId, BookStoryCreateDTO dto) {
55+
// Facade를 통해 필요한 정보 조회
56+
MemberSharedDTO memberInfo = memberQueryFacade.getMemberBasicInfo(memberId);
57+
// ...
58+
}
59+
}
60+
```
61+
62+
### 원칙 2: 엔티티 관계와 ID 필드의 공존
63+
64+
JPA의 이점을 사용하기 위해 연관관계 매핑(`@ManyToOne`, `@OneToMany`)은 유지하기로 하였습니다.
65+
그리고 코드 레벨에서는 연관된 객체에 대한 직접 참조를 팀 규칙으로 금지했습니다.
66+
67+
**문제**: 연관된 객체의 ID를 어떻게 조회할 것인가?
68+
69+
**해결**: 읽기 전용 ID 필드 추가
70+
71+
```java
72+
@Entity
73+
public class BookStory extends BaseEntity {
74+
// 실제 관계 매핑 (직접 접근 금지)
75+
@ManyToOne(fetch = FetchType.LAZY)
76+
@JoinColumn(name = "member_id")
77+
private Member member;
78+
79+
// 연관 객체의 ID를 안전하게 조회하기 위한 읽기 전용 필드
80+
@Column(name = "member_id", insertable = false, updatable = false)
81+
private String memberId;
82+
83+
// ❌ 금지: member.getId()
84+
// ✅ 허용: getMemberId()
85+
}
86+
```
87+
88+
이를 통해 `bookStory.getMemberId()`로 다른 도메인의 객체를 로딩하지 않고 외래 키 ID를 조회할 수 있었습니다.
89+
90+
### 원칙 3: 공유 DTO를 통한 데이터 통신
91+
92+
> 도메인의 경계를 넘나드는 모든 데이터는 반드시 공유 DTO(SharedDTO)여야 한다.
93+
94+
```java
95+
// MemberQueryFacade.java
96+
public interface MemberQueryFacade {
97+
// ✅ 공유 DTO 반환
98+
MemberSharedDTO.BasicInfo getMemberBasicInfo(String memberId);
99+
100+
// ❌ 엔티티 직접 반환 금지
101+
// Member getMember(String memberId);
102+
}
103+
```
104+
105+
Facade 메서드는 항상 `global.dto` 패키지에 정의된 공유 DTO 형태로 데이터를 반환했습니다.
106+
107+
---
108+
109+
## 3. Facade 패턴의 한계와 문제점
110+
111+
### 문제 1: 팀 약속에 불과함
112+
113+
위에서 정한 원칙들은 컴파일러나 테스트 도구로 검증할 수 없는 팀원 간의 약속에 불과했습니다.
114+
115+
```java
116+
// 이렇게 해도 컴파일 에러가 발생하지 않음
117+
@Service
118+
public class SomeService {
119+
private final MemberRepository memberRepository; // ❌ 규칙 위반
120+
121+
public void someMethod() {
122+
memberRepository.findById("123");
123+
}
124+
}
125+
```
126+
127+
- 실수로 규칙을 위반해도 컴파일 에러가 발생하지 않아서 알아차리지 못할 수도 있음
128+
- 코드 리뷰에 의존해서 팀원들이 직접 확인해주어야 함
129+
130+
### 문제 2: 엔티티 관계 설정 시 예외 허용
131+
132+
새로운 엔티티를 생성할 때에는 JPA를 위해 어쩔 수 없이 연관된 엔티티를 설정해야 합니다.
133+
하지만 외부 도메인으로 데이터를 넘길 때에는 항상 공유 DTO를 넘기기로 **원칙3**에서 정했기 때문에 방법이 없었습니다.
134+
135+
- 그래서 엔티티를 생성할 때 관계 설정의 경우에만 프록시 객체를 통한 엔티티 직접 반환 메서드를 추가했습니다.
136+
137+
```java
138+
// MemberQueryFacade.java
139+
public interface MemberQueryFacade {
140+
// 일반적인 DTO 반환 메서드
141+
MemberSharedDTO.BasicInfo getMemberBasicInfo(String memberId);
142+
143+
// ❌ 예외: 관계 설정을 위한 프록시 반환 (원칙 3 위반)
144+
Member findMemberReferenceById(String memberId);
145+
}
146+
147+
// 사용하는 쪽
148+
@Service
149+
public class BookStoryCommandService {
150+
private final MemberQueryFacade memberQueryFacade;
151+
152+
public Long createBookStory(String memberId, CreateDTO dto) {
153+
// 엔티티 타입(Member)을 알아야 하므로 결합도 증가
154+
Member memberProxy = memberQueryFacade.findMemberReferenceById(memberId); //
155+
BookStory bookStory = BookStory.builder()
156+
.member(memberProxy) // JPA가 객체를 요구
157+
.build();
158+
// ...
159+
}
160+
}
161+
```
162+
163+
하지만 이 방식은 서로 다른 도메인의 정보를 알아야만 한다는 문제가 여전히 남았습니다.
164+
- 여전히 외부 도메인의 엔티티 타입(ex. `Member`)을 알아야 함
165+
- Facade가 DTO만 반환한다는 원칙 3을 위반
166+
167+
### 문제 3: Facade가 단순 서비스를 연결만 하는 경우에는 불필요한 Facade를 거치는 느낌
168+
169+
```java
170+
// 단순 조회의 경우
171+
public class MemberQueryFacadeImpl implements MemberQueryFacade {
172+
private final MemberQueryService memberQueryService;
173+
174+
@Override
175+
public MemberSharedDTO.BasicInfo getMemberBasicInfo(String memberId) {
176+
// 그냥 Service 메서드를 1:1로 호출만 함
177+
return memberQueryService.getMemberBasicInfo(memberId);
178+
}
179+
}
180+
```
181+
182+
단순 조회의 경우 Facade가 내부 Service를 1:1로 호출하는 구조가 되어, "굳이 Facade가 필요한가?"라는 의문이 생겼습니다.
183+
184+
---
185+
186+
## 4. 결론: Facade에서 Spring Modulith로
187+
188+
이러한 한계들로 인해 저희는 **Spring Modulith**로 전환을 결정했습니다.
189+
190+
Facade 패턴의 시도를 통해 배운 점:
191+
- 도메인 분리의 중요성 인식
192+
- 모듈 간 통신 방식에 대한 고민
193+
- 엔티티 관계 설정은 근본적인 해결이 필요
194+
195+
Spring Modulith가 이러한 문제들을 어떻게 해결했는지는 [Spring Modulith 아키텍처 문서](./02_spring_modulith.md)를 참고해주세요.

0 commit comments

Comments
 (0)