프록시와 연관관계 관리
8. 프록시와 연관관계 관리
8.1 프록시
- 즉시 로딩: 객체를 데이터베이스에서 조회할 때 연관된 다른 객체를 함께 조회하는 것을 말한다.
- 지연 로딩: 객체를 데이터베이스에서 조회할 때 해당 객체만 조회하고 연관된 객체는 실제 사용할 때 데이터베이스에서 조회하는 것을 말한다.
- 프록시: 지연 로딩 기능을 사용할 때 연관된 객체에 실제 엔티티 객체 대신 연결된 가짜 객체를 말한다.
- 예를 들어, Member 객체의 Team 참조변수에 가짜 객체의 참조 값을 할당하는데 이를 프록시 객체라고 한다.
8.1.1 프록시 기초
- EntityManager.find() 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.
- EntityManager.getReference() 메소드는 데이터베이스를 조회하지 않고 프록시 객체를 반환한다.
- 프록시 클래스는 실제 클래스를 상속받아서 만들어진다. 따라서 사용자 입장에서 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체에 대한 참조를 보관한다. 그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
- 프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
// 프록시 클래스 예상 코드
public class MemberProxy extends Member {
Member target = null;
@Override
public String getName() {
if (target == null) {
// DB 조회
// 실제 엔티티 생성 및 참조 보관
// target = ...;
}
return target.getName();
}
}
- 프록시의 초기화 과정
- 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.
- 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라 한다.
- 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
- 프록시 객체는 생성된 실제 엔티티 객체의 참조를 target 멤버변수에 보관한다.
- 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
- 프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 개체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크시에 주의해서 사용해야 한다.
- 프록시 객체와 엔티티는 타입이 다르므로 두 객체의 타입 비교시 getClass() 메소드가 아닌 instanceof 연산자를 사용해야한다.
- 영속성 컨텍스트에 찾는 엔티티가 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.
- 반대로 em.getReference()로 프록시 객체를 받아오면 같은 트랜잭션에서 em.find()로 같은 객체를 조회해도 실제 엔티티가 아닌 프록시를 반환한다.
- 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
8.1.2 프록시와 식별자
- 엔티티를 프록시로 조회할 때 식별자 값을 파라미트로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
Team foundTeam = em.getReference(Team.class, team.getId());
foundTeam.getId(); // 영속성 컨텍스트에 엔티티 생성을 요청하지 않는다. 즉 초기화하지 않는다.
- 프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화하지 않는다.
- 단 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우에만 초기화하지 않는다.
- 엔티티 접근 방식을 필드(@Access(AccessType.FIELD))로 설정하면 프록시 객체를 초기화한다. ==> 실제로 해보면 초기화하지 않는다.
- 연관관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있다.
Member foundMember = em.find(Member.class, member.getId()); // 엔티티
Team foundTeam = em.getReference(Team.class, team.getId()); // 프록시
foundMember.setTeam(foundTeam);
8.1.3 프록시 확인
- JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
- 조회한 엔티티가 엔티티인지 프록시인지 확인하려면 클래스명을 직접 출력해보면 된다.
boolean isLoaded = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundTeam);
boolean isLoaded2 = emf.getPersistenceUnitUtil().isLoaded(foundTeam);
System.out.println("isLoaded = " + isLoaded);
System.out.println("isLoaded2 = " + isLoaded2);
System.out.println("foundTeam.getClass().getName() = " + foundTeam.getClass().getName());
8.2 즉시 로딩과 지연 로딩
- 즉시 로딩: 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 지연 로딩: 연관된 엔티티를 실제 사용할 때 조회한다.
8.2.1 즉시 로딩
- @ManyToOne의 fetch속성을 FetchType.EAGER로 지정한다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("team");
em.persist(team);
Member member = new Member();
member.setName("member");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member foundMember = em.find(Member.class, member.getId());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
select m.id, m.name, m.team_id, t.id, t.name,
from Member m
left outer join Team t on m.team_id=t.team_id
where m.id=?
- 즉시 로딩시 회원을 조회할 때 팀을 조인하여 함께 조회한다.
- 회원이 팀을 참조하고 있지 않을수도 있으므로 외부 조인을 사용한다.
@JoinColumn의 nullable=false 속성을 사용하거나 @ManyToOne의 optional=false 속성을 사용하여 외래 키 NULL을 허용하지 않도록 설정하여 내부 조인을 이용할 수 있다.
8.2.2 지연로딩
- @ManyToOne의 fetch속성을 FetchType.LAZY로 지정한다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("team");
em.persist(team);
Member member = new Member();
member.setName("member");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member foundMember = em.find(Member.class, member.getId());
Team foundTeam = foundMember.getTeam(); // 프록시 객체
System.out.println("==========");
System.out.println("foundTeam.getName() = " + foundTeam.getName()); // 이 시점에 팀 조회
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
select m.id, m.name, m.team_id
from Member m
where m.id=?
select t.id, t.name
from Team t
where t.id=?
- 팀 객체를 실제로 사용하는 시점에 데이터베이스에서 조회한다. 즉 foundTeam.getName() 메소드 호출 순간에 팀 객체를 조회한다.
- 영속성 컨텍스트에 조회 대상이 있으면, 프록시 객체대신 실제 객체를 사용한다.
// 한 트랜잭션 안에서 수행 할 경우이다.
em.flush();
em.clear();
Member foundMember1 = em.find(Member.class, member.getId());
Member foundMember2 = em.getReference(Member.class, member.getId());
System.out.println("foundMember1.getClass().getName() = " + foundMember1.getClass().getName()); // 엔티티 객체
System.out.println("foundMember2.getClass().getName() = " + foundMember2.getClass().getName()); // 엔티티 객체
// 한 트랜잭션 안에서 수행 할 경우이다.
em.flush();
em.clear();
Member foundMember1 = em.getReference(Member.class, member.getId());
Member foundMember2 = em.find(Member.class, member.getId());
System.out.println("foundMember1.getClass().getName() = " + foundMember1.getClass().getName()); // 프록시 객체
System.out.println("foundMember2.getClass().getName() = " + foundMember2.getClass().getName()); // 프록시 객체
8.3 지연 로딩 활용
- 주문 관리 시스템 모델
- 회원은 팀 하나에만 소속할 수 있다. (N:1)
- 회원은 여러 주문내역을 가진다 (1:N)
- 주문내역은 상품정보를 가진다 (N:1)
- Member와 Team은 자주 함께 사용되어 즉시 로딩으로 설정한다.
- Member와 Order는 가끔 사용되어 지연 로딩으로 설정한다.
- Order와 연관된 Product는 자주 함께 사용되어 즉시 로딩으로 설정한다.
select m.id, m.name, m.team_id, t.id, t.name
from Member m
left outer join Team t on m.team_id=t.id
where m.id=?
8.3.1 프록시와 컬렉션 래퍼
- 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 이를 컬렉션 래퍼라고 한다.
- 영속화된 엔티티의 컬렉션의 클래스 타입을 출력하면 “org.hibernate.collection.internal.PersistentBag” 가 출력된다.
- 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.
- 컬렉션 지연 로딩시 컬렉션 자체에 접근해도 초기화 되지 않는다. 실제 데이터를 조회해야 초기화 된다.
- 아래 그림에서 member.getOrders()을 호출해도 컬렉션은 초기화 되지 않는다.
- member.getOrders().get(0)을 호출하면 Order와 Product가 함께 조회된다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
team.setName("team");
em.persist(team);
Member member1 = new Member();
member1.setName("member1");
member1.setTeam(team);
em.persist(member1);
Member member2 = new Member();
member2.setName("member2");
member2.setTeam(team);
em.persist(member2);
em.flush();
em.clear();
Team foundTeam = em.find(Team.class, team.getId());
// foundTeam.getMembers().getClass().getName() = org.hibernate.collection.internal.PersistentBag
System.out.println("foundTeam.getMembers().getClass().getName() = " + foundTeam.getMembers().getClass().getName());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
8.3.2 JPA 기본 페치 전략
- @ManyToOne, @OneToOne: 즉시 로딩
- @OneToMany, @ManyToMany: 지연 로딩
- 모든 연관관계에 지연 로딩을 사용하는 것을 추천하고, 개발이 어느정도 완료된 단계에 왔을 때 필요한 곳에만 즉시 로딩을 사용하도록 최적화 하는 것이 좋다.
8.3.3 컬렉션에 FetchType.EAGER 사용 시 주의점
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
- 서로 다른 컬렉션을 2개 이상 조인할 때, A 테이블을 N, M 두 테이블과 일대다 조인하는 경우에 SQL 실행 결과가 N * M이 되면서 너무 많은 데이터를 반환할 수 있다.
- 컬렉션 즉시 로딩은 항상 외부조인을 사용한다.
- 팀에서 회원 조회시 팀에 소속된 회원이 없을 수도 있기 때문에 외부 조인을 사용한다.
- @ManyToOne, @OneToOne
- (optional = false): 내부 조인
- (optional = true): 외부 조인
- @OneToMany, @ManyToMany
- (optional = false): 외부 조인
- (optional = true): 외부 조인
8.4 영속성 전이: CASCADE
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 기능이다.
8.4.1 영속성 전이: 저장
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child1 = new Child();
child1.setName("child1");
Child child2 = new Child();
child2.setName("child2");
Parent parent = new Parent();
parent.setName("parent");
child1.setParent(parent);
child2.setParent(parent);
parent.getChildren().add(child1);
parent.getChildren().add(child2);
em.persist(parent); // Parent에 연관된 Child 객체들 저장
em.flush();
em.clear();
Parent foundParent = em.find(Parent.class, parent.getId());
for (Child child : foundParent.getChildren()) {
System.out.println("child.getName() = " + child.getName());
}
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
- 영속성 전이는 연관관계 매핑과는 관련이 없다. 단지 엔티티를 영속화 할 때 연관된 엔티티도 같이 영속화 해준다. 따라서 연관 관계 설정은 직접 해줘야한다.
8.4.2 영속성 전이: 삭제
- 엔티티 삭제시 영속성 전이를 사용하면, 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제된다.
- 삭제 순서는 외래 키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제한다.
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Child> children = new ArrayList<>();
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child1 = new Child();
child1.setName("child1");
Child child2 = new Child();
child2.setName("child2");
Parent parent = new Parent();
parent.setName("parent");
child1.setParent(parent);
child2.setParent(parent);
parent.getChildren().add(child1);
parent.getChildren().add(child2);
em.persist(parent);
em.persist(child1);
em.persist(child2);
em.flush();
em.clear();
Parent foundParent = em.find(Parent.class, parent.getId());
em.remove(foundParent);
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
8.4.3 CASCADE 종류
- CascadeType의 종류는 여러가지가 있다.
public enum CascadeType {
/** Cascade all operations */
ALL,
/** Cascade persist operation */
PERSIST,
/** Cascade merge operation */
MERGE,
/** Cascade remove operation */
REMOVE,
/** Cascade refresh operation */
REFRESH,
/**
* Cascade detach operation
*
* @since 2.0
*
*/
DETACH
}
8.5 고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 고아 객체 제거라 한다.
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
- 또한 부모를 제거하면 자식도 같이 제거된다.
- 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 즉 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 한다.
- https://hibernate.atlassian.net/browse/HHH-6709
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Child child = new Child();
child.setName("child");
Parent parent = new Parent();
parent.setName("parent");
child.setParent(parent);
parent.getChildren().add(child);
em.persist(parent);
em.persist(child);
em.flush();
em.clear();
Parent foundParent = em.find(Parent.class, parent.getId());
foundParent.getChildren().remove(0);
//em.remove(foundParent);
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
8.6 영속성 전이 + 고아객체, 생명주기
- CascadeType.ALL + orphanRemoval = true 를 동시에 사용하면, 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
- 자식을 저장하려면 부모에 등록만 하면 된다(CASCADE).
Parent foundParent = em.find(Parent.class, parent.getId());
foundParent.addChild(child1);
foundParent.addChild(child2);
- 자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval).
Parent foundParent = em.find(Parent.class, parent.getId());
foundParent.getChildren().remove(0;
8.7 추가사항
- 엔티티 접근 방식을 필드(@Access(AccessType.FIELD))로 설정하고, 식별자 조회시 프록시 객체가 초기화 된다고 책에 나와있는데 실제로 해보면 초기화가 되지 않는 이유?
- “서로 다른 컬렉션을 2개 이상 조인할 때, A 테이블을 N, M 두 테이블과 일대다 조인하는 경우에 SQL 실행 결과가 N * M이 되면서 너무 많은 데이터를 반환할 수 있다.” 부분에서 실제로 Member에 Address, Order 두 개의 컬렉션을 만들고 즉시로딩으로 조회 하면 예외가 발생하여 조회 자체가 안되는 이유는?
Leave a comment