[JPA] 자바 ORM 표준 JPA 프로그래밍(기본편) 5 - 프록시와 연관관계 관리

유승선 ·2022년 11월 14일
0
post-thumbnail

프록시

JPA 를 사용하면서 프록시는 굉장히 중요한 역활을 하고 있다. 객체적으로 생각한다면은 멤버와 팀이 있을때 서로가 참조 값을 가지고 있는것은 특별하게 느껴지지 않지만 데이터베이스 테이블 관점으로 볼때는 얘기가 다르다.

멤버가 팀의 참조값을 가지고 있다면 호출 시점에서 데이터베이스 관점으로 어쩔수 없이 팀의 테이블도 같이 읽어야한다. 그리고 PK와 FK로 연결이 되어있는 멤버와 팀 관계에서 두 테이블을 함께 읽기 위해서는 조인 쿼리를 사용하면서 팀을 안본다 하더라도 객체적으로 참조값이 있기때문에 불필요한 SQL 쿼리가 날라간다.

멤버를 조회할때 팀도 함께 조회해야 할까?

프록시 기초

먼저 기초를 집고 넘어가겠다. em.find()를 하면서 클래스 정보와 PK를 입력하게 되면은 객체를 찾을 수 있다는건 여러번 사용해봤다. 그런데 find() 메서드를 쓰게 되면 우리는 실제 엔티티 객체를 조회하지만, em.getReference() 를 사용하게 되면 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다

강의에서 말하기를 .getReference() 는 실제로 사용되는 메서드는 아니지만 프록시의 기본을 알려주기 위해 사용된 메서드다.

프록시 특징

프록시는 원래 중요하게 생각안한 개념이었지만 실제로 스프링에서 사용되는 여러가지 다른 기능에 프록시가 언급되고 상당히 중요한 개념이란걸 알게되어 깊게 다루고 싶었다.

프록시는 가짜 엔티티라고 생각하면 된다. 실제 클래스를 상속 받았기 때문에 메서드를 포함해서 변수도 전부 같은 또 다른 객체이다.

그러나 다른점은 프록시 객체는 실제 객체의 참조(target) 을 보관하고 프록시 객체를 호출하면 사실 실제 객체의 메서드를 호출한다.

프록시 객체의 초기화

JPA 에서 프록시로 조회하게 되면은 나타나는 단계이다.

  1. 프록시로 객체 생성, getName() 메서드 호출
  2. 프록시 객체를 호출하면 사실 실제 객체의 메서드를 호출해야 하기때문에 먼저 영속성 컨텍스트에 접근
  3. 영속성 컨텍스트에 존재하지 않음으로 DB 조회 후 영속성 컨텍스트 1차 캐시에 등록
  4. 실제 Entity 생성
  5. 실제 객체가 생성되었기 때문에 프록시 객체에서 부른 getName() 메서드는 실제 객체의 getname()이 같이 호출되어서 이름 반환

프록시의 특징을 요약했다. 이 중에서 가장 중요하게 넘겨야 할 부분만 추가 설명하겠다.

  1. 프록시 객체는 처음 사용할때 한번만 초기화 -> 초기화가 된 후에 프록시는 실제 객체의 참조값을 가지고 있기 때문에 같은 메서드를 부르게 되더라도 또 영속성 컨텍스트에 접근하는게 아니고 실제 객체의 메서드를 부른다.

  2. 프록시 객체를 초기화 할때, 프록시 객체가 실제 엔티티로 바뀌는건 아님, 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능 -> 프록시가 엔티티로 바뀌는게 아니고 실제 엔티티를 향한 참조값을 가지고 있다고 생각해야한다.

  3. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화 하면 문제 발생 -> 이 부분은 아직 와닿는 특징은 아니다. 그런데 실제로 운영하게 되면은 트랜잭션과 영속성 컨텍스트를 같이 닫는 일이 많다 하다 (EntityManager.close()) 이때 프록시를 통해서 위와 같은 초기화 작업을 하게 되면 LazyInitialization Exception 이라는 자주 보는 예외를 터트리기 때문에 조심하라고 강조 했다)


즉시 로딩과 지연 로딩

프록시와 관련된 기초는 알았고 다시 주제로 돌아와서,

멤버를 조회할때 팀도 함께 조회해야 할까?

지연로딩

지연 로딩 전략을 사용해서 프록시로 조회할 수 있다.

연관관계 설정 어노테이션 안에 fetch 전략을 Lazy로 바꿀 수 있다.

앞에 설명했던 프록시를 잘 생각해보자. 프록시는 실제 엔티티를 상속받은 가짜 엔티티다. 그리고 프록시에서 어떤 메서드가 실행되기 전에는 실제 객체가 콜이 안되는데 이 특징을 이용한 전략이다.

Lazy 전략으로 바꾸기 전에 문제점은 멤버 객체를 조회한것 만으로도 멤버와 팀 테이블에 쿼리가 날라가며 같이 조회를 했었다. 이건 DB 시점으로 봤을때는 굉장히 낭비가 심한거고 나중에 설명할 N+1 문제에 직면하게 된다.

하지만 위에 예시에서 Lazy 전략으로 바꾼 후에는 멤버를 조회한것만으로 팀객체에 메서드가 콜 되지가 않아서 데이터 베이스를 안조회하고 결국 JPA 입장에서는 멤버 테이블에만 SELECT 문이 나간것이다.

그리고 내가 실제로 팀 객체를 조회해야 할때서야 비로서 팀 엔티티가 활성화 되어서 팀을 조회하는 쿼리가 나가기 때문에 불필요한 쿼리 소모를 줄일 수 있다는 특징이 있다.

즉시 로딩

아까 질문과 반대로 만약에 멤버와 팀을 같이 자주 사용하는 경우를 생각해보자.

이때는 Eager 전략을 통해서 멤버 조회시 팀도 같이 조회할 수 있게 만든다.

즉 멤버 객체에 find()를 날려서 조회하게 되면 항상 팀도 같이 조회한다는 뜻이다.

지연 로딩 활용 - 실무

실제로는 Lazy 전략이 필수로 사용된다. Eager 전략은 선택적으로 사용하는것을 강의에서는 더 강조했는데 이건 나중에 JPQL의 fetch 조인을 활용해서 Eager 전략과 같은 효과를 낼 수 있다고 한다.

마지막에 즉시 로딩은 상상하지 못한 쿼리가 나간다고 강조했는데 이 부분은 N+1 문제를 얘기하는것이다. N+1 문제는 즉시 로딩 전략을 사용하면서 JPQL 을 사용하게 되면 원래는 한번만 날라가야 하는 SQL 쿼리문이 즉시 로딩으로 인해 N 번이 더 처리된다 해서 N+1 문제라고 한다.

원래는 이해하지 못했지만 강의를 여러번 들으면서 이해가 됐다. 추가적으로, X to One 관계 같은경우 기본이 즉시로딩이기 때문에 신경써서 Lazy로 바꿔보도록 하자.

N+1 10 분 테코톡에도 커버한 내용이 있으니 확인하면 좋을거 같다.


영속성 전이: CASCADE

이번 주제도 1회차에 강의를 봤을때 잘 이해가 안갔던 부분이지만 이제야 리뷰를 한다. 우리가 영속성 컨텍스트에 등록을 하기 위해서는 persist() 함수를 호출 해줘야 하는데 일일이 다 persist()를 호출하기에는 번거로울 때가 있다.

연관된 엔티티도 함께 영속 상태로 만들고 싶을때 CASCADE 를 활성화 시켜주면 된다.

간단하게 요약된 코드이다. 지금 Child는 @OneToMany 형태로 Parent 클래스와 연관이 된 상태이다. 이때 만약에 cascade 옵션을 활성화 시켜주고 parent 클래스를 영속해주면 parent 와 연관된 child 엔티티들은 전부 같이 영속화가 진행된다.

Parent 클래스에서 위와 같이 Child가 @ManyToOne 주인관계를 가지고 읽기만 가능 mappedBy를 parent에 걸어서 @OneToMany 관계를 만들었다고 생각하자. 이때 addChild() 함수로 Child 를 계속해서 넣을수가 있는데,

이렇게 객체를 생성해주고 영속성 컨텍스트에 올릴려면은 parent 도 persist() 등록해주고 child 도 persist() 등록을 해줘야하는 불편함이 있다.

하지만 이제 옵션을 바꿔보자.

아까와 마찬가지로 Child를 생성해주고 Parent 클래스에 전부 더한후에 이번엔 Parent 에만 persist()를 호출 해줬는데 이제는 Child에도 persist가 자동으로 날라가는것을 확인 가능했다.

CASCADE는 정말 심플하게 특정 클래스를 persist() 할때 그와 연관된 클래스를 전부 persist() 해줄거다 라고 얘기하는거다.

편리를 위해 만들어진 기능이고 CASCADE 옵션은 ALL 아니면 PERSIST 를 사용해주면 된다.

그리고 사용되는 경우는

  1. 하나의 부모가 자식을 관리할때 (예: 계시판 - 첨부파일) 소유자가 하나일때 사용하자.

  2. 생명주기가 같을 때, 혹은 유사할 떄.

jpaShop 프로젝트에서는 Order 상품을 저장함과 동시에 배송 정보도 같이 persist() 하면서 생명주기를 맞춰주었다. 이런 이유로 Delivery 옵션에 CASCADE 옵션이 활성화 된걸 확인 가능하다.

추가적으로 아이템 상품을 담는 OrderItems 객체에도 CASCADE 옵션이 켜져있어서 오더 객체를 persist() 함과 동시에 안에 연관 되어있는 OrderItems 객체들도 같이 persist() 되었다.

profile
성장하는 사람

0개의 댓글