[JPA 기본] 프록시와 연간관계 매핑
포스트
취소

[JPA 기본] 프록시와 연간관계 매핑

프록시

em.find()와 em.getReference()의 차이

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

프록시 특징

  • 실제 클래스를 상속 받아서 만들어진다.
  • 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
  • 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
  • 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크시 주의해야 한다.
    • == 연산자가 아니라 instance of 연산자를 사용한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
    • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.

프록시 객체의 초기화

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

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
    • 예시
      • emf.getPersistenceUnitUtil().isLoaded(mr)
  • 프록시 클래스 확인 방법
    • entity.getClass().getName()를 출력해본다.
    • javasist or HibernateProxy라는 키워드가 클래스명에 포함되어 있는지 확인한다.
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);
    • JPA 표준은 강제 초기화가 없다.
    • JPA에서 강제 초기화하려면 member.getName()같이 호출해서 초기화해야 한다.

즉시 로딩과 지연 로딩

  • 가급적 지연 로딩만 사용한다.
    • 특히 실무에서는 지연 로딩이 많이 사용된다.
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생할 수 있다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne과 @OneToOne은 기본 설정이 즉시 로딩이다.
    • fetch을 LAZY로 설정해줘야 한다.
  • @OneToMany와 @ManyToMany는 기본 설정이 지연 로딩이다.
  • 함께 사용되는 정도에 따라서 사용되는 옵션이 다르다. (이론상)
    • 즉시 로딩
      • 함께 쓰는 엔티티가 자주 같이 쓰이는 경우
    • 지연 로딩
      • 함께 쓰는 엔티티가 가끔 같이 쓰이는 경우
  • 실무에서는 전부 지연 로딩을 사용하고 필요할 때 패치 조인이나 엔티티 그래프 기능일 사용하는 방식을 많이 사용한다.

즉시 로딩

  • 조회 대상이 되는 엔티티와 연관관계가 있는 엔티티들을 함께 조회하는 방식
  • 함께 쓰는 엔티티가 자주 같이 쓰이는 경우에 사용하기 좋다.
  • 예상하지 못한 SQL이 발생할 수도 있다.
  • JPQL에서 N+1 문제를 일으킨다.
엔티티 정의
@Entity
@Data
public class Department {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;  
}

@Entity
@Data
public class Employee {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String username;
	
	@ManyToOne(fetch = FetchType.EAGER) 
	@JoinColumn(name = "department_id")
	private Department department;
}
테스트
Department department = new Department();
department.setName("deptA");
em.persist(department);

Employee employee = new Employee();
employee.setUsername("empA");
employee.setDepartment(department);
em.persist(employee);

em.flush();
em.clear();

Employee findEmployee = em.find(Employee.class, employee.getId()); //SELECT 실행 (조인을 통해서 한꺼번에 가져온다.)
System.out.println("findEmployee.getDepartment().getClass() : " + findEmployee.getDepartment().getClass());

System.out.println("=============================");
findEmployee.getDepartment().getId(); //SELECT 실행하지 않음
System.out.println("=============================");
findEmployee.getDepartment().getName(); //SELECT 실행하지 않음
System.out.println("=============================");

tx.commit();
select
    e1_0.id,
    d1_0.id,
    d1_0.name,
    e1_0.username 
from
    Employee e1_0 
left join
    Department d1_0 
        on d1_0.id=e1_0.department_id 
where
    e1_0.id=?

지연 로딩

  • 조회 대상이 되는 엔티티와 연관관계가 있는 엔티티들을 사용하는 시점에만 각각 조회하는 방식
  • 함께 쓰는 엔티티가 가끔 같이 쓰이는 경우에 사용하기 좋다.
  • 실무에서 자주 쓰이는 방식
엔티티 정의
@Entity
@Data
public class Department {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;  
}

@Entity
@Data
public class Employee {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String username;
	
	@ManyToOne(fetch = FetchType.LAZY) 
	@JoinColumn(name = "department_id")
	private Department department;
}
테스트
Department department = new Department();
department.setName("deptA");
em.persist(department);

Employee employee = new Employee();
employee.setUsername("empA");
employee.setDepartment(department);
em.persist(employee);

em.flush();
em.clear();

Employee findEmployee = em.find(Employee.class, employee.getId()); //Employee에 대한 SELECT 실행
System.out.println("findEmployee.getDepartment().getClass() : " + findEmployee.getDepartment().getClass());

System.out.println("=============================");
findEmployee.getDepartment().getId(); //SELECT 실행하지 않음
System.out.println("=============================");
findEmployee.getDepartment().getName(); //Department에 대한 SELECT 실행
System.out.println("=============================");

tx.commit();
select
    e1_0.id,
    e1_0.department_id,
    e1_0.username 
from
    Employee e1_0 
where
    e1_0.id=?

select
    d1_0.id,
    d1_0.name 
from
    Department d1_0 
where
    d1_0.id=?

영속성 전이

  • 특정 엔티티와 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
    • 예시
      • 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 경우
  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공한다.
  • 아래의 두 가지 경우에서 해당하는 경우에 사용하면 좋다.
    • 대상이 된 주 엔티티와 연관된 엔티티의 라이프 사이클이 동일할 때
    • 대상이 된 주 엔티티에만 연관된 엔티티가 사용된 경우
  • 관련 어노테이션
    • @xxxToyyy
      • cascade 옵션을 사용하면 영속성 전이 기능을 사용할 수 있다.
      • 실제로는 ALL하고 PERSIST정도만 사용한다.
        • CascadeType.ALL
          • 모두 적용
        • CascadeType.PERSIST
          • 영속
        • CascadeType.REMOVE
          • 삭제
        • CascadeType.MERGE
          • 병합
        • CascadeType.REFRESH
          • REFRESH
        • CascadeType.DETACH
          • DETACH

엔티티 정의

@Entity
@Data
public class Parent {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;  
	
	@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
	private List<Child> childs = new ArrayList<>();
	
	//연관관계 편의 메소드
	public void addChild(Child child) {
		childs.add(child);
		child.setParent(this);
	}
}

@Entity
@Data
public class Child {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name; 
	
	@ManyToOne
	@JoinColumn(name = "parent_id")
	Parent parent;
}

테스트

Child child1 = new Child();
Child child2 = new Child();

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

em.persist(parent); //INSERT 3번 실행
//em.persist(child1);
//em.persist(child2);

em.flush();
em.clear();

System.out.println("=============================");
Parent findParent = em.find(Parent.class, parent.getId()); //SELECT 실행 (Parent 한정)
System.out.println("=============================");

tx.commit(); //DELETE 실행

고아 객체

  • 고아 객체
    • 부모 엔티티와 연관관계가 끊어진 자식 엔티티
  • 고아 객체 삭제
    • 고아 객체를 자동으로 삭제하는 것
  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 취급된다.
  • 참조하는 곳이 하나일 때 사용해야 한다.
  • 특정 엔티티가 개인 소유할 때만 사용한다.
  • @OneToOne와 @OneToMany만 사용할 수 있다.
  • 고아 객체 삭제 기능을 활성화 했을 때 부모 객체를 제거하면 자식 객체도 함께 제거된다.
    • CascadeType.REMOVE처럼 동작한다.
  • 관련 어노테이션
    • @xxxToyyy
      • orphanRemoval = true 옵션을 사용하면 고아 객체 삭제 기능을 사용할 수 있다.

엔티티 정의

@Entity
@Data
public class Parent {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;  
	
	@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Child> childs = new ArrayList<>();
	
	//연관관계 편의 메소드
	public void addChild(Child child) {
		childs.add(child);
		child.setParent(this);
	}
}

@Entity
@Data
public class Child {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name; 
	
	@ManyToOne
	@JoinColumn(name = "parent_id")
	Parent parent;
}

테스트

Child child1 = new Child();
Child child2 = new Child();

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

em.persist(parent); //INSERT 3번 실행
//em.persist(child1);
//em.persist(child2);

em.flush();
em.clear();

System.out.println("=============================");
Parent findParent = em.find(Parent.class, parent.getId()); //SELECT 실행 (Parent 한정)
System.out.println("=============================");
findParent.getChilds().remove(0); //SELECT 실행 (Child 한정)
System.out.println("=============================");

tx.commit(); //DELETE 실행

공통 코드

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); //애플리케이션 전체 공유 (persistence.xml 참조)
EntityManager em = emf.createEntityManager(); //한번 쓰고 버려야함, 쓰레드간 공유하지 않음
EntityTransaction tx = em.getTransaction(); //조회를 제외한 DML 작업시 필수로 사용
tx.begin();

try {
    //실행 내용
} catch (Exception e) {
    e.printStackTrace();
    tx.rollback();
} finally {
    em.close();
}

emf.close();

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.