[자바 ORM 표준 JPA 프로그래밍] 13주차 스터디

박서영·2026년 8월 23일

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

  • J2SE 환경에서 JPA 사용 시 개발자가 직접 엔티티 매니저를 생성하고 트랜잭션도 관리해야함.
  • 스플이 또는 J2SE 컨테이너 환경 → JPA 사용 시, 컨테이너가 제공하는 전략 따라야함.

(1) 스프링 컨테이너의 기본 전략

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

트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 동일함.

즉, 트랜잭션 시작 시, 영속성 컨텍스트를 생성하고 트랜잭션 종료 시 영속성 컨텍스트를 종료함. 또한, 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근함.

  • 스프링 프레임워크의 경우 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 어노테이션을 선언해 트랜잭션을 시작
    • 메소드 실행 직전 스프링의 트랜잭션 AOP가 먼저 동작함
    • 대상 메소드 정상 종료 시에는 트랜잭션을 커밋하면서 종료
  • 트랜잭션 커밋 시:
    • JPA는 영속성 컨텍스트를 플러시해 변경 내용을 데이터베이스에 반영한 후, 트랜잭션을 커밋.
    • 즉, 영속성 컨텍스트의 변경 내용이 정상 반영됨
  • 예외 발생 시:
    • 트랜잭션을 롤백하고 종료. 이때는 플러시를 호출하지 않음

예제 분석

@Controller
class HelloController {
	@Autowired HelloService helloService;
	
	public void hello() {
		Member member = helloService.logic();
	}
}

@Service
class HelloService {
	
	@PersistenceContext //엔티티 매니저 주입
	EntityManager em;
	
	@Autowired Repository1 repository1;
	@Autowired Repository2 repository2;
	
	//트랜잭션 시작
	**@Transactional**
	public void logic() {
		repository.hello();
		
		//member는 영속 상태
		Member member = repository2.findMember();
		return member;
	} // 트랜잭션 종료
}

@Repository
class Repository1 {
	
	@PersistenceContext
	EntityManager em;
	
	public void hello() {
		em.xxx(); //영속성 컨텍스트 접근
	}
}

@Repository
class Repository2 {
	
	@PersistenceContext
	EntityManager em;
	
	public Member findMember() {
		return em.find(Member.class, "id1"); //영속성 컨텍스트 접근
	}
}
  • @PersistenceContext 어노테이션: 스프링 컨테이너가 엔티티 매니저를 주입

HelloService.logic()

  • @Transactional을 선언해 메소드 호출 시, 트랜잭션을 우선 시작
  • repository2.findMember()를 통해 조회한 member 엔티티는 트랜잭션의 범위 내에 존재, 따라서 영속성 컨텍스트의 관리를 받음 → 즉, 현재 영속 상태.
  • @Transactional 선언 메소드 정상 종료 시, 트랜잭션을 커밋. 이때 영속성 컨텍스트 역시 종료. 따라서 조회한 엔티티는 준영속 상태가 됨.

트랜잭션이 같으면 같은 영속성 컨텍스트 사용

  • 다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용함
  • 위 코드의 두 번의 영속성 컨텍스트 접근 모두 동일한 영속성 컨텍스트를 사용함.

트랜잭션이 다르면 다른 영속성 컨텍스트를 사용

  • 여러 스레드에서 동시에 요청이 와서 같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름
  • 즉, 스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당해 같은 엔티티 매니절ㄹ 호출해도 접근하는 영속성 컨텍스트가 달라, 멀티 스레드 상황에서도 안전함.

13.2 준영속 상태와 지연 로딩

  • 조회한 엔티티가 서비스와 레포지토리 계층에서는 영속성 컨텍스트에 관리되며 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 됨.
@Entity
public class Order {
	@Id @GeneratedValue
	private Long id;
	
	@ManyToOne (fetch = FetchType.LAZY) //지연로딩 전략
	private Member member;
}
  • 프레젠테이션 계층에서 엔티티가 준영속 상태 ⇒ 변경 감지와 지연로딩이 동작하지 않음.
  • 즉, 지연 로딩 시점에서 예외 발생 가능

준영속 상태와 변경 감지

  • 변경감지기능의 경우 데터를 주로 보여주는 역할인 프레젠테이션 계층에서는 거의 발생하지 않음.
  • 책임의 모호성이 발생하지 않게하기 위해서라도, 프레젠테이션 계층에서 데이터를 변경하지 안않고, 서비스 계층에서 비즈니스 로직을 처리하는 것이 좋음

준영속 상태와 지연 로딩

  • 뷰를 렌더링 할 때, 연관된 엔티티 역시 함께 사용해야하는데 이때, 연관 엔티티를 지연로딩으로 설정해 프록시 객체로 조회했을 때 ⇒ 아직 초기화하지 않은 프록시 객체 사용 시, 실제 데이터를 불러오기 위해 초기화를 시도
    • 하지만 준영속 상태는 영속성 컨텍스트가 없기에 지연 로딩이 불가 → 예외가 발생함
  • 해결 방법:
    • 뷰가 필요한 엔티티를 미리 로딩하기
    • OSIV를 사용해 엔티티를 항상 영속 상태로 유지하기

해결 방법1: 뷰가 필요한 엔티티를 미리 로딩

: 엔티티가 준영속 상태여도, 미리 로딩하거나 초기화해 반환하기에 지연 로딩이 발생하지 않음.

어디에서 미리 로딩하는지에 따라 3가지 방법이 존재

  • 글로벌 페치 전략 수정
  • JPQL 페치 조인
  • 강제 초기화

(1) 글로벌 페치 전략 수정

글로벌 페치 전략을 아래처러 지연 로딩에서 즉시로딩으로 변경

@Entity
public class Order {
	
	@Id @GeneratedValue
	private Long id;
	
	@ManyToOne(fetch = FetchType.EAGER) //즉시 로딩 전략
	private Member member;
}
  • fetch 타입을 변경하면 애플리케이션 전체에서 해당 엔티티를 로딩할 때마다 해당 전략을 사용 ⇒ 그렇기에 글로벌 페치 전략이라고 부름
  • 단점:
    • 사용하지 않는 엔티티를 로딩해야함
    • N+1 문제가 발생함
      • N+1 문제: 처음 조회한 데이터 수만큼 다시 SQL을 사용해 조회하는 문제 → 조회 성능에 치명적이기에 최우선적인 최적화 대상

        ⇒ JPQL 페치 조인으로 해결 가능

(2) JPQL 페치 조인

  • JPQL 호출 시점에 함께 로딩할 엔티티를 선택하는 방식
  • 글로벌 페치 전략을 즉시 로딩으로 설정할 시 애플리케이션 전체에 영향을 줘 비효율적.

페치 조인 사용 전

JPQL: select o from Order o
SQL: select * from Order

페치 조인 사용 후

JPQL:
	select o
	from Order o
	**join fetch** o.member
  • 페치 조인 사용 시, SQL JOIN을 사용해 페치 조인 대상까지 함께 조회 → N+1 문제가 발생하지 않음. (연관 엔티티를 이미 로딩했기에 글로벌 페치 전략은 무의미.)
  • 단점
    • 무분별하게 사용 시, 화면에 맞춘 레포지토리 메소드가 증가 → 프레젠테이션 계층이 데이터 접근 계층을 침법하는 것

(3) 강제 초기화

  • 영속성 컨텍스트가 살아있을 때, 프레젠테이션 계층이 필요한 엔티티를 강제 초기화해 반환하는 방법
class OrderService {
	
	@Transactional
	public Order findOrder (Long id) {
		Order order = orderRepository.findOrder(id);
		**order.getMember().getName(); //프록시 객체를 강제로 초기화함**
		
		return order;
	}
}
  • 프레젠테이션 계층에서 필요한 프록시 객체를 영속성 컨텍스트가 살아 있을 때, 강제로 초기화해서 반환하면 이미 초기화했기에 준영속 상태에서도 사용 가능
  • 하이버네이트 사용 시, initialize() 메소드를 사용해 프록시를 강제로 초기화 가능
    • JPA 표준에는 프록시 초기화 메소드가 없으며, 초기화 여부만 확인 가능

(4) FACADE 계층 추가

  • 프레젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법
  • 뷰를 위한 프록시 초기화는 서비스 계층이 아니라 FACADE 계층에서 담당

FACADE 계층의 역할과 특징

  • 프레젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리

  • 프레젠테이션 계층에서 필요한 프록시 객체를 초기화

  • 서비스 계층을 호출해 비즈니스 로직을 실행

  • 레포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾음

  • 예제코드

  • class OrderFacade {
    	
    	@Autowired OrderService orderService;
    	
    	public Order findOrder(Long id) {
    		Order order = orderService.findOrder(id);
    		order.getMember().getName();
    		
    		return order;
    	}
    }
    
    class OrderService {
    	
    	public Order findOrder(id) {
    		return orderRepository.findOrder(id);
    	}
    }
    • 이전에 OrderService에 있던 프록시 초기화 코드가 OrderFacade로 이동함

    준영속 상태와 지연 로딩 문제점

    • 뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류 발생 가능성이 높음
      • 이유: 뷰 개발 시에는 엔티티 클래스를 보고 개발을 하는 것이지, 초기화 여부 확인을 위해 FACADE나 서비스 클래스까지 열어보는 것이 번거롭고 놓치기 쉬워서
    • 이런 단점을 해결하는 방법이 OSIV.

    13.3 OSIV

    OSIV: 영속성 컨텍스트를 뷰까지 열어둔다는 의미.

    • 영속성 컨텍스트가 살아있으면 엔티티가 영속 상태로 유지되기 때문에, 뷰에서 지연로딩을 사용할 수 있음

    (1) 과거 OSIV: 요청 당 트랜잭션

    • 단순한 구현방법: 클라이언트의 요청이 들어오자마자, 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션 역시 종료하는 것 ⇒ 이것을 요청 당 트랜잭션 방식의 OSIV라함.
      • 영속성 컨텍스트가 처음부터 끝까지 살아있기에 조회한 엔티티도 영속 상태를 유지함
      • 뷰에서도 지연로딩이 가능하기에 엔티티를 미리 초기화할 필요가 없음

    문제점

    • 컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있음 예) 보안상의 이유로 고객의 이름을 XXX로 변경해 뷰에 전달 → 뷰 렌더링 이후, 트랜잭션을 커밋해 고객의 이름이 변경된 고객의 이름 XXX로 변경되는 문제가 발생함.
    • 프레젠테이션 계층에서 데이터 수정을 막는 방법:
      • 엔티티를 읽기 전용 인터페이스로 제공:
        • 엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프레젠테이션 계층에 제공
      • 엔티티 래핑
        • 엔티티의 읽기 전용 메소드만 가진 엔티티를 감싼 객체를 만들고 이것을 프레젠테이션 계층에 반환하는 방법
      • DTO만 반환
        • 가장 전통적인 방법으로 프레젠테이션 계층에 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 것
        • 단점: OSIV를 사용하는 장점을 살릴 수 없고, 엔티티를 거의 복사하는 듯한 DTO 클래스를 만들어야함

    (2) 스프링 OSIV: 비즈니스 계층 트랜잭션

    스프링 프레임워크가 제공하는 OSIV 라이브러리

    OSIV를 서블릿 필터에서 적용할지, 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택해 사용하면 됨.

    • 하이버네이트 OSIV 서블릿 필터
    • 하이버네이트 OSIV 스프링 인터셉터
    • JPA OEIV 서블릿 필터
    • JPA OEIV 스프링 인터셉터

    스프링 OSIV 분석

    • 스프링 프레임워크의 OSIV = 비즈니스 계층에서 트랜잭션을 사용하는 OSIV. ⇒ 즉, OSIV는 사용하지만, 트랜잭션은 비즈니스 계층에서만 사용함

    동작 원리

    • 클라이언트 요청이 들어오면 영속성 컨텍스트를 생성 (이때 트랜잭션은 시작X)
    • 서비스 계층에서 트랜잭션 시작 → 생성해둔 영속성 컨텍스트에 트랜잭션을 시작
    • 비즈니스 로직을 실행하고 서비스 계층 종료 후, 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시
      • 이때 트랜잭션만 종료하고, 영속성 컨텍스트는 살려둠
    • 클라이언트의 요청이 끝날 때, 영속성 컨텍스트를 종료

    트랜잭션 없이 읽기

    • 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야함
      • 만약, 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 예외 발생
    • 트랜잭션 없이 읽기: 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이를 이르는 말.
    • 프레젠테이션 계층에서는 트랜잭션이 없기에, 엔티티를 수정할 수 없음. → 이전의 단점 보완.
    • 트랜잭션 없이 읽기를 사용해 프레젠테이션 계층에서 지연된 로딩 사용 가능

    스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV 정리

    • 영속성 컨텍스트를 프레젠테이션 계층까지 유지
    • 프레젠테이션 계층에는 트랜잭션이 없으므로 엔티티 수정 불가
    • 프레젠테이션 계층에는 트랜잭션이 없지만, 트랜잭션 없이 읽기를 사용해 지연 로딩 가능

    스프링 OSIV 주의사항

    • 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층 호출 시 문제가 발생

    13.4 너무 엄격한 계층

    아래 예제는 상품 구매 후 구매 결과 엔티티를 조회하기 위해 컨트롤러에서 레포지토리를 직접 접근

    class OrderController {
    	@Autowired OrderService orderService;
    	@Autowired OrderRepository orderRepository;
    	
    	public String orderRequest (Order order, Model model) {
    		long Id = orderService.order(order); //상품 구매
    		
    		//레포지토리 직접 접근
    		Order orderResult = orderRepository.findOne(id);
    		model.addAttribute("order", orderResult);
    		...
    	}
    }
    
    @Transactional
    class OrderService {
    	@Autowired OrderRepository orderRepository;
    	
    	public Long order(order) {
    		//...비즈니스 로직
    		return orderRepository.save(order);
    	}
    }
    
    class OrderRepository {
    	@PersistenceContext EntityManager em;
    	
    	public Order findOne(Long id) {
    		return em.find(Order.class, id);
    	}
    }
    • OSIV 사용 이전: 프레젠테이션 계층에서 사용할 지연 로딩 엔티티를 미리 초기화 해야했음
      • 초기화: 서비스 계층/FACADE 계층이 담당 (영속성 컨텍스트가 살아있는 계층)

    ⇒ OSIV 사용: 영속성 컨텍스트가 프레젠테이션 계층까지 살아있기에 미리 초기화할 필요X

    • 단순한 엔티티 조회의 경우, 컨트롤러에서 레포지토리 직접 호출해도 문제 없음
    • OSIV를 사용하면 과거 EJB 시절 DTO를 만들어 반환하고, 엔티티가 계층을 뛰어넘기 힘들다는 불편함을 줄이고 더 유연하고 실용적인 관점으로 접근할 수 있음.
    profile
    이불 밖은 위험해.

    0개의 댓글