@Transaction
a()
이런식으로 밖에서 a()를 호출하면 a()를 감싸는 a'()가 실행된다.
즉, 프록시이다. a'()의 트랜잭션이 실행되는것이다. 즉, 호출한 메소드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.
@Service
class HelloService{
@PersistenceContext //엔티티 매니저 주입
EntityManager em;
@Autowired Repository1 repository1;
@Autowired Repository2 repository2;
//트랜잭션 시작
@Transactional
public void logic();
repository1.hello();
//members는 영속상태이다.
Member member = repository2.findMember();
return member;
//트랜잭션 종료
}
@Repository
class Repository1{
@PersistenceContext
EntityManager em;
public void hello(){
em.xxx(); //영속성 컨텍스트 접근
}
}
@Entity
public class Order{
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) //지연 로딩 전략
private Member member;
컨테이너 환경의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프레젠테이션 계층(=> 여기서는 컨트롤러 단을 말함)에서 엔티티는 준영속상태이다. 따라서 변경감지와 지연로딩이 동작하지 않는다.
아래 코드는 컨트롤러에 있는 로직인데 지연 로딩 시점에 예외가 발생한다.
class OrderController{
public String view(Long orderId){
Order order = orderService.findOne(orderId);//트랜잭션
Member member = order.getMember();
//다른 트랜잭션, 트랜잭션 아님 -> 여기서 영속성 컨텍스트 끝나있음 , 하지만 여기서 예외는 안터짐, 왜냐면 프록시라서
member.getName(); // 여기서부터
//예외가 터짐, 위에 order.getMember()할때는
//가짜클래스를 돌려줘서 저때는 예외가 안터지나,
//member.getName()할때는 영속성 컨텍스트가 끝나있으니,
//예외가 터짐!!
준영속 상태의 지연로딩 문제를 해결하는 방법 2가지
1. 뷰가 필요한 엔티티를 미리 로딩해두는 방법
2. OSIV를 사용해서 엔티티를 항상 영속상태로 유지하는 방법
-> 엔티티클래스에서 FetchType을 Eager로 바꾸기
-> 프록시로 대체하지 않고 바로 리턴함 -> 문제점!! -> N+ 1 문제가 있음 -> 쿼리가 하나 더 나감 . 하나의 쿼리를 썼는데 N개가 더 나감.
JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용한다. 따라서 즉시 로딩이든 지연로딩이든 구분하지 않고 JPQL쿼리 자체에 충실하게 SQL을 만든다.
1. select o from Oreder o JPQL을 분석해서 select from Order SQL을 생성한다.
2. 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
3. Order.member의 글로벌 페치 전략이 즉시로딩(eager)이므로 order를 로딩하는 즉시 연관된 member도 로딩해야한다. 즉, order별로 쿼리가 나감
4. 연관된 member를 영속성 컨텍스트에서 찾는다.
5. 만약 영속성 컨텍스트엥 없으면 SELECT FROM MEMBER WHERE id=? SQL을 조회한 order 엔티티 수만큼 실행된다.
위의 N+1 문제를 해결하기 위해 JPQL 페치 조인을 사용한다
select from Order o inner join Member m 을 select from Order o inner join fetch Member m로 바꾼다.
페치 조인을 사용하면 SQL JOIN을 사용해서 페치조인대상까지 함께 조회한다. 따라서 N+1문제가 발생하지 않는다. (연관된 엔티티를 이미 로딩했으므로 글로벌 페치 전략은 무의미하다.)
OSIV(open session in view)는 영속성컨텍스트를 뷰까지 열어둔다는 뜻이다. 즉. 컨트롤러까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연로딩을 사용할 수 있다.
아래코드 :
컨트롤러에서 멤버이름을 변경해서 렌더링해서 뷰에 넘겨주었다. 개발자의 의도는 단순히 뷰에 노출할때만 고객의 이름을 XXX로 변경하고 싶은 것이지 실제 데이터베이스에 있는 고객 이름까지 변경하고 싶은 것이아니다.
class MemberControlle{
public String viewMember(Long id){
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상의 이유로 이렇게 바꿔줌
model.addAttribute("member", member);
문제있는 코드이다.
요청당 트랜잭션 방식의 OSIV는 뷰를 렌더링한 후에 트랜잭션을 커밋한다. 트랜잭션을 커밋하면 당연히 영속성 컨텍스트에 플러시하니까 영속성 컨텍스트의 변경 감지 기능이 작동해서 변경된 엔티티를 데이터베이스에 반영해버린다. 결국 데이터베이스에 멤버의 이름이 XXX로 바뀌게 되는 문제가 발생한다.
해결방법 -> 비지스 로직을 먼저 수행하게하기