| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- CS
- Android
- cloud
- Spring
- frontend
- spring boot
- TypeScript
- blockchain
- SQL
- architecture
- DL
- Design
- VUE
- Studying
- springboot
- Database
- DB
- Algorithm
- http
- Kotlin
- Python
- java
- Network
- inflearn
- OS
- docker
- react
- GCP
- AI
- 배포
- Today
- Total
소소한 지식 저장소
[스프링 DB 2편 - 데이터 접근 활용 기술] 8. 데이터 접근 기술 - 활용 방안 본문
1. 스프링 데이터 JPA 예제의 트레이드오프
앞 장의 어댑터 구조는 ItemService가 기존 ItemRepository 계약만 알도록 유지한다. 구현 기술을 바꿔도 서비스 코드를 수정하지 않아도 되므로 DI와 OCP에 유리하다.
어댑터 구조의 클래스 의존 관계

ItemService는 ItemRepository에만 의존한다. JpaItemRepositoryV2가 기존 인터페이스를 구현하면서SpringDataJpaItemRepository로 위임하는 어댑터 역할을 한다.
어댑터 구조의 런타임 객체 의존 관계

실행 시 itemService → jpaItemRepositoryV2 → SpringDataJpaItemRepository 프록시 순으로 호출된다. 서비스는 구현체 교체를 알 필요가 없다.
하지만 중간 어댑터와 실제 구현을 모두 만들고 유지해야 하므로 클래스 수와 구조 복잡도가 증가한다. 안정적인 계약을 유지하는 비용과 단순한 구조·개발 속도 사이에는 트레이드오프가 있다.
2. 어댑터를 제거한 직접 의존 구조
다른 선택은 ItemService가 스프링 데이터 JPA 리포지토리를 직접 참조하도록 바꾸는 것이다. 이 경우 ItemService 수정이 필요해 DI·OCP의 이점은 줄지만, 어댑터가 사라져 구조는 단순해진다.
직접 의존 구조의 클래스 의존 관계

ItemService가 SpringDataJpaItemRepository에 직접 의존한다. 어댑터는 없지만 리포지토리 구현 기술이 바뀌면 서비스 코드도 함께 변경해야 한다.
직접 의존 구조의 런타임 객체 의존 관계

실행 경로는 itemService → SpringDataJpaItemRepository 프록시로 짧아진다. 이 단순성이 구조 안정성보다 중요한 프로젝트도 있다.
판단 기준
| 선택 | 장점 | 비용 |
| 어댑터 유지 | 구현체 교체 시 서비스 코드 보호, DI·OCP 준수 | 클래스·유지보수 대상 증가 |
| 직접 의존 | 구조와 개발 흐름이 단순 | 리포지토리 기술 변경 시 서비스 코드 수정 |
추상화와 인터페이스도 유지보수 비용이 든다. 프로젝트 규모, 변경 가능성, 팀 역량, 초기 개발 속도를 고려해 비용보다 효과가 큰 경우에만 추상화를 도입한다.
3. 실용적인 구조 - 기본 기능과 복잡한 쿼리 분리
기본 CRUD와 단순 조회는 스프링 데이터 JPA에 맡기고, 동적 조건·복잡한 조회는 Querydsl 전용 리포지토리에 분리한다. 이 구조는 스프링 데이터 JPA의 생산성과 Querydsl의 복잡한 쿼리 작성 장점을 함께 사용한다.

ItemRepositoryV2는 스프링 데이터 JPA 기본 기능을, ItemQueryRepositoryV2는 Querydsl 기반 복잡한 조회를 담당한다. 서비스는 두 역할을 직접 구분해 사용한다.
ItemRepositoryV2
package hello.itemservice.repository.v2;
import hello.itemservice.domain.Item;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
}
JpaRepository를 상속하므로 기본 CRUD와 필요 시 단순 쿼리 메서드를 제공한다.
ItemQueryRepositoryV2
package hello.itemservice.repository.v2;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import java.util.List;
import static hello.itemservice.domain.QItem.item;
@Repository
public class ItemQueryRepositoryV2 {
private final JPAQueryFactory query;
public ItemQueryRepositoryV2(EntityManager em) {
this.query = new JPAQueryFactory(em);
}
public List<Item> findAll(ItemSearchCond cond) {
return query.select(item)
.from(item)
.where(
maxPrice(cond.getMaxPrice()),
likeItemName(cond.getItemName()))
.fetch();
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
}
이 클래스는 복잡한 조회에만 집중한다. where()에 전달한 조건 메서드는 null을 반환하면 무시되고, 남은 조건은 AND로 결합된다.
4. ItemServiceV2
ItemServiceV2는 단순 CRUD에는 ItemRepositoryV2를 사용하고, 조건 검색에는 ItemQueryRepositoryV2를 사용한다. 기존 ItemServiceV1을 보존하기 위해 별도 클래스로 만든다.
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import hello.itemservice.repository.v2.ItemQueryRepositoryV2;
import hello.itemservice.repository.v2.ItemRepositoryV2;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional
public class ItemServiceV2 implements ItemService {
private final ItemRepositoryV2 itemRepositoryV2;
private final ItemQueryRepositoryV2 itemQueryRepositoryV2;
@Override
public Item save(Item item) {
return itemRepositoryV2.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = findById(itemId).orElseThrow();
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
return itemRepositoryV2.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCond cond) {
return itemQueryRepositoryV2.findAll(cond);
}
}
5. V2Config
package hello.itemservice.config;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.jpa.JpaItemRepositoryV3;
import hello.itemservice.repository.v2.ItemQueryRepositoryV2;
import hello.itemservice.repository.v2.ItemRepositoryV2;
import hello.itemservice.service.ItemService;
import hello.itemservice.service.ItemServiceV2;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
@RequiredArgsConstructor
public class V2Config {
private final EntityManager em;
private final ItemRepositoryV2 itemRepositoryV2; //SpringDataJPA
@Bean
public ItemService itemService() {
return new ItemServiceV2(itemRepositoryV2, itemQueryRepository());
}
@Bean
public ItemQueryRepositoryV2 itemQueryRepository() {
return new ItemQueryRepositoryV2(em);
}
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
애플리케이션은 ItemServiceV2를 사용한다. 반면 기존 ItemRepositoryTest는 ItemRepository를 테스트하므로 JpaItemRepositoryV3 빈도 유지한다. ItemRepositoryV2와 ItemQueryRepositoryV2에는 각각 별도 테스트가 필요하다.
6. 데이터 접근 기술 조합과 트랜잭션
기술 선택에는 단 하나의 정답이 없다. JPA·스프링 데이터 JPA·Querydsl은 생산성이 높지만 학습 비용이 있고, 아주 복잡한 통계 쿼리에는 직접 SQL이 더 적합할 수 있다. 일반적으로는 JPA·스프링 데이터 JPA·Querydsl을 기본으로 두고, 해결하기 어려운 일부 쿼리에 JdbcTemplate 또는 MyBatis를 함께 사용하는 방식이 실용적이다.
트랜잭션 매니저
- JPA·스프링 데이터 JPA·Querydsl은 JPA를 사용하므로 스프링 부트가 JpaTransactionManager를 등록한다.
- JdbcTemplate·MyBatis는 JDBC를 직접 사용하지만 JpaTransactionManager는 DataSourceTransactionManager 기능도 대부분 지원한다.
- 따라서 JpaTransactionManager 하나로 JPA·JdbcTemplate·MyBatis 작업을 하나의 트랜잭션에 묶고 함께 롤백할 수 있다.
JPA와 JDBC를 함께 쓸 때의 플러시 주의점
JPA는 엔티티 변경을 즉시 DB에 반영하지 않고 기본적으로 트랜잭션 커밋 시점에 flush한다. 같은 트랜잭션 안에서 JPA로 데이터를 바꾼 직후 JdbcTemplate/MyBatis로 읽으면 JDBC 쪽은 아직 DB에 반영되지 않은 변경을 읽지 못할 수 있다.
따라서 JPA 변경 뒤 JDBC 직접 접근이 이어지는 경우에는 JDBC 호출 전에 JPA flush가 완료되도록 처리해야 한다.
7. 최종 정리
| 주제 | 핵심 |
| 구조 선택 | DI·OCP를 지키는 어댑터 구조와 단순한 직접 의존 구조에는 유지보수·개발 속도 트레이드오프가 있다. |
| 리포지토리 분리 | 기본 CRUD·단순 조회는 Spring Data JPA, 복잡한 동적 조회는 Querydsl로 역할을 나눈다. |
| 서비스 변경 | 단순한 구조를 선택하면 서비스가 구체 리포지토리를 직접 알고, 구현 기술 교체 비용이 커진다. |
| 기술 조합 | 기본은 JPA·Spring Data JPA·Querydsl, 난해한 일부 SQL은 JdbcTemplate/MyBatis로 보완할 수 있다. |
| 트랜잭션 | JpaTransactionManager는 JPA와 JDBC 기반 기술을 하나의 트랜잭션으로 묶을 수 있다. |
| 플러시 | JPA 변경 후 JDBC를 호출할 때 DB 반영 시점이 필요하면 flush를 고려한다. |
'INFLEARN' 카테고리의 다른 글
| [스프링 DB 2편 - 데이터 접근 활용 기술] 7. 데이터 접근 기술 - Querydsl (0) | 2026.07.26 |
|---|---|
| [스프링 DB 2편 - 데이터 접근 활용 기술] 6. 데이터 접근 기술 - 스프링 데이터 JPA (0) | 2026.07.19 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 5. 데이터 접근 기술 - JPA (0) | 2026.07.19 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 3. 데이터 접근 기술 - 테스트 (0) | 2026.07.17 |
| [스프링 DB 2편 - 데이터 접근 활용 기술] 2. 데이터 접근 기술 - 스프링 JdbcTemplate (0) | 2026.07.13 |
