프록시와 AOP, 스프링 빈(@Component와 @Configuration)

랏 뜨·2025년 8월 1일

🔎 Overview

  필자는 AOP 에 관심이 많다.
공부해오고 프로젝트를 진행해오면서 조금씩 AOP 관련 지식을 쌓아가며 AOP 기술을 점진적으로 발전시키며 활용해왔고, 프로젝트 중 하나에는 어노테이션을 이용해서 AOP 처리를 통해 인가를 적용한 후 코드 및 DB 호출중복을 방지하기 위해 ThreadLocal에 관련 정보를 저장해서 비즈니스 로직으로 전달하는 코드까지 구현해보았다.

  어느정도 AOP 에 대한 지식이 있다고 생각하고 있던 찰나, 최근 스프링 관련 강의를 들으며 공부를 하다보니 문득 궁금증이 생겼다.

'내가 구현한 AOP를 처리하는 프록시 객체는 뭘까?'
'어떤 건 프록시고 어떤 건 실제 객체네? 뭐지?'

최근 스프링의 원리를 확실하게 이해하기 위해 강의를 들으며, 궁금한 부분은 바로바로 조사하고 공부하는 습관이 들어있다.
이번에 공부한 내용을 기록으로 남겨두면 여러모로 도움이 많이 될 것 같아서, 관련 내용을 따로 정리해보았다.


1️⃣ AOP 와 프록시

AOP

  • 프로그래밍 패러다임
  • 횡단 관심사를 중심으로 프로그램을 설계하는 방법
    • 여러 객체에 걸쳐 공통적으로 적용되는 기능
    • ex) 로깅, 트랜잭션, 캐싱 ...
  • SOLIDOCP 원칙을 준수
    • AOP 는 기존 기능을 전혀 수정하지 않고, 로깅, 트랜잭션 등 부가기능을 추가
    • 즉, 일종의 확장에 해당
  • 횡단 관심사를 하나로 모아 관리함으로, 코드의 유지보수성을 높이고 비즈니스 로직과 횡단 관심사를 분리하는 곳이 목표

프록시

  • 디자인 패턴
  • AOP 가 제시한 패러다임을 수행할, 대리자 역할의 객체를 생성하여 사용
  • 프록시 객체를 통해, 타겟(실제 객체) 전후횡단 관심사를 처리 가능

2️⃣ 스프링 빈과 프록시 - @Component, @Configuration

@Component 과 프록시

  • @Component 로 등록된 스프링 빈은 원칙적으로 실제 객체
    • 실제 객체가 스프링 컨테이너의 빈으로 등록
    • @Component 를 포함하고 있는 @Controller , @Service 등도 마찬가지
  • 만약 컴포넌트 내에서 AOP 를 구현한 로직이 하나라도 존재할 경우, 실제 객체가 아닌 프록시 객체가 빈으로 등록
    • @Transactional 또한 내부에서 AOP 를 구현
    • 따라서 클래스 또는 내부 메서드@Transactional 이 하나라도 존재할 경우, 스프링 빈 등록 시 이를 탐지하여 타겟(실제 인스턴스)를 감싸고 있는 프록시 객체로 등록
  • 프록시 객쳬 또한 실제 객체와 마찬가지로 @Component 로 등록되어 있으므로, 싱글톤 객체로 빈에 등록

@Configuration 과 프록시

  • @Configuration 으로 등록된 클래스는, 프록시 객체가 스프링 빈으로 등록
    • CGLIB 라이브러리를 사용하여 바이트코드가 조작 된 프록시 객체
  • @ComponentScan 진행 시, @Configuration 내의 모든 @Bean 을 찾아서 메서드명을 이름으로, 스프링 컨테이너에 스프링 빈으로 등록

3️⃣ 프록시 동작 흐름

@Component

  • AOP한 번이라도 구현한 경우 프록시 객체가 스프링 빈으로 등록
  • 해당 비즈니스 로직이 호출되면 프록시 객체가 로직을 처리

🌊 FLOW

  • 프록시 객체로 등록되었을 경우 :
    • Case1 -> @Transactional , @AspectAOP 가 적용된 경우
      • 프록시 객체가 해당 로직 실행 전후로 AOP 에 등록된 코드를 수행 후 proceed()타겟 메서드 실행
    • Case2 -> @Transactional , @AspectAOP 가 적용되지 않은 경우
      • 프록시 객체AOP 를 적용하지 않고 바로 proceed()타겟 메서드 실행

@Configuration

  • 일반적으로 서버가 시작될 때, 스프링 빈 초기화 -> 스프링 의존관계 주입(DI) 순서로 진행
  • @Configuration 으로 등록된 CGLIB 프록시 객체의 역할을 여기서 확인 가능

🌊 FLOW

  1. 애플리케이션 시작 시 스프링 빈을 등록하고 초기화하는 과정에서, @Configuration@Bean 메서드를 모두 호출
  2. 해당 메서드가 다른 객체의 주입을 필요로 할 경우:
    • Case1 -> 해당 객체가 스프링 빈으로 등록되어 있다면, 해당 싱글톤 객체를 주입
    • Case2 -> 해당 객체가 아직 스프링 빈으로 등록되어 있지 않다면, 새로운 싱글톤 객체를 생성하여 스프링 빈으로 등록 후 해당 객체를 주입
  3. 과정을 반복하여 모든 @Configuration@Bean 메서드를 스프링 빈으로 등록

  • 즉, @Configuration 의 가장 큰 역할은, 해당 클래스 내 모든 스프링 빈 객체의 싱글톤화

💡 @Configuration 또는 @Bean 이 없다면?

1️⃣ @Bean 만 단독으로 사용

  • 컴포넌트 스캔 자체가 불가능
  • 따라서 스프링 빈 등록 과정에서 @Bean 메서드 자체가 호출 불가능
  • 스프링 빈 등록 실패

2️⃣ @Configuration 만 단독으로 사용

  • 컴포넌트 스캔은 가능
  • 따라서 CGLIB 프록시 객체가 스프링 빈으로 등록
  • 하지만 @Bean 이 없으므로, 스프링 빈 등록 과정에서 메서드를 호출 불가능
  • 내부의 메서드들은 스프링 빈 등록 실패

3️⃣ @Component + @Bean 조합으로 사용

  • @Component 로 인해 실제 객체가 스프링 빈으로 등록
  • 스프링 빈 등록 과정에서 @Bean 메서드 또한 호출 가능
  • 하지만 @Bean 메서드 내부에서 다른 빈의 주입이 필요할 경우, 해당 메서드를 매번 호출
  • TypeA 라는 을 여러 @Bean 메서드에서 DI 할 경우, 매번 다른 TypeA 인스턴스가 생성
  • 따라서 빈 객체의 싱글톤이 보장되지 않음

⚒️ JDK Dynamic Proxy

  • 타겟 클래스인터페이스의 구현체인 경우, AOP 적용 시 항상 JDK Dynamic Proxy 를 사용
    • 인터페이스 메서드만 가로챌 수 있음
    • 확장된 기능은 가로챌 수 없음
    • 메인 메서드 실행 클래스@EnableAspectJAutoProxy(proxyTargetClass = true) 설정을 해줌으로, 항상 CGLIB 프록시를 사용하도록 변경할 수 있음
    • 스프링부트 2.0 이상 버전부터는 proxyTargetClass = true 가 기본값으로 설정되어, 사실상 JDK 동적 프록시는 사용되지 않음

public interface UserService {
	void findById(Long userId);
    void findByUsername(String username);
}

@Service
public class UserServiceImpl implements UserService {

	@Override
    void findById(Long userId) {
    // 구현
    }
    
    @Override
    void findByUsername(String username) {
    // 구현
    }
    
    void findByEmail(String email) {
    // 구현
    }
}
  • 현재 시나리오 :
    1. 인터페이스 UserService 가 존재
    2. 인터페이스 구현체 UserServiceImpl 이 존재
    3. @Service 로 이 클래스를 스프링 빈으로 등록

➡️ 스프링 빈 등록 구성

1️⃣ AOP 미적용 시

  • 빈이름 : userServiceImpl
  • 빈타입 : UserServiceImpl
  • 빈실체 : new UserServiceImpl()

  • AOP 미적용 시, 프록시 객체가 아닌 실제 인스턴스스프링 빈으로 등록
  • 따라서 @Service 가 붙어있는 UserserviceImpl 을 대상으로 스프링 빈 등록

2️⃣ AOP 적용 시

  • 빈이름 : userServiceImpl
  • 빈타입 : UserService
  • 빈실체 : UserService 구현 프록시 객체

  • 현재 UserServiceImpl 의 경우, 인터페이스의 구현체
  • 인터페이스 구현체AOP 를 적용하면, 반드시 JDK 동적 프록시에 의해 프록시 객체 생성
  • JDK 동적 프록시는 인터페이스 대상 프록시
  • 따라서 빈타입빈실체인터페이스 타입으로 등록

🥷 UserServiceImpl DI 🆚 UserService DI

1) UserServiceImpl DI :

  • JDK 동적 프록시는 인터페이스 주입 시에만 사용
  • UserServiceImpl 에 대해 AOP 를 적용했더라도, UserServiceImplDI 받으면 실제 UserServiceImpl 인스턴스가 주입

2) UserService DI:

  • UserServiceImpl 에 대해 AOP 를 적용하면, UserServiceDI 받아서 UserService 내의 모든 기능은 AOP 가 적용
  • UserService 인터페이스가 특정 인터페이스를 상속받고, 그 인터페이스는 다른 특정 인터페이스를 상속받는 구조라도, AOPUserServiceImpl 을 대상으로 적용되면 UserService 내의 모든 기능에서는 AOP 가 적용
  • 확장된 기능은 사용할 수 없음

📋 즉, JDK 동적 프록시를 사용하면 확장된 기능에 대한 AOP 처리가 불가능하다.


📌 CGLIB

  • 클래스 기반 프록시
  • 인터페이스 구현체가 아닌 클래스AOP 적용 시, 해당 클래스 타입의 프록시 객체스프링 빈으로 등록
  • UserService 관련 동일한 시나리오를 가정:

➡️ 스프링 빈 등록 구성

1️⃣ AOP 미적용 시

  • 빈이름 : userServiceImpl
  • 빈타입 : UserServiceImpl
  • 빈실체 : new UserServiceImpl()

  • @Component 클래스이므로, 동일하게 실제 객체 생성

2️⃣ AOP 적용 시

  • 빈이름 : userServiceImpl
  • 빈타입 : UserServiceImpl
  • 빈실체 : UserServiceImpl 상속 프록시 객체

  • CGLIB빈 등록 시 모든 것을 클래스 기반으로 등록
  • 따라서 UserServiceImplDI 받을 수 있으므로, 해당 클래스의 모든 기능에 AOP 를 적용 가능


🖇️ JDK Dynamic Proxy 🆚 CGLIB

1️⃣ JDK 동적 프록시

  • 인터페이스 정의 메서드만 가로챌 수 있다는 명확한 한계
  • AOP 를 구현체 대상으로 설정했을 때, 인터페이스 DIAOP 가 작동할 수 있지만 구현체 DI프록시 객체가 아닌 실제 구현체 인스턴스가 등록되므로 AOP 작동 불가능

🔗 SOLID

1) ISP 준수

  • JDK 동적 프록시 특성상, AOP 적용을 위해서는 인터페이스 DI를 강제

2) OCP 위반

  • 기능 확장의 경우, 인터페이스 구현체에서 추가적으로 구현하는 것이 일반적
  • 하지만 AOP 를 적용하기 위해서는 앞서 말했듯, 인터페이스 DI가 강제
    • 인터페이스 구현체에서 기능을 확장하더라도, 해당 기능에 대한 AOP 적용 불가능
  • 기능 확장AOP 를 적용하기 위해서는, 기존 인터페이스 수정이 필수불가결

2️⃣ CGLIB

  • 클래스를 대상으로 프록시 객체 생성
  • 구현체를 스프링 빈에 등록할 수 있으므로, AOP 작동에 관해서 자유로움

🔗 SOLID

1) ISP , DIP 위반 가능

  • CGLIB인터페이스 DI인터페이스 구현체 DI가 모두 가능
  • 인터페이스 DI의 경우에는 JDK 동적 프록시와 마찬가지로, ISPDIP 를 준수
  • 단, 기능 확장 후 AOP 적용을 위해 구현체 DI를 할 경우, ISPDIP 를 위반
  • 하지만 JDK 동적 프록시 사용 시 기존 인터페이스 수정 없이는 확장된 기능 AOP 적용 불가능 이라는 큰 단점을 해소 가능

2) OCP 준수

  • 기존 인터페이스 코드의 수정이 필요 없으므로 변경에는 확실히 닫혀있음
  • CGLIB프록시 객체구현체를 상속하기 때문에, 기능 확장 후 AOP 적용이 얼마든지 가능
  • 단 이 경우 구현체 DI가 필요할 수 있으므로, ISPDIP 를 위반
  • CGLIB 가 훨씬 유연한 방식을 제공하므로, 현재 스프링 2.0 이상 버전부터는 프록시 객체 사용 시 CGLIB 사용이 기본값으로 변경
  • CGLIB 를 사용함으로 얻을 수 있는 이점이 더 많다!

검수) Google Gemini ( https://gemini.google.com/app )

profile
기록

0개의 댓글