DDD 정리 노트 — 1장 중심 핵심 정리
예제코드 : https://github.com/Sejin-999/DDD-study/tree/main/ch1
들어가며
일단, 소프트웨어 아키텍처 중 핵사고날 , 클린 아키텍처 같은 아키텍처 패턴을 이해하기 위해서 공부를 하다보니
일반적으로 개발했던 레이어 아키텍처와 다른 부분이 도메인에 대한 부분이였다. 외부 세계에 대한 이해를
시작하기 위해서는 가장 중심인 도메인에 대한 이해가 필요했고, 그래서 DDD를 공부하기 시작했다.
도메인 중심의 개발을 이해해야 하는데, 도메인? 엔티티관리하는 그건가? 부터 시작했다
공부하다보면 다르다는거를 알 수 있는데, 결국에는 이를 잘 이해하기 위해서는 (최근 각광받고있는 아키텍처 패턴) DDD_(Domain Driven Design, 도메인 주도 설계) 에 대해서 이해하는 것이 필요하다
일반적인 레이어 아키텍처에서 흐름대로 이해하기 직관적인 코드로 작성을하다가
최근 회사 팀에서 관련된 적용에 대한 이야기가 많아져서, 따라가기 위해 공부를 하다보니
결론적으로 DDD부터 공부해서 도메인 주도 설계에 대한 이해를 하기위한 내용이다.
배경
기존의 일반적인 레이어 아키텍처에서는 DB 테이블 중심으로 엔티티를 설계하고,
비즈니스 규칙은 Service 계층에 집중되는 경우가 많다.
이로 인해 도메인 객체는 단순한 데이터 컨테이너(Anemic Model)가 되고,
규칙은 여러 Service에 분산되어 유지보수가 어려워진다.
그래서 나는 ENUM 과 Validation 등을 이용해서 각 엔티티에 대한 부분의
코드를 분리해서 관리하였는데 (만들다 보니 관리가 어려워서 했었다)
이를 해결하는 좋은 방법중 하나가 이 DDD 인데, 이걸 굳이해야할까
라는 생각이 있었어서 관심이없다가 이번에 공부하다 보니 꽤나 좋은거 같다.
일단 트랙잭션단위별로 분리해서 관리하다보니 좀더 현실적인 설계가 가능해지고
진정한의미에 OOP를 구현하기에도 더 유리하다고 생각이 들었다.
DDD는 이러한 문제를 해결하기 위해
**비즈니스 규칙의 위치를 명확히 하고,
도메인 모델을 시스템의 중심에 두는 설계 방식**이다.
일단 , 우리가 볼 레이어 아키텍처는 빈약하다 라고 가정하고 시작할건데
당연한거지만, 상황따라 필요에 따라 더 나은 방법이 있는거지
새로나왔다고, 좀더 뭐가 좋다는 이유로 정답을 찾는거는 아니다
Anemic Model (빈약한 도메인 모델)
정의
데이터만 있고, 비즈니스 로직이 없는 객체 모델
즉:
- 클래스에 필드(get/set)만 있고
- 행위(비즈니스 규칙, 검증, 상태 변경 로직) 는 전부
- Service, Manager 같은 외부 계층에 몰려 있는 구조
이거는 DDD에서 말하는 빈약한 도메인 모델을 의미하는거고, 실제 세계에 개발에서는
상황따라 다른거다.
예를들어 기능 10개만드는데 DDD 하겠다고, 까불면 멋만 있고 실속없는 인간이라고 볼 수 있다.
중요한것은 정말 필요한가에 대한 이해와 판단의 근거가 있어야 한다.
걍 좋으니까. 뭐 새로나오고 큰회사에서 쓰니까 이거는 개발자로써 책임감 없는소리다.
예시코드 - 레이어 아키텍처에서의 데이터모델 (Anemic Model)
Java - Anemic Model 예시
@Entity
@Table(name = "users")
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String status; // ACTIVE, INACTIVE 등
// 비즈니스 로직이 없음 - 단순 데이터만 보관
}
Node.js - Anemic Model 예시
export class User {
id?: number;
name: string;
email: string;
status: string; // ACTIVE, INACTIVE 등
constructor(name: string, email: string) {
this.name = name;
this.email = email;
this.status = 'ACTIVE';
}
}
핵심 질문
- 도메인이란 무엇인가
- 도메인 모델은 기존 레이어 아키텍처와 무엇이 다른가
- 엔티티와 밸류는 어떻게 구분해야 하는가
- 비즈니스 규칙은 Service에 있어야 하는가, 도메인에 있어야 하는가
- Service를 여러 개로 나누는 것과 DDD는 무엇이 다른가
논의 요약
1. 도메인과 도메인 모델
- 도메인은 소프트웨어로 해결하려는 문제 영역이다.
- 도메인 모델은 도메인의 개념, 상태, 규칙, 행위를 코드로 표현한 것이다.
- 도메인 모델은 단순한 데이터 구조가 아니라 규칙을 가진 모델이다.
2. 엔티티(Entity)
- 고유 식별자(ID)를 가진다.
- 시간이 지나도 동일성을 유지한다.
- 상태가 변해도 같은 대상이다.
- 동일성 비교 기준은 값이 아니라 ID다.
예:
- Order
- User
3. 밸류(Value Object)
- 식별자가 없다.
- 값 자체가 의미다.
- 불변 객체로 설계하는 것이 원칙이다.
- 값이 같으면 같은 객체로 취급한다.
예:
- Money
- Address
판단 원칙:
- 헷갈리면 밸류부터 시작한다.
- 라이프사이클과 식별 관리가 필요해지면 엔티티로 승격한다.
4. 잘못된 초기 설계 (Anemic Model)
- 엔티티는 필드와 getter/setter만 가진다.
- 비즈니스 규칙은 Service에 집중된다.
- 상태 변경을 setter로 직접 수행한다.
문제점:
- 규칙이 여러 Service에 분산된다.
- 규칙 중복과 불일치가 발생한다.
- 도메인 객체가 스스로를 보호하지 못한다.
예시코드 - 레이어 아키텍처에서의 Service, Repository 코드
Java - Service (비즈니스 규칙이 Service에 집중)
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
// 비즈니스 규칙이 Service에 있음
public User createUser(String name, String email) {
// 규칙: 이메일 중복 체크
if (userRepository.findByEmail(email).isPresent()) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
// 규칙: 이메일 형식 검증
if (!email.contains("@")) {
throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다.");
}
User user = new User();
user.setName(name);
user.setEmail(email);
user.setStatus("ACTIVE"); // 규칙: 기본 상태 설정
return userRepository.save(user);
}
public User updateUserStatus(Long id, String status) {
// 규칙: 상태 변경 가능 여부 체크
if (!"ACTIVE".equals(status) && !"INACTIVE".equals(status)) {
throw new IllegalArgumentException("유효하지 않은 상태입니다.");
}
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
// 규칙: 이미 같은 상태면 변경 불가
if (user.getStatus().equals(status)) {
throw new IllegalArgumentException("이미 해당 상태입니다.");
}
user.setStatus(status); // setter로 직접 변경
return userRepository.save(user);
}
}
Node.js - Service (비즈니스 규칙이 Service에 집중)
export class UserService {
private userRepository: UserRepository;
async createUser(name: string, email: string): Promise<User> {
// 규칙: 이메일 중복 체크
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error('이미 존재하는 이메일입니다.');
}
// 규칙: 이메일 형식 검증
if (!email.includes('@')) {
throw new Error('올바른 이메일 형식이 아닙니다.');
}
const user = new User(name, email);
user.status = 'ACTIVE'; // 규칙: 기본 상태 설정
return this.userRepository.save(user);
}
async updateUserStatus(id: number, status: string): Promise<User> {
// 규칙: 상태 변경 가능 여부 체크
if (status !== 'ACTIVE' && status !== 'INACTIVE') {
throw new Error('유효하지 않은 상태입니다.');
}
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('사용자를 찾을 수 없습니다.');
}
// 규칙: 이미 같은 상태면 변경 불가
if (user.status === status) {
throw new Error('이미 해당 상태입니다.');
}
user.status = status; // 직접 변경
return this.userRepository.save(user);
}
}
Repository 예시
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
export class UserRepository {
async findByEmail(email: string) {
return prisma.user.findFirst({ where: { email } });
}
async save(user: User) {
return prisma.user.create({ data: user });
}
}
5. 도메인 모델 중심 설계 (Rich Domain Model)
- 비즈니스 규칙의 주인은 도메인 객체다.
- 상태 변경은 의미 있는 도메인 메서드를 통해서만 수행한다.
- setter를 통해 상태를 직접 변경하지 않는다.
핵심 원칙:
- 상태 + 규칙은 반드시 같은 객체에 둔다.
- “이 상태가 가능한가?”를 도메인이 스스로 판단한다.
예:
- order.setStatus(PAID) ❌
- order.pay(money) ⭕️
예시코드 - DDD 패턴의 엔티티 코드 (Rich Domain Model)
Java - Rich Domain Model 예시
// Value Object - Email
@Embeddable
@Getter
@NoArgsConstructor
public class Email {
private String value;
public Email(String value) {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("올바른 이메일 형식이 아닙니다.");
}
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Email email = (Email) o;
return value != null ? value.equals(email.value) : email.value == null;
}
}
// Entity - User (비즈니스 규칙 포함)
@Entity
@Table(name = "domain_users")
@Getter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Email email;
@Enumerated(EnumType.STRING)
private UserStatus status;
// 생성자 - 비즈니스 규칙 포함
public User(String name, Email email) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("이름은 필수입니다.");
}
this.name = name;
this.email = email; // Email Value Object는 이미 검증됨
this.status = UserStatus.ACTIVE; // 도메인 규칙: 기본 상태
}
// 비즈니스 규칙이 도메인 객체에 있음
public void activate() {
if (this.status == UserStatus.ACTIVE) {
throw new IllegalStateException("이미 활성화된 사용자입니다.");
}
this.status = UserStatus.ACTIVE;
}
public void deactivate() {
if (this.status == UserStatus.INACTIVE) {
throw new IllegalStateException("이미 비활성화된 사용자입니다.");
}
this.status = UserStatus.INACTIVE;
}
// 의미 있는 메서드 - setter 대신
public void changeEmail(Email newEmail) {
if (this.status == UserStatus.INACTIVE) {
throw new IllegalStateException("비활성화된 사용자는 이메일을 변경할 수 없습니다.");
}
this.email = newEmail;
}
// 도메인 규칙: 상태 확인
public boolean isActive() {
return this.status == UserStatus.ACTIVE;
}
// setter 없음 - 상태 변경은 의미 있는 메서드를 통해서만
}
Node.js - Rich Domain Model 예시
// Value Object - Email
export class Email {
private readonly value: string;
constructor(value: string) {
if (!value || !value.includes('@')) {
throw new Error('올바른 이메일 형식이 아닙니다.');
}
this.value = value;
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// Entity - User (비즈니스 규칙 포함)
export class User {
id?: number;
private name: string;
private email: Email;
private status: UserStatus;
constructor(name: string, email: Email) {
if (!name || name.trim().length === 0) {
throw new Error('이름은 필수입니다.');
}
this.name = name;
this.email = email; // Email Value Object는 이미 검증됨
this.status = UserStatus.ACTIVE; // 도메인 규칙: 기본 상태
}
// 비즈니스 규칙이 도메인 객체에 있음
activate(): void {
if (this.status === UserStatus.ACTIVE) {
throw new Error('이미 활성화된 사용자입니다.');
}
this.status = UserStatus.ACTIVE;
}
deactivate(): void {
if (this.status === UserStatus.INACTIVE) {
throw new Error('이미 비활성화된 사용자입니다.');
}
this.status = UserStatus.INACTIVE;
}
// 의미 있는 메서드 - setter 대신
changeEmail(newEmail: Email): void {
if (this.status === UserStatus.INACTIVE) {
throw new Error('비활성화된 사용자는 이메일을 변경할 수 없습니다.');
}
this.email = newEmail;
}
isActive(): boolean {
return this.status === UserStatus.ACTIVE;
}
// setter 없음 - 상태 변경은 의미 있는 메서드를 통해서만
}
6. Application Service의 역할
- 흐름 제어 및 유스케이스 조합 담당
- 트랜잭션 경계 관리
- 도메인 객체의 행위를 호출할 뿐, 판단하지 않는다
Application Service는:
- if로 비즈니스 규칙을 판단하지 않는다
- 도메인 규칙을 구현하지 않는다
Application Service 예시
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
// Application Service는 흐름 제어만 담당
public User createUser(String name, String emailValue) {
// 도메인 규칙 체크는 도메인 객체가 담당
Email email = new Email(emailValue);
// 중복 체크는 도메인 규칙이 아니라 인프라 관심사
if (userRepository.findByEmail_Value(emailValue).isPresent()) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
// 도메인 객체 생성 - 비즈니스 규칙은 User 생성자에 있음
User user = new User(name, email);
return userRepository.save(user);
}
// 도메인 객체의 행위를 호출할 뿐, 판단하지 않음
public User activateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
// 비즈니스 규칙 판단은 도메인 객체가 함
user.activate(); // 도메인이 스스로 판단
return userRepository.save(user);
}
}
export class UserService {
async createUser(name: string, emailValue: string): Promise<User> {
// 도메인 규칙 체크는 도메인 객체가 담당
const email = new Email(emailValue);
// 중복 체크는 도메인 규칙이 아니라 인프라 관심사
const existing = await this.userRepository.findByEmail(emailValue);
if (existing) {
throw new Error('이미 존재하는 이메일입니다.');
}
// 도메인 객체 생성 - 비즈니스 규칙은 User 생성자에 있음
const user = new User(name, email);
return this.userRepository.save(user);
}
async activateUser(id: number): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('사용자를 찾을 수 없습니다.');
}
// 비즈니스 규칙 판단은 도메인 객체가 함
user.activate(); // 도메인이 스스로 판단
return this.userRepository.save(user);
}
}
7. 일반 레이어 아키텍처와의 관계
- DDD는 레이어 아키텍처를 부정하지 않는다.
- Controller / Service / Repository 구조는 그대로 유지된다.
- 달라지는 것은 책임 분리 기준이다.
차이점:
- 일반 레이어: Service가 규칙의 중심
- DDD: 도메인이 규칙의 중심
8. “Service를 여러 개로 나누면 되지 않나?”에 대한 결론
- Service 분리는 관리 문제다.
- 도메인 모델링은 책임 위치 문제다.
- Service를 아무리 나눠도 규칙이 도메인 밖에 있으면 문제는 그대로다.
정답:
- Service는 여러 개여도 된다.
- 비즈니스 규칙은 하나의 도메인 객체에 고정되어야 한다.
결정 사항
- 비즈니스 규칙의 주인은 Service가 아니라 도메인 객체다.
- 엔티티는 데이터 구조가 아니라 규칙과 행위의 집합이다.
- Service는 조정자 역할만 수행한다.
- Service 분리로는 도메인 분산 문제를 해결할 수 없다.
USER CRUD 예시 코드
Anemic Model 패턴의 CRUD
Java - Controller 예시
@RestController
@RequestMapping("/api/anemic/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
User user = userService.createUser(request.getName(), request.getEmail());
return ResponseEntity.ok(user);
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
Optional<User> user = userService.getUserById(id);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/{id}/status")
public ResponseEntity<User> updateUserStatus(
@PathVariable Long id,
@RequestBody UpdateStatusRequest request) {
User user = userService.updateUserStatus(id, request.getStatus());
return ResponseEntity.ok(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
Node.js - Controller 예시
import { Request, Response } from 'express';
import { UserService } from '../service/UserService';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.createUser(req.body.name, req.body.email);
res.json(user);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async getAllUsers(req: Request, res: Response): Promise<void> {
try {
const users = await this.userService.getAllUsers();
res.json(users);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
}
async getUserById(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.getUserById(parseInt(req.params.id));
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async updateUserStatus(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.updateUserStatus(
parseInt(req.params.id),
req.body.status
);
res.json(user);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async deleteUser(req: Request, res: Response): Promise<void> {
try {
await this.userService.deleteUser(parseInt(req.params.id));
res.status(204).send();
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
}
// server.ts에서 사용
const userController = new UserController();
app.post('/api/anemic/users', (req, res) => userController.createUser(req, res));
app.get('/api/anemic/users', (req, res) => userController.getAllUsers(req, res));
app.get('/api/anemic/users/:id', (req, res) => userController.getUserById(req, res));
app.put('/api/anemic/users/:id/status', (req, res) => userController.updateUserStatus(req, res));
app.delete('/api/anemic/users/:id', (req, res) => userController.deleteUser(req, res));
Rich Domain Model 패턴의 CRUD
Java - Controller 예시
@RestController
@RequestMapping("/api/domain/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
User user = userService.createUser(request.getName(), request.getEmail());
return ResponseEntity.ok(user);
}
@PutMapping("/{id}/activate")
public ResponseEntity<User> activateUser(@PathVariable Long id) {
try {
User user = userService.activateUser(id);
return ResponseEntity.ok(user);
} catch (IllegalStateException | IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}/deactivate")
public ResponseEntity<User> deactivateUser(@PathVariable Long id) {
try {
User user = userService.deactivateUser(id);
return ResponseEntity.ok(user);
} catch (IllegalStateException | IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}/email")
public ResponseEntity<User> changeEmail(
@PathVariable Long id,
@RequestBody ChangeEmailRequest request) {
try {
User user = userService.changeUserEmail(id, request.getEmail());
return ResponseEntity.ok(user);
} catch (IllegalStateException | IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
}
Node.js - Controller 예시
import { Request, Response } from 'express';
import { UserService } from '../service/UserService';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.createUser(req.body.name, req.body.email);
res.json({
id: (user as any).id,
name: user.getName(),
email: user.getEmail().getValue(),
status: user.getStatus(),
});
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async getAllUsers(req: Request, res: Response): Promise<void> {
try {
const users = await this.userService.getAllUsers();
res.json(
users.map(u => ({
id: (u as any).id,
name: u.getName(),
email: u.getEmail().getValue(),
status: u.getStatus(),
}))
);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
}
async getUserById(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.getUserById(parseInt(req.params.id));
res.json({
id: (user as any).id,
name: user.getName(),
email: user.getEmail().getValue(),
status: user.getStatus(),
});
} catch (error: any) {
res.status(404).json({ error: error.message });
}
}
async activateUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.activateUser(parseInt(req.params.id));
res.json({
id: (user as any).id,
name: user.getName(),
email: user.getEmail().getValue(),
status: user.getStatus(),
});
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async deactivateUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.deactivateUser(parseInt(req.params.id));
res.json({
id: (user as any).id,
name: user.getName(),
email: user.getEmail().getValue(),
status: user.getStatus(),
});
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async changeUserEmail(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.changeUserEmail(
parseInt(req.params.id),
req.body.email
);
res.json({
id: (user as any).id,
name: user.getName(),
email: user.getEmail().getValue(),
status: user.getStatus(),
});
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
async deleteUser(req: Request, res: Response): Promise<void> {
try {
await this.userService.deleteUser(parseInt(req.params.id));
res.status(204).send();
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
}
// server.ts에서 사용
const userController = new UserController();
app.post('/api/domain/users', (req, res) => userController.createUser(req, res));
app.get('/api/domain/users', (req, res) => userController.getAllUsers(req, res));
app.get('/api/domain/users/:id', (req, res) => userController.getUserById(req, res));
app.put('/api/domain/users/:id/activate', (req, res) => userController.activateUser(req, res));
app.put('/api/domain/users/:id/deactivate', (req, res) => userController.deactivateUser(req, res));
app.put('/api/domain/users/:id/email', (req, res) => userController.changeUserEmail(req, res));
app.delete('/api/domain/users/:id', (req, res) => userController.deleteUser(req, res));
차이점 요약:
| 항목 | Anemic Model | Rich Domain Model |
|---|---|---|
| 상태 변경 | user.setStatus("ACTIVE") |
user.activate() |
| 비즈니스 규칙 위치 | Service에 집중 | 도메인 객체에 포함 |
| 검증 로직 | Service에서 if문으로 체크 |
도메인 메서드 내부에서 처리 |
| 의미 표현 | setter로 직접 변경 | 의미 있는 메서드로 표현 |
다음 액션
- 2장 학습 진행
- 아키텍처 4계층
- 도메인 계층의 위치
- DIP가 왜 DDD의 전제 조건인지 정리
'Software Design' 카테고리의 다른 글
| MSA에 대해 배우자 - 2. 데이터 저장 방식을 마이크로화 하자 (0) | 2025.03.23 |
|---|---|
| MSA에 대해 배우자 -1 - 개념과 특징,목적 (0) | 2025.03.13 |