프록시와 지연 로딩

회원과 팀 모두 조회

public void printUserAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();

    System.out.println("회원 이름 : " + member.getUserName());
    System.out.println("소속팀 : " + team.getName());
}

회원 정보만 조회

public String printUser(String memberId) {
    Member member = em.find(Member.class, memberId);

    System.out.println("회원 이름 : " + member.getUserName());
}

회원 정보만 조회하는 경우에는 team 객체에 접근을 하지 않으므로, Team객체를 조회해서 가지고 있는 것은 비효율적이다. 그래서 JPA에서는 위의 예제에서 처럼 team.getName()과 같이 직접적으로 객체를 사용하기 전까지는 로딩을 하지 않는 데 이를 지연로딩이라고 한다. 그리고 이러한 지연 로딩을 지원하기 위해 가짜 객체를 주입하게 되는 이를 프록시라고 한다.

프록시

proxy

프록시 위와 같이 실제 엔티티를 상속받아서 만들어진 가짜 클래스이다. 이 프록시는 실제 엔티티에 대한 참조 정보를 가지고 있어, 사용자가 객체에 대한 접근을 요청하게 되면 이 프록시가 실제 엔티티에 접근해서 값을 가져오는 것이다.

프록시 객체 조회 방법

// 엔티티 직접 조회 - 영속성 컨텍스트에 없으면 DB 조회
Member member = em.find(Member.class, 100L);

// 엔티티를 실제 사용하는 시점까지 미루는 프록시 객체
Member member = em.getReference(Member.class, 100L);.

getReference를 이용하면 프록시 객체를 반환받게 된다, 만약 이미 실제 엔티티가 생성되어 영속성 컨텍스트에 저장되어 있는 경우에는 getReference 호출 시 실제 엔티티가 반환된다.

위의 엔티티에서 member.getName()처럼 실제 사용할 때 해당 프록시가 실제 엔티티의 생성을 요청해서 실제 객체가 생성되는 데 이를 프록시 객체의 초기화라고 한다.

proxy_initialization

위의 프로세스 보면, client가 getName과 같이 실제 객체에 대한 접근을 요청하면, 프록시 객체는 DB에 실제 객체를 요청해서 실제 객체를 만들어 해당 엔티티로 getName을 요청한 다음 값을 client에게 전달하게 된다.

실제 엔티티가 생성된다고 해서 프록시가 사라지는 것이 아니고 중간 매개체 역할을 수행하게 된다. 또, 실제 엔티티를 생성하기 위해 영속성 컨텍스트를 거쳐서 초기화를 수행하는데, 만약 엔티티가 준영속 상태이면 프록시 생성이 안되고, org.hibernate.LazyInitilizationException 예외가 발생한다.

프록시를 이용하면 연관관계 설정을 진행할때 유용하게 활용할 수 있다.

Member member=em.find(Member.class,"member1");
Team team=em.getReference(Team.class,"team1");
member.setTeam(team);

위 처럼 Member 엔티티에 team 정보를 설정하려고 할때, 실제 team 객체 대신 프록시 객체를 전달해서 연관관계를 설정할 수 있다. Team 객체를 받아오는 getReference는 DB로 접근을 하지 않으므로 접근 성능을 높일 수 있다.

프록시 객체 초기화 확인

//프록시 객체의 초기화가 이루어졌는지 확인하기 위한 메소드
boolean isloaded=PersistenceunitUtil.isLoaded(Object entity)

//조회한 엔티티가 실제 엔티티인지 프록시인지 확인하는 방법
member.getClass().getName();

proxy_getclass

isloaded 메소들를 이용해서 프록시 인스턴스가 초기화 됬는지 판별할 수 있다.

getClass()를 활용하게 되면, 프록시의 경우 위 처럼 클래스명 뒤에 ..javassist..가 붙는 것을 확인할 수 있다.

지연 로딩과 즉시 로딩

지연 로딩은 이전에서 다뤘던 것 처럼 실제 엔티티가 사용되기 전까지 실제 엔티티를 생성하지 않는 방식이다. Member와 Team 엔티티가 있을때, 멤버에 대한 조회를 수행할 때, 팀 객체를 사용하지 않으면 팀 객체는 생성되지 않는다.

반면, 즉시 로딩은 연관된 엔티티까지 함께 조회를 하는 방식이다.

즉시 로딩

@Entity
public class Member {

    //...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

위와 같이 fetch=FetchType.EAGER을 명시해서 즉시로딩을 하도록 설정할 수 있다. 즉시로딩을 하게 되면 아래의 sql 문이 실행되어 연관된 엔티티까지 모두 조회된다.

SELECT M.MEMBER_ID,M.TEAM_ID,M.USERNAME,T.TEAM_ID,T.NAME
FROM MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID=T.TEAM_ID
WHERE MEMBER_ID="member1"

위와 같이 조인을 통해 두 개의 엔티티를 한번의 쿼리로 조회하게 된다. INNER JOIN 방식이 아니라, OUTER JOIN 방식이 활용되는 것을 알 수 있는데, 이는 Member에 팀 정보가 설정되지 않을 수 있기 때문에 그런 것이다.

즉, JOIN COLUMN인 TEAM_ID가 null을 허용하면 OUTER JOIN이, null을 허용하지 않으면 INNER JOIN이 실행된다.

지연 로딩

@Entity
public class Member {

    //...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

위와 같이 fetch=FetchType.LAZY을 명시해서 지연로딩 하도록 설정할 수 있다. 아래의 sql이 실행되면서 Member 엔티티만 가져오게 된다.

SELECT M.MEMBER_ID,M.TEAM_ID,M.USERNAME
FROM MEMBER M
WHERE MEMBER_ID="member1"

실제로 team.getName과 같이 team 엔티티를 접근하게 되면 그제서야 실제 객체가 생성된다.

지연 로딩 vs 즉시 로딩

이것은 애플리케이션의 성격에 따라서 나눠지게 되는데, 만약 member, team 엔티티를 같이 사용하는 일이 많은 경우 즉시 로딩을 이용해서 조회 한번만 하는 것이 좋게 된다.

무조건 즉시 로딩이 효율적이지 않다고 할 수는 없다. 애플리케이션의 로직에 따라 사용하는 방식을 달리해야한다.

지연,즉시 로딩 혼합 활용 예제

lazy_initialization_exercise

위의 비즈니스 요구사항을 보면 Member 와 Team는 함께 자주 사용되므로 즉시 로딩을, Member 와 Order는 자주 사용하지 않으므로 지연 로딩을, Order와 Product는 함께 자주 사용되므로 즉시로딩으로 설정하면 된다.

Member Class

@Entity
public class Member{
  @Id
  private String id;
  private String username;
  private Integer age;

  @ManyToOne(fetch=FetchType.EAGER)
  private Team team;

  @OneToMany(mappedBy="team",fetch=FetchType.LAZY)
  List<Order> orders;
}

위의 예제에서 보면 OneToMany 연관관계에 대응되는 컬렉션이 있음을 알 수 있는데, 해당 연관관계에서 프록시가 사용되면 이 컬렉션 객체에 대한 초기화는 어떻게 될까?

컬렉션 래퍼라는 것이 존재하는데, 이것이 컬렉션에 대한 프록시를 생성한다고 이해하면 된다.

JPA의 기본 전략

  1. @ManyToOne, @OneToOne 과 같이 하나의 엔티티만을 가지고 있는 엔티티에 대해서는 즉시로딩을 수행한다.
  2. @OneToMany, @ManyToMany와 같이 여러 개의 엔티티를 가지고 있는 경우, 컬렉션을 변수로 사용해야하므로 지연 로딩을 활용한다.

컬렉션 초기화를 위해 join 함수를 이용하다 보며 많은 양의 데이터를 조회 하게되므로 필수적으로 사용할 때만 로딩할 수 있도록 지연 로딩을 활용한다.

영속성 전이 CASCADE

연관된 관계에 있는 엔티티들은 모두 영속 상태를 유지해야 된다.

Parent class

@Entity 
public class Parent{
  @Id
  @GeneratedValue
  private Long id;

  @OneToMany(mappedBy="parent")
  private List<Child> children=new ArrayList<Child>();
}

Child Class

@Entity 
public class Child{
  @Id
  @GeneratedValue
  private Long id;

  @ManyToOne
  private Parent parent;
}

객체 생성

Parent parent=new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2);

이 처럼 영속 상태를 만들기 위해 부모 엔티티를 영속 상태로 만들고, 자식 엔티티를 영속 상태로 만들어야 한다.

영속 전이: 저장

하지만, 영속성 전이를 설정하게 되면 부모 엔티리를 영속상태로 만들떄, 자식 엔티티도 영속 상태로 자동으로 만들어지게 된다.

Parent class

@Entity 
public class Parent{
  @Id
  @GeneratedValue
  private Long id;

  @OneToMany(mappedBy="parent",cascade=CascadeType.PERSIST)
  private List<Child> children=new ArrayList<Child>();
}

객체 생성

Parent parent=new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);

위 처럼 부모에 대해 영속 상태로 만들면 자식은 자동으로 영속 상태로 만들어 지게 된다.

영속 전이: 삭제

영속 상태에서 제거하기 위해서는 원래는 각각의 엔티티에 대해 아래처럼 삭제 명령을 수행해야 했다

Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);

하지만 영속성 전이를 이용하면 아래와 같이 부모 엔티티를 삭제 하게되면 자동적으로 자식 엔티티들도 삭제가 된다.

cascade 조건 추가

@OneToMany(mappedBy="parent",cascade=CascadeType.REMOVE)
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);

여러 개의 CASCADE 활용

CASCADE에는 PERSIST,MERGE,REMOVE,REFRESH,DETACH,ALL 와 같은 옵션을 설정할 수 있다.

여러 개의 cascade를 동시에 적용하려면 아래 처럼 적용하면 된다

cascade={
  CascadeType.PERSIST,
  CascadeType.REMOVE
}

고아 객체

부모 엔티티와 연관관계가 끊긴 자식 엔티티를 고아 객체라고 한다. JPA에서는 이러한 고아 객체에 대해 자동으로 삭제해주는 기능을 제공한다.

@Entity
public class Parent{
  @Id
  @GeneratedValue
  private Long id;

  @OneToMany(mappedBy="parent", orphanRemoval=true)
  private List<Child> children=new ArrayList<Child>();
}

위와 같이 설정하게 되면 자식 엔티티에 대한 참조을 제거하면 자식 엔티티가 자동으로 삭제된다.

Parent parent=em.find(Parent.class,id);
parent.getChildren().remove(0);

위 처럼 1번째 자식을 제거했을 때, 1번쨰 자식의 엔티티도 제거된다.

해당 기능은 이렇게 자식을 참조하는 객체 이처럼 하나인 @OneToMany, @OneToOne 에서만 사용가능하다. 만약 다른 곳에도 참조를 하고 있는데, 삭제를 해버리면 에러가 발생하게 된다.

추가로, 부모 엔티티를 제거하면 자식 엔티티는 고아 객체가 되는데, 이때에도 자식 엔티티는 삭제된다. 이는 CasadeType.REMOVE와 유사한 성격을 가지고 있다.

영속성 전이 + 고아 객체 제거 기능

CascadeType.ALL + orphanRemoval=true로 설정해놓은 상태에서 아래와 같이 부모 엔티티 만을 이용해서 자식 엔티티를 저장 및 제거할 수 있다.

Parent parent=em.find(Parent.class,parentId);
parent.addChild(child1)

Parent parent=em.find(Parent.class,parentId);
parent.getChildren().remove(child1)

References

book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저

book_link

태그: ,

카테고리:

업데이트:

댓글남기기