Spring DB : 각 데이터 접근기술 활용 방안
- Spring/Spring
- 2023. 1. 29.
들어가기 전
이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.
스프링 데이터 JPA와 트레이드 오프
중간에서 JpaItemRepositoryV2가 ItemService와 JpaItemRepository의 어댑터 역할을 해준 덕분에 ItemService가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있고, 클라이언트인 ItemService의 코드를 변경하지 않아도 되는 장점이 있다.
그런데 이 때 한 가지 고민이 생긴다. Adapator를 사용해서 Service 계층과 논리적으로도 완벽히 계층 분리를 할 수 있었지만, Adapter가 하나 더 추가되면서 전체 구조가 너무 복잡해진다는 것이다.
고민
구조를 맞추기 위해서 중간에 어댑터(JpaItemRepositoryV2 - SimpleJpaRepository 사이)가 들어가면서 전체 구조가 너무 복잡해지고 사용하는 클래스도 많아지는 단점이 있다. 단순하게 위임하는 것인데 굳이 어댑터가 필요할까?
- 개발자 입장에서는 어댑터 관련 많은 코드를 구현해야함.
- 유지보수 관점에서 ItemService를 변경하지 않고 Repository 구현체를 갈아 끼울 수 있다는 장점은 있지만, 반대로 구조가 복잡해지면서 어뎁터 코드가 추가되고 이것도 같이 유지보수를 해야한다는 단점이 있음.
위와 같이 어댑터를 이용해서 하는 상황이라면, 반대로 Service 계층에서 SimpleJpaRepository와 JpaItemRepositoryV2를 각각 추가해서 그냥 쓰는 것도 방법이 될 수 있다.
위처럼 클래스 의존 관계를 만들어준다면, 클래스의 의존 관계는 좀 더 단순해진다. 그렇지만 Repository 계층에서의 사용하는 기술의 변화로 Service 계층에서도 변화가 있어야 한다. 아무튼 모든 것을 만족시킬 수는 없는 상황이 온다.
트레이드 오프
- DI를 선택하면 구조가 복잡해진다.
- 단순한 구조를 선택하면, 코드 변경 시 유지보수 해야할 범위가 또 다른 의미로 넓어진다.
Spring Data JPA를 도입하고자 했을 때, 위와 같이 트레이드 오프를 고려해야 하게 되었다. 그 부분을 좀 더 살펴보면 이런 내용이 된다.
- DI, OCP를 지키기 위해 어댑터를 도입하고, 더 많은 코드를 유지한다. (구조의 안정성)
- 어댑터를 제거하고 구조를 단순하게 가져가지만 DI, OCP를 포기하고 ItemService 코드를 직접 변경한다. (코드 생산성 향상)
어떠한 것을 선택해도 괜찮은 상황이 될 수 있다. 예를 들면 어떤 상황에서는 구조의 안정성이 중요할 수도 있고, 반대로 개발의 편의성이 중요할 수도 있다. 만약 구조적으로 확장성이 있을 경우가 많다면 구조의 안정성이 중요할 것이다.
어설픈 추상화는 독이 될 수도 있다. 추상화도 비용이 들기 때문이다. 추상화에서 사용되는 비용은 무엇을 의미할까? 추상화에 사용되는 비용은 유지보수 관점에서 비용을 의미한다. 추상화 유지보수 비용은 무엇을 의미하는걸까?
- 인터페이스를 이용하면 디버깅 과정에서 어떤 구현체를 썻는지 파악하는데 어려움이 있음. → 비용 발생.
인터페이스는 의존 관계 결합을 적게 가져가지만, 반대로 디버깅에서 유지보수 비용을 발생시킨다. 그렇다면 추상화는 언제 해야하고, 어떻게 개발을 해야할까? 좋은 방법 중 하나는
가장 간단하게 구현할 수 있는 것으로 구현한다. 그리고 나중에 규모가 커지고, 이 때 추상화를 조금 해줬을 때 발생하는 비용 편익이 더 좋을 때 리팩토링으로 추상화를 추가해주는 것이 좋다고 한다.
실용적인 데이터 구조
마지막에 queryDSL을 사용한 리포지토리는 Spring Data JPA를 사용하지 않았다.(아래에서 코드 확인). QueryDSL로 반복적으로 사용되는 코드들 역시 직접 구현해서 코드의 구현 양이 늘어났다. 이런 것은 Spring Data JPA를 이용하면 반복 코드의 작성량을 줄일 수 있다.
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em); // QueryDSL은 JPQL을 만들어주는 역할을 한다. JPA는 Entity Manager를 가지고 동작한다.
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
...
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return queryFactory
.select(item)
.from(item)
.where(itemNameLike(itemName), maxPrice(maxPrice)) // null이면 무시함. "," 이걸로 하는 경우 and로 연결됨.
.fetch();
}
private BooleanExpression itemNameLike(String itemName) {
return StringUtils.hasText(itemName) ? item.itemName.like("%" + itemName + "%") : null;
}
...
}
그렇다면 복잡한 쿼리와 단순한 쿼리를 분리해서 각각의 책임을 가지도록 해보면 어떨까?
복잡한 쿼리의 분리
기본 CRUD와 단순 조회는 스프링 데이터 JPA가 사용, 복잡한 쿼리는 QueryDSL이 담당한다. 이렇게 하면 적은 비용으로 코드를 유지해볼 수 있다.
- ItemSerivce가 itemRepositoryV2, itemQueryRepositoryV2에 모두 의존하는 방법이 존재
- ItemService는 ItemQueryRepositoryV2에 의존. ItemQueryRepositoryV2는 ItemRepositoryV2(JPARepository)를 포함
쿼리 분리 관련 코드
- ItemRepositoryV2 : JpaItemRepository 인터페이스를 상속받음. 단순한 쿼리를 담당함.
- ItemQueryRepositoryV2 : JpaRepository에서 대웅할 수 없는 동적 쿼리를 담당함.
- ItemService : ItemRepositoryV2 + ItemQueryRepositoryV2에 모두 의존하고 필요한 메서드를 불러와서 처리함.
다양한 데이터 접근 기술 조합
스프링에서 사용할 수 있는 데이터 접근 기술은 여러가지가 있다. 그럼 어떤 데이터 접근 기술을 사용하는 것이 좋을까? 현재 비즈니스 상황 / 프로젝트 구성원의 역량에 따라서 결정하는 것이 적합하다.
- 프로젝트 구성원이 JPA를 모르면 많은 러닝커브 비용이 발생함.
- JPA는 매우 복잡한 통계 쿼리에는 맞지 않음.
추천하는 방향은 JPA + 스프링 데이터 JPA + QueryDSL을 기본으로 사용한다. 이 기술로 해결되지 않는 쿼리는 JdbcTemplate이나 MyBatis를 함께 사용하는 것이다.
다양한 데이터 접근 기술 → 트랜잭션 매니저 선택
JPA, 스프링 데이터 JPA는 JPA 기술을 사용하는 것이기 때문에 트랜잭션 매니저로 JpaTransactionManager를 선택하면 된다. JPA를 사용하면 스프링부트는 자동으로 JpaTransactionManager를 등록해준다. 그런데 JdbcTemplate, MyBatis와 같은 기술들은 내부에서 JDBC를 직접 사용하기 때문에 DataSourceTransactionManager를 사용한다. 따라서 JPA, JdbcTemplate 두 기술을 함께 사용하면 트랜잭션 매니저가 달라진다. 결국 트랜잭션을 하나로 묶을 수 없는 문제가 발생한다. 그런데 이 부분은 걱정하지 않아도 된다.
- JPA를 사용하면 스프링 부트는 JpaTransactionManager를 등록해줌.
- JpaTransactionManager는 JdbcTemplate, MyBatis를 지원해줌.
- 따라서 JpaTransactionManager만 사용하면 됨.
JpaTransactionManager는 DataSourceTransactionManager가 제공하는 기능도 대부분 제공한다. JPA는 결국 내부에서는 DataSource와 JDBC Connection을 사용하기 때문이다. 따라서 JdbcTemplate, MyBatis와 함께 사용할 수 있다. 결과적으로 JpaTransactionManager를 하나만 스프링 빈에 등록하면 JPA, JdbcTemplate, MyBatis 모두를 하나의 트랜잭션으로 묶어서 사용할 수 있다.
다양한 데이터 접근 기술 → 주의점
JPA, JdbcTemplate을 함께 사용할 경우 JPA의 플러시 타이밍에 주의해야한다. JPA 데이터를 변경하면 변경 사항을 즉시 데이터베이스에 반영하지는 않는다. 기본적으로 트랜잭션이 커밋되는 시점에 변경 사항을 데이터베이스에 반영한다. 그래서 하나의 트랜잭션 안에서 JPA를 통해 데이터를 변경한 다음에 Jdbctemplate으로 DB와 통신한 경우, JdbcTemplate에서는 JPA가 변경한 데이터를 읽지 못하는 문제가 발생한다.
이 문제를 해결하려면 JPA 호출이 끝난 시점에 JPA가 제공하는 플러시라는 기능을 사용해서 JPA의 변경 내역을 데이터베이스에 반영해줘야한다. 그래야 그 다음에 호출되는 JdbcTemplate에서 JPA가 반영한 데이터를 사용할 수 있다.
'Spring > Spring' 카테고리의 다른 글
Spring : 동일 타입 여러 스프링 빈 주입 받기 (0) | 2023.02.07 |
---|---|
Spring DB : 스프링 트랜잭션 전파 기본 (1) | 2023.02.01 |
스프링 DB : Query DSL 관련 (0) | 2023.01.28 |
스프링 AOP : 프록시 기술과 한계 (0) | 2022.02.06 |
스프링 AOP : 프록시 내부 참조 → AOP 적용 안되는 문제 (0) | 2022.02.06 |