Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 자바의 정석 기초편 ch11
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 타임리프
- 스프링 고급 - 스프링 aop
- 스프링 mvc2 - 로그인 처리
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch14
- 스프링 mvc2 - 검증
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch2
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch13
- 2024 정보처리기사 수제비 실기
- @Aspect
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch7
- 스프링 입문(무료)
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 게시글 목록 api
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch8
- 스프링 db1 - 스프링과 문제 해결
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch12
Archives
- Today
- Total
나구리의 개발공부기록
데이터 접근 기술 - 활용방안, 스프링 데이터 JPA 예제와 트레이드 오프, 실용적인 구조, 다양한 데이터 접근 기술 조합 본문
인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 2편 - 데이터 접근 활용
데이터 접근 기술 - 활용방안, 스프링 데이터 JPA 예제와 트레이드 오프, 실용적인 구조, 다양한 데이터 접근 기술 조합
소소한나구리 2024. 9. 23. 14:23 출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2
** 여러가지 데이터 접근 기술을 실무에 적용할 때 고민거리나 팁에 대해서 설명하는 강의
1. 스프링 데이터 JPA 예제와 트레이드 오프
(1) 스프링 데이터 JPA 예제의 고민
- 중간에서 JpaItemRepositoryV2가 어댑터 역할을 해준 덕분에 ItemService가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있었고 ItemService의 코드를 변경하지 않을 수 있었음
- 이런 추상화된 구조를 맞추기 위해서 중간에 어댑터가 들어가게 되면서 클래스가 많아지고, 전체 구조가 복잡해 지게 되었음
- 실제 이 코드를 구현해야하는 개발자 입장에서 보면 중간에 아주 간단한 코드임에도 어댑터도 만들고 구현 코드까지 만들어야 하는 불편함이 생기며, 단순하게 다른 계층으로 위임하는 코드까지 발생함
- 사실 지금 정도의 기능은 그냥 서비스에서 스프링 데이터가 제공하는 기능을 사용하면 코드 한줄로 간편하게 끝낼 수 있었음
- 유지보수 관점에서 ItemService를 변경하지 않고 ItemRepository의 구현체를 변경할 수 있도록 하여 DI, OCP 원칙을 지키는 장점이 오히려 구조가 복잡해지면서 어댑터 코드와 실제 코드까지 함께 유지보수 해야하는 어려움도 발생했음
- DI, OCP 중요한데, 실용성에 있어서는 아키텍처(구조적인 부분)에서 어떤 선택을 할때 생각보다 고민해야 할 것들이 더 존재함
(2) 다른 선택
- ItemService 코드를 일부 고쳐서 직접 스프링 데이터 JPA를 사용하는 방법을 선택 할 수 있음
- DI, OCP 원칙을 포기하는 대신 복잡한 어댑터를 제거하고 구조를 단순하게 가져갈 수 있는 장점이 있음
(3) 트레이드 오프 - 절충
- DI, OCP를 지키기 위해 어댑터를 도입하고 더 많은 코드를 유지할 것인가 -> 구조의 안정성
- 어댑터를 제거하고 구조를 단순하게 가져가지만 DI, OCP를 포기하고 ItemService 코드를 직접 변경할 것인가 -> 단순한 구조와 개발의 편리성
- 미래에 크게 구조적으로 확장되거나 바뀔 것 같지 않고, 서비스에서 바로 리포지토리를 빨리 호출하는 작고 단순한 프로젝트를 진행하는데 이런 추상화 구조를 도입하는 것이 효율적인 것인지는 생각을 해볼 필요가 있음
- 이런식으로 작은 프로젝트나 빨리 개발을 진행해야 하는 상황에서는 단순한 구조를 선택하는 것이 더 나은 선택일 수 있음
- 여기서 발생하는 트레이드 오프는 구조의 안정성 vs 단순한 구조와 개발의 편리성 사이의 선택이며 둘 중의 하나의 정답이 존재하지는 않기에 상황에 따라 구조의 안정성이 매우 중요할 수 있고 단순한 것이 더 나을 수도 있음
- 개발을 할 때는 항상 자원이 무한한 것은 아니며 어설픈 추상화(인터페이스)는 오히려 독이 되는 경우도 발생하게 됨
- 단순하게 코드가 증가하는 비용 뿐 아니라, 뭔가 코드에 문제가 있을 때 한번에 코드를 보고 분석하는 것과 코드를 타고 들어가고 구현체를 선택하면서 구조를 고민하면서 분석하는것 자체가 비용이 발생하고 있는 것
- 추상화(인터페이스)도 비용(리소스)이 들며, 이 추상화 비용을 넘어설 만큼 효과가 있을 때 추상화를 도입하는 것이 실용적임(유지보수 관점의 비용)
- 아키텍처는 헥사고날 등 여러가지 좋은 아키텍처가 많지만 이런 아키텍처가 그냥 좋다고 하니까 아무 생각없이 아키텍처를 도입하는 것은 오히려 프로젝트 규모에 맞지않아 코드가 더 많아지고 복잡해지는 결과가 나올 수 있음
- 이런 DB 기술들을 미래에 다른 기술로 바뀔 가능성은 실무에서도 생각보다 높지 않으며, 오히려 작게 시작한 프로젝트가 커지면 리펙토링하는 것보다 코드를 새로 짜는것이 더 나은 경우도 있는데 이렇게 되면 기존에 추상화를 도입해서 코드를 작성한 시간은 비용은 낭비가 되는 것
- 만일 이런 상황에서 작은 프로젝트를 단순한 구조로 빠르게 개발해놨다면, DB를 바꿀일이 없어서 계속 큰 유지보수가 없다면 오히려 비용을 세이브 한 것
- 규모와 상황에 따라 이런 트레이드오프가 발생하는데 진행하는 프로젝트의 상황에 맞게 적절한 선택을 하는 개발자가 좋은 개발자가 되는 것
** 김영한님의 개인적인 생각 - 개발 초보는 고수님의 의견을 따르자
- 프로젝트가 굉장히 크고 규모 있는 프로젝트로 이미 많이 성장을 했고 구조적으로 개선해야 한다고 판단이 들면 추상화가 많이 있는 아키텍처를 선택하는 경우가 많음(이것도 100%는 아니며 위의 같은 상황임에도 단순한 구조를 선택하는 경우도 있음)
- 하지만 보통은 프로젝트는 점점 늘려가는 경우가 많은데 이럴때는 간단하고 단순하면서 빨리 해결할 수 있는 방법을 선택하고 그 후에 그 프로젝트가 커지면 리팩토링을 하는 것이 더 실용적인 선택이라고 생각 한다.
2. 실용적인 구조
1) Querydsl + 스프링 데이터 JPA
- Querydsl 을 사용한 JpaRepositoryV3는 스프링 데이터 JPA를 사용하지 않았는데, 스프링 데이터 JPA의 기능은 최대한 살리면서 Qeurydsl도 편리하게 사용할 수 있는 구조로 변경을 진행
- 스프링 데이터 JPA의 기능을 제공하는 리포지토리와 Querydsl을 사용해서 복잡한 쿼리 기능을 제공하는 리포지토리로 분리하여 기본 CRUD와 같은 단순한 기능은 JPA가 담당하고, 복잡한 쿼리는 Querydsl이 담단하는 구조를 만들 수 있음
- ItemService도 기존의 ItemRepository를 사용할 수 없기 때문에 코드 변경을 해야함
2) ItemRepositoryV2
- JpaRepository를 상속받아서 스프링 데이터 JPA의 기능을 제공하는 리포지토리가 됨
- 기본 CRUD기능은 해당 리포지토리를 사용함
- 공통으로 제공되는 기능 외의 단순 조회 쿼리를 여기에 추가해도 됨
package hello.itemservice.repository.v2;
public interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
}
3) ItemQueryRepositoryV2
- Querydsl을 사용해서 복잡한 동적 쿼리를 실행함
- 해당 클래스는 Querydsl을 사용한 쿼리 문제에 집중 되어있어서 해당 부분만 유지보수 하면 됨
package hello.itemservice.repository.v2;
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(
likeItemName(cond.getItemName()),
maxPrice(cond.getMaxPrice()))
.fetch();
}
// 조건들을 메서드추출로 따로 빼서 관리
private BooleanExpression maxPrice(Integer maxPrice) {
if (maxPrice != null) {
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)) {
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}
4) ItemServiceV2
- ItemRepositoryV2와 ItemQeuryRepositoryV2를 의존하는 ItemServiceV2를 생성
- 각 메서드의 기능 중 단순 CRUD는 itemRepositoryV2를, 동적쿼리 부분은 itemQueryRepositoryV2를 사용함
package hello.itemservice.service;
@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 = itemRepositoryV2.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) 설정 및 실행
(1) V2Config
- 실제 사용시 설정은 컴포넌트로 스캔으로 자동 주입 받아서 사용함
- ItemRepository를 구현 및 주입하는 코드는 원래 제거하고 테스트 코드를 다시 만들어야하지만 테스트를 실행하지 않을 것이므로 오류 방지를위해 JpaItemRepositoryV3를 구현시킴
package hello.itemservice.config;
@Configuration
@RequiredArgsConstructor
public class V2Config {
private final EntityManager em;
private final ItemRepositoryV2 itemRepositoryV2; // SpringDataJPA
@Bean
public ItemService itemService() {
return new ItemServiceV2(itemRepositoryV2, itemQueryRepositoryV2());
}
@Bean
public ItemQueryRepositoryV2 itemQueryRepositoryV2() {
return new ItemQueryRepositoryV2(em);
}
// Test에서 ItemRepository를 사용하고 있어서 JpaItemRepositoryV3를 구현체로 생성
// 원래는 이렇게 사용하면 안되고 테스트 코드를 다시 만들어야하는게 맞음
@Bean
public ItemRepository itemRepository() {
return new JpaItemRepositoryV3(em);
}
}
(2) ItemServiceApplication @Import 수정
- V2Config를 @Import에 설정
@Import(V2Config.class)
(3) 실행
- test는 JpaItemRepositoryV3를 사용하기 때문에 의미없고, H2 데이터 베이스를 띄우고 애플리케이션을 실행해서 동작해보면 정상적으로 기능들이 동작함
** 참고
- 스프링 데이터 JPA가 제공하는 커스텀 리포지토리를 사용해도 비슷하게 문제를 해결할 수 있음
- 구조에 정답은 없지만, 구조적인 고민을 깊에 해야하는 상황이 아닌 스프링 데이터 JPA와 Querydsl을 가지고 실용적이고 빠르게 개발을 하고자할 때 지금 사용한 구조를 사용하는 것도 좋은 방법임
- 요구사항 변경이 있을 때에도 각 리포지토리의 역할이 정해져있어서 동적쿼리에대한 수정은 Querydsl 클래스에, 간단한 부분은 스프링 데이터 JPA에 추가하여 대응도 가능함
- 나중에 리포지토리를 바꾸거나, 데이터 접근 기술을 다른 기술로 바꾸었을 때 구조적인 유연성은 떨어질 수 있지만, 처음에는 이렇게 단순하게 접근을하고 이후 프로젝트가 커지면면 추상화에 대한 고민을 하면 됨
- 애초에 프로젝트 규모가 설계단계에서부터도 매우 크거나, 매우 큰 프로젝트를 다시 재조립해서 만들어야 하는 등의 상황에서는 처음부터 추상화를 고려한 구조를 도입 하는 것이 좋으나, 추상에 대한 고민보다 일단 스프링 데이터로 프로젝트를 해봐야 하는 상황이라면 지금과 같은 실용적인 구조를 선택하는 것을 권장함
- 선택은 개발자의 몫
3. 다양한 데이터 접근 기술 조합
- 어떤 데이터 접근 기술을 선택하는 것이 좋은지에 대한 정답은 비즈니스 상황과 현재 프로젝트 구성원의 역량에 따라 결정되어야 하기 때문에 정답이 존재할 수는 없음
- JdbcTemplate이나 MyBatis 같은 기술들은 SQL을 직접 작성해야하는 단점은 존재해도 기술이 단순하기 때문에 SQL에 익숙한 개발자라면 금방 적응할 수 있음
- JPA, 스프링 데이터 JPA, Querydsl 같은 기술들은 개발 생산성을 혁신할 수 있지만 학습 곡선이 높다는 부분을 감안해야하며, 매우 복잡한 동적쿼리를 주로 작성해야 하는 경우에는 잘 맞지는 않음(SQL로 통계쿼리 작성 등)
(1) 강의에서 추천하는 방향
- JPA, 스프링 데이터 JPA, Querydsl을 숙달해서 기본으로 사용하고 만약 복잡한 쿼리를 써야하는데 해결이 잘 되지 않을 때 JdbcTemplate이나 MyBatis를 함께 사용하는 것을 권장함
- 실무에서는 95% 정도는 JPA, 스프링 데이터 JPA, Querydsl 등으로 해결함(비율은 프로젝트마다 다름)
(2) 트랜잭션 매니저 선택
- JPA, 스프린 데이터 JPA, Querydsl는 모두 JPA 기술을 사용하는 것이기 때문에 트랜잭션 매니저로 JpaTransactionManager를 사용해야 하는데, 스프링 부트로 위 기술들을 사용하면 해당 트랜잭션을 자동으로 스프링 빈에 등록해 줌
- JdbcTemplate, MyBatis는 내부에서 JDBC를 직접 사용하기 때문에 DataSourceTransactionManager나 JdbcTransactionManager가 사용됨
- 두 기술을 함께 사용하면 트랜잭션 매니저가 달라 트랜잭션을 하나로 묶을 수 없는 문제가 발생할 것이라고 생각이 들겠지만JpaTransactionManager는 DataSourceTransactionManager가 제공하는 기능도 대부분 제공하기 때문에 걱정하지 않아도 됨
- JPA라는 기술도 결국 내부에서는 DataSource와 JDBC 커넥션을 사용하기 때문에 JdbcTemplate, MyBatis와 함께 사용할 수 있음
- 결과적으로 JpaTransactionManager만 스프링 빈에 등록되어있으면 JPA, JdbcTemplate, MyBatis 모두를 하나의 트랜잭션으로 묶어서 사용이 가능하며 함께 롤백도 가능함
(3) 주의점
- JPA와 JdbcTemplate을 함께 사용한다고 가정을 했을 때 JPA 플러시 타이밍에 주의해야 함 -> JPA를 학습을 해야 제대로 이해할 수 있음
- JPA는 데이터를 변경하면 변경 사항을 즉시 데이터베이스에 반영하지 않고 기본적으로 트랜잭션이 커밋되는 시점에 변경 사항을 데이터베이스에 반영함
- 하나의 트랜잭션 안에서 JPA를 통해 데이터를 변경한 다음 JdbcTemplate을 호출하는 경우 JdbcTemplate에서는 JPA가 변경한 데이터를 읽지 못하는 문제가 발생함
- 해당 문제를 해결하려면 JPA 호출이 끝난 시점에 JPA가 제공하는 플러시라는 기능을 사용해서 JPA의 변경 내역을 데이터베이스에 반영해주어야 그 다음에 호출되는 JdbcTemplate에서 JPA가 반영한 데이터를 사용할 수 있음
3) 정리
- 구조 선택에서의 하나의 정답은 없음
- 이러한 트레이드 오프를 해야한다는 것을 알고 현재 상황에 더 맞는 적절한 선택을 하는 좋은 개발자가 있을 뿐이다
- 이러한 고민하는 시간이 쌓이고 선택을하는 과정이 좋은 개발자가 되는 과정이다