JPA - 프록시와 연관관계 관리

JPA 프록시와 연관관계 관리


JPA에서는 find를 할시 연관관계를 가진 데이터를 조인을 통해 한번에 가져오게된다.

근데 만약 Member라는 객체와 Team이 있는데 Member의 데이터만 필요한 로직에서 굳이 Team의 데이터를 조회해 와야할까??

이러한 고민을 해결하기 위해 프록시를 통해 해결할 수 있다.

프록시


JPA에서 테이블을 조회할때 사용하는 메서드는 2가지가 있다.

- em.find() / 실제 엔티티 객체 조회
- em.getReference() / 데이터 베이스 조회를 미루는 가짜(프록시)

em.getReference()로 조회하게 되면 Entity class가 아닌 프록시 class를 생성하게된다.

Member findMember1 = em.find(Member.class , member.getId()); // Entity 조회
Member findMember2 = em.getReference(Member.class , member.getId()); // 하이버네이트 프록시 클래스

System.out.println(findMember1.getClass()); // Member
System.out.println(findMember2.getClass()); // proxyClass

위와 같이 조회시 각각 다른 클래스를 리턴하는것을 볼 수 있다.

프록시 클래스

  • 실제 클래스를 상속 받아서 만들어진다

  • 실제 클래스와 겉 모양이 같다

  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용한다. (이론상)

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다

  • 프록시 객체를 호출하면 프록시 객체는 오버로딩을 통해 상속받은 식제 객체의 메서드 호출한다.

프록시 객체의 초기화

Member findMember = em.getReference(Member.class , member.getId()); // 하이버네이트 프록시 클래스

System.out.println(findMember.getName()); // SELECT SQL 호출

find()는 find()메서드가 호출되는 순간 SELECT SQL을 호출하여 데이터를 셋팅하게된다.
하지만 getReference()메서드는 호출되는 순간 SELECT SQL을 호출하지 않고 해당 Entity를 타겟으로 상속받은 프록시 객체를 리턴한다.

그후 실제로 데이터를 사용하는 시점에 SELECT SQL을 호출하여 값을 세팅한다. 이것을 초기화라고 한다.

client의 요청 => 프록시 클래스가 영속성 컨텍스트에 요청 =>  
영속성 컨텍스트에서 DB조회후 실제 Entity 생성 => 프록스 클래스의 target을 Entity와 연결

프록시 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화

  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님 , 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패 , 대신 instance of 사용)

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환

    • 영속성컨텍스트에 있는 데이터가 있기때문에 굳이 데이터 낭비를 하지않는다.

    • JPA의 매커니즘은 한 영속성컨텍스트 안에서의 == 비교는 항상 True여야 한다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 , 프록시를 초기화하는 문제발생
    (하이버 네이트는 org.hibernate.LazyInitializationExceptoon 예외를 터트림)

프록시 확인

- 프록시 인스턴스의 초기화 여부 확인  

PersistenceUnitUtil.isLoaded(Object entity)

- 프록시 클래스 확인 방법  

entity.getClass().getName()출력(..javasist... or HibernateProxy)

- 프록시 강제 초기화  

org.hibernate.Hibernate.initialize(entity);

- 참고 : JPA 표준은 강제 초기화 없음  

강제 호출 : member.getName();

프록시의 로직을 통해 JPA의 지연로딩과 즉시로딩을 이해할 수 있다.

즉시로딩과 지연로딩


프록시 클래스에서 필요할때 SELECT SQL이 호출되는것을 알고 있다.

프록시 클래스를 통해 JPA는 지연로딩을 지원한다.

지연로딩 LAZY


  • @ManyToOne(fetch = FetchType.LAZY)

  • fetch = FetchType.LAZY를 사용시 연관되있는 데이터를 모두 조인을 통해 가져오는게 아니라 주가되는 entity만 조회하게 된다.

  • fetch = FetchType.LAZY 를 사용하면 프록시 객체를 반환하며 필요시 SELECT SQL을 호출한다.

@Entity
public class Member{
    @Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	
	@Column(name = "name")
	private String username;

	@ManyToOne(fetch = FetchType.LAZY) // 프록시 객체 반환 지연로딩
	@JoinColumn(name = "TEAM_ID")
 	private Team team;
}

Member findMember = em.find(Member.class , member.getId());
/*
SELECT * FROM MEMBER WHERE member_id = XXX
*/

findMember.getTeam();
/*
SELECT * FROM TEAM WHERE team_id = XXX
*/

즉시로딩 EAGER


  • @ManyToOne(fetch = FetchType.EAGER)

  • 연관관계인 테이블을 조인하여 member와 team을 둘다 가져온다.

@Entity
public class Member{
    @Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;
	
	@Column(name = "name")
	private String username;

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

Member findMember = em.find(Member.class , member.getId());
/*
SELECT m.* , t.*
FROM MEMBER m INNER JOIN TEAM t ON m.team_id = t.team_id
WHERE member_id = XXX
*/

findMember.getTeam(); // 쿼리가 실행되지 않는다.

지연로딩과 즉시로딩 주의


  • 모든 연관관계에 지연 로딩을 사용해라

  • 가급적 지연 로딩만 사용 (특히 실무)

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 (성능 저하)

  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

  • JPQL fetch 조인이나 , 엔티티 그래프 기능을 사용해라

  • @ManyToOne , @OneToOne은 기본이 즉시 로딩

  • @OneToMany , @ManyToMany는 기본이 지연 로딩

영속성 전이 : CASCADE


JPA에서 연관관계를 가진 데이터를 insert한다면 어떻게 될까??

Team team = new Team();
team.setName("TEAM");

em.persist(team); 
 
Member member = new Member();
member.setUsername("JPA");
member.setTeam(team);

em.persist(member);

JPA 매커니즘으로 인해 영속성컨텍스와 insert를 위해 2번의 persist()를 호출하게 된다.

프로젝트를 하다보면 한 - 두개가 아닌 4 - 5개의 데이터를 인설트하는 서비스 로직이 존제할 수도 있다.
그럼 그때마다 persist를 호출해야한다.

이것을 해결하기위해 CASCADE를 지원한다.

CASCADE


  • cascade = CascadeType.XXX

Parent

@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "parent_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent" , cascade = CascadeType.ALL , orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) { // 편의 메서드
        this.childList.add(child);
        child.setParent(this);
    }
}
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent); // cascade를 통한 영속성 관리, persist 한번만 실행
  • 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다

  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공한다

CASECADE 종류

- ALL : 모두적용 

- PERSIST : 영속

- REMOVE : 삭제

- MERGE : 병합

- REFRESH : REFRESH

- DETACH : DETACH

ALL , PERSIST 를 자주 사용 한다.

고아 객체


부모랑 자녀를 관리하는 객체를 고아 객체라고 한다.

주의 할 점으로 부모랑 자녀가 종속관계일때 사용해야 한다.

  • 고아객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

  • orphanRemval = true

  • parent.getChildList().remove(0)

@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "parent_id")
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        this.childList.add(child);
        child.setParent(this);
    }
}

고아객체 주의할 점


  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능

  • 참조하는 곳이 하나일 때 사용해야함! (*)

  • 특정 엔티티가 개인 소유할 때 사용 (*)

  • @OneToOne , @OneToMany만 가능

  • 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다.
    따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다.
    이것은 CascadeType.REMOVE처럼 동작한다.

영속성 전이 + 고아객체 , 생명주기


  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화 , em.remove()로 제거

  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다

  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용하다

Categories:

Updated:

Leave a comment