[Spring AOP] @Transactional의 Proxy 작동 방식

2025. 3. 11. 08:07·Spring
728x90
반응형
SMALL

 @Transactional의 Proxy 작동 방식 (Spring AOP 기반)

 

Spring에서 @Transactional은 AOP(Aspect-Oriented Programming)와 프록시 패턴(Proxy Pattern)을 사용하여 트랜잭션을 관리한다.

이 과정에서 Spring의 프록시 기반 동작 방식을 이해하는 것이 중요하다.

 

프록시 객체(Proxy)가 원본 객체(Target)를 감싸고 있음이란?

 

Spring에서 @Transactional이 적용된 메서드를 실행할 때 프록시 객체가 원본 객체를 감싼다는 말은 프록시 객체가 원본 객체(Target)의 실행을 대신 관리한다는 의미다.

 

즉, 프록시 객체가 원본 객체를 대리(Proxy)하여 트랜잭션을 제어하는 역할을 수행한다.

 


쉽게 이해하는 프록시 개념

 

프록시는 대리인과 비슷한 개념이다. 예를 들어

• 우리가 고객센터에 전화를 걸면, 직접 CEO와 통화하는 것이 아니라 고객 상담원(프록시)을 통해 문제를 해결한다.

• 상담원이 우리가 원하는 요청을 CEO(실제 객체)에게 전달하고, CEO의 응답을 대신 받아 처리한다.

 

이처럼 Spring에서 프록시 객체는 원본 객체(Target)의 메서드를 감싸고, 실행을 대신하며 트랜잭션을 관리한다.

 


 프록시가 동작하는 구조 (Spring AOP)

1. 클라이언트가 @Transactional이 붙은 메서드를 호출하면, 원본 객체가 실행되기 전에 프록시 객체가 먼저 호출됨.

2. 프록시 객체는 트랜잭션을 시작하고, 원본 객체(Target)의 메서드를 실행.

3. 실행이 끝난 후 예외가 발생하지 않으면 트랜잭션을 커밋(commit).

4. 예외가 발생하면 트랜잭션을 롤백(rollback).

 

 이 과정에서 원본 객체를 직접 호출하는 것이 아니라, 프록시 객체가 원본 객체를 감싸고 실행을 대신 처리하는 것이 핵심이다.

 


1. @Transactional이 어떻게 동작하는가?

 

@Transactional이 붙은 메서드를 호출하면 Spring이 자동으로 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 커밋(Commit), 예외가 발생하면 롤백(Rollback)하는 기능을 수행한다.

 

트랜잭션 적용 과정

1. 프록시 객체(Proxy)가 원본 객체(Target)를 감싸고 있음.

2. 클라이언트가 @Transactional이 선언된 메서드를 호출.

3. 프록시 객체가 트랜잭션을 시작한 후, 실제 원본 객체(Target)의 메서드를 실행.

4. 메서드가 정상 종료되면 commit() 호출.

5. 예외가 발생하면 rollback() 호출.

6. 트랜잭션이 종료되면 프록시가 호출을 반환.

 


2. 프록시 기반 트랜잭션 관리 방식

 

Spring은 @Transactional을 처리하기 위해 프록시(Proxy) 패턴을 사용한다.

프록시 방식에는 두 가지 방식이 존재한다.

 

1) JDK Dynamic Proxy (인터페이스 기반)

• 인터페이스가 있는 경우 Spring은 기본적으로 JDK 동적 프록시(JDK Dynamic Proxy)를 사용한다.

• 프록시 객체가 인터페이스를 구현하고, 실제 구현체(원본 객체)로 메서드 호출을 위임.

 

예제 코드

@Transactional
public interface ProductService {
    void createProduct();
}

@Service
public class ProductServiceImpl implements ProductService {
    @Override
    public void createProduct() {
        System.out.println("상품 생성");
    }
}

 

프록시 작동 방식

ProductService productService = new ProductServiceImpl();
ProductService proxy = (ProductService) Proxy.newProxyInstance(
    productService.getClass().getClassLoader(),
    new Class[]{ProductService.class},
    new TransactionInvocationHandler(productService) // 트랜잭션 처리
);

• Proxy.newProxyInstance(...)를 사용하여 동적 프록시를 생성.

• TransactionInvocationHandler가 트랜잭션을 관리하는 역할을 수행.

 


 2) CGLIB (클래스 기반 프록시)

• 인터페이스가 없는 경우 Spring은 CGLIB(Code Generation Library)를 사용하여 프록시를 생성.

• CGLIB은 원본 클래스를 상속받아 프록시 객체를 생성.

 

 예제 코드

@Transactional
@Service
public class ProductService {
    public void createProduct() {
        System.out.println("상품 생성");
    }
}

 CGLIB 프록시 작동 방식

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ProductService.class);
enhancer.setCallback(new TransactionInterceptor());
ProductService proxy = (ProductService) enhancer.create();

• Enhancer.setSuperclass(ProductService.class)를 통해 클래스 기반의 프록시 객체 생성.

• TransactionInterceptor가 트랜잭션을 관리.

 


3. 프록시 기반 @Transactional의 주의점

 

 1) 같은 클래스 내에서 @Transactional 메서드 호출 시 트랜잭션이 적용되지 않음

@Service
public class ProductService {
    @Transactional
    public void createProduct() {
        System.out.println("상품 생성");
    }

    public void processOrder() {
        createProduct(); // 같은 클래스 내에서 호출 → 트랜잭션 적용 안 됨!
    }
}

이유

• processOrder()가 같은 클래스 내의 createProduct()를 직접 호출하면 프록시를 거치지 않음.

• 프록시는 외부에서 접근할 때만 작동하며, 내부 호출은 원본 객체가 실행하므로 트랜잭션이 적용되지 않음.

 

 해결 방법

• self-invocation 문제 해결을 위해 ApplicationContext를 이용하여 프록시 객체를 호출해야 함.

@Autowired
private ApplicationContext context;

public void processOrder() {
    context.getBean(ProductService.class).createProduct(); // 프록시를 통해 호출
}

 

 


 2) private 메서드에는 @Transactional이 적용되지 않음

@Service
public class ProductService {
    @Transactional
    private void createProduct() { //  트랜잭션 적용 안 됨
        System.out.println("상품 생성");
    }
}

이유

• @Transactional은 프록시 기반으로 작동하는데, 프록시는 private 메서드를 가로챌 수 없음.

• 따라서 트랜잭션을 적용하려면 public으로 선언해야 한다.

 


3) final 클래스 또는 메서드에는 @Transactional이 적용되지 않음

@Service
@Transactional
public final class ProductService { // final 클래스는 CGLIB 프록시 생성 불가
    public void createProduct() {
        System.out.println("상품 생성");
    }
}

이유

• CGLIB은 원본 클래스를 상속받아 프록시를 생성하므로 final 클래스에서는 동작하지 않음.

• 메서드가 final이면 메서드 오버라이딩이 불가능하여 트랜잭션 처리가 적용되지 않음.

 

 해결 방법

• final을 제거하여 프록시가 정상적으로 동작하도록 변경해야 함.

 


4) @Transactional(readOnly = true)는 쓰기 작업을 차단하지 않음

• @Transactional(readOnly = true)는 읽기 전용 트랜잭션을 제공하여 성능을 최적화하지만, 실제로 쓰기 작업을 막지는 않음.

• 데이터베이스 수준에서 강제하려면 DB 설정 변경이 필요.

@Transactional(readOnly = true)
public void getProducts() {
    productRepository.save(new Product("스마트폰")); //  INSERT 가능 (주의!)
}

📌 해결 방법

• readOnly = true를 설정할 경우 쓰기 작업을 수행하지 않도록 주의해야 함.

• 만약 쓰기 작업을 방지하려면 데이터베이스에서 READ ONLY 모드 설정 필요.

 


4. 정리

적용 대상동작 방식

JDK Dynamic Proxy 인터페이스 기반 프록시 생성 (Proxy.newProxyInstance)
CGLIB Proxy 클래스 기반 프록시 생성 (Enhancer.setSuperclass)
같은 클래스 내 호출 프록시를 거치지 않아 @Transactional 적용 안 됨
private 메서드 프록시가 가로챌 수 없어 트랜잭션 적용 안 됨
final 클래스/메서드 CGLIB 프록시 생성 불가
@Transactional(readOnly = true) 쓰기 작업을 차단하지 않음 (DB 설정 필요)

 

 @Transactional이 정상적으로 작동하려면:

• 인터페이스가 없는 경우 CGLIB을 사용하므로 final을 피할 것.

• 자기 자신을 호출하지 않도록 주의하고, ApplicationContext에서 프록시를 가져와 사용할 것.

• private 메서드에서는 작동하지 않으므로 public 메서드에서 호출할 것.

728x90
반응형
SMALL

'Spring' 카테고리의 다른 글

[Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유  (0) 2025.03.11
[Spring AOP] JDK Dynaic Proxy에서 내부호출이 안되는 이유  (0) 2025.03.11
[Lombok] @Builder, DTO 접근제한자 적절한 설정 방법  (0) 2025.03.11
[Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유  (0) 2025.03.11
[Spring/Serialization-Deserialization] Date, LocalDate, LocalDateTime 형변환 방법, 직렬화/역직렬화 문제 해결하기  (0) 2025.03.07
'Spring' 카테고리의 다른 글
  • [Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유
  • [Spring AOP] JDK Dynaic Proxy에서 내부호출이 안되는 이유
  • [Lombok] @Builder, DTO 접근제한자 적절한 설정 방법
  • [Lombok] @Builder를 클래스가 아닌 메서드에 적용하는 이유
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325) N
      • 면접 준비 (22) N
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11) N
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      CSS
      Spring
      자바스크립트
      jsp request
      자바 알고리즘
      HTML
      백준
      jsp기초
      Springframework
      자바
      프로그래머스
      Til
      spring redis
      리눅스
      리눅스마스터2급
      redis
      springsecurity
      자바기초
      JavaScript
      Spring Data Redis
      리눅스마스터2급정리
      스프링프레임워크
      java
      자바 면접
      JS
      레디스
      redis 채팅
      자바 반복문
      자바 면접질문
      JSP
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [Spring AOP] @Transactional의 Proxy 작동 방식
    상단으로

    티스토리툴바