이번에 데이터베이스를 변경하면서 DDD와 Clean Architecture의 필요성을 느꼈습니다.
참고: 데이터베이스를 변경한 이유는 아래 포스팅에 있습니다.
친구 정보, Redis에서 MySQL로 옮긴 이유
상황현재 사용자의 친구 정보들을 "카카오톡 친구 API"를 통해 받은 뒤, redis에 저장해 놓고 있었다.하지만 "자신의 친구들 중 친한 친구를 표시할 수 있다."라는 기능을 제공하고 있고, 이 정보 또
kdozlo.tistory.com
기존 방식의 문제점
1. DB 엔티티와 도메인 로직의 결합
- 기존 방식에서는 DB 엔티티(Friend)에 도메인 로직(예: toggleFavorite)을 포함하여 구현했습니다.
- 이로 인해 데이터베이스와 도메인 로직 간의 강결합이 발생하며, 다음과 같은 문제가 생길 수 있습니다:
- Redis에서 JPA로 전환할 경우, @RedisHash, @Indexed 등의 어노테이션을 JPA 어노테이션으로 변경해야 합니다. 또한 Redis에서는 ID 값이 friend:consumerId:toConsumerId 형식으로 저장되지만, JPA에서는 다른 방식으로 변경해야 합니다.
- 이러한 변경은 단순히 엔티티 수정에 그치지 않고, 서비스 레이어까지 영향을 줄 가능성이 있습니다.
- 예: 서비스에서 Redis의 ID 형식에 의존하는 로직을 수정해야 함.
- 이로 인해 데이터베이스와 도메인 로직 간의 강결합이 발생하며, 다음과 같은 문제가 생길 수 있습니다:
@Getter
@RedisHash(value = "friend")
public class Friend implements Serializable {
@Id
private String id; //friend:consumerId:toConsumerId
@Indexed
private Long consumerId;
@Indexed
private Long toConsumerId;
@Indexed
private Boolean isFavorite;
@Builder
private Friend(String id, Long consumerId, Long toConsumerId, Boolean isFavorite) {
this.id = id;
this.consumerId = consumerId;
this.toConsumerId = toConsumerId;
this.isFavorite = isFavorite;
}
public void toggleFavorite() {
this.isFavorite = !this.isFavorite;
}
public static Friend from(Long consumerId, FriendDto friendDto, Long toConsumerId) {
return Friend.builder()
.id(consumerId + ":" + toConsumerId)
.consumerId(consumerId)
.toConsumerId(toConsumerId)
.isFavorite(friendDto.getFavorite())
.build();
}
}
2. 서비스 로직과 데이터베이스 강결합
- FriendService는 직접적으로 FriendRepository를 호출하고, 엔티티(Friend)를 사용해 데이터를 조회 및 수정합니다.
- 이로 인해 3가지 문제점이 존재합니다.
- 책임 분리 부족: 서비스 로직에서 데이터베이스 세부 사항(쿼리 방식, 저장소 구현 등)을 알아야 하므로, 서비스와 저장소 간의 책임이 명확히 분리되지 않음.
- 변경에 취약: 데이터베이스가 변경되거나 저장소의 동작 방식이 수정되면, 서비스 로직도 함께 변경되어야 함.
- 테스트 어려움: 서비스 계층의 비즈니스 로직 테스트가 데이터베이스에 의존하게 되어, Mock Repository를 사용한 테스트를 작성하는 데 복잡성이 증가함.
DDD와 클린 아키텍처의 필요성
1. DDD (Domain-Driven Design)
- 도메인 모델과 DB 엔티티의 분리
- 도메인 모델이 데이터베이스 기술에 종속되지 않도록 설계합니다.
- 도메인 모델: Friend는 toggleFavorite과 같은 도메인 로직만 포함하며, 데이터 저장 방식에 대한 구체적인 구현은 없습니다.
- DB 엔티티: 데이터베이스 저장소에 필요한 어노테이션이나 ID 저장 방식은 별도의 엔티티 객체에서 처리합니다.
2. 클린 아키텍처 (Clean Architecture)
- 의존성 역전 원칙 (DIP, Dependency Inversion Principle)
- FriendService는 데이터 저장소(FriendRepository)의 인터페이스에만 의존하며, 저장소의 구현체(FriendRepositoryImpl)는 infrastructure 계층에서 제공합니다.
- 따라서 데이터 저장 방식(JPA, Redis, MongoDB 등)을 변경하더라도, 서비스 로직은 영향을 받지 않습니다.
- 서비스 레이어 비즈니스 로직의 테스트 가능성 향상
- 비즈니스 로직이 외부 의존성(데이터베이스, 외부 API 등)과 분리되므로, 독립적으로 테스트할 수 있습니다.
- 서비스 계층 테스트 시 Mock Repository나 Fake Repository 사용하여 테스트를 작성할 수 있습니다.
- 데이터베이스가 필요 없는 서비스 로직 테스트가 가능해집니다.
DDD와 클린 아키텍처 도입시 구조
- controller/
- FriendController.java
- service/
- FriendService.java
- domain/
- model/
- Friend.java
- port/
- FriendRepository.java(interface)
- infrastructure/
- entity/
- FriendEntity.java
- repository/
- FriendRepositoryImpl.java, FriendJpaRepository.java(interface)
추가로 헥사고날 아키텍처까지 적용할 계획이였으나, 설계과정에서 헥사고날 아키텍처까지는 현재 프로젝트에 과하다는 생각이 들었습니다.
자세한 이야기는 다음 포스팅에서 하겠습니다.
'개발' 카테고리의 다른 글
헥사고날 아키텍처 적용: 서비스와 도메인의 독립성 확보 (0) | 2025.01.27 |
---|---|
친구 목록 동기화 시 기존 데이터를 업데이트하지 않고 새로운 데이터로 저장되는 문제 (0) | 2025.01.17 |
친구 정보, Redis에서 MySQL로 옮긴 이유 (0) | 2025.01.10 |
permitAll()로 요청한 api들이 JwtFilter를 거쳐서 가는 문제 (0) | 2024.04.29 |
예외 처리시 원하는 예외 응답값이 안오는 경우 (4) | 2024.04.29 |