프록시
프록시란?
JPA에서 말하는 **프록시(Proxy)**는 실제 엔티티 객체를 감싸고 있는 가짜 객체(대리 객체)입니다.
- 이 프록시는 실제 객체처럼 행동하지만, 내부적으로는 실제 객체를 참조하지 않은 상태로 존재합니다.
- 실제 객체가 필요한 순간(=속성 접근)까지는 DB에 접근하지 않고 대기하다가,
- 속성이 호출되는 시점에 DB에서 데이터를 조회해서 영속성 컨텍스트에 등록하고, 실제 객체처럼 행동합니다.
Member member = entityManager.getReference(Member.class, 1L);
System.out.println(member.getClass());
// class com.example.Member$HibernateProxy$...
System.out.println(member.getName());
// 이 순간 SELECT 쿼리 실행됨
프록시의 특징 요약
항목 | 설명 |
실제 클래스? | 아님. 실제 엔티티를 상속한 프록시 클래스 |
쿼리 발생 시점? | 필드 접근 시점에 SELECT 쿼리 발생 |
사용 목적 | 지연 로딩을 가능하게 하기 위해 |
동일성(==) 보장? | JPA는 프록시든 실제 객체든 동일성 보장 |
왜 프록시를 쓰는가?
- 성능 최적화 → 불필요한 쿼리 실행을 피하고, 실제 필요한 시점까지 DB 접근을 미룸
- 지연 로딩 구현 → 연관관계에서 LAZY 설정을 사용하면 프록시를 통해 연관된 객체를 나중에 로딩할 수 있음
- JPA 내부 최적화 → getReference()로 불필요한 데이터 조회 없이 식별자만 가진 객체를 먼저 넘기고, 필요한 시점에만 조회
getReference()의 핵심 작동 원리
getReference()는 실제 엔티티 대신 프록시 객체를 반환합니다.
이 프록시는 필드가 실제로 사용되는 순간에 SELECT 쿼리를 날려
데이터를 로딩하고, 이후는 마치 원래 엔티티처럼 동작합니다.
예시 코드
@Entity
public class Member {
@Id
private Long id;
private String name;
// getters, setters
}
테스트 코드
// 테스트 코드
Member member = entityManager.getReference(Member.class, 1L);
System.out.println("프록시 객체 반환 완료");
// 아직 SELECT 쿼리가 실행되지 않음
System.out.println("member.getName() = " + member.getName());
// 이 시점에 SELECT 쿼리 실행
출력
프록시 객체 반환 완료
Hibernate: select member0_.id as id1_0_0_, member0_.name as name2_0_0_ from Member member0_ where member0_.id=?
member.getName() = 홍길동
프록시 객체를 통해 이미 존재하는 것처럼 객체를 전달합니다.
실제로 속성을 사용할 때만 DB에서 SELECT로 값 조회해서 영속성 컨텍스트에 등록합니다.
프록시가 그 데이터를 자신의 진짜 속성처럼 전달합니다.
즉 속성을 사용하지않는다면 쿼리는 DB에 전달되지않습니다. 속성을 사용하게 될 경우 DB에 쿼리가 전달되어 실제 엔티티의 값이 영속성 컨텍스트에 저장됩니다.
이후 프록시가 실제 엔티티의 값을 전달받아서 자신의 속성인 것처럼 전달해줍니다.
그렇기에 우리는 프록시로 반환된 member에서 .getName( )과 같은 메서드로 실제 DB의 값을 전달 받을 수 있습니다.
JPA의 동일성 보장
왜 JPA는 동일성(==)을 보장하려고 할까?
위에서 얘기했듯이 프록시가 되었든 실제 영속성 컨텍스트에 저장된 객체를 전달 받든 동일성을 보장해줘야합니다.
현재 어플리케이션에서 개발자가 코드를 작성하는데 있어 조작하는 객체는 동일해야지만 같은 결과를 기대할 수 있습니다.
이걸 이해하면 JPA가 단순히 ORM이 아니라 트랜잭션과 데이터 일관성 중심의 프레임워크라는 걸 알 수 있습니다.
정답
JPA는 "트랜잭션 단위의 엔티티 일관성"을 유지하기 위해 동일성을 보장합니다.
JPA가 동일성(Identity)을 보장해야 하는 이유
JPA는 같은 트랜잭션 내에서 동일한 식별자(ID)를 가진 엔티티는 항상 동일한 객체 인스턴스로 반환되도록 보장합니다.
이는 단순한 편의 기능이 아니라, JPA가 추구하는 트랜잭션 일관성과 변경 추적 기능의 핵심 원칙입니다.
동일성 보장의 필요성
변경 감지(Dirty Checking)의 정확한 동작이 기능이 제대로 동작하려면 하나의 엔티티 인스턴스만 존재해야 하며, 이를 위해 동일한 객체가 유지되어야 한다.
- JPA는 영속 상태의 엔티티에 대해 스냅샷을 보관하고, 트랜잭션 커밋 시점에 변경 여부를 비교하여 UPDATE 쿼리를 자동 생성한다.
1차 캐시 최적화
- 동일한 ID에 대해 여러 번 조회 요청이 들어올 경우, DB에 반복적으로 접근하지 않고 영속성 컨텍스트(1차 캐시) 에 있는 동일한 객체를 반환함으로써 성능을 최적화할 수 있습니다.
트랜잭션 내 정합성 유지를 해야한다. 그렇지 않으면 객체 간 상태 불일치가 발생하여 예기치 않은 결과를 초래할 수 있다.
- 트랜잭션은 논리적으로 하나의 작업 단위이며, 그 내부에서는 ID가 같은 엔티티는 항상 동일한 객체로 간주되어야 합니다.
연관관계 처리 및 프록시 안정성동일한 객체를 보장하면 프록시와 실체 간 혼란을 방지할 수 있다.
- 프록시 객체와 실체 객체가 각각 따로 존재할 경우, == 비교에서 false가 발생하거나 Lazy Loading 시점에 예외가 발생하는 등 다양한 문제가 생길 수 있습니다.
동일성 없으면 벌어질 수 있는 문제
Member m1 = em.find(Member.class, 1L);
Member m2 = em.find(Member.class, 1L);
m1.setName("홍길동");
m2.setName("이순신");
tx.commit();
만약 m1과 m2가 서로 다른 객체라면?
- 두 객체가 각자 UPDATE를 발생시켜 충돌하거나,
- 마지막에 수정한 값만 반영되어 예상 못한 결과 발생 가능
→ 동일 객체를 반환해야 "하나의 객체만 추적" 가능
프록시를 사용함으로써 쓰기 지연이 되기에 쿼리 최적화가 가능하다는 것이 핵심입니다.
다음에는 지연로딩과 즉시로딩으로 학습해보도록 하겠습니다.
'Dev Framework > Spring' 카테고리의 다른 글
[Spring] HttpServlet 완벽 정리 (0) | 2025.03.31 |
---|---|
[Spring] 스프링 AOP - 1 (0) | 2025.03.28 |
[JPA] JPA 영속성 컨텍스트 완전 정복 - 1 (0) | 2025.03.23 |
[SpringBoot] Lombok 사용 시 부모 생성자 호출 문제와 해결 방안 (0) | 2024.05.15 |
[Spring Security] 02. Http Basic이 아닌 OAuth2를 사용하는 이유 (0) | 2024.03.07 |
프록시
프록시란?
JPA에서 말하는 **프록시(Proxy)**는 실제 엔티티 객체를 감싸고 있는 가짜 객체(대리 객체)입니다.
- 이 프록시는 실제 객체처럼 행동하지만, 내부적으로는 실제 객체를 참조하지 않은 상태로 존재합니다.
- 실제 객체가 필요한 순간(=속성 접근)까지는 DB에 접근하지 않고 대기하다가,
- 속성이 호출되는 시점에 DB에서 데이터를 조회해서 영속성 컨텍스트에 등록하고, 실제 객체처럼 행동합니다.
Member member = entityManager.getReference(Member.class, 1L);
System.out.println(member.getClass());
// class com.example.Member$HibernateProxy$...
System.out.println(member.getName());
// 이 순간 SELECT 쿼리 실행됨
프록시의 특징 요약
항목 | 설명 |
실제 클래스? | 아님. 실제 엔티티를 상속한 프록시 클래스 |
쿼리 발생 시점? | 필드 접근 시점에 SELECT 쿼리 발생 |
사용 목적 | 지연 로딩을 가능하게 하기 위해 |
동일성(==) 보장? | JPA는 프록시든 실제 객체든 동일성 보장 |
왜 프록시를 쓰는가?
- 성능 최적화 → 불필요한 쿼리 실행을 피하고, 실제 필요한 시점까지 DB 접근을 미룸
- 지연 로딩 구현 → 연관관계에서 LAZY 설정을 사용하면 프록시를 통해 연관된 객체를 나중에 로딩할 수 있음
- JPA 내부 최적화 → getReference()로 불필요한 데이터 조회 없이 식별자만 가진 객체를 먼저 넘기고, 필요한 시점에만 조회
getReference()의 핵심 작동 원리
getReference()는 실제 엔티티 대신 프록시 객체를 반환합니다.
이 프록시는 필드가 실제로 사용되는 순간에 SELECT 쿼리를 날려
데이터를 로딩하고, 이후는 마치 원래 엔티티처럼 동작합니다.
예시 코드
@Entity
public class Member {
@Id
private Long id;
private String name;
// getters, setters
}
테스트 코드
// 테스트 코드
Member member = entityManager.getReference(Member.class, 1L);
System.out.println("프록시 객체 반환 완료");
// 아직 SELECT 쿼리가 실행되지 않음
System.out.println("member.getName() = " + member.getName());
// 이 시점에 SELECT 쿼리 실행
출력
프록시 객체 반환 완료
Hibernate: select member0_.id as id1_0_0_, member0_.name as name2_0_0_ from Member member0_ where member0_.id=?
member.getName() = 홍길동
프록시 객체를 통해 이미 존재하는 것처럼 객체를 전달합니다.
실제로 속성을 사용할 때만 DB에서 SELECT로 값 조회해서 영속성 컨텍스트에 등록합니다.
프록시가 그 데이터를 자신의 진짜 속성처럼 전달합니다.
즉 속성을 사용하지않는다면 쿼리는 DB에 전달되지않습니다. 속성을 사용하게 될 경우 DB에 쿼리가 전달되어 실제 엔티티의 값이 영속성 컨텍스트에 저장됩니다.
이후 프록시가 실제 엔티티의 값을 전달받아서 자신의 속성인 것처럼 전달해줍니다.
그렇기에 우리는 프록시로 반환된 member에서 .getName( )과 같은 메서드로 실제 DB의 값을 전달 받을 수 있습니다.
JPA의 동일성 보장
왜 JPA는 동일성(==)을 보장하려고 할까?
위에서 얘기했듯이 프록시가 되었든 실제 영속성 컨텍스트에 저장된 객체를 전달 받든 동일성을 보장해줘야합니다.
현재 어플리케이션에서 개발자가 코드를 작성하는데 있어 조작하는 객체는 동일해야지만 같은 결과를 기대할 수 있습니다.
이걸 이해하면 JPA가 단순히 ORM이 아니라 트랜잭션과 데이터 일관성 중심의 프레임워크라는 걸 알 수 있습니다.
정답
JPA는 "트랜잭션 단위의 엔티티 일관성"을 유지하기 위해 동일성을 보장합니다.
JPA가 동일성(Identity)을 보장해야 하는 이유
JPA는 같은 트랜잭션 내에서 동일한 식별자(ID)를 가진 엔티티는 항상 동일한 객체 인스턴스로 반환되도록 보장합니다.
이는 단순한 편의 기능이 아니라, JPA가 추구하는 트랜잭션 일관성과 변경 추적 기능의 핵심 원칙입니다.
동일성 보장의 필요성
변경 감지(Dirty Checking)의 정확한 동작이 기능이 제대로 동작하려면 하나의 엔티티 인스턴스만 존재해야 하며, 이를 위해 동일한 객체가 유지되어야 한다.
- JPA는 영속 상태의 엔티티에 대해 스냅샷을 보관하고, 트랜잭션 커밋 시점에 변경 여부를 비교하여 UPDATE 쿼리를 자동 생성한다.
1차 캐시 최적화
- 동일한 ID에 대해 여러 번 조회 요청이 들어올 경우, DB에 반복적으로 접근하지 않고 영속성 컨텍스트(1차 캐시) 에 있는 동일한 객체를 반환함으로써 성능을 최적화할 수 있습니다.
트랜잭션 내 정합성 유지를 해야한다. 그렇지 않으면 객체 간 상태 불일치가 발생하여 예기치 않은 결과를 초래할 수 있다.
- 트랜잭션은 논리적으로 하나의 작업 단위이며, 그 내부에서는 ID가 같은 엔티티는 항상 동일한 객체로 간주되어야 합니다.
연관관계 처리 및 프록시 안정성동일한 객체를 보장하면 프록시와 실체 간 혼란을 방지할 수 있다.
- 프록시 객체와 실체 객체가 각각 따로 존재할 경우, == 비교에서 false가 발생하거나 Lazy Loading 시점에 예외가 발생하는 등 다양한 문제가 생길 수 있습니다.
동일성 없으면 벌어질 수 있는 문제
Member m1 = em.find(Member.class, 1L);
Member m2 = em.find(Member.class, 1L);
m1.setName("홍길동");
m2.setName("이순신");
tx.commit();
만약 m1과 m2가 서로 다른 객체라면?
- 두 객체가 각자 UPDATE를 발생시켜 충돌하거나,
- 마지막에 수정한 값만 반영되어 예상 못한 결과 발생 가능
→ 동일 객체를 반환해야 "하나의 객체만 추적" 가능
프록시를 사용함으로써 쓰기 지연이 되기에 쿼리 최적화가 가능하다는 것이 핵심입니다.
다음에는 지연로딩과 즉시로딩으로 학습해보도록 하겠습니다.
'Dev Framework > Spring' 카테고리의 다른 글
[Spring] HttpServlet 완벽 정리 (0) | 2025.03.31 |
---|---|
[Spring] 스프링 AOP - 1 (0) | 2025.03.28 |
[JPA] JPA 영속성 컨텍스트 완전 정복 - 1 (0) | 2025.03.23 |
[SpringBoot] Lombok 사용 시 부모 생성자 호출 문제와 해결 방안 (0) | 2024.05.15 |
[Spring Security] 02. Http Basic이 아닌 OAuth2를 사용하는 이유 (0) | 2024.03.07 |