Post

Domain Repository에 JPA 스타일 배치 메서드 추가하기

Domain Repository에 JPA 스타일 배치 메서드 추가하기

Clean Architecture를 적용한 프로젝트에서 Domain Repository에 JPA의 deleteAllInBatch와 같은 배치 메서드를 추가해야 했다. 도메인 순수성을 유지하면서 JPA의 편의성을 활용하는 방법을 정리한다.

기존 구조

기본적인 Domain Repository 인터페이스:

1
2
3
4
5
6
7
8
9
public interface ConcertDateRepository {
    ConcertDate save(ConcertDate concertDate);
    Optional<ConcertDate> findById(Long id);
    List<ConcertDate> findAll();
    void delete(ConcertDate concertDate);
    void deleteById(Long id);
    boolean existsById(Long id);
    long count();
}

확장의 필요성

테스트 환경에서 데이터 정리를 위해 deleteAllInBatch() 메서드가 필요했다. JpaRepository의 편의 메서드들을 Domain Repository에서도 사용하고 싶었다.

해결 방법

1단계: Repository 인터페이스 확장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface ConcertDateRepository {
    // 기본 CRUD 메서드들
    ConcertDate save(ConcertDate concertDate);
    List<ConcertDate> saveAll(List<ConcertDate> concertDates);
    Optional<ConcertDate> findById(Long id);
    List<ConcertDate> findAll();
    List<ConcertDate> findAllById(List<Long> ids);
    
    // 삭제 메서드들
    void delete(ConcertDate concertDate);
    void deleteById(Long id);
    void deleteAll();
    void deleteAll(List<ConcertDate> concertDates);
    void deleteAllById(List<Long> ids);
    
    // 배치 삭제 메서드들 (JpaRepository 스타일)
    void deleteAllInBatch();
    void deleteAllInBatch(List<ConcertDate> concertDates);
    void deleteAllByIdInBatch(List<Long> ids);
    
    // 존재 확인 및 개수
    boolean existsById(Long id);
    long count();
    
    // 배치 업데이트 메서드들
    int bulkUpdateStatus(ConcertDateStatus newStatus, ConcertDateStatus oldStatus);
    int bulkUpdateAvailableSeats(Long concertId, int newAvailableSeats);
}

2단계: 구현체에서 JpaRepository 메서드 위임

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Repository
public class ConcertJpaRepository implements ConcertDateRepository {

    private final SpringDataConcertDateRepository concertDateRepo;
    
    @PersistenceContext
    private EntityManager entityManager;

    // 배치 삭제 메서드들
    @Override
    @Transactional
    public void deleteAllInBatch() {
        concertDateRepo.deleteAllInBatch();
    }

    @Override
    @Transactional
    public void deleteAllInBatch(List<ConcertDate> concertDates) {
        List<ConcertDateEntity> entities = concertDates.stream()
                .map(this::toEntity)
                .collect(Collectors.toList());
        concertDateRepo.deleteAllInBatch(entities);
    }

    @Override
    @Transactional
    public void deleteAllByIdInBatch(List<Long> ids) {
        concertDateRepo.deleteAllByIdInBatch(ids);
    }

    // 배치 업데이트 메서드들
    @Override
    @Transactional
    public int bulkUpdateStatus(ConcertDateStatus newStatus, ConcertDateStatus oldStatus) {
        return entityManager.createQuery(
                "UPDATE ConcertDateEntity cd SET cd.status = :newStatus WHERE cd.status = :oldStatus")
                .setParameter("newStatus", newStatus)
                .setParameter("oldStatus", oldStatus)
                .executeUpdate();
    }
}

3단계: 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class ConcertDateService {
    
    private final ConcertDateRepository concertDateRepository;
    
    // 배치 삭제 사용
    @Transactional
    public void cleanupExpiredConcertDates() {
        concertDateRepository.deleteAllInBatch();
    }
    
    @Transactional
    public void deleteExpiredDates(List<ConcertDate> expiredDates) {
        concertDateRepository.deleteAllInBatch(expiredDates);
    }
    
    @Transactional
    public void deleteDatesByIds(List<Long> ids) {
        concertDateRepository.deleteAllByIdInBatch(ids);
    }
    
    @Transactional
    public void bulkCancelScheduledDates() {
        concertDateRepository.bulkUpdateStatus(
            ConcertDateStatus.CANCELLED, 
            ConcertDateStatus.SCHEDULED
        );
    }
}

장점

  1. 도메인 순수성 유지: Infrastructure 계층의 JPA 특성이 Domain 계층으로 누출되지 않음
  2. 성능 향상: 배치 작업으로 다수의 개별 쿼리를 하나의 쿼리로 처리
  3. 편의성: JpaRepository의 편의 메서드들을 Domain Repository에서 사용 가능
  4. 일관성: 모든 데이터 접근이 Domain Repository를 통해 이루어짐

주의사항

  • 배치 메서드는 성능상 이점이 있지만, 도메인 객체의 생명주기 콜백을 우회한다
  • @Modifying 쿼리 사용 시 영속성 컨텍스트와의 동기화 문제를 고려해야 한다
  • 복잡한 비즈니스 로직이 필요한 경우, 개별 도메인 객체를 통한 처리를 고려해야 한다

결론

Clean Architecture의 원칙을 지키면서도 JPA의 배치 기능을 활용할 수 있다. Repository 인터페이스를 적절히 확장하고, 구현체에서 JPA 기능을 위임하는 방식으로 두 가지 장점을 모두 얻을 수 있다.

This post is licensed under CC BY 4.0 by the author.