1. 프록시
1.1. 프록시의 필요성
문제 상황: Member를 조회할 때 항상 Team도 함께 조회해야 할까?

// 케이스 1: 회원과 팀 정보를 함께 사용하는 경우
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()); // Team 정보 필요
}
// 케이스 2: 회원 정보만 사용하는 경우
public void printUser(String memberId) {
Member member = em.find(Member.class, memberId);
System.out.println("회원 이름: " + member.getUsername()); // Team 정보 불필요
}
위 두 케이스에서 em.find()는 항상 Member와 연관된 Team까지 함께 조회하게 된다. 이는 성능상 비효율적이다.
1.2. 프록시 기초
JPA는 이 문제를 해결하기 위해 프록시(Proxy)라는 가짜 엔티티 객체를 제공한다.
// 실제 엔티티 조회
Member member = em.find(Member.class, "member1"); // 즉시 DB 조회
System.out.println("member.getClass() = " + member.getClass());
// 출력: class hellojpa.Member (실제 엔티티)
// 프록시 엔티티 조회
Member proxyMember = em.getReference(Member.class, "member1"); // DB 조회 지연
System.out.println("proxyMember.getClass() = " + proxyMember.getClass());
// 출력: class hellojpa.Member$HibernateProxy$xxx (프록시 객체)
em.find() vs em.getReference()
- em.find(): 데이터베이스를 통한 즉시 실제 엔티티 객체 조회
- em.getReference(): 데이터베이스 조회를 미루는 프록시 엔티티 객체 반환

1.3. 프록시 특징
프록시는 실제 클래스를 상속받아 만들어지기 때문에 겉모양이 실제 클래스와 동일하다.
// 프록시 객체 구조 (개념적)
class MemberProxy extends Member {
private Member target = null; // 실제 엔티티 참조
public String getName() {
if (target == null) {
// 초기화: 실제 엔티티 조회
this.target = ...; // DB에서 실제 Member 조회
}
return target.getName(); // 실제 엔티티의 메소드 호출
}
}
1.4. 프록시 객체의 초기화
Member proxyMember = em.getReference(Member.class, "member1"); // DB 조회 X
// 프록시 객체 사용
proxyMember.getName(); // 이 시점에서 DB 조회 (초기화)

초기화 과정
- 프록시 객체의 메소드 호출 (getName())
- 프록시 객체가 영속성 컨텍스트에 초기화 요청
- 영속성 컨텍스트가 데이터베이스 조회
- 실제 엔티티 생성 및 프록시 객체와 연결
- 프록시 객체가 실제 엔티티의 메소드 호출
1.5. 프록시의 특징 정리
- 초기화는 한 번만: 프록시 객체가 초기화되면 실제 엔티티에 접근 가능
- 타입 비교 주의: 프록시는 원본을 상속받으므로 == 비교 실패
Member m1 = em.find(Member.class, memberId); Member m2 = em.getReference(Member.class, memberId); System.out.println(m1 == m2); // false (프록시 vs 실제) System.out.println(m1 instanceof Member); // true System.out.println(m2 instanceof Member); // true (프록시도 Member 타입) - 영속성 컨텍스트에 있으면 실제 엔티티 반환
Member m1 = em.find(Member.class, memberId); // 실제 엔티티 Member m2 = em.getReference(Member.class, memberId); // 실제 엔티티 반환 - 준영속 상태에서 초기화 불가
Member proxyMember = em.getReference(Member.class, "member1"); em.detach(proxyMember); // 준영속 상태로 변경 proxyMember.getName(); // LazyInitializationException 발생
1.6. 프록시 확인
// 프록시 인스턴스 초기화 여부 확인
PersistenceUnitUtil util = em.getEntityManagerFactory().getPersistenceUnitUtil();
boolean isLoaded = util.isLoaded(entity);
// 프록시 클래스 확인
System.out.println(entity.getClass().getName());
// 출력 예: com.example.Member$HibernateProxy$...
// 프록시 강제 초기화 (하이버네이트 전용)
Hibernate.initialize(entity);
// JPA 표준 강제 초기화 방법
entity.getSomeField(); // 메소드 호출로 초기화
2. 즉시 로딩과 지연 로딩
2.1. 지연 로딩 (LAZY)
연관된 엔티티를 실제 사용할 때까지 조회를 지연시키는 전략이다.

@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정
@JoinColumn(name = "TEAM_ID")
private Team team; // 프록시 객체로 조회
}
지연 로딩 사용 예시

// Member만 조회 (Team은 조회 안됨)
Member member = em.find(Member.class, "member1");
System.out.println("member = " + member.getName());
// Team 사용 시점에 조회
Team team = member.getTeam(); // 이 시점에서 Team 조회 (프록시 초기화)
System.out.println("team = " + team.getName());
실행되는 SQL
-- em.find(Member.class, "member1") 실행 시
SELECT m.* FROM MEMBER m WHERE m.ID = ?
-- member.getTeam().getName() 실행 시
SELECT t.* FROM TEAM t WHERE t.ID = ?
2.2. 즉시 로딩 (EAGER)
연관된 엔티티를 함께 조회하는 전략이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 설정
@JoinColumn(name = "TEAM_ID")
private Team team; // 함께 조회
}
즉시 로딩 사용 예시
// Member와 Team을 함께 조회
Member member = em.find(Member.class, "member1");
System.out.println("member = " + member.getName());
System.out.println("team = " + member.getTeam().getName()); // 이미 조회됨
실행되는 SQL (조인 사용)
-- em.find(Member.class, "member1") 실행 시
SELECT m.*, t.*
FROM MEMBER m
LEFT OUTER JOIN TEAM t ON m.TEAM_ID = t.ID
WHERE m.ID = ?
2.3. 프록시와 즉시로딩 주의사항
중요 규칙
- 가급적 지연 로딩만 사용 (특히 실무에서)
- 즉시 로딩은 JPQL에서 N+1 문제 발생
- 기본 페치 전략 변경 필요
- @ManyToOne, @OneToOne: 기본이 즉시 로딩 → LAZY로 변경
- @OneToMany, @ManyToMany: 기본이 지연 로딩 → 변경 불필요
N+1 문제 예시
// JPQL 사용 시
List<Member> members = em.createQuery(
"SELECT m FROM Member m", Member.class)
.getResultList();
// 실행되는 SQL (Member 수만큼 Team 조회 쿼리 추가)
// 1. SELECT m.* FROM MEMBER m
// 2. SELECT t.* FROM TEAM t WHERE t.ID = ? (N번 실행)
3. 지연 로딩 활용
3.1. 다양한 연관관계 로딩 전략
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// 자주 함께 사용 -> 즉시 로딩 (이론적)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 가끔 사용 -> 지연 로딩
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
// 자주 함께 사용 -> 즉시 로딩 (이론적)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
3.2. 실무 적용 가이드라인
핵심 원칙: 모든 연관관계에 지연 로딩을 사용하라!
// 올바른 설정 예시
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY) // 항상 LAZY
@JoinColumn(name = "TEAM_ID")
private Team team;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 항상 LAZY
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
즉시 로딩 대체 방법
- Fetch Join: JPQL에서 함께 조회
String jpql = "SELECT m FROM Member m JOIN FETCH m.team"; List<Member> members = em.createQuery(jpql, Member.class).getResultList(); - 엔티티 그래프: @NamedEntityGraph 사용
- 배치 사이즈: @BatchSize 애노테이션 사용
4. 영속성 전이: CASCADE
4.1. 개념
특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만드는 기능이다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
4.2. 영속성 전이 저장 (⭐⭐)

// CASCADE 없을 때
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // Parent 저장
em.persist(child1); // Child1 저장
em.persist(child2); // Child2 저장 (총 3번 persist)
// CASCADE 있을 때
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // Parent만 저장하면 Child도 자동 저장
4.3. CASCADE 종류
| CASCADE 타입 | 설명 |
| ALL | 모든 영속성 전이 적용 (PERSIST + REMOVE + MERGE + REFRESH + DETACH) |
| PERSIST | 부모 저장 시 자식도 저장 |
| REMOVE | 부모 삭제 시 자식도 삭제 |
| MERGE | 부모 병합 시 자식도 병합 |
| REFRESH | 부모 refresh 시 자식도 refresh |
| DETACH | 부모 detach 시 자식도 detach |
4.4. CASCADE 주의사항
CASCADE는 연관관계 매핑과 무관하다!
- 단순히 편의 기능 제공
- 부모-자식 관계에서 사용 (라이프사이클이 동일할 때)
- 다른 엔티티에서도 참조하면 사용하지 말 것
// 잘못된 사용 예시
@Entity
public class Member {
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>(); // Order는 다른 곳에서도 참조 가능
}
5. 고아 객체
5.1. 개념
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.
@Entity
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
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
5.2. 고아 객체 제거 동작
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(0); // 컬렉션에서 자식 제거
// 실행되는 SQL
// DELETE FROM CHILD WHERE ID = ?
5.3. 고아 객체 주의사항
- 참조하는 곳이 하나일 때만 사용
// 여러 곳에서 참조하는 경우 사용 금지 @Entity public class Product { @ManyToOne private Category category; // 여러 OrderItem에서 참조 가능 } - @OneToOne, @OneToMany에서만 사용 가능
- 개념적 소유 관계일 때 사용
- 부모 삭제 시 자식도 함께 삭제됨 (CascadeType.REMOVE와 유사)
6. 영속성 전이 + 고아 객체, 생명주기
6.1. 조합 사용
CascadeType.ALL + orphanRemoval = true를 함께 사용하면 부모 엔티티를 통해 자식의 생명주기를 완전히 관리할 수 있다.
@Entity
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<>();
// 연관관계 편의 메서드
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
public void removeChild(Child child) {
children.remove(child);
child.setParent(null);
}
}
6.2. 생명주기 관리
// 자식의 생명주기를 부모가 관리
Parent parent = new Parent("부모");
Child child1 = new Child("자식1");
Child child2 = new Child("자식2");
// 자식 추가 (생성)
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // 자식도 함께 저장
// 자식 제거 (삭제)
parent.removeChild(child1); // 자식1 삭제 (고아 객체)
// 부모 수정 (자식 업데이트)
Child child3 = new Child("자식3");
parent.addChild(child3); // 자식3 추가
parent.getChildren().get(0).setName("수정된 자식"); // 자식 수정
em.merge(parent); // 모든 변경사항 적용
6.3. DDD Aggregate Root 구현
도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
// 주문 Aggregate Root
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "order",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
// 비즈니스 메서드들
public void addOrderItem(Product product, int count) {
OrderItem orderItem = new OrderItem(this, product, count);
orderItems.add(orderItem);
}
public void removeOrderItem(OrderItem orderItem) {
orderItems.remove(orderItem);
}
public void cancel() {
// 주문 취소 로직
this.status = OrderStatus.CANCELLED;
}
// 주문 금액 계산
public int getTotalPrice() {
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
7. 실전 예제 - 연관관계 관리
7.1. 글로벌 페치 전략 설정
모든 연관관계를 지연 로딩으로 설정
// 회원 엔티티
@Entity
@Table(name = "member")
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member") // 기본이 LAZY
private List<Order> orders = new ArrayList<>();
}
// 주문 엔티티
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // LAZY로 명시적 설정
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) // LAZY 설정
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
// 주문상품 엔티티
@Entity
@Table(name = "order_item")
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // LAZY 설정
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY) // LAZY 설정
@JoinColumn(name = "item_id")
private Item item;
private int orderPrice;
private int count;
}
// 배송 엔티티
@Entity
@Table(name = "delivery")
public class Delivery {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY) // LAZY 설정
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
}
7.2. 영속성 전이 설정
연관된 엔티티의 생명주기를 함께 관리
// 주문 엔티티 - 연관된 엔티티들에 영속성 전이 설정
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
// OrderItem에 영속성 전이 ALL 설정
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
// Delivery에 영속성 전이 ALL 설정
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
// 연관관계 편의 메서드
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
// 생성 메서드
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
order.setOrderDate(LocalDateTime.now());
order.setStatus(OrderStatus.ORDER);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
return order;
}
// 비즈니스 로직
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
// 조회 로직
public int getTotalPrice() {
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
7.3. 서비스 레이어 예시
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
private final EntityManager em;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
private final OrderRepository orderRepository;
// 주문 생성
@Transactional
public Long order(Long memberId, Long itemId, int count) {
// 엔티티 조회
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다."));
Item item = itemRepository.findById(itemId)
.orElseThrow(() -> new EntityNotFoundException("상품을 찾을 수 없습니다."));
// 배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
delivery.setStatus(DeliveryStatus.READY);
// 주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
// 주문 생성 (영속성 전이로 인해 delivery, orderItem 자동 저장)
Order order = Order.createOrder(member, delivery, orderItem);
// 주문 저장
em.persist(order);
return order.getId();
}
// 주문 취소
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("주문을 찾을 수 없습니다."));
// 주문 취소 (연관된 엔티티들도 함께 업데이트)
order.cancel();
}
// 주문 조회 (지연 로딩 최적화)
public OrderDto getOrder(Long orderId) {
// 페치 조인으로 한 번에 조회
Order order = em.createQuery(
"SELECT o FROM Order o " +
"JOIN FETCH o.member m " +
"JOIN FETCH o.delivery d " +
"JOIN FETCH o.orderItems oi " +
"JOIN FETCH oi.item i " +
"WHERE o.id = :orderId", Order.class)
.setParameter("orderId", orderId)
.getSingleResult();
return new OrderDto(order);
}
}
8. 정리
8.1. 핵심 원칙
- 지연 로딩을 기본으로 사용
- 모든 @ManyToOne, @OneToOne에 fetch = FetchType.LAZY 설정
- 필요할 때만 Fetch Join이나 엔티티 그래프 사용
- 영속성 전이 신중하게 사용
- 라이프사이클이 완전히 동일한 부모-자식 관계에서만 사용
- CascadeType.ALL + orphanRemoval = true 조합으로 생명주기 관리
- 프록시 이해 필수
- 타입 비교는 instanceof 사용
- 초기화 여부 확인 가능
- 준영속 상태에서 초기화 불가
8.2. 실무 적용 체크리스트
✅ 해야 할 것
- 모든 연관관계에 지연 로딩 적용
- 필요할 때만 Fetch Join 사용
- 영속성 전이는 진짜 부모-자식 관계에서만
- 연관관계 편의 메서드 구현
- 서비스 레이어에서 트랜잭션 관리
❌ 하지 말 것
- 즉시 로딩 사용
- 불필요한 영속성 전이 설정
- 여러 곳에서 참조하는 엔티티에 고아 객체 제거 설정
- 프록시를 ==로 비교
프록시와 연관관계 관리는 JPA 성능 최적화의 핵심이다. 지연 로딩을 기본으로 사용하고, 필요할 때만 적절한 최적화 기법을 적용하는 것이 실무에서 안정적인 애플리케이션을 구축하는 길이다.
'Spring > JPA' 카테고리의 다른 글
| [Basic-9] 객체지향 쿼리 언어(JPQL) (0) | 2026.01.06 |
|---|---|
| [Basic-8] 값 타입 (0) | 2026.01.06 |
| [Basic-6] 고급 관계 매핑 (0) | 2026.01.06 |
| [Basic-5] 연관관계 매핑 심화 (0) | 2026.01.06 |
| [Basic-4] 연관관계 매핑 기초 (0) | 2026.01.06 |
