1) 프록시
프록시를 왜 배울까?
Member와 Team이 연관관계이고 Member를 조회할 때 Team도 함께 조회해야 할까?
- 둘 다 출력 -> 이 때는 팀과 멤버 둘 다 가져와야한다.
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
System.out.println("소속팀: " + team.getName());
}
- 회원만 출력 -> 이 때도 연관관계라고 사용하지 않는 팀까지 가져오면 손해다.
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
}
상황에 맞게 사용하기위하여 지연로딩과 즉시로딩을 알아야하고 이를 기초로 프록시 매커니즘을 이해해야한다.
프록시 특징
프록시 기초
- em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- 실제 클래스를 상속 받아서 만들어짐 , 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
- 프록시 객체는 실제 객체의 참조(target)를 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
프록시 객체의 초기화 흐름
Member member = em.getReference(Member.class, “id1”);
member.getName();
- 클라이언트가 엔티티 메소드 호출
- 프록시 객체가 생성 되고 초기화(영속성 컨텍스트에)를 요청한다
- 영속성 컨텍스트를 거쳐 DB에 조회 되고 실제 Entity 생성된다.
- 생성된 Entity에 프록시의 Member target이 걸리고 target.getName()이 호출된다.

프록시 특징
여기서 중요한 점은 프록시 반영하던 엔티티를 반영하던 동일한 트랜잭션 내에서는 동일한 객체의 주소를 반영 할 수 있게 유지시켜준다는게 JPA의 성질 (어차피 다루는건 똑같이 다룬다.)
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- getReference()를 통해 먼저 프록시를 만들고 반환했으면 find()로도 프록시가 호출된다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림/ 실무에서 많이 만난다.)
프록시 확인 메소드들
- 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
- 프록시 클래스 확인 방법 :
entity.getClass().getName() 출력(..javasist.. or
HibernateProxy…)
- 프록시 강제 초기화 :
org.hibernate.Hibernate.initialize(entity);
2) 지연로딩과 즉시로딩
지연 로딩 LAZY을 사용해서 프록시로 조회

@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;
..
}
위와 같이 만들고
- find()로 조회 했을 때 Team은 fetch 속성이 LAZY로 지연로딩되어 프록시로 가져오게 된다.
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
team.getName();
즉시 로딩 EAGER를 사용해서 함께 조회

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
- Member조회시 항상 Team도 조회
- JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용
(특히 실무에서 즉시 로딩을 사용하지 마라! )
(모든 연관관계에 지연 로딩을 사용해라!)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 바꿔줘야한다.
- @OneToMany, @ManyToMany는 기본이 지연 로딩
3) 영속성 전이: CASCADE 와 고아객체
영속성 전이: CASCADE에 대해
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때
ex) 1대다 관계의 부모(1) 엔티티를 저장할 때 자식(다) 엔티티도 함께 저장.
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
영속성 전이: CASCADE - 주의!
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
CASCADE의 종류
-
CascadeType.ALL : 저장과 삭제의 라이프 사이클을 함께 할 것인지.
-
CascadeType.PERSIST : 저장만 함께 할 것인지.
-
CascadeType.REMOVE : 삭제만 함께 할 것인지.
-
실무에서 잘 사용 안하는 것들:
MERGE: 병합
REFRESH: REFRESH
DETACH: DETACH
CASCADE 그럼 언제 사용하나?
- 하나의 부모가 자식들을 관리할 때 (단일 소유자 일 때)
ex) 게시판과 첨부파일의 경로
- 다른 데랑 연관관계가 있을때는 사용하면 안된다. 운영이 힘들어짐
- 라이프 사이클이 거의 유사할 때 사용한다.
고아 객체
- 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST , orphanRemoval = true)
이렇게 사용
- 자식 엔티티를 컬렉션에서 제거 했을 때, 테이블에서도 같이 사라진 것을 확인 가능
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);
고아 객체 - 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함! (특정 엔티티가 개인 소유할 때 사용)
- @OneToOne, @OneToMany만 가능
- 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
영속성 전이와 고아객체 동시 사용에서 얻는 의미
자식을 부모만을 이용하여 생명주기 관리를 가능하게한다!
- CascadeType.ALL + orphanRemovel=true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용 (이런 것이 있다는 것만 알고 넘어가기)