이론으로만 알던 TDD와 DDD를 loopers에서 직접 적용하면서 "왜 이렇게 해야 하는가"를 몸으로 깨달은 기록.
TDD 방법론은 무엇인가?
TDD란 Test Driven Development의 약자로 '테스트 주도 개발'이다.
소프트웨어 방법론으로 작은 단위 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다. TDD의 개발 절차에는 Red -> Green -> Refactor를 반복하면서 Red 단계에서는 테스트 케이스에 맞는 실패하는 테스트 코드를 먼저 작성하고, Green 단계에서는 테스트를 성공시키기 위한 최소한의 코드를 작성한다. 마지막으로 Refactor 단계에서 중복 코드 제거, 일반화 등 리팩토링을 한다.
이 과정에서 중요한것은 실패하는 테스트 코드를 작성하기 전에는 동작하는 실제 코드를 작성하지 않는것과 테스트 코드를 통과하기 위해서는 최소한의 코드를 작성하는 것이다. 이를 통해 불필요한 설계를 테스트 케이스를 작성하면서 피할 수 있고, 정확한 요구 사항에 집중할 수 있게 된다.
TDD를 직접 써보고 나서
전에 테스트 코드를 공부할 때는 로직 구현을 먼저 완료하고, 테스트 코드를 작성하였다. 이 과정에서 왜 테스트 코드가 중요하다고 하는지 정확하게 알 지 못하였다. 그러다 보니 중요한 케이스를 빠뜨리기 쉽고, 무엇보다 테스트 코드 작성이 지루했다. 이미 코드가 정상적으로 동작을 하고있는데 테스트 코드 작성이 왜 중요한지 이해를 할 수 없었다.
TDD로 테스트 코드를 먼저 작성을 하고 로직을 작성하니 상황이 바뀌었다. 아직 로직이 없으니 테스트 케이스를 먼저 작성해야 했고, 테스트 케이스에 따라 테스트 코드를 먼저 작성하면서 어떤 API가 필요하고 필요 없는지를 생각하게 되었다.
TDD를 하면서 DDD에 대해서도 알게 되면서 Service가 아닌 Domain에 로직을 넣고, Domain을 설계해야 한다는 것을 알게 되었다.
DDD란 무엇인가?
DDD는 “데이터를 저장하는 객체”가 아니라
“업무 규칙을 수행하는 객체”를 만들기 위한 설계 방법론이다.
DDD를 이용하면 비즈니스 도메인에 집중하기 때문에 요구사항을 더 잘 이해하고 모델링 할 수 있고, 도메인 모델링을 통해 각 요소가 높은 응집도를 갖게 되어 코드의 가독성과 유지보수성이 향상된다.
DDD: Entity가 없어도 가능하다.
DDD를 처음 접했을 때 "서비스를 최대한 단순하게 만들고, 도메인에 넣는다"는 말을 들었다. 하지만 나는 MyBatis 환경에서 개발을 하면서 '@Entity' 클래스를 쓰지 않으니 DDD는 불가능하다고 생각했었다.
하지만 DDD는 ORM 기술이 아니라 설계 방식이다. '@Entity'의 유무가 핵심이 아니었다. DDD에서의 Entity는 식별자와 생명주기를 가지는 도메인 객체이고, DDD의 핵심은 비즈니스 규칙, 상태 변화 규칙, 불변성, 행위를 객체 안에 넣는 것을 의미한다. DTO에서 도메인 객체를 분리하면 MyBatis 환경에서도 DDD를 적용할 수 있다는 것을 알게 되었다.
MyBatis를 사용하면 자연스럽게 'DB -> DTO -> Service' 흐름이 강해진다. 비즈니스 로직이 모두 Service에 쌓이는 Transaction Script 패턴이 된다. 단순 CRUD라면 괜찮지만, 주문/결제/쿠폰처럼 규칙이 복잡해질수록 Service가 길어질 수 밖에 없는 구조로 이어진다.
핵심 개념 정리: Entity, VO, DTO
내가 알고있던 Entity가 아니라는 것을 알고 Entity와 VO, DTO에 대해 정리를 해보았다.
Entity (Model)
고유 ID가 있고, 상태가 변할 수 있으며, 비즈니스 로직을 직접 가진다.
public class User {
private UserId id;
private String loginId;
private String password;
private String name;
public void changePassword(String newPassword) {
validatePassword(newPassword);
this.password = newPassword;
}
}
VO (Value Object)
값 자체가 의미인 객체. ID가 없고, 불변이며, 생성 시점에 스스로 유효성을 검증한다. JPA의 @Embedded로 별도 테이블 없이 컬럼으로 저장되는 경우가 일반적이다.
public class Email {
private final String value;
public Email(String value) {
if (!value.contains("@")) {
throw new CoreException(ErrorType.BAD_REQUEST,
"이메일 형식이 올바르지 않습니다.");
}
this.value = value;
}
}
DTO (Data Transfer Object)
레이어 간 데이터를 전달만 하는 객체. 비즈니스 로직이 없고, Controller ↔ Facade ↔ Service 경계에서 사용한다.
// 요청 DTO
public record SignUpRequest(
String loginId, String password, String name
) {}
// 응답 DTO (비밀번호는 포함하지 않음)
public record UserResponse(
String loginId, String name, String email
) {}
한눈에 비교
| Entity | VO | DTO | |
|---|---|---|---|
| DB 저장 | O 별도 테이블 | 컬럼(Embedded) | X |
| 고유 ID | O | X | X |
| 불변 | X | O | 보통 O |
| 비즈니스 로직 | O | O (자기검증) | X |
| 사용 위치 | domain | domain | 레이어 경계 |
DB 중심 사고 → 비즈니스 중심 사고
그동안 테이블 구조를 먼저 생각하고 코드를 짰다. DDD를 공부하면서 이 사고방식이 얼마나 Service를 망가뜨리는지 알게 됐다.
멘토님이 “기계적으로 코딩하지 말고 도메인 설계와 비즈니스 로직을 고민해야 한다”라고 말씀하셨을 때는 그 의미를 잘 이해하지 못했다. 그런데 지금 돌아보면, 단순히 DB 구조를 설계하라는 뜻이 아니었다.
도메인 객체는 DB 테이블 구조를 그대로 옮겨놓은 객체가 아니라, 실제 비즈니스 개념과 업무 규칙을 표현하는 객체라는 의미였다. 즉, 객체 안에 데이터만 담는 것이 아니라 업무 규칙과 행위를 함께 담아야 한다는 뜻이었다.
- 테이블 = 객체 X
- 비즈니스 개념 = 객체 O, DB는 단순 저장소
- 객체는 상태 + 행동 + 규칙을 함께 가진다
프로젝트 규모가 커지면 정책이 늘고, 예외 케이스가 늘고, Service가 자연스럽게 비대해진다. 도메인 객체에 책임을 나눠주면 Service는 흐름 조율(orchestration)과 트랜잭션 관리에만 집중할 수 있다.
결론 — DDD의 핵심은 비즈니스 개념을 객체로 모델링하고, 관련된 데이터와 행위를 하나의 책임으로 묶어 복잡한 비즈니스 로직을 이해하기 쉽고 유지보수하기 좋은 구조로 만드는 것이다. 결국 DDD는 객체지향 설계를 비즈니스 복잡성에 체계적으로 적용하는 방법론이라고 생각한다.
'CS & 공부한 내용' 카테고리의 다른 글
| [Spring Boot] JWT를 이용한 인증/인가 구현하기(Spring Security X) (1) | 2024.11.15 |
|---|---|
| [Spring Boot] 스레드(Thread) (1) | 2024.11.15 |
| [Spring Boot] Spring Security를 이용하여 인증인가 구현하기 (0) | 2024.11.15 |
| [Spring Boot] JWT와 세션의 차이 (1) | 2024.11.07 |
| [Spring Boot] 필터( Filter ) (0) | 2024.11.07 |