프록시와 연관관계 관리

이상훈·2022년 10월 5일
0

Jpa

목록 보기
7/17

김영한님의 인프런 강의 '자바 ORM 표준 JPA 프로그래밍'을 참고했습니다.

프록시

find() vs getReference()

  • EntityManager.find()
Member member = em.find(Member.class, "member1");

JPA에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find()를 사용한다. 이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다. 따라서 엔티티를 실제 사용하든 말든 데이터베이스를 조회하게 된다.

  • EntityManager.getReference()
Member member = em.getReference(Member.class, "member1");

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 getReference 메서드를 사용하면 된다. 이 메서드를 호출하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

getReference()는 실제로는 거의 안쓴다. 프록시 메커니즘을 이해해야 뒤에 나오는 중요한 개념인 즉시 로딩과 지연 로딩을 이해할 수 있다.


프록시 구조

  • 실제 클래스를 상속 받아서 만들어짐.
  • 실제 클래스와 겉모양이 같음.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상).
  • 프록시 객체는 실제 객체의 참조(target)를 보관.


프록시 객체의 초기화

프록시 객체는 member.getName()처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.

Member member = em.getReference(Member.class, “id1”); 
member.getName();

  • 0. em.getReference()를 통해 프록시 객체를 반환한다.

  • 1. 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.

  • 2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.

  • 3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.

  • 4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 target 멤버 변수에 보관한다.

  • 5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.


프록시의 특징

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

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

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야 함.

    JPA에서 엔티티 타입을 비교할 때는 프록시일 수도 있고 원본일 수도 있으므로 "=="을 사용 하지말고 instance of를 사용하자.

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

    반대로 이미 처음에 프록시로 조회하면 em.find 호출해도 프록시로 반환한다.
    WHY? : by 영속 엔티티의 동일성 보장 특성

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

    앞으로 굉장히 많이 보게 될 예외


프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
  • 프록시 강제 초기화, by 하이버네이트
    org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음
    강제 호출: member.getName()

즉시 로딩과 지연 로딩

프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다. 회원이 팀에 소속해 있다고 가정해 보자.

회원 엔티티를 조회할 때 연관된 팀 엔티티도 함께 데이터베이스에서 조회하는 것이 좋을까? 아니면 회원 엔티티만 조회해 두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스에서 조회하는 것이 좋을까? JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지 방법을 제공한다.


지연 로딩

지연 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 지정하면 된다. 참고로 @OneToMany@ManyToMany는 기본이 지연 로딩.

@Entity
public class Member {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "USERNAME")
	private String name;

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

지연 로딩을 실행해보자.

Member member = em.find(Member.class, "member1");

회원만 조회하고 팀은 조회하지 않는다. 대신 조회한 회원의 team 멤버 변수에 프록시 객체를 넣어둔다.


Team team = member.getTeam();

반환된 팀 객체는 프록시 객체다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 그래서 지연 로딩이라 한다.


team.getName();

실제 데이터가 필요한 순간이 되어서야 데이터베이스르 조회해서 프록시 객체를 초기화한다.


즉시 로딩

즉시 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정하면 된다. 참고로 @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 fetch 속성을 지정해 주지 않으면 자동으로 즉시 로딩으로 실행된다.

@Entity
public class Member {

	//생략(지연 로딩과 동일)
	...
    
	@ManyToOne(fetch = FetchType.EAGER) //**
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	.. 
 }

즉시 로딩을 실행해 보자.

Member member = em.find(Member.class, "member1");
회원을 조회하는 순간 팀도 함께 조회한다. 이때 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다. 여기서는 회원과 팀을 조인해서 쿼리 한 번으로 두 엔티티를 모두 조회한다.
Team team = member.getTeam();

이후 member.getTeam()을 호출하면 이미 로딩된 팀 엔티티를 반환한다.


정리

이론적으로 다음은 적절해 보인다.

  • if Member와 Team은 자주 함께 사용 -> 즉시 로딩
  • if Member와 Order는 가끔 사용 -> 지연 로딩
  • if Order와 Product는 자주 함께 사용 -> 즉시 로딩

BUT!! 😮😮

모든 연관관계에 지연 로딩을 사용하자. 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. 또한 JPQL에서 N+1 문제도 야기한다. 즉시 로딩이 필요할 경우엔 뒤에 나올 JPQL fetch 조인이나 엔티티 그래프 기능을 사용하자.


영속성 전이와 고아 객체

영속성 전이

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다.

예를 들어 부모 엔티티와 자식 엔티티가 일대 다 관계라고 하자.
영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다. CASCADE의 속성을 ALL이나 PERSIST로 지정하면 된다.

@Entity
public class Parent {
	...
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();

	...
}

부모만 저장해도 연관된 자식들까지 한번에 저장된다.

private static void saveWithCascade(EntityManager em) {
	
	Child child1 = new Child();
    Child child2 = new Child();

	Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().add(child1);
    parent.getChildren().add(child1);
   
    em.persist(parent); //부모 저장, 연관된 자식들 저장
}

단지 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공해 줄 뿐이다. 연관관계를 매핑하는 것과 아무 관련이 없다.

  • CASCADE의 종류
    주로 ALL, PERSIST 사용
    • ALL : 모두 적용

    • PERSIST : 영속

    • REMOVE : 삭제

    • MERGE : 병합


고아 객체

JPA는 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라 한다. 이 기능을 사용하면 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 할 수 있다. orphanRemoval을 true로 설정하면 된다.

@Entity
public class Parent {

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

다음 사용 코드를 보자.

Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);	//자식 엔티티를 컬렉션에서 제거

실행 결과 SQL은 다음과 같다.

DELETE FROM CHILD WHERE ID=?

특징

  • 특정 엔티티가 개인 소유하는 엔티티에만 적용해야 함. 삭제하고자하는 엔티티를 여러 엔티티에서 참조할 시 사용하면 안됨.

  • @OneToOne, @OneToMany만 가능

  • 부모를 제거하면 자식은 고아가 된다. 따라서 부모를 제거하면 자식도 함께
    제거된다. CascadeType.REMOVE처럼 동작한다.


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

CascadeType.ALL + orphanRemovel=true

  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다. 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용
profile
Problem Solving과 기술적 의사결정을 중요시합니다.

0개의 댓글