em.find와 em.getReference()의 차이가 뭘까? em.find()는 엔티티 조회에 쓰이며, 1차 캐시에서 필요한 엔티티를 찾고 없다면 DB까지 가서 엔티티를 가지고 와 1차 캐시에 넣어둔다. em.getReference()도 조회에 쓰이지만 Proxy를 조회해온다는 점이 다르다. 프록시 객체는 em.getReference()를 하게 되면, 우선 실제 엔티티의 틀(속성, 메서드 등)을 그대로 상속받아오지만 Entity target=null로 아무것도 가리키지 않는다. 따라서 실제 엔티티를 가리키지 않기 때문에 SELECT 쿼리가 발생하지 않는다. 이후,
Member reference = em.getReference(Member.class, member.getId())
reference.getName() 으로 실제 엔티티의 내용을 사용할 일이 오게되면 그제서야 Entity target = 실제 엔티티 객체를 가리키며 초기화된다.

사용하는 입장에서는 이게 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 되지만 프록시를 알아두면 좋다.

프록시 객체는 이처럼 target에 실제 객체의 참조를 보관해서, 프록시 객체를 호출하면 프록시는 실제 객체를 호출한다.
reference.getName()처럼 프록시 객체를 사용할 일이 생기면 초기화가 일어난다. 1. 영속성 컨텍스트에 초기화를 요청하면 2. 영속성 컨텍스트는 찾고자하는 Entity가 있는지 확인한다. 3. 없으면 영속성 컨텍스트가 DB에 요청해서 해당 엔티티 객체를 반환받고 4. 영속성 컨텍스트는 1차 캐시에 해당 엔티티를 저장하고 5. 프록시 객체에게 실제 엔티티 객체를 반환하여 초기화를 완료한다. DB를 다녀오면 SELECT 쿼리가 발생하며, 이후 추가적으로 엔티티 메서드나 속성을 사용할 일이 생겨도 SELECT가 발생하지 않는다.

프록시 객체는 처음 사용(reference.getClass()(X))할때만 한번만 초기화되고 이후에는 초기화할 필요가 없다. 프록시 객체를 초기화하면 실제 엔티티객체로 변하는게 아니라, 프록시 객체가 엔티티 객체를 가리킬 뿐(접근 가능해질 뿐)이다. Entity target = 실제 엔티티 객체 프록시 객체는 == 비교가 아니라 instanceof를 통해 객체비교를 해야 올바르게 비교할 수 있다.
1. Member findMember = em.find(Member.class, m.getId()) ➡️ Entity객체
2. Member reference = em.getReference(Member.class, m.getId()) ➡️ 같은 Entity 객체
❔이유는 이미 영속성 컨텍스트에 실제 엔티티 객체를 가져온 상태인데, 굳이 메모리를 써가면서 프록시 객체를 따로 또 생성할 이유가 없기 때문이다. 그리고 JPA는 같은 트랜잭션 안에서 동일한 엔티티에 대해 같음을 보장하기 때문에 이를 만족하기 위해 엔티티 객체가 반환된다.
1. Member reference = em.getReference(Member.class, m.getId()) ➡️ Proxy객체
2. Member findMember = em.find(Member.class, m.getId()) ➡️ 같은 Proxy 객체
❔이뉴는 이미 영속성 컨텍스트에 프록시 객체를 만들어둔 상태인데, 굳이 메모리를 써가면서 실제 엔티티를 따로 생성할 이유가 없기 때문이다. 그리고 JPA는 같은 트랜잭션 안에서 동일 엔티티에 대해 같음을 보장해야하기 때문에 이를 만족하기 위해 같은 프록시 객체가 반환된다.
만약 em.getReference()로 프록시 객체를 생성하고나서 em.detach(member)이나 em.clear()이나 em.close()로 더이상 영속성 컨텍스트의 도움을 받을 수 없는 준영속상태가 되면, 초기화할때 LazyInitializationException 예외가 발생할 수 있다. 이유는 더이상 프록시 객체가 영속성 컨텍스트에서 관리되는 객체가 아니기 때문이다.
🔎 프록시 인스턴스가 초기화됐는지 확인하는 방법 : PersistenceUnitUtil.isLoaded(Object entity)
🔎 프록시의 클래스를 확인하는 방법 : entity.getClass()
🔎 프록시를 강제 초기화하는 방법 : 프록시 인스턴스명.getXXX() 실제로 사용하는 수밖에 없다.
지연로딩 LAZY((fetch = FetchType.LAZY))를 사용해서 Member를 조회해온다고 가정해보자. 그럼 연관관계에 있는 Team은 프록시 객체로 생성된다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
MemberTest member1 = new MemberTest();
member1.setName("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
MemberTest m = em.find(MemberTest.class, member1.getId()); //Member 만 SELECT-초기화
System.out.println("m = " + m.getTeam().getClass()); // Proxy
m.getTeam().getName(); // Team SELECT-초기화
실제로 사용하는 시점에 Team엔티티 객체로 초기화되면서 SELECT 쿼리가 나감을 확인할 수 있다.
만약 Member과 Team을 같이 사용할 일이 많다면 즉시로딩 EAGER(fetch = FetchType.EAGER)를 사용해서 em.find할때 Member와 Team을 함께 SELECT해오게 된다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
MemberTest member1 = new MemberTest();
member1.setName("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
MemberTest m = em.find(MemberTest.class, member1.getId()); //Member과 Team 초기화
System.out.println("m = " + m.getTeam().getClass()); // Entity
m.getTeam().getName();
가급적이면 실무에서는 지연로딩을 사용하는 것을 권장한다. 예상치못하게 SQL이 날아갈 수 있고 즉시로딩은 JPQL에서 N+1문제가 발생하기 때문이다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
MemberTest member1 = new MemberTest();
member1.setName("member1");
member1.setTeam(team);
em.persist(member1);
MemberTest member2 = new MemberTest();
member2.setName("member2");
member2.setTeam(team2);
em.persist(member2);
em.flush();
em.clear();
List<MemberTest> members = em.createQuery("select m from MemberTest m",
MemberTest.class).getResultList(); // JPQL : 그대로 SQL 번역, 1회 쿼리, 이후 Member 갯수만큼(N) Team SELECT
// SQL : select * from Member
// EAGER
// SQL : select * from Team where TEAM_ID = member.TEAM_ID
//LAZY 변경시 1회 쿼리
JPQL로 작성하면 ""안에 있는 쿼리대로 MemberTest테이블만 조회하는 쿼리가 나간다. 이후 객체와 매핑될때 (fetch = FetchType.EAGER)를 확인하고 연관된 Team객체를 하나씩 SELECT해오게 된다. 그래서 1번 MemberTest를 모두 조회해오고나서 각 MemberTest와 연관된 Team객체를 SELECT하여 MemberTest 리스트 갯수만큼 N개의 쿼리가 발생한다. 그래서 N+1문제라고 한다. 이 문제가 발생하면 1. 지연로딩으로 바꾸고 2. fetch join으로 바꾸면 한방 쿼리가 되서 1번의 쿼리만 나가 해결된다.
연관관계 매핑 어노테이션 중에 @~ToOne으로 끝나는 매핑은 모두 즉시로딩이 기본적으로 설정되어있다. 따라서 LAZY로 변경해주는 것이 필수다. 모든 연관관계는 실무에서 지연로딩을 사용하는 것을 권장하고, JPQL을 사용한다면 fetch join과 엔티티 그래프 기능을 사용하는 것을 권장한다.
지연로딩, 즉시로딩과는 전혀 관계없는 개념이다. 특정 엔티티를 영속상태로 만들때 연관된 엔티티도 함께 영속화해주는 기능이다. 예를 들어서 Parent클래스와 Child클래스가 있을때 Parent클래스.list로 Child클래스가 연관되어있는 경우 Parent클래스 객체를 em.persist하면 자동으로 list로 엮인 Child클래스 객체도 em.persist가 되어 INSERT 쿼리가 나간다. List를 가진 곳에 cascade 옵션을 이용해서 CascadeType.ALL이나 CascadeType.PERSIST를 사용하면 영속성전이를 사용할 수 있다.

@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // cascade : 컬렉션 내용 모두 persist
List<Child> children = new ArrayList<>();
...
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // CASCADE X :3개 persist 필요, CASCADE O : 1개 persist로 컬렉션 내용 모두 INSERT
// em.persist(child1);
// em.persist(child2);
주의할 점은 연관관계 매핑이라는 아무 관련이 없다는 것이다. 단지 연관된 엔티티도 함께 영속성 컨텍스트에 영속화해주는 것(em.persist)임을 기억하자. 예를 들면 게시판이나 첨부파일 같은 기능을 개발한다고 했을때 하나의 게시판에 여러 댓글이 달리는 것처럼 한 엔티티(게시판)에서만 여러 엔티티(댓글들)를 관리하고 소유자가 하나(게시판)일 경우에만 cascade옵션을 사용해야한다. 즉, 단일 엔티티(게시판)에만 완전히 여러 엔티티(댓글)가 종속적이고 + LifeCycle(게시판, 댓글)이 거의 유사할 때 사용해야한다.
cascade 종류에는 ALL(모두 적용), PERSIST(영속), REMOVE(삭제), 등이 있는데 ALL과 PERSIST만 사용하는 게 좋다. 나머지는 삭제되는 등 위험하기 때문이다.
부모엔티티와 자식엔티티(부모 클래스에 정의된 속성 중 List로 자식엔티티가 연관된 경우)가 있을때, 자식엔티티가 부모엔티티와 연관관계가 끊어지면 자동으로 DELETE 쿼리로 삭제되는 것을 말한다. 이 기능은 부모 클래스에 자식 컬렉션으로 정의된 속성에 orphanRemoval=true를 적용하면 기능을 사용할 수 있다.
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL,orphanRemoval = true) // orphanRemoval : 참조 제거 엔티티 삭제
List<Child> children = new ArrayList<>();
/*
* 고아객체 조건(cascade = CascadeType.ALL/CascadeType.PERSIST + orphanRemoval = true)
* 1. 부모 엔티티 삭제 (em.remove(parent)) : 부모,자식 모두 DELETE
* 2. 부모 엔티티와 연관된 자식 엔티티 컬렉션 제거 (parent.getChildren().remove(0)) : 자식 DELETE
* */
public void addChild(Child child){
children.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); // CascadeType.ALL/PERSIST로 부모,자식 INSERT
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildren().remove(0); // 참조 제거된 엔티티 삭제
부모가 삭제되면 자동으로 List컬렉션으로 설정된 자식들도 삭제된다.
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // CascadeType.ALL/PERSIST로 부모,자식 INSERT - LifeCycle 관리(DAO, Repository x)
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent); // 부모 삭제 -> 자식 컬렉션 함께 삭제 - LifeCycle 관리(DAO, Repository x)
고아객체를 사용할때 주의할 점은 참조하는 곳이 하나일때만 사용해야하고, 특정 엔티티만 개인 소유할 때만 사용해야한다. @OneToOne이나 @OneToMany 처럼 한 엔티티에서 여러 엔티티를 관리하거나 하나의 엔티티를 관리할때만 사용할 수 있다. CasecadeType.ALL/PERSIST + orphanRemoval = true를 함께 사용하면 영속성 컨텍스트에 저장할때 함께 연관된 엔티티도 등록하면서 고아객체가 되면 자동으로 삭제하는 기능을 가지게 된다. 두 옵션을 활성화하면 부모 엔티티를 통해서 자식 엔티티를 관리하므로 생명주기를 관리할 수 있다고도 말할 수 있다. LifeCycle이 부모와 자식이 동일할때 사용하면좋다.