2025. 3. 11. 22:42ㆍDevelop/Spring
프록시라는 개념은 추상적으로는 이해가 가는데 눈에 보이질 않으니 잘 와닿지가 않았다. 제대로 정리해보자.
프록시는 대리인(대신 처리하는 사람) 이라고 생각하면 된다.
즉, "어떤 객체(실제 객체)를 직접 호출하는 것이 아니라, 중간에서 대신 처리해주는 객체" 라는 뜻이다.
프록시를 다음과 같이 비유를 들어보자.
영화배우와 매니저
- 한 영화배우(실제 객체)가 있어.
- 하지만 배우는 너무 바빠서 기자 인터뷰 요청을 직접 받지 않고, 매니저(프록시 객체)를 통해서만 스케줄을 조정해.
- 매니저는 배우의 인터뷰 일정을 조율하고, 필요하면 취소하거나 조정하는 역할을 해.
프록시의 역할
- 인터뷰를 요청하면, 배우가 직접 받는 것이 아니라 매니저가 대신 받고 필터링해.
- 매니저가 "이 인터뷰는 안 되겠는데요"라고 하면 인터뷰가 진행되지 않음.
- 배우는 연기에만 집중하면 되고, 스케줄 관리는 매니저(프록시)가 해 줌.
프록시가 없다면
// 실제 인터뷰를 하는 배우 클래스
public class Actor {
public void giveInterview() {
System.out.println("🎤 배우: 인터뷰를 진행합니다!");
}
}
// 배우가 인터뷰를 직접 수행
public class Main {
public static void main(String[] args) {
Actor actor = new Actor();
actor.giveInterview(); // 배우가 직접 인터뷰
}
}
프록시를 적용한다면
// 실제 인터뷰를 하는 배우 클래스
public class Actor {
public void giveInterview() {
System.out.println("🎤 배우: 인터뷰를 진행합니다!");
}
}
// 프록시 역할을 하는 매니저 클래스
public class Manager {
private Actor actor;
public Manager(Actor actor) {
this.actor = actor;
}
public void scheduleInterview(boolean isImportant) {
if (isImportant) {
System.out.println("📅 매니저: 중요한 인터뷰입니다! 배우에게 전달합니다.");
actor.giveInterview();
} else {
System.out.println("⛔ 매니저: 이 인터뷰는 중요하지 않으므로 거절합니다.");
}
}
}
// 배우가 프록시(매니저)를 통해 인터뷰를 수행
public class Main {
public static void main(String[] args) {
Actor actor = new Actor();
Manager manager = new Manager(actor);
// 중요하지 않은 인터뷰
manager.scheduleInterview(false); // 거절됨
// 중요한 인터뷰
manager.scheduleInterview(true); // 배우가 인터뷰 진행
}
}
배우는 중요한 인터뷰만 진행할 수 있고, 불필요한 인터뷰는 매니저가 필터링함.
배우는 연기에만 집중할 수 있음.
보안, 로깅, 성능 개선 등의 부가 기능을 프록시에서 추가할 수 있음.
Spring에서의 프록시 적용
Spring에서 AOP(@Transactional, @Cacheable, @Security) 등을 사용할 때,
내부적으로 프록시 객체가 생성되어 실제 객체 대신 실행을 조정한다.
클라이언트 → Proxy (AOP) → 실제 객체 → 결과 반환
프록시를 이용한 @Transactional 동작
@Service
public class BankService {
@Transactional
public void transferMoney() {
System.out.println("💰 송금 진행 중...");
// DB에서 돈을 보내고 받는 작업
}
}
@Transactional을 사용하면, Spring이 자동으로 프록시 객체를 생성해서 트랜잭션을 관리한다.
- 프록시 객체가 트랜잭션을 시작하고 (setAutoCommit(false))
- transferMoney()를 실행한 뒤, 예외 발생 시 rollback(), 정상 실행 시 commit() 해 줌.
🎯 정리
✅ 프록시란?
👉 실제 객체를 직접 호출하는 것이 아니라, 중간에서 대신 호출하는 대리 객체
✅ 프록시의 역할
- 필터링 → 불필요한 요청을 막아줌 (ex: 매니저가 인터뷰 필터링).
- 부가 기능 추가 → 보안, 로깅, 트랜잭션 관리 가능 (ex: 비서가 CEO 이메일 필터링).
- 핵심 로직 보호 → 실제 객체는 핵심 로직에 집중할 수 있음.
✅ Spring에서 프록시 사용 예시
- AOP 기반 @Transactional → 트랜잭션 관리
- @Security → 권한 체크
- @Cacheable → 캐싱 기능 추가
그럼 영속성 컨텍스트에서 프록시는 어떻게 동작하나?
Spring의 JPA에서 영속성 컨텍스트(Persistence Context) 는 프록시 객체를 활용하여 지연 로딩(Lazy Loading) 을 처리한다.
즉, DB에서 즉시 데이터를 가져오지 않고, 필요할 때 실제 데이터를 조회하는 방식
🎭 비유: 도서관의 대출 시스템
🏛️ 시나리오
- 대학교 도서관에서 학생(클라이언트)이 책을 빌리려고 해.
- 하지만 모든 책을 도서관에 직접 두는 것이 아니라, 필요한 경우에만 창고(데이터베이스)에서 꺼내도록 설계되었어.
- 학생이 책을 빌리려고 하면, 도서관 직원(프록시)이 먼저 가짜 책(Proxy 객체)을 제공하고,
실제 책이 필요한 순간에 창고에서 가져다 줘.
🎯 프록시의 역할
- 학생이 책을 요청하면, 진짜 책을 주는 것이 아니라 "책이 있습니다"라는 더미 객체(프록시)를 줌.
- 학생이 책의 내용을 읽으려고 하면, 그때서야 창고에서 책을 꺼내서 가져다 줌(실제 DB 조회).
- 하지만, 책을 요청했지만 읽지 않으면 실제 데이터는 불러오지 않음 → 성능 최적화.
지연 로딩(Lazy Loading)과 프록시 객체
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(fetch = FetchType.LAZY) // 지연 로딩
private LibraryCard libraryCard;
}
- Student 엔티티를 조회하면 LibraryCard는 아직 로딩되지 않음.
- 대신, LibraryCard는 프록시 객체(가짜 객체) 로 설정됨.
- student.getLibraryCard().getCardNumber(); 를 호출하는 순간, 실제 DB에서 데이터를 가져옴.
Student student = entityManager.find(Student.class, 1L);
LibraryCard card = student.getLibraryCard(); // 아직 DB 조회 X (프록시 객체)
// 프록시가 실제 데이터를 가져오는 시점
String cardNumber = card.getCardNumber(); // 여기서 DB 조회 발생
JPA는 Hibernate를 통해 프록시 객체를 생성한다.
아래 코드를 실행해보면, 프록시 객체인지 확인할 수 있다.
Student student = entityManager.getReference(Student.class, 1L);
System.out.println(student.getClass());
class com.example.Student$HibernateProxy$XYZ123
Student$HibernateProxy$XYZ123 👉 Hibernate가 생성한 프록시 객체!
이 프록시는 실제 데이터가 필요할 때만 DB에서 데이터를 가져오는 역할을 함.
🛑 프록시 관련 문제점
❌ 1. 프록시 강제 초기화 문제 (LazyInitializationException)
JPA의 영속성 컨텍스트는 트랜잭션이 끝나면 닫히기 때문에,
영속성 컨텍스트가 닫힌 후 프록시 객체를 접근하면 LazyInitializationException 예외가 발생한다.
public void getStudent() {
Student student = studentRepository.findById(1L).get();
LibraryCard card = student.getLibraryCard(); // 프록시 객체
entityManager.close(); // 트랜잭션 종료
System.out.println(card.getCardNumber()); // 예외 발생 (LazyInitializationException)
}
여기서 OSIV를 살펴볼 수 있다.
OSIV(Open Session in View)
OSIV는 영속성 컨텍스트를 View 렌더링이 끝날 때까지 개방된 상태로 유지하는 방식이다.
osiv = true(default)

osiv = false
osiv가 true이면 controller와 view 단 까지 영속성 컨텍스트 범위가 늘어나 있기 때문에 그 때 까지 프록시객체에 접근이 가능할 것이다. (그냥 객체도 가능)
하지만 false이면 영속성 컨텍스트가 service와 repository 까지이기 때문에 controller에서 프록시 객체 혹은 그냥 객체에 접근하려 하면 LazyInitializationException가 발생하는 것이다.
✅ osiv가 true일 경우 장점
- 편리한 지연 로딩: Controller나 View에서도 지연 로딩 사용 가능
- 유연한 설계: 복잡한 화면 구성에서도 필요한 시점에 데이터 로딩 가능
- 개발 생산성: LazyInitializationException 걱정 없이 개발 가능
❎ osiv가 true일 경우 단점
- 데이터베이스 커넥션 점유: 컨트롤러와 뷰 렌더링이 끝날 때까지 DB 커넥션을 유지
- 실시간 트래픽: 커넥션 보유 시간이 길어져 서버 자원을 많이 사용
- 성능 이슈: 과도한 DB 커넥션 사용으로 인한 성능 저하 가능성
결론: 사실 OSIV를 비활성화(false) 하여 사용하는 것이 좋음
왜냐하면 controller에서는 요청을 받고 응답을 보내주는 책임만 질 뿐 프록시객체나 객체에 접근하는 것을 하는 건 좋지 않다.
따라서 false로 설정해두고 하는 것이 좋다.
spring:
jpa:
open-in-view: false
❌ 2. 프록시 객체 비교 문제
Student student1 = entityManager.find(Student.class, 1L);
Student student2 = entityManager.getReference(Student.class, 1L);
System.out.println(student1 == student2); // false가 나올 수도 있음!
find()는 진짜 객체를 반환하고, getReference()는 프록시 객체를 반환
따라서 동일한 엔티티라도 객체 비교(==)가 다르게 나올 수 있음.
'Develop > Spring' 카테고리의 다른 글
AOP? (0) | 2025.03.12 |
---|---|
테스트 코드 (0) | 2025.03.12 |
ORM은 왜 생겨났는가? JDBC부터 ORM까지 여정 (0) | 2025.03.11 |
일정관리 앱 트러블 슈팅 (0) | 2025.02.04 |
Spring에서 서버와 클라이언트 통신방식 정리 (0) | 2025.01.27 |