Proxy
프록시란?
실제 엔티티 대신 사용되는 가짜 객체
| 1
2
3
4
5
6
7
8
 | // DB를 통해 실제 엔티티 객체 조회
Member member = em.find(Member.class, 1L);
// DB조회를 미루는 가짜 프록시 엔티티 객체 조회
Member member = em.getReference(Member.class, 1L);  
System.out.println("member = " + member.getClass());
// member = class Member$HibernateProxy$...
member.getName();  // 이 순간 DB 조회
 | 
  - 프록시 객체에 getName()호출
- 프록시 객체가 실제 엔티티가 생성되지 않았으면 영속성 컨텍스트에 초기화 요청
- 영속성 컨텍스트가 DB 조회
- 실제 Entity 생성
- 프록시 객체가 실제 Entity를 참조하도록 연결
- 실제 Entity의 getName()반환
프록시 주의사항
프록시는 한 번만 초기화 된다
| 1
2
3
 | Member refMember = em.getReference(Member.class, 1L);
refMember.getName();  // 초기화
refMember.getAge();   // 이미 초기화됨, DB 조회 X
 | 
프록시 객체가 실제 엔티티로 바뀌는 것이 아니다
| 1
2
3
4
5
6
7
8
 | Member refMember = em.getReference(Member.class, 1L);
System.out.println("before: " + refMember.getClass());
// before: Member$HibernateProxy$...
refMember.getName();  // 초기화
System.out.println("after: " + refMember.getClass());
// after: Member$HibernateProxy$...  (여전히 프록시!)
 | 
  - 프록시 객체를 통해 실제 엔티티에 접근하는 것
- 초기화 후에도 프록시 객체는 프록시 객체
타입 비교는 == 대신 instanceof 사용
| 1
2
3
4
5
6
 | Member member1 = em.find(Member.class, 1L);      // 실제 엔티티
Member member2 = em.getReference(Member.class, 2L);  // 프록시
System.out.println(member1.getClass() == member2.getClass());  // false
System.out.println(member1 instanceof Member);  // true
System.out.println(member2 instanceof Member);  // true (권장)
 | 
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 실제 엔티티 반환
| 1
2
3
4
 | Member member1 = em.find(Member.class, 1L);  // 실제 엔티티
Member member2 = em.getReference(Member.class, 1L);  // 실제 엔티티 반환!
System.out.println(member1 == member2);  // true
 | 
  - JPA는 같은 트랜잭션 안에서 ==비교 시 true를 보장
- 이미 영속성 컨텍스트에 있으면 getReference()도 실제 엔티티 반환
준영속 상태일 때 프록시를 초기화하면 문제 발생
| 1
2
3
4
5
6
 | Member refMember = em.getReference(Member.class, 1L);
em.detach(refMember);  // 준영속 상태
// em.clear();
// em.close();
refMember.getName();  // LazyInitializationException 발생!
 | 
  - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태에서 프록시 초기화 시 예외 발생
지연 로딩과 즉시 로딩
지연로딩 (LAZY Loading)
연관된 엔티티를 실제 사용할 때까지 조회를 미루는 전략
| 1
2
3
4
5
6
7
8
9
10
 | @Entity
public class Member {
    @Id
    private Long id;
    private String username;
    
    @ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}
 | 
| 1
2
3
4
5
6
7
8
 | Member member = em.find(Member.class, 1L);
// SELECT * FROM MEMBER WHERE ID = 1  (Member만 조회)
Team team = member.getTeam();  // 프록시 객체
System.out.println(team.getClass());  // Team$HibernateProxy$...
team.getName();  // 이 순간 Team 조회
// SELECT * FROM TEAM WHERE ID = ?
 | 
  - Member 조회 시 Team은 프록시 객체로 가져옴
- team.getName()호출 시점에 실제 Team을 DB에서 조회
즉시로딩(EAGER Loading)
연관된 엔티티를 함께 조회하는 전략
| 1
2
3
4
5
6
7
8
9
10
 | @Entity
public class Member {
    @Id
    private Long id;
    private String username;
    
    @ManyToOne(fetch = FetchType.EAGER)  // 즉시 로딩
    @JoinColumn(name = "team_id")
    private Team team;
}
 | 
| 1
2
3
4
5
6
7
 | Member member = em.find(Member.class, 1L);
// SELECT M.*, T.* FROM MEMBER M
// JOIN TEAM T ON M.TEAM_ID = T.ID
// WHERE M.ID = 1
Team team = member.getTeam();  // 이미 조회됨, 프록시 아님
System.out.println(team.getClass());  // Team (실제 엔티티)
 | 
  - Member 조회 시 Team도 함께 조회 (join 사용)
- Team은 프록시가 아닌 실제 엔티티
LAZY, EAGER 선택 기준
| 1
2
3
4
5
6
7
8
9
10
 | // Member와 Team이 있을 때
// 경우 1: Member만 자주 사용
Member member = memberRepository.findById(1L);
// → LAZY 사용 (Team은 필요할 때만 조회)
// 경우 2: Member와 Team을 항상 함께 사용
Member member = memberRepository.findById(1L);
Team team = member.getTeam();
// → EAGER 사용? NO! 여전히 LAZY 권장
 | 
→ 가급적 LAZY만 사용하자
EAGER의 문제점
예상하지 못한 SQL 발생
| 1
2
3
 | // Member → Team, Order, Address 등 여러 관계가 EAGER면?
Member member = em.find(Member.class, 1L);
// 엄청나게 복잡한 JOIN 쿼리 발생!
 | 
JPQL에서 N+1 문제 발생
| 1
2
3
4
5
6
7
8
 | List<Member> members = em.createQuery("select m from Member m", Member.class)
    .getResultList();
// 1. SELECT * FROM MEMBER (members 조회)
// 2. SELECT * FROM TEAM WHERE ID = ? (member1의 team 조회)
// 3. SELECT * FROM TEAM WHERE ID = ? (member2의 team 조회)
// 4. SELECT * FROM TEAM WHERE ID = ? (member3의 team 조회)
// ...
// N개의 Member 조회 시 N번의 Team 조회 = N+1 문제
 | 
N+1 문제는 Fetch Join 으로 해결 가능.
| 1
2
3
4
5
6
7
 | // JPQL에서 fetch join 사용
List<Member> members = em.createQuery(
    "select m from Member m join fetch m.team", Member.class)
    .getResultList();
// SELECT M.*, T.* FROM MEMBER M
// JOIN TEAM T ON M.TEAM_ID = T.ID
// 한 방 쿼리로 해결
 | 
→ Fetch Join도 만능은 아니다. 페이징 처리나 컬렉션 페치 조인 시 주의가 필요하다.
@ManyToOne , @OneToOne 은 즉시로딩 디폴트
| 1
2
3
4
5
 | @ManyToOne  // 기본값: fetch = FetchType.EAGER
@OneToOne   // 기본값: fetch = FetchType.EAGER
@OneToMany  // 기본값: fetch = FetchType.LAZY
@ManyToMany // 기본값: fetch = FetchType.LAZY
 | 
→ 반드시 LAZY로 명시하자
| 1
2
 | @ManyToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY)
 | 
영속성 전이 (CASCADE)
CASCADE 란?
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
| 1
2
3
4
5
6
7
8
 | @Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();
}
 | 
CASCADE가 없는 경우
| 1
2
3
4
5
6
7
8
9
10
 | Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1);  // 각각 persist 해야 함
em.persist(child2);  // 각각 persist 해야 함
 | 
CASCADE가 있는 경우
| 1
2
3
4
5
6
7
8
 | Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);  // parent만 persist하면 children도 자동 persist
 | 
CASCADE 옵션 종류
| 1
2
3
4
5
6
7
8
9
10
11
12
13
 | public enum CascadeType {
    ALL,       // 모두 적용
    PERSIST,   // 영속
    REMOVE,    // 삭제
    MERGE,     // 병합
    REFRESH,   // 새로고침
    DETACH     // 준영속
}
//주로 사용하는 옵션
//CascadeType.ALL: 모든 작업 전이
//CascadeType.PERSIST: 저장 시에만 전이
//CascadeType.REMOVE: 삭제 시에만 전이
 | 
CASCADE 사용 시점
주의 → CASCADE는 연관관계 매핑과 아무 관련 없다.
단지 엔티티를 영속화 할때 연관된 엔티티도 함께 영속화 하는 편리함을 제공
사용조건 →
  - 단일 엔티티에 완전 종속일 때 (라이프사이클이 있는 경우)
- 소유자가 하나일 때
| 1
2
3
4
5
6
 | // 좋은 예: Parent-Child (Parent만 Child 관리)
Parent → Child (O)
// 나쁜 예: 여러 곳에서 Child 참조
Parent → Child
Member → Child  //(Child를 여러 곳에서 관리하면 안됨!)
 | 
고아 객체 (Orphan Removal)
고아 객체 제거란?
부모엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
| 1
2
3
4
5
 | @Entity
public class Parent {
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
 | 
| 1
2
3
4
 | Parent parent = em.find(Parent.class, 1L);
parent.getChildren().remove(0);  // 컬렉션에서 제거
// DELETE FROM CHILD WHERE ID = ?  (자동으로 삭제 쿼리 실행)
 | 
orphanRemoval vs CASCADE.REMOVE
| 1
2
3
4
5
6
7
8
9
10
 | // CascadeType.REMOVE
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
// 부모가 삭제되면 자식도 삭제
// orphanRemoval
@OneToMany(mappedBy = "parent", orphanRemoval = true)
// 부모와의 관계가 끊어지면 자식 삭제
// CASCADE.REMOVE: 부모 엔티티가 삭제될 때만 자식도 삭제
// orphanRemoval: 부모와의 연관관계가 끊어질 때도 자식 삭제
 | 
사용 조건
  - 참조하는 곳이 하나일 때만 사용해야 한다.
- 특정 엔티티의 개인 소유일 때
| 1
2
3
4
5
6
 | // 좋은 예: 게시판 - 첨부파일
Board → File (Board만 File 관리)
// 나쁜 예: 여러 곳에서 참조
Board → File
Member → File  //(File을 여러 곳에서 참조하면 안됨!)
 | 
영속성 전이 + 고아 객체
생명주기 관리
CascadeType.ALL + orphanRemoval = true
| 1
2
3
4
5
6
7
 | @Entity
public class Parent {
    @OneToMany(mappedBy = "parent", 
               cascade = CascadeType.ALL,
               orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
 | 
  - 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
- 자식을 저장하려면 부모만 등록하면 됨 → CASCADE
- 자식을 삭제하려면 부모에서 제거만 하면 됨 → OrphanRemoval
| 1
2
3
4
5
6
7
8
 | // 저장
Parent parent = new Parent();
Child child = new Child();
parent.addChild(child);
em.persist(parent);  // child도 자동 저장
// 삭제
parent.getChildren().remove(0);  // child 자동 삭제
 | 
→ DDD의 Aggregate Root 개념 구현 시 유용