7주차 Unit 7.1 — 프록시 패턴

Psj·2026년 6월 5일

F-lab

목록 보기
234/239

Unit 7.1 — 프록시 패턴

F-LAB JAVA · 7주차 · Phase 7 · @Transactional
★ 깊이 파기 — @Transactional 의 핵심 기반, Phase 7 시작


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 프록시 패턴 의 정의는?
  • 프록시의 역할 4가지는?
  • JDK 동적 프록시 의 동작은?
  • CGLIB 프록시 의 동작은?
  • Spring 의 프록시 선택 기준은?
  • AOP 와 프록시 의 관계는?
  • @Transactional 의 기반 으로서 프록시는?
  • JPA Lazy Loading 의 프록시 연결은?
  • Phase 7.2 (★★★) 예고 는?

🎯 핵심 한 문장

프록시 패턴은 실제 객체의 대리인 (Proxy) 을 만들어 호출을 가로채고 부가 기능 (트랜잭션·로깅·보안) 을 자동 적용하는 5주차 디자인 패턴이며, 자바에서는 JDK 동적 프록시 (인터페이스 기반, java.lang.reflect.Proxy) 와 CGLIB 프록시 (구체 클래스의 자식 클래스 동적 생성) 두 종류가 있고 Spring Boot 2.x+ 는 기본 CGLIB 를 사용하며, 이 프록시가 @Transactional 자동화와 JPA Lazy Loading 모두의 직접적 기반이다.
프록시 패턴 (Proxy Pattern) 은 5주차 디자인 패턴의 하나로 — 실제 객체 (Real Subject) 의 대리인 (Proxy) 을 만들어 클라이언트가 직접 객체에 접근하지 않고 프록시를 통해 접근 하게 한다.
자바의 두 종류 프록시 — (1) JDK 동적 프록시 (java.lang.reflect.Proxy, 인터페이스 기반, 인터페이스의 구현 클래스를 런타임 생성, InvocationHandler 로 호출 가로채기), (2) CGLIB 프록시 (구체 클래스 기반, 대상 클래스의 자식 클래스를 바이트코드 조작으로 동적 생성, MethodInterceptor 로 가로채기).
Spring 의 선택 — Spring Boot 2.x+ 부터 기본 CGLIB (이전엔 인터페이스 있으면 JDK, 없으면 CGLIB), 이유는 인터페이스 없는 클래스도 프록시 가능 + 일관성, 설정으로 변경 가능 (spring.aop.proxy-target-class=false).
프록시의 역할 4가지 — (1) 호출 가로채기 (Interception), (2) 부가 기능 (Cross-cutting Concern, 트랜잭션·로깅·보안·캐시), (3) 진짜 객체에 위임, (4) 결과 후처리.
이 프록시 패턴이 @Transactional 자동화의 직접 기반 (다음 Unit 7.2 ★★★) + JPA 의 Lazy Loading (@ManyToOne(fetch = LAZY) 시 프록시 객체) + 5주차 학습의 정점 이다.

비유 — 비서 (대리인)

프록시 패턴 = 비서:

상황:
  - CEO (실제 객체)
  - 비서 (프록시)
  - 손님 (클라이언트)

비서의 역할:
  1. 호출 가로채기:
     - 모든 손님 = 비서 통과
     - CEO 직접 X

  2. 부가 기능:
     - 손님 등록 (보안)
     - 회의록 작성 (로깅)
     - 일정 조율 (트랜잭션)

  3. CEO 에게 위임:
     - 검증 통과 시
     - CEO 호출

  4. 결과 후처리:
     - 회의 결과 정리
     - 다음 회의 준비

두 가지 비서 유형:

[1] 인터페이스 비서 (JDK 동적):
  - 회사 표준 양식 (인터페이스)
  - CEO 와 같은 양식 따름
  - 표준화

[2] CGLIB 비서 (구체):
  - CEO 의 직접 자녀 (상속)
  - CEO 모든 메서드 오버라이드
  - 양식 불필요

Spring 의 선택:
  - Spring Boot 2.x+ = CGLIB 기본
  - 일관성 (인터페이스 없어도 OK)

활용:
  - @Transactional (다음 Unit ★★★)
  - JPA Lazy Loading
  - 5주차 + 7주차 응축

ILIC:
  - 1020 @Transactional 모두 프록시
  - 매 메서드 호출 = 프록시 통과

→ 프록시 = 대리인, 호출 가로채기 + 부가 기능, JDK/CGLIB, @Transactional 기반.


🧭 9개 섹션 로드맵

1. 프록시 패턴 정의
2. 프록시의 역할 4가지
3. JDK 동적 프록시
4. CGLIB 프록시
5. Spring 의 선택
6. AOP 와 프록시
7. @Transactional 의 기반
8. JPA Lazy Loading 연결
9. Phase 7.2 ★★★ 예고

1️⃣ 프록시 패턴 정의

1.1 정의

프록시 패턴 (Proxy Pattern):

  실제 객체의 대리인:
    - Proxy (대리인)
    - Real Subject (실제 객체)
    - 클라이언트는 Proxy 와만 통신

  GoF 디자인 패턴
  → 5주차 학습

1.2 구조

프록시 구조:

  Subject (인터페이스 또는 추상)
       ↑
       ├── RealSubject (실제 객체)
       └── Proxy (대리인)
           - RealSubject 참조
           - 같은 인터페이스 구현
           - 호출 가로채기

  Client → Proxy → RealSubject

1.3 기본 형태

// 1. Subject (인터페이스)
interface ShipmentService {
    void process(Long id);
}

// 2. RealSubject (실제)
class ShipmentServiceImpl implements ShipmentService {
    @Override
    public void process(Long id) {
        // 진짜 비즈니스 로직
    }
}

// 3. Proxy (대리인)
class ShipmentServiceProxy implements ShipmentService {
    private final ShipmentService real;   // 진짜 참조
    
    public ShipmentServiceProxy(ShipmentService real) {
        this.real = real;
    }
    
    @Override
    public void process(Long id) {
        // 1. 호출 전 (부가 기능)
        System.out.println("Before: " + id);
        
        // 2. 진짜 호출
        real.process(id);
        
        // 3. 호출 후 (부가 기능)
        System.out.println("After: " + id);
    }
}

// 사용
ShipmentService service = new ShipmentServiceProxy(
    new ShipmentServiceImpl()
);
service.process(1L);
// 출력:
// Before: 1
// (진짜 로직)
// After: 1

1.4 정적 vs 동적 프록시

정적 vs 동적 프록시:

  정적 프록시 (위 코드):
    - 컴파일 시 프록시 클래스 작성
    - 메서드마다 직접 위임 코드
    - 코드 ↑ (보일러플레이트)

  동적 프록시:
    - 런타임에 자동 생성
    - JDK 동적 프록시 (인터페이스)
    - CGLIB (구체 클래스)
    - Spring 이 사용

1.5 5주차 학습 회상

5주차 학습:

  GoF 디자인 패턴:
    - 생성 / 구조 / 행동
    - Proxy = 구조 패턴

  관련 패턴:
    - Decorator (장식, 비슷)
    - Adapter (변환)
    - Facade (외부 간결화)

→ 다 비슷한 정신

1.6 ILIC 의 맥락

ILIC 의 프록시 활용

ILIC 의 코드:
  @Service
  public class ShipmentService {
      @Transactional
      public void process(Long id) {
          // 비즈니스
      }
  }

  Spring 의 동작 (자동):
    1. Spring Boot 시작 시
    2. @Service 감지 → 빈 등록
    3. @Transactional 감지 → 프록시 생성
       - ShipmentService 의 프록시 (자식 클래스)
       - 호출 가로채기
    4. 프록시를 빈으로 등록 (진짜 X)
    5. @Autowired 받는 곳에 프록시 주입

  매 호출:
    - service.process(1L)
    - 프록시.process(1L)
    - 트랜잭션 시작
    - 진짜 process 호출
    - 트랜잭션 commit / rollback

→ 1020 메서드 모두 프록시

1.7 자기 점검 답변

프록시 패턴의 정의는?

:
1. 프록시:

  • 실제 객체 대리인
  1. 구조:

    • Subject + RealSubject + Proxy
  2. 5주차:

    • 구조 패턴
  3. 종류:

    • 정적 / 동적

2️⃣ 프록시의 역할 4가지

2.1 역할 1 — 호출 가로채기

역할 1 — 호출 가로채기 (Interception):

  클라이언트 → Proxy:
    - Proxy 가 메서드 호출 받음
    - 진짜 객체 즉시 호출 X
    - 가로채서 부가 작업

  핵심:
    "메서드 호출 = 가로챌 수 있는 지점"

2.2 역할 2 — 부가 기능

역할 2 — 부가 기능 (Cross-cutting Concern):

  비즈니스 로직 외:
    - 트랜잭션
    - 로깅
    - 보안 (인증/인가)
    - 캐시
    - 성능 측정
    - 예외 처리

  횡단 관심사:
    - 여러 클래스에 공통
    - "옆으로 가로지르는"
    - AOP 의 개념

2.3 역할 3 — 진짜에 위임

역할 3 — 진짜에 위임 (Delegation):

  부가 기능 완료 후:
    - Proxy.process()
    - real.process()  ← 진짜 호출
    - 반환값 받음

  의미:
    - Proxy 가 비즈니스 X
    - 진짜 객체가 비즈니스
    - Proxy = 양념

2.4 역할 4 — 결과 후처리

역할 4 — 결과 후처리:

  진짜 호출 후:
    - 정상 반환 → commit / 로그 / 캐시
    - 예외 발생 → rollback / 알림

  완전한 흐름:
    1. 호출 가로채기
    2. 부가 작업
    3. 진짜 위임
    4. 결과 후처리

→ 4 단계

2.5 코드로 보는 4 단계

// 4 단계 코드
public class ShipmentServiceProxy implements ShipmentService {
    private final ShipmentService real;
    
    @Override
    public void process(Long id) {
        // ① 호출 가로채기 (지금 여기)
        long start = System.currentTimeMillis();
        log.info("Calling process({})", id);
        
        // ② 부가 작업
        tm.begin();   // 트랜잭션 시작
        
        try {
            // ③ 진짜에 위임
            real.process(id);
            
            // ④ 결과 후처리 (정상)
            tm.commit();
            log.info("Success: {}ms", System.currentTimeMillis() - start);
        } catch (Exception e) {
            // ④ 결과 후처리 (예외)
            tm.rollback();
            log.error("Failed", e);
            throw e;
        }
    }
}
// → 4 단계 모두
class ShipmentService { void process(Long id) {} }
ShipmentService real;
class TM { void begin() {} void commit() {} void rollback() {} }
TM tm;
org.slf4j.Logger log;

2.6 횡단 관심사 시각화

횡단 관심사 시각화:

  ┌─ ShipmentService.process() ─┐
  │  ┌──────────────────────┐    │
  │  │ 트랜잭션 시작 (프록시) │    │ ← 횡단
  │  │ 로깅 (프록시)         │    │ ← 횡단
  │  │ ┌──────────────────┐ │    │
  │  │ │  비즈니스 로직     │ │    │ ← 핵심
  │  │ └──────────────────┘ │    │
  │  │ 트랜잭션 commit       │    │ ← 횡단
  │  │ 로깅 후처리           │    │ ← 횡단
  │  └──────────────────────┘    │
  └─────────────────────────────┘

  - 핵심 = 비즈니스
  - 횡단 = 프록시 자동
  - 분리 = SoC

2.7 ILIC 의 맥락

ILIC 의 프록시 역할

ILIC 의 ShipmentService.process(1L) 호출:

  ① 호출 가로채기:
    - 프록시가 받음
    - 시작 시각 기록

  ② 부가 기능:
    - 트랜잭션 시작 (@Transactional)
    - 로깅 (메서드 진입)
    - 메트릭 시작 (관측성)

  ③ 진짜 위임:
    - 진짜 ShipmentService.process()
    - 비즈니스 로직

  ④ 결과 후처리:
    - 정상 → commit + 메트릭 종료
    - 예외 → rollback + 알림

  → 비즈니스 코드 깔끔
  → 모든 부가 기능 자동

2.8 자기 점검 답변

프록시의 역할 4가지는?

:
1. 호출 가로채기:

  • Interception
  1. 부가 기능:

    • Cross-cutting
  2. 진짜에 위임:

    • Delegation
  3. 결과 후처리:

    • 정상 / 예외

3️⃣ JDK 동적 프록시

3.1 JDK 동적 프록시 정의

JDK 동적 프록시:

  자바 표준 동적 프록시:
    - java.lang.reflect.Proxy
    - 인터페이스 기반
    - 런타임 클래스 생성

  요구사항:
    - 인터페이스 필수
    - 인터페이스 없으면 X

3.2 InvocationHandler

// InvocationHandler 구현
public class ShipmentHandler implements InvocationHandler {
    private final ShipmentService real;
    
    public ShipmentHandler(ShipmentService real) {
        this.real = real;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 호출 가로채기 (모든 메서드)
        System.out.println("Before: " + method.getName());
        
        // 진짜에 위임
        Object result = method.invoke(real, args);
        
        // 후처리
        System.out.println("After");
        
        return result;
    }
}
class ShipmentService {}
ShipmentService real;
class Method {
    String getName() { return null; }
    Object invoke(Object o, Object[] args) throws Throwable { return null; }
}
class InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; }
}

3.3 프록시 생성

// JDK 동적 프록시 생성
ShipmentService real = new ShipmentServiceImpl();

ShipmentService proxy = (ShipmentService) Proxy.newProxyInstance(
    real.getClass().getClassLoader(),
    new Class[] { ShipmentService.class },   // 구현할 인터페이스
    new ShipmentHandler(real)                  // 호출 처리
);

// 사용
proxy.process(1L);
// 출력:
// Before: process
// (진짜 호출)
// After
class Proxy {
    static Object newProxyInstance(ClassLoader cl, Class<?>[] interfaces, Object handler) { return null; }
}
class ShipmentService { void process(Long id) {} }
class ShipmentServiceImpl extends ShipmentService {}
class ShipmentHandler {
    ShipmentHandler(ShipmentService s) {}
}

3.4 동작 원리

동작 원리:

  1. Proxy.newProxyInstance() 호출
  
  2. JVM 이 런타임에:
     - 인터페이스 (ShipmentService) 의 구현 클래스 생성
     - 모든 메서드는 InvocationHandler.invoke() 호출

  3. proxy.process(1L) 호출 시:
     - 동적 생성된 클래스의 process() 실행
     - 내부적으로 InvocationHandler.invoke() 호출
     - method = process, args = [1L]

  4. InvocationHandler 안:
     - 부가 기능
     - method.invoke(real, args)
     - 후처리

3.5 장단점

JDK 동적 프록시:

  장점:
    - 자바 표준 (별도 라이브러리 X)
    - 가벼움
    - 빠름

  단점:
    - 인터페이스 필수
    - 구체 클래스 만 있으면 X
    - 인터페이스 X 메서드 가로채기 X

3.6 한계

// 한계 — 인터페이스 없으면 X
public class ShipmentServiceNoInterface {
    public void process(Long id) {
        // 비즈니스
    }
}

// JDK 동적 프록시?
// → ❌ 안 됨!
// → 인터페이스 없음

// CGLIB 가 필요
class ShipmentServiceNoInterface { void process(Long id) {} }

3.7 ILIC 의 맥락

// ILIC 의 JDK 동적 프록시 케이스 (가정)

// 만약 ILIC 가 인터페이스 사용:
public interface ShipmentService {
    void process(Long id);
}

@Service
public class ShipmentServiceImpl implements ShipmentService {
    @Transactional
    @Override
    public void process(Long id) {
        // 비즈니스
    }
}

// Spring (Spring Boot 1.x 또는 spring.aop.proxy-target-class=false):
// → JDK 동적 프록시 사용

// 빈 등록:
// - 진짜 빈: ShipmentServiceImpl
// - 프록시: ShipmentService 의 구현 (JDK 동적)
// - 주입은 ShipmentService 타입으로

// 현재 Spring Boot 2.x+:
// → CGLIB 기본 (다음 섹션)
@interface Service {}
@interface Transactional {}

3.8 자기 점검 답변

JDK 동적 프록시의 동작은?

:
1. JDK 동적:

  • java.lang.reflect.Proxy
  1. 요구:

    • 인터페이스
  2. InvocationHandler:

    • 호출 처리
  3. 한계:

    • 인터페이스 X 시 X

4️⃣ CGLIB 프록시

4.1 CGLIB 정의

CGLIB (Code Generator Library):

  바이트코드 조작 라이브러리:
    - 자바 표준 X (외부)
    - 구체 클래스 자식 동적 생성
    - 인터페이스 없어도 OK

  Spring 이 내장 (Spring Boot 2+)

4.2 자식 클래스 생성

자식 클래스 생성:

  원본:
    class ShipmentService {
        public void process(Long id) { ... }
    }

  CGLIB:
    class ShipmentService$$EnhancerByCGLIB extends ShipmentService {
        @Override
        public void process(Long id) {
            // 가로채기
            interceptor.intercept(this, "process", new Object[]{id});
        }
    }

  → 자식 클래스 (extends)
  → 메서드 오버라이드

4.3 MethodInterceptor

// MethodInterceptor 구현
public class ShipmentInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        // 호출 가로채기
        System.out.println("Before: " + method.getName());
        
        // 진짜 호출 (부모 메서드)
        Object result = proxy.invokeSuper(obj, args);
        
        // 후처리
        System.out.println("After");
        
        return result;
    }
}
class MethodInterceptor {
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return null; }
}
class MethodProxy {
    Object invokeSuper(Object obj, Object[] args) throws Throwable { return null; }
}
class Method {
    String getName() { return null; }
}

4.4 프록시 생성

// CGLIB 프록시 생성
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ShipmentService.class);   // 부모 (구체 클래스)
enhancer.setCallback(new ShipmentInterceptor());

ShipmentService proxy = (ShipmentService) enhancer.create();

// 사용
proxy.process(1L);
// 출력:
// Before: process
// (진짜 호출)
// After
class Enhancer {
    void setSuperclass(Class<?> c) {}
    void setCallback(Object o) {}
    Object create() { return null; }
}
class ShipmentService { void process(Long id) {} }
class ShipmentInterceptor {}

4.5 동작 원리

동작 원리:

  1. CGLIB.Enhancer 사용
  
  2. 바이트코드 조작:
     - 원본 클래스 (ShipmentService) 분석
     - 자식 클래스 동적 생성
     - 메서드 오버라이드

  3. proxy.process(1L) 호출:
     - 자식 클래스의 process() 실행
     - MethodInterceptor.intercept() 호출
     - 부가 기능 + 부모 메서드 호출

4.6 장단점

CGLIB:

  장점:
    - 인터페이스 불필요
    - 구체 클래스 OK
    - 일관성 (모든 클래스 가능)

  단점:
    - 외부 라이브러리 (Spring 내장)
    - final 클래스 X (상속 불가)
    - final 메서드 가로채기 X
    - private 메서드 가로채기 X (다음 Unit 함정)
    - 생성자 호출 (1번)

4.7 final 의 문제

// final 의 문제 (Unit 4.1 도 다룸)

// ❌ final 클래스
public final class ShipmentService {
    @Transactional
    public void process(Long id) { }
}

// CGLIB:
// → 자식 클래스 만들기 (상속) X
// → 에러!

// ❌ final 메서드
public class ShipmentService {
    @Transactional
    public final void process(Long id) { }
}

// CGLIB:
// → 자식 클래스 OK
// → process 오버라이드 X (final)
// → 프록시 동작 X
// → @Transactional 무시

// 해결:
// - final 제거
// - 또는 JDK 동적 프록시 사용
@interface Transactional {}

4.8 ILIC 의 맥락

// ILIC 의 CGLIB 케이스 (현재 표준)

@Service   // 인터페이스 X
public class ShipmentService {   // 보통 인터페이스 없음
    @Autowired ShipmentRepository repo;
    
    @Transactional
    public void process(Long id) {
        Shipment s = repo.findById(id).orElseThrow();
        s.markAsShipped();
    }
}

// Spring Boot 2.x+:
// → CGLIB 사용
// → ShipmentService$$EnhancerByCGLIB 자동 생성
// → 자식 클래스 (ShipmentService extends)
// → process 오버라이드 + 트랜잭션

// final 사용 금지:
//   public final class ShipmentService ← ❌
//   public final void process ← ❌

// → ILIC 의 1020 메서드 모두 CGLIB 프록시
class Shipment { void markAsShipped() {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}

4.9 자기 점검 답변

CGLIB 프록시의 동작은?

:
1. CGLIB:

  • 바이트코드 조작
  1. 자식 클래스:

    • 동적 생성
  2. 인터페이스 X:

    • OK
  3. final 금지:

    • 상속 불가

5️⃣ Spring 의 선택

5.1 Spring 의 선택 기준

Spring 의 선택 기준:

  Spring Boot 1.x ~ 2.0 초기:
    - 인터페이스 있음 → JDK 동적 프록시
    - 인터페이스 없음 → CGLIB

  Spring Boot 2.x+ (2.0 이후):
    - 기본 CGLIB (모든 경우)
    - proxyTargetClass = true (기본)

  변경 가능:
    spring.aop.proxy-target-class=false
    → JDK 동적 (인터페이스만)

5.2 왜 CGLIB 기본?

왜 CGLIB 기본?:

  1. 일관성:
     - 인터페이스 유무 무관
     - 모든 빈 같은 방식

  2. 인터페이스 안 만들어도 OK:
     - 보일러플레이트 ↓
     - 단순

  3. 캐스팅 안전:
     - 구체 클래스 타입 사용 가능
     - JDK 동적은 인터페이스 타입만

→ Spring Boot 의 결정

5.3 두 방식의 차이

항목JDK 동적 프록시CGLIB
표준자바 표준외부 라이브러리
요구사항인터페이스 필수구체 클래스 (final X)
동작 방식인터페이스 구현자식 클래스 (extends)
핸들러InvocationHandlerMethodInterceptor
Spring Boot 2+옵션기본
성능빠름약간 느림 (바이트코드)
캐스팅인터페이스 타입만구체 타입 가능

5.4 변경 방법

# application.yml (JDK 동적 프록시로 변경)
spring:
  aop:
    proxy-target-class: false   # JDK 동적

# 기본 (Spring Boot 2.x+)
# spring.aop.proxy-target-class: true   # CGLIB
// 또는 @EnableTransactionManagement
@Configuration
@EnableTransactionManagement(proxyTargetClass = false)   // JDK 동적
public class AppConfig {
}

5.5 어떤 걸 선택?

어떤 걸 선택?:

  기본 (CGLIB):
    - 거의 모든 신규 프로젝트
    - 일관성

  JDK 동적:
    - 인터페이스 강제 (DI / 테스트)
    - 옛 시스템 호환

  실무:
    - 거의 모두 CGLIB 기본
    - 명시적 변경 X

5.6 ILIC 의 맥락

// ILIC = Spring Boot 3 = CGLIB 기본

// 코드:
@Service
public class ShipmentService {   // 인터페이스 X
    @Transactional
    public void process(Long id) { }
}

// Spring 동작:
// 1. @Service 감지 → 빈 등록 시도
// 2. @Transactional 감지 → 프록시 필요
// 3. CGLIB 프록시 생성
//    - ShipmentService$$EnhancerByCGLIB 동적 생성
//    - extends ShipmentService
//    - process 오버라이드
// 4. 프록시를 빈 등록 (진짜 X)
// 5. @Autowired 시 프록시 주입

// 사용:
@Autowired ShipmentService service;   // 프록시
service.process(1L);                    // 프록시 호출
// → 트랜잭션 시작 → 진짜 호출 → 트랜잭션 commit
class ShipmentService { void process(Long id) {} }
ShipmentService service;
@interface Service {}
@interface Transactional {}
@interface Autowired {}

5.7 자기 점검 답변

Spring 의 프록시 선택 기준은?

:
1. Spring Boot 2.x+:

  • CGLIB 기본
  1. 이유:

    • 일관성
  2. 변경:

    • proxy-target-class
  3. 실무:

    • CGLIB 거의 모두

6️⃣ AOP 와 프록시

6.1 AOP 정의

AOP (Aspect-Oriented Programming):

  관점 지향 프로그래밍:
    - 횡단 관심사 분리
    - 비즈니스 로직 + 부가 기능 분리
    - 모듈성 ↑

  핵심 개념:
    - Aspect (관점)
    - Advice (조치)
    - Pointcut (적용 지점)
    - Join Point (실행 지점)

6.2 AOP 의 구현 방식

AOP 구현 방식:

  1. 프록시 기반 (Spring AOP):
     - 메서드 호출 시점
     - JDK 동적 / CGLIB
     - 가볍 / 제한적

  2. 바이트코드 위빙 (AspectJ):
     - 컴파일 시 또는 로드 시
     - 모든 지점 (필드 접근 등)
     - 강력 / 무거움

  Spring 의 선택:
    - Spring AOP (프록시 기반)
    - 일반 케이스 충분
    - @AspectJ 어노테이션 활용

6.3 AOP 의 용어

AOP 용어:

  Aspect (관점):
    - 횡단 관심사의 모듈화
    - @Aspect 클래스

  Advice (조치):
    - 실제 동작
    - @Before / @After / @Around 등

  Pointcut (절단점):
    - 어디에 적용?
    - 메서드 패턴

  Join Point (결합점):
    - 실제 실행 지점
    - 메서드 호출 등

  Weaving (위빙):
    - Aspect 를 코드에 적용
    - 프록시 생성 = 위빙

6.4 @Around 예시

// @Aspect 예시
@Aspect
@Component
public class TransactionAspect {
    
    @Around("@annotation(transactional)")   // Pointcut
    public Object handleTransaction(
            ProceedingJoinPoint pjp,
            Transactional transactional) throws Throwable {
        
        // Before
        TransactionStatus status = tm.getTransaction(...);
        
        try {
            // Around (진짜 메서드)
            Object result = pjp.proceed();
            
            // After (정상)
            tm.commit(status);
            return result;
        } catch (Exception e) {
            // After (예외)
            tm.rollback(status);
            throw e;
        }
    }
}

// → Spring 의 @Transactional 의 동작 (개념)
// → 다음 Unit 7.2 ★★★ 상세
class TransactionStatus {}
class TransactionManager {
    TransactionStatus getTransaction(Object o) { return null; }
    void commit(TransactionStatus s) {}
    void rollback(TransactionStatus s) {}
}
TransactionManager tm;
class ProceedingJoinPoint { Object proceed() throws Throwable { return null; } }
@interface Aspect {}
@interface Component {}
@interface Around { String value(); }
@interface Transactional {}

6.5 AOP 와 프록시의 관계

AOP 와 프록시:

  Spring AOP:
    - 프록시 패턴 활용
    - 어노테이션 (@Transactional) 감지
    - 프록시 자동 생성

  프록시 = AOP 의 구현 기반:
    - 호출 가로채기 = Join Point
    - 부가 기능 = Advice
    - 프록시 생성 = Weaving

→ 프록시 = AOP 의 엔진

6.6 ILIC 의 맥락

ILIC 의 AOP

ILIC = Spring AOP (프록시 기반):
  - @Transactional 의 자동화
  - @Cacheable 의 캐시
  - @PreAuthorize 의 보안
  - @Async 의 비동기

  모두 AOP + 프록시:
    1. 어노테이션 감지
    2. 프록시 생성 (CGLIB)
    3. 호출 가로채기
    4. 부가 기능

  ILIC 의 1020 메서드 + @Transactional:
    - 모두 프록시
    - AOP 의 결과
    - 자동화

→ 5주차 + 7주차 응축

6.7 자기 점검 답변

AOP 와 프록시의 관계는?

:
1. AOP:

  • 횡단 관심사
  1. 구현:

    • Spring AOP = 프록시
  2. 용어:

    • Aspect / Advice / Pointcut
  3. 관계:

    • 프록시 = AOP 의 엔진

7️⃣ @Transactional 의 기반

7.1 @Transactional 의 동작

@Transactional 의 동작 (개요):

  1. Spring Boot 시작:
     - Bean Post Processor 가
     - @Transactional 감지

  2. 프록시 생성:
     - CGLIB (기본)
     - 자식 클래스 동적 생성

  3. 빈 등록:
     - 프록시를 빈으로 (진짜 X)
     - @Autowired 시 프록시 주입

  4. 호출 시:
     - 프록시.process()
     - 트랜잭션 시작
     - 진짜.process()
     - 트랜잭션 commit / rollback

→ 다음 Unit 7.2 ★★★ 상세

7.2 어노테이션 감지

어노테이션 감지:

  @EnableTransactionManagement:
    - Spring Boot 자동 (대부분)
    - TransactionInterceptor 등록

  Bean Post Processor:
    - 빈 생성 후 후처리
    - @Transactional 감지
    - 프록시로 감쌈

7.3 TransactionInterceptor

TransactionInterceptor:

  Spring 의 핵심 클래스:
    - MethodInterceptor 구현
    - 모든 @Transactional 메서드 가로채기
    - PlatformTransactionManager 와 연동

  동작:
    1. 메서드 호출 가로채기
    2. @Transactional 옵션 확인
    3. PlatformTransactionManager 호출
    4. 진짜 메서드 호출
    5. 결과 처리

7.4 흐름 시각화

흐름 시각화:

  Application Code:
    @Autowired ShipmentService service;
    service.process(1L);
         ↓
  CGLIB 프록시 (ShipmentService$$EnhancerByCGLIB):
    - process() 메서드 (오버라이드)
    - TransactionInterceptor 호출
         ↓
  TransactionInterceptor:
    - @Transactional 옵션 읽기
    - PlatformTransactionManager.getTransaction()
         ↓
  PlatformTransactionManager (JpaTransactionManager):
    - EntityManager 생성
    - tx.begin()
         ↓
  진짜 ShipmentService.process():
    - 비즈니스 로직
    - Dirty Checking (JPA)
         ↓ (정상 반환)
  TransactionInterceptor:
    - PlatformTransactionManager.commit()
         ↓
  PlatformTransactionManager:
    - em.flush()
    - tx.commit()
    - em.close()
         ↓
  반환

7.5 5주차 + 6주차 + 7주차 응축

5+6+7주차 응축:

5주차:
  - 디자인 패턴 (프록시, DI, 템플릿+전략)
  - SoC

6주차:
  - DataSource
  - ACID
  - JdbcTemplate

7주차:
  - JPA (Phase 3-4)
  - PlatformTransactionManager (Phase 6)
  - 프록시 (Phase 7)

@Transactional:
  - 모든 응축
  - 어노테이션 1줄
  - 모든 기술 활용

7.6 다음 Unit 의 깊이

다음 Unit 7.2 ★★★:

  자세한 동작 원리:
    - Bean Post Processor
    - InfrastructureAdvisorAutoProxyCreator
    - 프록시 생성 과정
    - TransactionInterceptor
    - PlatformTransactionManager
    - 호출 흐름 전체

  면접 정점:
    - "@Transactional 어떻게 동작?"
    - 위 흐름 설명

  Phase 7 의 핵심

7.7 ILIC 의 맥락

ILIC 의 @Transactional 동작

ILIC 의 service.process(1L) 호출 시:

  1. 진짜 ShipmentService 가 아니라
     ShipmentService$$EnhancerByCGLIB (프록시)

  2. 프록시의 process() 메서드 실행:
     → TransactionInterceptor.invoke()

  3. TransactionInterceptor:
     → JpaTransactionManager.getTransaction()
     → EntityManager 생성
     → tx.begin()

  4. 진짜 ShipmentService.process() 호출:
     → repo.findById(id)
     → s.markAsShipped()
     → Dirty Checking 표시

  5. 정상 반환 후:
     → JpaTransactionManager.commit()
     → em.flush() (UPDATE SQL)
     → tx.commit()
     → em.close()

  → 1020 메서드 모두 이 흐름
  → ILIC 의 표준

7.8 자기 점검 답변

@Transactional 의 기반으로서 프록시는?

:
1. 프록시:

  • @Transactional 의 엔진
  1. TransactionInterceptor:

    • 가로채기
  2. PlatformTransactionManager:

    • 트랜잭션 처리
  3. 5+6+7주차 응축:

    • 완벽

8️⃣ JPA Lazy Loading 연결

8.1 JPA 의 또 다른 프록시

JPA 의 또 다른 프록시:

  @ManyToOne(fetch = LAZY):
    - 연관 객체 즉시 로딩 X
    - 프록시 객체 먼저

  접근 시:
    - 진짜 객체 로딩 (SQL)
    - 또는 이미 로딩된 객체

→ 프록시 패턴의 또 다른 활용

8.2 동작

@Entity
public class Shipment {
    @Id Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

// 사용
Shipment s = repo.findById(1L).orElseThrow();
// 이때 customer 는?
//
// s.customer 는 프록시 객체:
//   Customer$HibernateProxy$xxx
//   - extends Customer (CGLIB 와 비슷)
//   - 모든 메서드 오버라이드
//   - 호출 시 SQL 로딩

s.getCustomer().getName();
// → 이때 SQL 실행!
// → "SELECT * FROM customers WHERE id = ?"
// → 진짜 Customer 객체 반환
class Shipment {
    Long id;
    Customer customer;
    Customer getCustomer() { return null; }
}
class Customer { String getName() { return null; } }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }

8.3 final 의 문제 (재확인)

final 의 문제 (Unit 4.1 다룸):

  @Entity
  public final class Customer { ... }
  ↑ final 금지!

  이유:
    - Hibernate 가 자식 클래스 만들어야 (CGLIB 같은 패턴)
    - final 은 상속 불가
    - 프록시 생성 X

  → JPA 의 final 금지 = 프록시 때문

8.4 N+1 문제와 프록시

N+1 문제와 프록시:

  List<Shipment> all = repo.findAll();
  // 1 쿼리: SELECT * FROM shipments

  for (Shipment s : all) {
      s.getCustomer().getName();
      // 매번 프록시 → SQL
      // N 쿼리

      // 총: 1 + N
  }

  해결:
    - JOIN FETCH
    - @EntityGraph
    - default_batch_fetch_size

8.5 두 프록시 비교

항목@Transactional 프록시JPA Lazy 프록시
목적트랜잭션 관리지연 로딩
생성Spring AOPHibernate
라이브러리CGLIB비슷 (CGLIB-like)
호출 가로채기트랜잭션 + 메서드SQL 로딩
5주차프록시 패턴프록시 패턴
활용부가 기능성능 최적화

8.6 ILIC 의 맥락

// ILIC 의 두 프록시

@Service
public class ShipmentService {
    @Autowired ShipmentRepository repo;
    
    @Transactional   // ← 프록시 1 (Spring AOP)
    public Customer getCustomer(Long id) {
        Shipment s = repo.findById(id).orElseThrow();
        return s.getCustomer();   // ← 프록시 2 (JPA Lazy)
        // 이때 SQL 실행
    }
}

// 호출 시 동작:
// 1. CGLIB 프록시 (Spring)
//    → 트랜잭션 시작
// 2. 진짜 메서드 호출
//    → s.getCustomer() = Hibernate 프록시
//    → SQL 실행 (Customer 로딩)
// 3. 트랜잭션 commit
// 4. Customer 반환

// → 5주차 디자인 패턴 곳곳에 활용
class Customer {}
class Shipment { Customer getCustomer() { return null; } }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}

8.7 자기 점검 답변

JPA Lazy Loading 의 프록시 연결은?

:
1. JPA:

  • 또 다른 프록시
  1. Lazy Loading:

    • 프록시 → SQL
  2. final 금지:

    • 같은 이유
  3. 두 프록시:

    • Spring AOP + Hibernate

9️⃣ Phase 7.2 ★★★ 예고

9.1 Phase 7.2 의 깊이

✨ Phase 7 — @Transactional

Unit 7.1 — 프록시 패턴 ★ ← 여기
  - 프록시 정의 / 역할
  - JDK 동적 / CGLIB
  - Spring 의 선택
  - AOP 와 프록시

Unit 7.2 — @Transactional 동작 원리 ★★★:
  - Bean Post Processor
  - InfrastructureAdvisorAutoProxyCreator
  - 프록시 생성 과정 상세
  - TransactionInterceptor
  - 호출 흐름 전체
  - 면접 정점

Unit 7.3 — 5가지 함정 ★:
  - private 메서드
  - self-invocation
  - checked exception
  - propagation
  - readOnly

9.2 Unit 7.2 가 ★★★ 인 이유

Unit 7.2 가 ★★★ 인 이유:

  1. 면접 단골:
     - "@Transactional 동작 원리?"
     - 가장 자주 묻는 주제
     - 시니어 시험

  2. 실무 디버깅:
     - 함정 회피
     - 트러블슈팅
     - 성능 분석

  3. 5+6+7주차 응축:
     - 디자인 패턴 (5)
     - DB 접근 (6)
     - JPA + 트랜잭션 (7)

  4. 깊은 이해:
     - 추상화의 정점
     - 자바 진영 정수

9.3 다음 Unit 의 학습 흐름

다음 Unit 의 흐름:

  Unit 7.2 — @Transactional 동작 원리 ★★★:
    1. Spring Boot 시작 흐름
    2. Bean Post Processor 의 역할
    3. @Transactional 감지 (어노테이션 스캐닝)
    4. AutoProxyCreator
    5. 프록시 생성 (CGLIB)
    6. TransactionInterceptor 의 동작
    7. 호출 흐름 (메서드 호출 시)
    8. PlatformTransactionManager 와 연동
    9. 5+6+7주차 응축 종합

9.4 7주차 진행

🗂️ Part A (완주)
  ✅ Phase 1-4 (16)

🔄 Part B — 트랜잭션 추상화의 진화
  ✅ Phase 5 (2)
  ✅ Phase 6 (3)
  ✨ Phase 7 — @Transactional (1/3) — 모두 ★깊이
    ✅ Unit 7.1 프록시 패턴 ★ ← 여기
    ⏭ Unit 7.2 @Transactional 동작 원리 ★★★
    ⏭ Unit 7.3 @Transactional 5가지 함정 ★

총: 22/24 Unit (92%)

9.5 면접 단골 질문 매핑

Q핵심 답변
프록시 패턴?대리인 / GoF
역할?가로채기 + 부가 + 위임 + 후처리
JDK 동적?인터페이스 기반
CGLIB?자식 클래스
Spring 선택?CGLIB 기본 (2.x+)
final 금지?상속 불가
AOP?횡단 관심사
@Transactional 기반?프록시
JPA Lazy?같은 프록시 정신
Unit 7.2?★★★ 정점

9.6 자기 점검 체크리스트

프록시

  • 정의

4 역할

  • 가로채기 / 부가 / 위임 / 후처리

JDK 동적

  • 인터페이스

CGLIB

  • 자식 클래스

Spring 선택

  • CGLIB 기본

AOP

  • 횡단 관심사

@Transactional

  • 프록시 기반

JPA Lazy

  • 같은 정신

Phase 7.2

  • ★★★ 정점

9.7 추가 심화 질문

Q1: 프록시의 메모리 영향?

답:

  • 각 빈마다 프록시 1개 (메모리 ↑)
  • 클래스 정의도 추가 (PermGen / Metaspace)
  • 일반적으로 무시할 수준
  • 수천 개 빈이면 고려

Q2: ByteBuddy?

답:

  • CGLIB 대안
  • 더 빠르고 가벼움
  • Hibernate 가 사용
  • Spring 은 아직 CGLIB 기본

Q3: 프록시 디버깅?

답:

  • 객체 클래스명 확인 (CGLIB / Hibernate 포함)
  • toString() / hashCode()
  • IDE 의 디버거 (프록시 표시)
  • Spring 의 AopProxyUtils

Q4: AspectJ 와 차이?

답:

  • Spring AOP: 프록시 (메서드 호출만)
  • AspectJ: 바이트코드 위빙 (필드, 생성자 등)
  • AspectJ 가 더 강력
  • 대부분 Spring AOP 로 충분

Q5: @Transactional 의 self-invocation 문제와 프록시?

답:

  • 같은 클래스 내 호출 = 프록시 우회
  • this.method() = 진짜 객체
  • 프록시 안 거침
  • @Transactional 무시
  • → Unit 7.3 함정

🎯 핵심 요약 — 3줄 정리

1. 프록시 패턴 = 대리인 + 호출 가로채기

  • 5주차 GoF 디자인 패턴 (구조)
  • 4 역할: 호출 가로채기 / 부가 기능 / 진짜 위임 / 결과 후처리
  • 횡단 관심사 (트랜잭션·로깅·보안·캐시) 자동화

2. 두 종류 프록시

  • JDK 동적 프록시: 인터페이스 기반 (java.lang.reflect.Proxy + InvocationHandler)
  • CGLIB: 구체 클래스의 자식 동적 생성 (final 금지 + MethodInterceptor)
  • Spring Boot 2.x+ 기본 = CGLIB (일관성)

3. @Transactional + JPA Lazy 의 공통 기반

  • @Transactional: Spring AOP 의 프록시 (CGLIB)
  • JPA Lazy Loading: Hibernate 의 프록시 (Customer$HibernateProxy)
  • 둘 다 5주차 프록시 패턴의 적용
  • Unit 7.2 ★★★ = @Transactional 동작 원리 완벽 해부

📚 다음으로...

Unit 7.2 — @Transactional 동작 원리 ★★★

이번 Unit에서 프록시를 봤다면, 다음은 @Transactional 동작 원리 (★★★ 정점, 면접 단골).

  • Spring Boot 시작 흐름
  • Bean Post Processor 의 어노테이션 감지
  • 프록시 생성 과정 상세
  • TransactionInterceptor 의 동작
  • 메서드 호출 시 전체 흐름
  • 5+6+7주차 응축 종합

Phase 7 진행 상황

✨ Phase 7 — @Transactional (모두 ★ 깊이)
  ✅ Unit 7.1 프록시 패턴 ★ ← 여기
  ⏭ Unit 7.2 @Transactional 동작 원리 ★★★
  ⏭ Unit 7.3 @Transactional 5가지 함정 ★

7주차 누적 진행

🗂️ Part A (완주)
  ✅ Phase 1-4 (16)

🔄 Part B — 트랜잭션 추상화의 진화
  ✅ Phase 5 (2)
  ✅ Phase 6 (3)
  ✨ Phase 7 (1/3) — 모두 ★깊이

총: 22/24 Unit (92%)

★ 깊이 파기 — 프록시 패턴 완료, Phase 7 시작

profile
Software Developer

0개의 댓글