프로젝트를 진행하면서 TDD(Test-Driven Development)와 DDD(Domain-Driven Design)에 대해 궁금증이 생겨 스스로 공부한 내용을 공유하고자 합니다. 이 글에서는 TDD와 DDD의 정의와 차이를 설명하고, 실제 프로젝트에서 사용할 수 있는 CRUD 코드 예시를 통해 각 방법론의 적용 방식을 소개하겠습니다. 또한, 어제 TDD를 지향하는 개발자분과 나눈 이야기를 바탕으로 TDD를 실제 어떤식으로 적용하는지도 다뤄보겠습니다.
1. TDD란 무엇인가?
TDD는 "테스트를 먼저 작성하고, 이를 통과시키기 위해 코드를 작성하는" 개발 방법론입니다. 코드의 안정성과 신뢰성을 높이고, 리팩토링 과정에서도 기존 기능이 정상적으로 동작하는지 확인할 수 있다는 장점이 있습니다.
TDD의 세 가지 주요 단계
- Red: 실패하는 테스트를 작성합니다.
- Green: 테스트를 통과시키는 최소한의 코드를 작성합니다.
- Refactor: 코드를 개선하면서 테스트가 여전히 통과하는지 확인합니다.
장점
- 버그를 조기에 발견하고 수정할 수 있음
- 코드의 유지보수성과 확장성이 높아짐
- 문서화 대체 역할
단점
- 초기 작성 시간이 더 많이 소요될 수 있음
- 모든 테스트를 작성하는 것은 비효율적일 수 있음
2. DDD란 무엇인가?
DDD는 "도메인 모델을 중심으로 소프트웨어를 설계하고 개발하는" 방법론입니다. 복잡한 비즈니스 로직을 명확하게 표현하고, 도메인 전문가와 개발자가 같은 언어로 소통할 수 있도록 돕는 것이 목표입니다.
DDD의 핵심 요소
- 유비쿼터스 언어(Ubiquitous Language): 개발팀과 도메인 전문가가 공유하는 공통 언어
- 도메인 모델(Domain Model): 비즈니스 로직과 개념을 코드로 표현
- Bounded Context: 각 도메인의 경계를 명확히 구분
장점
- 복잡한 비즈니스 요구사항을 효과적으로 반영
- 팀 간 의사소통 원활화
- 높은 재사용성과 유지보수성
단점
- 초기 설계와 학습 곡선이 가파름
- 작은 프로젝트에서는 과잉 설계가 될 가능성
3. TDD와 DDD의 차이점
구분TDDDDD
목표 | 코드의 안정성과 테스트 커버리지 | 비즈니스 로직의 명확한 표현과 설계 |
접근 방식 | 테스트 코드 중심 | 도메인 모델 중심 |
적용 범위 | 모든 코드 영역 | 복잡한 비즈니스 로직 |
학습 난이도 | 비교적 쉬움 | 높음 |
4. 서비스 계층 CRUD 예시
다음은 TDD와 DDD를 각각 적용하여 서비스 계층의 UserService에서 CRUD 기능을 구현한 예시입니다.
TDD 기반 CRUD 구현 예시
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(UserDto userDto) {
User user = new User(userDto.getName(), userDto.getEmail());
return userRepository.save(user);
}
public Optional<User> getUser(Long id) {
return userRepository.findById(id);
}
public User updateUser(Long id, UserDto userDto) {
User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
user.update(userDto.getName(), userDto.getEmail());
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
위에 작성된 서비스 코드에 해당되는 유닛 테스트 예제
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void createUser_shouldSaveAndReturnUser() {
UserDto userDto = new UserDto("John Doe", "john.doe@example.com");
User user = new User("John Doe", "john.doe@example.com");
Mockito.when(userRepository.save(Mockito.any(User.class))).thenReturn(user);
User result = userService.createUser(userDto);
assertEquals("John Doe", result.getName());
assertEquals("john.doe@example.com", result.getEmail());
}
}
// /service
@Service
public class UserService {
private final UserRepository userRepository;
private final UserFactory userFactory;
public UserService(UserRepository userRepository, UserFactory userFactory) {
this.userRepository = userRepository;
this.userFactory = userFactory;
}
public User createUser(UserDto userDto) {
User user = userFactory.create(userDto);
return userRepository.save(user);
}
public Optional<User> getUser(Long id) {
return userRepository.findById(id);
}
public User updateUser(Long id, UserDto userDto) {
User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
user.update(userDto);
return userRepository.save(user);
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
// /domain
@Component
public class UserFactory {
public User create(UserDto userDto) {
return new User(userDto.getName(), userDto.getEmail());
}
}
5. 현업자의 테스트 전략
어제 현업에서 TDD를 지향하는 개발자와 대화를 나누면서, 테스트 전략에 대해 몇 가지 인사이트를 얻었습니다:
( 이 테스트 전략을 무조건 따라야한다의 의미가 아니라 이런식으로도 할 수 있다로 봐주시면 될 것같습니다.)
- 컨트롤러 테스트는 POSTMAN으로: 컨트롤러 계층은 주로 POSTMAN과 같은 툴로 테스트하며, 간단한 API 호출과 응답 검증을 진행한다고 합니다.
- 서비스 계층은 유닛 테스트로 꼼꼼히: 서비스 계층은 비즈니스 로직의 핵심이기 때문에, 유닛 테스트를 작성하여 논리적 오류를 방지하고, 코드 리팩토링 시 안정성을 확보한다고 합니다. (단위 테스트가 통과하면 통합 테스트는 대부분 통과하더라 라고 하시더군요)
- 모든 테스트 코드는 불필요: 모든 계층에 테스트를 작성하는 것은 비효율적이며, 핵심 비즈니스 로직 중심으로 테스트를 작성하는 것이 실용적이라는 의견을 밝혔습니다.
6. 마무리
TDD와 DDD는 각기 다른 목적과 장점을 가진 방법론이지만, 상황에 따라 적절히 선택하거나 결합하여 사용할 수 있습니다. 실무에서의 테스트 전략은 프로젝트의 성격과 팀의 목표에 따라 달라질 수 있음을 이해하는 것도 중요합니다.
취준생으로서 TDD와 DDD를 공부하고 현업자의 경험을 들어보니, 단순히 이론을 아는 것에서 끝나는 것이 아니라, 실제 프로젝트에 적용하고 그 효과를 체감하는 것이 중요하다는 점을 깨달았습니다. 여러분도 이러한 방법론을 자신의 프로젝트에 적용해 보면서 어떤 장단점을 가지고 있는지 판단한다면 좋을 것 같습니다.
Test-Driven Development (TDD) and Domain-Driven Design (DDD): A Symbiotic Relationship
In software development, two practices that have been making waves for their ability to deliver cleaner code and better design are…
roshancloudarchitect.me
https://stackoverflow.com/questions/1145634/tdd-ddd-and-encapsulation
TDD, DDD and Encapsulation
After several years of following the bad practice handed down from 'architects' at my place of work and thinking that there must be a better way, I've recently been reading up around TDD and DDD an...
stackoverflow.com
https://softwareengineering.stackexchange.com/questions/319759/how-to-combine-strict-tdd-and-ddd
How to combine strict TDD and DDD?
TDD is about designing code, guided by tests. Thus, typical layers aren't usually built upfront; they should slightly appear through refactoring steps. Domain-driven design involves a lot of techn...
softwareengineering.stackexchange.com
'궁금증' 카테고리의 다른 글
코드 커버리지(Code Coverage)란 무엇일까? (0) | 2024.12.16 |
---|---|
DDD에서 팩토리(Factory)를 왜 사용하는거죠? (1) | 2024.12.15 |
[SQL] Join vs SubQuery: 언제, 왜, 어떻게 선택할까? (1) | 2024.12.09 |
컨트롤러와 서비스에서 동일한 메서드명을 사용하는 것이 적절할까? (0) | 2024.12.06 |
메소드??? 함수?? 뭐가 다른거야? (0) | 2024.11.22 |