티스토리 뷰

이전 글: https://gojs.tistory.com/56

 

Spring Data JPA 알아보기

JPA를 사용해서 개발을 진행하는 경우에도 중복적으로 코드를 입력하게된다.Spring Data JPA는 이에 대해 보다 간편하게 개발할 수 있도록 지원하는 프로젝트이다.  org.springframework.data spring-data-jpa 3.

gojs.tistory.com

 

트랜잭션 범위의 영속성 컨텍스트

스프링 컨테이너는 기본적으로 트랜잭션 범위의 영속성 컨텍스트 전략을 가진다.

 

영속성 컨텍스트 범위

트랜잭션이 시작되면 영속성 컨텍스트가 생성되고, 트랜잭션이 끝나면 영속성 컨텍스트가 종료된다.

또한 하나의 트랙잭션에서는 하나의 영속성 컨텍스트에 접근한다.

 

 

@RestController
public class HelloController {

    private final HelloService helloService;
    
    public void hello() {
        Member member = helloService.do();
    }
}
@Service
public class HelloService {

    private final HelloRepository helloRepository;
    
    @Transactional
    public void do() {
        helloRepository.doSomething();
        
        Member member = helloRepository.findMember();
        return member;
    }
}
@Repository
public class HelloRepository {

    @PersistenceContext
    EntityManager em;
    
    public void doSomething() {
        ...
    }
    
    public Member findMember() {
        return em.find(Member.class, "id");
    }
}

위 예제를 보면 HelloService의 do() 메서드를 보면 트랜잭션 처리가 되어있다.

따라서 이 트랜잭션 내부에서 영속성 컨텍스트를 이용하여 데이터에 관련된 여러가지 동작들을 수행한다.

 

같은 영속성 컨텍스트 사용

한 트랜잭션 내부에서 여러 객체를 참조하여 여러 개의 EntityManager를 사용하게 되더라도 하나의 영속성 컨텍스트만 사용한다.

 

다른 영속성 컨텍스트 사용

반대로 두 개의 요청이 같은 EntityManager를 사용하더라도 각 트랜잭션의 영속성 컨텍스트를 사용한다.

 

준영속 상태와 지연 로딩

@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;
    
    ...
}
public class OrderController {

    public String view(Long orderId) {
        Order order = orderService.findOne(orderId);
        
        Member member = order.getMember();
        member.getName();
        
        ...
    }
}

위 예제와 같이 트랜잭션이 없는 표현 계층에서의 엔티티는 준영속 상태이다.

따라서 Order 엔티티에 지연 로딩으로 설정된 Member와의 다대일 관계는 정상적으로 동작하지 않는다.

 

변경 감지도 정상 동작하지 않는데, 이는 비즈니스 로직이 표현 계층에서 동작하지 않는 것이기 때문에 문제는 아니다.

그러나 뷰를 렌더링할 때 연관된 엔티티도 함께 사용하지 못하게 되는 점이 문제가 될 수 있다.

그렇다면 엔티티의 영속상태가 유지되지 않는 이슈를 해결하기 위한 방법들을 아래에서 알아보자.

 

필요한 엔티티를 미리 로딩하는 방법

글로벌 페치 전략 수정

@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.EAGER)
    private Member member;

    ...
}
Order order = orderService.findOne(orderId);
Member member = order.getMember();

// 이미 로딩된 엔티티
member.getName();

위와 같이 페치 전략을 즉시 전략으로 수정함으로써 필요한 엔티티를 미리 로딩할 수 있다.

그러나 아래와 같은 문제가 발생할 수 있다.

  • 사용하지 않는 엔티티 로딩
    • 연관 엔티티가 필요하지 않는 로직이 많은 경우에는 불필요한 로딩이 발생하는 비용을 감수해야한다.
  • N+1 문제
    • JPQL을 사용하여 즉시 로딩을 설정한 엔티티를 조회하게 되면 N+1 문제가 발생할 수 있다.
    • JPQL로 orderList를 조회한 경우, order의 연관 엔티티인 member를 조회하는 쿼리가 N번 발생한다.
    • JPQL 페치 조인으로 해결할 수는 있다.

 

JPQL 페치 조인

글로벌 페치 전략을 수정하게되면 많은 비효율이 발생하지만 JPQL 페치 조인을 사용함으로써 이러한 이슈를 회피할 수 있다.

// JPQL 페치 조인 사용 전
select o from Order o

// 발생 SQL: select * from Order
// + N번의 member 조회 쿼리
// JPQL 페치 조인 사용 후
select o from Order o join fetch o.member

// 발생 SQL: select o.*, m.* from Order o join Member m on o.member_id = m.id

JPQL 페치 조인은 아주 현실적인 대안이긴하지만 이와 같은 여러 화면 요구사항에 의해 생성되는 Repository Layer의 메서드가 많아질 수 있다.

표현 계층이 데이터 계층을 침범하고 관리가 어렵다는 단점도 존재하는 방법이다. 따라서 글로벌 페치 전략을 수정하는 방법과 트레이드 오프로 방식을 결정하면 될 것 같다.

 

강제 초기화

표현 계층에 결과를 반환하기 전 서비스 계층에서 필요한 엔티티를 강제로 초기화하는 방법이 있다.

public class OrderService {

    @Transactional
    public Order findOrder(Long id) {
        Order order = orderRepository.findOrder(id);
        
        // 프록시 객체 강제 초기화
        order.getMember().getName();
        
        return order;
    }
}
// 강제 초기화 메서드
org.hibernate.Hibernate.initialize(order.getMember());

이 방법을 사용하면 표현 계층에서 필요한 역할을 서비스 계층에서 담당한다는 단점이 존재한다.

이에 대한 보완책으로 파사드 계층을 분리하는 방법을 이용할 수도 있다.

 

파사드 계층 추가

파사드 계층 추가

위와 같이 표현 계층과 서비스 계층 사이에 파사드 계층을 두어서 뷰를 위한 초기화 역할을 담당하게 할 수 있다.

이렇게 구성하게 되면 표현 계층에서 필요한 프록시 객체를 초기화하는 역할을 수행하여 표현 계층과 서비스 계층의 역할을 명확하게 분리할 수 있다.

public class OrderFacade {

    private final OrderService orderService;
    
    public Order findOrder(Long id) {
        Order order = orderService.findOrder(id);
        order.getMember().getName();
        return order;
    }
}

중간 계층이 하나 더 생기기 때문에 관리할 코드가 많아진다는 단점이 있지만, 개인적으로는 조직 내에서 룰을 잘 정하기만 한다면 아주 좋은 방법인 것 같다.

 

OSIV를 사용하는 방법

엔티티의 영속상태가 유지되지 않는 이슈를 해결하기 위한 두 번째 방법은 OSIV를 사용하는 것이다.

 

요청당 트랜잭션 OSIV

 

과거 OSIV

OSIV는 초기에 요청당 트랜잭션 전략으로 구현되었다. 따라서 Filter나 Interceptor에서 트랜잭션이 시작하고 끝나는 방법을 사용했다.

 

public class MemberController {

    private final MemberService memberService;
    
    public String viewMember(Long id) {
        Member member = memberService.find(id);
        // 보안 이슈에 의한 회원 이름 마스킹 처리
        member.setName("***");
        
        model.addAttribute("member", member);
        ...
        
    }
}

그러나 뷰에서 엔티티의 데이터를 수정하는 경우 실제 데이터가 수정된다는 큰 단점이 존재한다.

위의 코드를 살펴보면 뷰에서는 보안상의 이유로 회원 이름을 마스킹 처리하지만, 실제 데이터가 바뀌어버린다.

 

public interface MemberView {
    String getName();
}
@Entity
public class Member implements MemberView {
    ...
}
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberView getMember(Long id) {
        return memberRepository.findById(id);
    }
}

위 코드와 같이 엔티티를 표현 계층에 제공할 때 읽기 전용 인터페이스로 제공하는 방법을 사용하면 표현 계층에서 데이터를 수정하는 것을 막을 수 있다.

 

public class MemberWrapper {
    private Member member;
    
    public MemberWrapper(Member member) {
        this.member = member;
    }
    
    public String getName() {
        return this.member.getName();
    }
}

또는 위 코드와 같이 엔티티 클래스를 래핑한 클래스를 구현하여 표현 계층에 제공하는 방법을 사용할 수도 있다.

 

public class MemberDto {
    private String name;
    
    // getter, setter
    ...
}

가장 전통적으로 사용하는 방법 중 하나로 DTO를 구현해서 제공하는 방법이 있다. 그러나 OSIV를 사용하는 장점을 살리기는 어렵다.

 

이와 같은 다양한 방법들을 활용하여 요청당 트랜잭션 OSIV를 보완할 수 있지만 여전히 단점이 남아있는 방법들이다.

이를 보완하기 위해 스프링 프레임워크에서 제공하는 OSIV 방식이 있다.

 

비즈니스 계층 트랜잭션 OSIV

스프링 OSIV

스프링 OSIV는 비즈니스 계층에서 트랜잭션 처리하여 표현 계층에서 데이터를 변경할 수 있다는 문제를 어느정도 해결했다.

스프링 OSIV의 동작 흐름은 아래와 같다.

  1. 클라이언트 요청이 들어오면 Filter나 Interceptor에서 영속성 컨텍스트 생성
  2. 서비스 계층에서 트랜잭션 생성하고 미리 생성되어있던 영속성 컨텍스트 사용
  3. 서비스 계층이 끝날 때 트랜잭션 커밋 수행하고 영속성 컨텍스트 플러시 수행
  4. 표현 계층까지 영속성 컨텍스트가 유지되어 엔티티는 영속 상태를 유지
  5. Filter나 Interceptor를 통해 나가는 응답에 대해 영속성 컨텍스트 종료 (플러시하지 않음)

 

 

스프링 OSIV에서 영속성 컨텍스트를 통한 변경은 트랜잭션 안에서 이루어진다.

만약 트랜잭션 밖에서 엔티티를 변경하고 플러시하면 TransactionRequiredException 예외가 발생한다. 하지만 조회는 가능하다.

따라서 스프링 OSIV에서 표현 계층은 엔티티를 수정할 수는 없으나 조회는 할 수 있는 계층이다.

 

public class MemberController {

    private final MemberService memberService;
    
    public String viewMember(Long id) {
        Member member = memberService.find(id);
        // 보안 이슈에 의한 회원 이름 마스킹 처리
        member.setName("***");
        
        model.addAttribute("member", member);
        ...
        
    }
}

위 코드에서 member.setName(...)가 수행될 때 변경 감지가 동작하여 영속성 컨텍스트 내부의 회원 이름을 ***로 변경할 것이다.

그러나 트랜잭션은 이미 커밋된 상태로 영속성 컨텍스트가 플러시 없이 종료되어 버린다.

만약 em.flush()를 강제로 호출하더라도 TransactionRequiredException 예외를 만나게 될 것이다.

 

 

public class MemberController {

    private final MemberService memberService;
    
    public String viewMember(Long id) {
        Member member = memberService.getMember(id);
        member.setName("***");
        
        memberSerivce.biz();
        
        return "view";
    }
}
public class MemberSerivce {

    @Transactional
    public void biz() {
        ...
    }
}

위의 코드와 같이 표현 계층에서 엔티티를 수정하고 나서 서비스 계층을 호출하면 새로운 트랜잭션이 생성된다.

그럼 새로운 트랜잭션이 끝날 때 다시 한 번 플러시가 발생하여 표현 계층에서 수정한 내용이 실제 데이터에 반영될 수 있다.

 

스프링 OSIV 주의사항

위 그림을 참고하여 해당 이슈에 대해 주의하자.

 

다음 글: https://gojs.tistory.com/58

 

컬렉션과 부가 기능

JPA는 Java에서 기본으로 제공하는 Collection, Set, List, Map 등을 지원한다. @Entitypublic class Team { @Id private String id; @OneToMany @JoinColumn private Collection members = new ArrayList(); ...}System.out.println("before persist: " +

gojs.tistory.com

'공부 > JPA' 카테고리의 다른 글

JPA 표준 예외 처리 알아보기  (0) 2024.10.06
JPA에서 컬렉션과 여러 기능 활용하기  (0) 2024.10.03
Spring Data JPA 활용하기  (0) 2024.09.22
객체지향 쿼리 언어 알아보기  (0) 2024.08.26
JPA 값 타입 구현하기  (0) 2024.07.27
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
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
글 보관함