Repository 리팩토링
최근 진행 중인 프로젝트에서 리팩토링 과정 중 문제점을 발견하고 해결방법을 생각해보고 적용한 내용을 공유해보려 한다. 클린 아키텍처를 기반으로 한 프로젝트에서 데이터 영속화를 담당하는 Repository가 단순한 데이터 조회를 넘어서, 비즈니스 로직인 DTO 매핑과 페이징 처리까지 수행하고 있는 것에서부터 의문이 시작되었다. Repository에서 그 책임 이상의 일들을 맡는 것 같았고 리팩토링이 필요하다 생각되었다.
아키텍처
먼저 프로젝트 아키텍처를 간략히 소개하자면, 클린아키텍처를 도입했고 도메인을 관리하는 레어이와 데이터 영속화를 담당하는 레이어가 분리되어있으며 도메인 레이어에서 명세한 Repository를 통해 데이터를 주고 받는다. 도메인 객체와 엔티티객체도 분리되어 있으며 데이터는 DTO를 통해 주고받고 각 레이어에서 도메인객체 혹은 엔티티객체로 매핑해서 사용한다.
기존로직
도메인 레이어에서 명세한 Repository는 다음과 같다.
public interface ItemReaderRepository {
...
Optional<ItemWithStockDto> findItemWithStockById(long itemId);
PageResponse<ItemWithStockDto> findAllItemsWithStock(SortablePaginationRequest sortablePaginationRequest);
}
기존에는 ItemReaderRepository가 ItemReaderRepository를 직접 구현했고, 데이터 조회뿐만 아니라 DTO 매핑과 페이징 처리도 함께 담당했다. 아래는 기존 코드의 일부이다.
@RequiredArgsConstructor
@Repository
public class ItemReaderJpaRepository implements ItemReaderRepository {
private final ItemJpaRepository itemJpaRepository;
private final ItemEntityPageMapper itemEntityPageMapper;
private final PageableFactory pageableFactory;
...
@Override
public Optional<ItemWithStockDto> findItemWithStockById(long itemId) {
return itemJpaRepository.findById(itemId)
.map(ItemEntity::toItemWithStockDto);
@Override
public PageResponse<ItemWithStockDto> findAllItemsWithStock(PaginationRequest paginationRequest) {
Pageable pageable = pageableFactory.createPageable(paginationRequest);
Page<ItemEntity> itemEntityPage = itemJpaRepository.findAll(pageable);
return itemEntityPageMapper.toItemWithStockPageFrom(itemEntityPage);
}
}
문제점
구현 코드를 작성할 때에는 문제를 못느꼈지만 테스트코드를 작성하는 과정에서 냄새를 감지했다. 보통 Repository는 Repository만 빠르게 테스트하기 위해 @DataJpaTest로 테스트로 작성을 주로 하는데 페이징 처리를 위해 ItemEntityPageMapper나 PageableFactory 클래스를 주입받기위해 @SpringBootTest로 테스트를 수행해야되는 상황이 발생했다.
또한 DTO로 변환하는 것도 비즈니스 로직의 일환으로 봐야하지 않을까하는 생각도 하게되었다. 이는 Repository가 담당할 책임을 넘어서며, SRP를 위반하고 있다고 생각했다.
Repository의 기본 책임은 데이터베이스와의 상호작용(데이터 조회, 저장, 삭제 등)만 하면된다. 그러나 기존 코드에서는 DTO 매핑과 페이징 처리와 같은 비즈니스 로직도 함께 처리하고 있었다.
리팩토링
이 문제를 해결하기 위해, 우선 Adapter와 Port를 도입했다.
1. ItemReaderPort와 ItemReaderAdapter 도입
데이터 조회와 관련된 로직만을 담당하는 인터페이스 ItemReaderPort와 이를 구현하는 ItemReaderAdapter를 작성하여 Repository의 순수한 데이터 조회 기능을 분리했다.
public interface ItemReaderPort {
...
Optional<ItemEntity> findItemById(long itemId);
Page<ItemEntity> findAllItems(Pageable pageable);
}
@RequiredArgsConstructor
@Repository
public class ItemReaderAdapter implements ItemReaderPort {
private final ItemJpaRepository itemJpaRepository;
...
@Override
public Optional<ItemEntity> findItemById(long itemId) {
return itemJpaRepository.findById(itemId);
}
@Override
public Page<ItemEntity> findAllItems(Pageable pageable) {
return itemJpaRepository.findAll(pageable);
}
}
이렇게 리팩토링함으로써, 순수한 데이터 조회 로직만을 분리하여 SRP를 준수하는 구조로 만들었다.
2. ItemReaderRepositoryAdapter 도입
DTO 매핑, 페이징 처리와 같은 비즈니스 로직은 별도의 서비스 클래스로 이동했다. ItemReaderRepositoryAdapter는 ItemReaderPort에서 데이터를 조회한 후, 이를 필요한 형태로 가공하는 역할을 담당한다.
@RequiredArgsConstructor
@Service
public class ItemReaderRepositoryAdapter implements ItemReaderRepository {
private final ItemReaderPort itemReaderPort;
private final ItemEntityPageMapper itemEntityPageMapper;
private final PageableFactory pageableFactory;
@Override
public boolean existsByName(String name) {
return itemReaderPort.existsByName(name);
}
@Override
public Optional<ItemWithStockDto> findItemWithStockById(long itemId) {
return itemReaderPort.findItemById(itemId)
.map(ItemEntity::toItemWithStockDto); // 비즈니스 로직 처리
}
@Override
public PageResponse<ItemWithStockDto> findAllItemsWithStock(SortablePaginationRequest sortablePaginationRequest) {
Pageable pageable = pageableFactory.createPageable(sortablePaginationRequest);
Page<ItemEntity> itemEntityPage = itemReaderPort.findAllItems(pageable);
return itemEntityPageMapper.toItemWithStockDtoPageResponse(itemEntityPage); // 비즈니스 로직 처리
}
}
이렇게 하면 Repository는 오직 데이터 조회만을 담당하게 되고, DTO 매핑 및 페이징과 같은 비즈니스 로직은 별도의 서비스에서 처리된다.
리팩토링 결과
- SRP 준수:
- ItemReaderAdapter는 데이터 조회에만 집중하고, 비즈니스 로직은 ItemReaderRepositoryAdapter로 분리하여 각 클래스가 단일 책임을 가질 수 있도록 했다.
- 유연한 확장:
- 비즈니스 로직이 추가되더라도 Repository에 새로운 로직을 추가할 필요 없이, 별도의 서비스 계층에서 쉽게 확장할 수 있는 구조가 되었다.
- 테스트 용이:
- 각각의 클래스가 명확한 역할을 가지므로, 각 책임에 맞는 테스트 코드를 작성하기 수월해졌다.
결론
이번 리팩토링을 통해 SRP를 위반하는 문제를 해결하고, 코드의 유지보수성과 확장성을 크게 향상시킬 수 있었다. 특히, Adapter 및 Port 패턴을 도입함으로써 데이터 조회와 비즈니스 로직을 명확히 분리하고, 클린 아키텍처 원칙을 따르는 구조를 만들었다.