JPA - 5

Single Ko·2023년 6월 10일
0

jpa

목록 보기
5/8

스프링 부트를 통해 JPA를 사용할시...

복잡한 설정이 다 자동화 되었다. persistence.xml도 없고 , LocalContainerEntityManagerFactoryBean도 없다.

스프링 부트를 통한 추가 설정은 스프링 부트 메뉴얼을 참고하자.

쿼리 파라미터 로그 남기기
로그에 다음을 추가하기: SQL 실행 파라미터를 로그로 남긴다. 하이버네이트 버전에 따라 설정이 달라진다.

스프링 부트 2.x, hibernate5
org.hibernate.type: trace

스프링 부트 3.x, hibernate6
org.hibernate.orm.jdbc.bind: trace

다만 하이버네이트에서 제공하는 로깅이 가시성이 아쉬운 부분이 있기 때문에 혹시 더 명확하게 보이는 것을 원한다면 외부 라이브러리를 이용하자.

spring-boot-datasource-decorator

implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:${version}")

<dependency>
    <groupId>com.github.gavlyukovskiy</groupId>
    <artifactId>p6spy-spring-boot-starter</artifactId>
    <version>${version}</version>
</dependency>

몇가지가 있는데 그 중에 p6spy를 사용하면 된다. README에 잘 나와있으므로 따로 더 설명은 안하겠다.

참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.

데이터베이스 테이블명, 컬럼명에 대한 관례는 회사마다 다르다. 보통은 스네이크 케이스에 대문자, 아니면 소문자 방식중에 하나를 지정해서 일관성 있게 사용한다. 이는 딱 지정된 것은 없고 그 프로젝트나, 회사에서 지정된 방식을 사용하면 된다.

Getter와 Setter
이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적이다. 하지만 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지는 않는다. 하지만 Setter는 문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하 도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다

값 타입 불변
값 타입은 변경 불가능하게 설계해야 한다.
@Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티티나 임베디드 타입( @Embeddable )은 자바 기본 생성자(default constructor)를 public 또는 protected 로 설정해야 한다. public 으로 두는 것 보다는 protected 로 설정하는 것이 그나마 더 안전하다.
JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다

엔티티 설계시 주의점

  1. 엔티티에는 가급적 Setter를 사용하지말자.
  2. 모든 연관관계는 지연로딩으로 설정
  3. 컬렉션은 필드에서 초기화 하자. ex)private List<Member> memberList = new ArrayList<>();

변경 감지와 병합(merge)

  • 준영속 엔티티를 수정하는 2가지 방법
    변경감지
    merge

변경감지(dirty checking)

  • 영속상태의 객체를 값만 수정하면 JPA가 알아서 커밋 타이밍에 변경 부분을 보고 업데이트 시켜주는 것.

병합(Merge)

  • 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회한다. 만약 조회시 엔티티가 있으면 merge, 없으면 save로 작동.
  • 병합을 사용할시 모든 값이 변경된다. 값이 없으면 null로 들어간다.(HTTP의 put과 같은 기능)

업데이트시에는 변경 감지를 사용합시다. 머지는 없다고 생각하세요.

API

API 스타일의 개발에서는 엔티티를 컨트롤러에 노출시키면 안된다. 거기다 validation까지 들어가게 된다면 entity가 너무 난잡해진다.

큰 문제는 Enttity를 바꾸면, API의 스펙 자체가 바뀌어 버린다. 엔티티와 API 스펙이 1:1로 매핑되어 있으면 안되고, 별도의 DTO 클래스를 만들어서 사용해라.

DTO를 받으면 핏이 맞춰저서 오기때문에 무슨 데이터만 받는지 확실하게 알 수 있다. 엔티티를 그대로 받으면 관련된 데이터가 전부 넘어오기 때문에 필요한게 무슨 데이터인지 API스펙을 까봐야 알 수 있다.

//API 스펙에 맞춰서 Entity가 아닌 DTO를 만들어서 하라. Entity를 절대 외부에 노출시키거나 파라미터로 받지마라.

//Entity 노출문제, Array가 넘어와서 확장성이 없다

@JsonIgnore => Entitiy에 Json으로 넘기기 싫은 데이터에 붙여주면 JSON data로 넘기지 않음. 문제는, 다양한 케이스에 대응하기 힘들다. 또 엔티티에 화면을 위한 기능이 자꾸 들어오고 있다. (좋지 않음) -> 결국 하자는 말은 계속해서 하는 말이지만, 엔티티를 직접 Controller계층에 노출하지 마라. DTO를 만들어서 따로 대응해줘라.

JSON을 반환할때, Array를 바로 반환하면 스펙이 굳어버려서 확장이 불가능하다. 이런 것을 해결하기위해 껍데기 클래스를 만들어 줘라.

@Data
@AllArgsConstructor
static class Result<T> {
    private T data;
	private int count; // 이렇게 count같은것을 확장 가능.
}

API의 튜닝 - (등록과 수정은 거의 문제가 발생되지 않는데, 조회에서 대부분의 문제가 나타난다고함.)

xxxToOne의 관계를 알아보자

version1

@GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); // Lazy 강제 초기화
            order.getDelivery().getAddress(); // Lazy 강제 초기화
        }
        return all;
}
  • order member 와 order address 는 지연 로딩이다. 따라서 실제 엔티티 대신에 프록시 존재
  • jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 예외 발생

다른 방법으로는 Hibernate5Module 을 스프링 빈으로 등록하면 해결(스프링 부트 사용중), 그리고 위의 코드처럼 for문을 돌려서 Lazy 강제 초기화를 시키면 된다.

스프링 부트 3.0 미만: Hibernate5Module 등록

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'


JpashopApplication 에 다음 코드를 추가하자
@Bean
Hibernate5Module hibernate5Module() {
 return new Hibernate5Module();
}

스프링 부트 3.0 이상: Hibernate5JakartaModule 등록

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'

@Bean
Hibernate5JakartaModule hibernate5Module() {
return new Hibernate5JakartaModule();
}
@Bean
Hibernate5Module hibernate5Module() {
 Hibernate5Module hibernate5Module = new Hibernate5Module();
 //강제 지연 로딩 설정
 hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING,
true);
 return hibernate5Module;
}

엔티티를 직접 노출할 때는 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리 해야 한다. 안그러면 양쪽을 서로 호출하면서 무한 루프가 걸린다.

참고: 앞에서 계속 강조했듯이 정말 간단한 애플리케이션이 아니면 엔티티를 API 응답으로 외부로 노출하는 것은 좋지 않다. 따라서 Hibernate5Module 를 사용하기 보다는 DTO로 변환해서 반환하는 것이 더 좋은 방법이다.

주의: 지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매우 어려워 진다.

항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라!

참고로 위의 version1은 저런식으로 절대 사용하지 않는다고 보면 되서, 사실 의미가 없다. 저런식으로 짜면 안된다는 것만 알아두자.

version2

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
	return orderRepository.findAllByString(new OrderSearch()).stream()
                .map(SimpleOrderDto::new)
                .collect(toList());
}

String findAllByString = "select o From Order o join o.member m";


@Data
static class SimpleOrderDto {
	private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

	public SimpleOrderDto(Order order) {
    	orderId = order.getId();
        name = order.getMember().getName(); //Lazy 초기화
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress(); //Lazy 초기화
   }
}

//ORDER 조회(2개) N+1문제 1(order조회)+2(member,delivery)+2(member,delivery),
//여기서 member, delivery를 조회해서 한번에 가지고오면 3번에 끝나지만 문제는 Id를 타서 where절로 가져와서 id가 2인 사람만가지고옴.(지연로딩이기 때문에.. 그렇다고 즉시 로딩으로 바꾸면 큰일남)
//나머지 아이디는 없음..따라서 그 아이디에 따라 계속 쿼리가 나감. ORDER에 연관된 테이블이 더 많으면 1+4, 1+5등 점점 늘어남.

즉 version2도 문제가 많음.

version3
Fetchjoin으로 문제 해결

String sql = "select o from Order o" +
			 " join fetch o.member m" +
             " join fetch o.delivery d";
             
 @GetMapping("/api/v3/simple-orders")
 public List<SimpleOrderDto> ordersV3() {
 	return orderRepository.findAllWithMemberDelivery().stream()
    		.map(SimpleOrderDto::new)
            .collect(toList());
 }
  • version2와 다른 것은 sql뿐이다. fetch join을 이용해서 쿼리 한번에 들고 와버린다.
  • 지연 로딩이 일어나지 않는다.

version4
version3에서는 Entity로 조회한후 DTO로 변환했다. 바로 DTO를 사용하는 방식이다.

    @GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
        return queryRepository.findOrderDtos();
    }
    
    
return em.createQuery("select new jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address) " +
                        "from Order o " +
                        "join o.member m " +
                        "join o.delivery d", OrderSimpleQueryDto.class)
                .getResultList();

join 부분은 version3와 쿼리가 똑같다. 하지만 select절에서 내가 원하는 것만 조회해서 가져 온다. select에서 네트워크를 조금 더 적게 사용한다.
다만 version3와 version4에는 우열이 없다. 좋은점과 안좋은점이 있고(Trade Off), 상황에 따라 선택이 일어난다.

v3는 재사용성이 높다, v4는 좀더 최적화되서 네트워크 최적화가 이루어져있다(생각보다 미비). 리포지토리 최적화때문에 재사용성이 떨어진다. 리포지토리가 API 스팩에 맞춰 들어가져 있다.

Repository는 가급적 순수하게 유지하는 것이 좋다. 따라서 v4 버전의 Repository는 아예 따로 분리해서 사용하는 편이 좋다고한다. 그 분리한 Repository에는 DTO로 바로 직접 조회하는 쿼리들을 모아두는 편.

쿼리 방식 권장순서
1. 우선 엔티티를 DTO로 변환하는 방법
2. 필요하면 페치조인으로 성능 최적회 -> 대부분 여기서 해결
3. 그래도 안되면 DTO로 직접 조회하는 방식
4. 최후의 방법으로는 JPA가 제공하는 네이티브SQL이나, JDBC Template사용(Mybatis도 상관없음, 직접 쿼리를 짜는방식을 사용한다는 뜻)

컬렉션 조회 최적화

일대다 관계의 조회를 최적화하는 법을 알아보자. 조회하는 순간 데이터가 순식간에 뻥튀기 된다.

version1

@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
	List<Order> all = orderRepository.findAllByString(new OrderSearch());
    for (Order order : all) {
    	order.getMember().getName();
        order.getDelivery().getAddress();
        order.getOrderItems()
 				.forEach(o ->o.getItem().getName());
        }
    return all;
}

위에서 봤던것 처럼 엔티티를 노출했고, Lazy Loading을 강제초기화 했고, 엔티티에 @JsonIgnore를 걸어줘야된다.

version2

@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
	List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    return orders.stream()
    	.map(OrderDto::new)
        .collect(Collectors.toList());
}


@Getter
public class OrderDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
   // private List<OrderItem> orderItems; (x)
	private List<OrderItemDto> orderItems;

    public OrderDto(Order order) {
        orderId =order.getId();
        name = order.getMember().getName();
        orderDate=order.getOrderDate();
        orderStatus=order.getStatus();
        address=order.getDelivery().getAddress();
//        order.getOrderItems().stream().forEach(o -> o.getItem().getName());
//        orderItems = order.getOrderItems();
		orderItems = order.getOrderItems().stream()
        		.map(OrderItemDto::new)
                .collect(Collectors.toList());
    }
}
  • 얼핏보면 문제없는것처럼 보인다. OrderDto로 변환해서 잘 반환해준 것 같다. 하지만...
  • DTO로 반환하라는것은 DTO내에서도 Entity에 대한 의존을 완전히 끊어야 된다.
  • 즉, OrderDto에있는 List도 OrderItem을 반환하면 안되고 DTO를 반환해야 된다는 뜻이다.
  • 기존과 같이 Lazy Loading으로 인해 가져올때 컬렉션에서는 기존보다 쿼리가 더 많이 나간다. 1+N 문제가 발생한다.

version3

xxToOne 에서 처럼 fetch join으로 해결하면 될까? -> 컬렉션에서는 조인이 되면서 중복되는 객체가 개수만큼 나올 수 있다.

order 하나에 order_item이 3개라면 , 3개를 주문했다는 것을 한번 가르쳐 주면 되는데 똑같은 데이터를 3번이나 날려준다는 것이다.

일대다에서는 1이 아니라 다를 기준으로 row수가 나온다.

"select distinct o from Order o " +
	"join fetch o.member m " +
    "join fetch o.delivery d " +
    "join fetch o.orderItems oi " +
    "join fetch oi.item i", Order.class)

이때 사용할 수 있는 것이 바로 distinct이다. JPA는 중복을 제거해주는데, Entity의 중복도 걸러주는 역할도 한다. 그럼 이렇게 사용하면 모든것이 해결됐는가?

=> 해결되지 않았다. 컬렉션의 fetch join에는 치명적인 단점이 있다. 바로 페이징이 불가능하다는 것이다. (정확히는 하이버네이트를 사용하면 메모리에서 페이징 시도, 경고로그남김)

=> 한가지 더
컬렉션 패치 조인은 하나만 사용할 수 있다. 둘 이상의 컬렉션 패치 조인을 하면 데이터가 부정합하게 조회될 수 있다.

페이징 + 컬렉션 조회를 하려면 어떻게 해야할까?

먼저 ToOne 관계는 전부 fetch join을 한다. 그리고 컬렉션은 지연 로딩으로 조회를 한다. 연관된 데이터를 조회하기위해 나가는 나머지 쿼리들을 @BatchSize를 통해 가져온다

쿼리들을 모았다가 IN 절로 날림.

이 batchsize는 각각 설정도 되지만, 글로벌 설정으로 해두면 될것. 또한 이 BatchSize는 100 ~ 1000개 사이로 정할 수 있다.

애플리케이션은 100이든, 1000이든 결국 데이터를 다 가져와야 하므로 메모리 사용량은 같다. 1000으로하면 성능상 가장 좋지만, 순간 부하 확 늘어난다. DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.

version4

DTO 직접 조회

@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4() {
	return orderQueryRepository.findOrderQueryDtos();
}


public List<OrderQueryDto> findOrderQueryDtos() {
	List<OrderQueryDto> result = findOrders();
    result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });

        return result;
    }


 private List<OrderQueryDto> findOrders() {
 		return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address) " +
                                "from Order o " +
                                "join o.member m " +
                                "join o.delivery d", OrderQueryDto.class)
                .getResultList();
    }

private List<OrderItemQueryDto> findOrderItems(Long orderId) {
		return em.createQuery( "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count) " +
                        "from OrderItem oi " +
                        "join oi.item i " +
                        "where oi.order.id = :orderId", OrderItemQueryDto.class)
                .setParameter("orderId", orderId)
                .getResultList();
    }
  • JPQL을 짜더라도 바로 Collection을 못넣는다.
  • 따라서 toOne관계를 먼저 처리하고,(OrderQueryDto), toMany 관계는 각각 별도로 처리했다.( Collection을 조회하는 쿼리를 또 따로 짠다.)
  • 그 후 order를 조회한뒤, 컬렉션은 따로 만든 쿼리문으로 다시 조회를해서 for문으로 넣어준다.

왜 이런 방법을 선택하는가?

  • toOne관계는 join해도 row수가 증가하지 않지만 toMany관계는 join을 하면 row수가 증가한다.
  • toOne은 조인으로 최적화가 쉬워서 한번에 조회하고, toMany관계는 최적화가 어려움으로 별도로 조회함.
  • 결론적으로 N+1 문제가 터졌다.. 이것을 어떻게 해결하면 좋은가?

version5
컬렉션의 DTO 직접조회 최적화

public List<OrderQueryDto> findAllByDto_optimization() {
	List<OrderQueryDto> result = findOrders();
	Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
	result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
	return result;
}


private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {

        List<OrderItemQueryDto> orderItems = em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count) " +
                                "from OrderItem oi " +
                                "join oi.item i " +
                                "where oi.order.id in :orderIds", OrderItemQueryDto.class)
                .setParameter("orderIds", orderIds)
                .getResultList();

        return orderItems.stream()
                .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
    }

private List<Long> toOrderIds(List<OrderQueryDto> result) {
	return result.stream()
    		.map(OrderQueryDto::getOrderId)
            .collect(Collectors.toList());
    }
  • toOne 관계는 기존과 같이 join문으로 쿼리를 날린다.
  • toMany는 좀 달라지는데, 바로 IN절을 이용해서 toOne에서 얻은 식별자로 필요한 orderItem을 한번에 다 가져오는 것이다.
  • 이렇게 하면 쿼리 2번으로 해결이 가능하다.
  • Map을 사용해서 성능을 조금더 최적화 했다.

정리
컬렉션 데이터 조회도 마찬가지다.

  1. 엔티티 조회후 DTO 변환
  2. fetch조인으로 쿼리수 최적화
  3. 컬렉션 최적화 - 페이징 필요(batchsize), 페이징 필요x (fetch join distinct사용)
  4. 엔티티 조회 방식으로 안되면 DTO 조회방식으로 해결
  5. DTO 조회 방식으로도 해결이 안되면 JDBC Template, Native SQL, MyBatis ...

엔티티 조인 방식은 fetch join이나 batchsize같이 코드를 거의 수정하지 않고, 옵션을 약간 변경해서 다양한 성능 최적화를 시도 할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 떄 많은 코드를 변경해야 한다.

어지간한 문제는 엔티티 조인으로 다 해결이 된다. 문제는 DTO직접 조회까지 할 경우면, 서비스가 엄청 크고 복잡하다는 뜻인데, 이걸 DTO 직접조회로 해결할 수 있는가가 의문. 차라리 이때는 캐시를 사용하거나 하는게 더 좋다.

만약 캐시를 사용한다면? 엔티티를 직접 캐시에 올리면 절대 안된다. DTO를 캐시에 올려야 한다. 영속성 컨텍스트에서 관리되는 엔티티를 직접 캐시에 올리면 문제가 발생할 수 있다.

OSIV

Open Session In View : 하이버네이트
Open EntityManager In View : JPA
(관례상 OSIV라고 많이 한다.)

스프링은 그냥 open in view라고 함
Spring.jpa.open-in-view: true (기본값)

JPA에서는 서비스 계층에서 Transaction이 시작되면 데이터베이스 커넥션을 가지고온다보면 된다.

그럼 언제 DB에 커넥션을 돌려줄까?

OSIV ON

  • 이것은 OSIV가 켜져있으면 트렌젝션이 다 끝나도 반환을 하지 않는다. API의 경우에는 API 응답이 끝날 때 까지 , 화면인 경우에는 View Template 응답이 다 나갈때까지 영속성 컨텍스트와 데이터 베이스 커넥션을 끝까지 유지한다.

  • 그래서 지금까지 View Template이나 API 컨트롤러에서 지연 로딩이 가능했던 것이다. 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다. 이것 자체가 큰 장점이다.

문제는 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고, 유지해야 한다.

OSIV OFF

  • OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.

문제는 OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다. 결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다

그렇다면 어떤것이 좋고 어떻게 해야 하는가?

꺼야 하는가 켜야하는가? - 성능을 위해선 꺼야되지만, 또 켜져있으면 많은 이점을 제공한다.

고객 서비스 같은 실시간 API는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 켠다.

이런 문제를 해결하기위한 몇가지 방법이 있다.

커멘드와 쿼리 분리

실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command와 Query를 분리하는 것이다.

보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다. 하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다.

그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미 있다.

단순하게 설명해서 다음처럼 분리하는 것이다.

OrderService: 핵심 비즈니스 로직
OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)

보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다

profile
공부 정리 블로그

0개의 댓글