실용적인 구조와 구조적 안정성 간의 고민
지금까지, JdbcTemplate, Mybatis, Spring Data JPA, QueryDsl을 쭉 공부해봤다. 그리고 각각의 장단점도 배워봤고 이 기술들을 사용해서 서비스 레이어에서 데이터를 접근해봤다. 그런데 기존의 서비스 구조를 다시 한번 상기해보자.
ItemService
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import java.util.List;
import java.util.Optional;
public interface ItemService {
Item save(Item item);
void update(Long itemId, ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findItems(ItemSearchCond itemSearch);
}
- ItemService 인터페이스이다. 물론, 서비스는 인터페이스를 따로 만들지 않아도 된다고 생각한다. 이것도 고민거리 중 하나인데 생각해보면 서비스는 최대한 순수한 자바 코드를 사용해서 구현하면 할수록 좋다. 그러려고 노력하고 있고. 그렇기 때문에 서비스는 특정 기술에 의존하는 게 비교적 적다. 그럼 기술을 갈아끼우는 작업이 없는데 인터페이스가 굳이 필요할까?에 대한 고민이 첫번째.
- 그럼에도 불구하고 인터페이스를 만들고 그 구현체를 만들었긴 했으니 일단 계속 진행하자.
ItemServiceV1
package hello.itemservice.service;
import hello.itemservice.domain.Item;
import hello.itemservice.repository.ItemRepository;
import hello.itemservice.repository.ItemSearchCond;
import hello.itemservice.repository.ItemUpdateDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class ItemServiceV1 implements ItemService {
private final ItemRepository itemRepository;
@Override
public Item save(Item item) {
return itemRepository.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
itemRepository.update(itemId, updateParam);
}
@Override
public Optional<Item> findById(Long id) {
return itemRepository.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCond cond) {
return itemRepository.findAll(cond);
}
}
- ItemService를 구현한 구현체이다. 그리고 이 서비스는 지금 코드로만 보면 단순히 ItemRepository에 위임하고 있는 수준이다.
- 그런데 ItemRepository는 인터페이스고 구현체가 다양하다. JdbcTemplate, Mybatis, Spring Data JPA, QueryDsl을 사용한 여러 구현체가 있는데 여기서 나의 고민이 발생한다.
구현 기술이 바뀌더라도 사용하는 클라이언트 코드에는 변경이 없도록 의존관계를 주입받아 사용하여 OCP 원칙을 지키는 설계를 반드시 지켜야 하는가?
아주 좋은 설계이고 유지보수에 굉장히 도움이 되는 설계이다. 그런데 이 원칙을 고수하고자 하면 이러한 모양이 그려진다.
- 생각해보면 Spring Data JPA를 사용하면, 그냥 JpaRepository를 상속받은 인터페이스를 하나 만들면 Spring Data JPA가 알아서 구현체를 만들어주기 때문에 서비스는 그냥 JpaRepository를 상속받은 인터페이스만 주입받으면 된다.
- 그런데 서비스는 ItemRepository를 주입받기 때문에 이 ItemRepository를 구현하는 구현체를 만들고 그 구현체가 Spring Data JPA를 사용하도록 중간 다리 하나가 더 필요해진다.
만약, 아래와 같은 그림으로 설계하면 어떨까?
- 서비스는 바로 Spring Data JPA를 사용하기 위해 만들어진 Repository를 주입받는 것이다.
- 이러면 중간 다리도 필요없고 복잡한 구조도 사라지고 그림이 매우 간단해진다. 가장 좋은건 개발자가 단순하게 바로 JpaRepository를 상속받은 인터페이스를 직접 주입받아 사용하면 되니까 개발 속도가 빨라진다는 점이다.
트레이드 오프
이 부분이 트레이드 오프가 된다.
- 1 → DI, OCP를 지키기 위해 중간에 어댑터를 도입하고 더 많은 코드를 유지하거나
- 2 → 어댑터를 제거하고 구조를 단순하게 가져가지만, DI와 OCP를 포기하고 ItemService 코드를 직접 변경한다.
1번의 장점
자, DI와 OCP를 지킨다면 추후 데이터 접근 기술이 바뀌어도 서비스 코드는 변경할 필요가 전혀 없다. 이 서비스가 의존하는건 인터페이스지 구현체가 아니기 때문에 어떤 기술을 사용하던지 그 인터페이스를 구현한 구현체만 만들어 빈으로 등록하면 된다. 즉, 구조의 안정성이 생긴다.
1번의 단점
개발 속도와 개발의 편리성이 떨어지고 코드가 많아진다. 쉽게 말해, 안해도 되는 작업을 구조의 안정성을 위해 해야하는 공수가 생긴다.
→ 정답은 없다.
내가 내린 결론은 정답은 없다이다. 만약, 프로젝트 규모가 굉장히 크고 추후 기술의 변경점이 반드시 생길 것 같다고 판단되면 구조의 안정성을 가져가는게 좋은 판단이라고 생각된다. 그러나, 빠른 개발이 가장 우선이고 추후 기술의 변경점이 많지 않을 것 같다고 판단되면 실용적인 구조가 더 좋은 판단이라고 생각된다. 쉽게 말해, 현재 상황에 맞게 적절한 선택을 해야 한다는 말이다.
다양한 데이터 접근 기술 조합
또 다른 주제로, 이렇게 여러 기술을 배워봤는데 그래서 어떤 기술을 선택하면 좋을까? 이 부분 역시 하나의 정답이 있는건 아니라고 생각한다. JdbcTemplate이나 Mybatis와 같은 기술들은 SQL을 직접 작성해야 하는 단점은 있지만 기술이 단순하기 때문에 SQL에 익숙한 개발자라면 러닝 커브가 굉장히 낮다.
JPA, 스프링 데이터 JPA, QueryDsl과 같은 기술들은 개발 생산성을 혁신할 수 있지만, 학습 곡선이 높아 이런 부분을 감안해야 한다. 그리고 매우 복잡한 통계 쿼리를 주로 작성하는 경우에는 잘 맞지 않는다.
그래서, 메인 시나리오는 이렇다.
→ 스프링 데이터 JPA, QueryDsl을 기본으로 사용하고 복잡하고 통계형 쿼리를 사용해야 하는 경우에 JdbcTemplate이나 MyBatis를 함께 사용하는 것.
거의 대부분의 경우 스프링 데이터 JPA와 QueryDsl만으로 문제가 해결이 되는데 가끔 네이티브 SQL을 사용해야 하는 경우가 생기긴 한다. 그런 경우에 JdbcTemplate, Mybatis를 사용하면 된다.
트랜잭션 매니저 선택
JPA, Spring Data JPA, QueryDsl은 모두 JPA 기술을 사용하는 것이기 때문에 트랜잭션 매니저로 JpaTransactionManager를 사용한다. 위 기술들을 사용하면 스프링 부트는 자동으로 JpaTransactionManager를 스프링 빈으로 등록한다. 그런데 JdbcTemplate, Mybatis와 같은 기술들은 DataSourceTransactionManager를 사용한다. 따라서 JPA와 JdbcTemplate 두 기술을 함께 사용하면 트랜잭션 매니저가 달라진다. 결국 트랜잭션을 하나로 묶을 수 없는 문제가 발생한다. 어떻게 하면 되지? 트랜잭션을 하나로 묶을 수 없다면 데이터베이스 커넥션이 달라진다는 얘기고 커넥션이 달라진다는 것은 두 작업간의 동시적으로 동기화를 할 수 없다는 얘긴데 말이다. 동기화가 안된다는 것은 롤백과 커밋이 불가능하다는 얘기다.
JpaTransactionManager의 지원
위 걱정은 할 필요가 없다. JpaTransactionManager는 놀랍게도 DataSourceTransactionManager가 제공하는 기능도 대부분 제공한다. JPA라는 기술도 결국 내부에는 다 DataSource와 JDBC 커넥션을 사용할 수 밖에 없기 때문이다. 따라서 JdbcTemplate, Mybatis와 함께 사용할 수 있다. 결과적으로 JpaTransactionManager만 스프링 빈에 등록하면 JPA, JdbcTemplate, Mybatis 모두를 하나의 트랜잭션으로 묶어 사용할 수 있다. 그 말은 커밋도 롤백도 다 가능해진다는 의미다.
주의할 점은, JPA와 JdbcTemplate을 함께 사용할 경우 JPA 플러시 타이밍을 고려해야 한다. JPA는 데이터를 변경할 때 변경 사항을 즉시 데이터베이스에 반영하는 게 아니라 커밋 시점에 변경 사항을 데이터베이스에 반영한다. 쓰기 지연이라는 기술을 내부적으로 사용하기 때문에 그렇다. 그런데 이런 경우에 하나의 트랜잭션 안에서 JPA를 통해 데이터를 변경한 다음 JdbcTemplate을 호출해 사용하는 경우 JdbcTemplate에서는 JPA가 변경한 데이터를 읽지 못하는 문제가 발생한다. 같은 커넥션이라고 해도 이건 커넥션이나 트랜잭션의 문제가 아니라 기술이 다르기 때문에 JPA가 처리하는 방식이 커밋 시점에 한번에 쓰기에 대한 쿼리를 날리기 때문에 그렇다. 그래서 이렇게 한 트랜잭션에서 JPA로 데이터를 변경한 다음 JdbcTemplate을 호출하는 경우가 있다면 JdbcTemplate을 호출하기 전에 JPA가 제공하는 플러시를 사용해서 JPA의 변경 내역을 바로 데이터베이스에 적용해줘야 한다.
'Spring + Database' 카테고리의 다른 글
[Renewal] @Transactional 내부 호출 주의 (2) | 2024.12.11 |
---|---|
[Renewal] 스프링 트랜잭션 이해 (0) | 2024.12.08 |
[Renewal] Spring Data JPA (4) | 2024.12.07 |
[Renewal] MyBatis (4) | 2024.12.06 |
[Renewal] JdbcTemplate (8) | 2024.12.06 |