티스토리 뷰

반응형

아래의 내용은 토비의 스프링 2권 5장을 읽고 정리한 내용입니다.

 

 

 

1. AOP를 위한 Proxy 구현 방식


[ AOP에 대한 이해 ]

AOP는 부가 기능을 핵심 기능으로부터 분리하기 위해 등장한 기술이다. 부가 기능을 분리함으로써 우리는 해당 로직을 재사용할 수 있고, 핵심 기능은 핵심 역할에만 집중할 수 있도록 도와준다.

Spring의 AOP는 기본적으로 프록시 방식으로 동작하도록 되어 있는데, Spring에서 AOP를 활용하기 위해서는 @EnableAspectJAutoProxy 어노테이션을 붙여주어야 하며, 이에 대한 옵션으로 proxyTargetClass가 있다.

그리고 이 옵션을 주지 않으면 스프링 빈을 찾지 못하는 등의 에러가 발생할 수 있는데, 이를 이해하기 위해서 우리가 수동으로 구현하는 프록시 방식과 Spring이 자동으로 구현하는 프록시 방식에 대해 알아보고 왜 이 옵션이 생기게 되었는지 이해보도록 하자.

 

 

[ 수동으로 직접 Proxy 구현 ]

앞서 설명하였듯이 AOP를 구현하는 기본적인 방법은 프록시 패턴을 이용하는 것인데, 우리가 수동으로 직접 AOP를 구현하고자 한다면 다음과 같이 구현할 수 있다.

예를 들어 다음과 같은 DiscountController와 DiscountService 및 구현체(RateDiscountService)가 있다고 하자.

@RestController
@RequiredArgsConstructor
public class DiscountController {
    private final DiscountService discountService;
} 

public interface DiscountService {

    int discount();

}

@Service
public class RateDiscountService implements DiscountService {

    @Override
    public int discount() {
        ...
    }

}

 

우리는 RateDiscountService의 discount 메소드가 호출되기 이전에 부가 기능을 적용하기 위해 다음과 같이 인터페이스를 구현(implements)하여 Proxy 객체를 직접 구현할 수 있다.

@Service
public class RateDiscountServiceProxy implements DiscountService {
   
    // 여기서는 RateDiscountService에 해당한다.
    private DiscountService discountService;    

    public DiscountServiceProxy(DiscountService discountService) {
        this.discountService = discountService;
    }

    @Override
    public int discount() {
        // 1. 메소드 호출 전의 부가 기능 처리

        // 2. 실제메소드 호출
        this.discountService.discount()

        // 3. 메소드 호출 후의 부가 기능 처리
    }
}

 

실제 객체의 메소드가 호출 전/후에 처리해야 하는 부가기능을 추가함으로써 AOP를 구현할 수 있다.

하지만 이러한 방식은 다음과 같은 문제점을 가지고 있다.

  1. 불필요하게 DiscountService 타입의 빈이 2개 등록됨
  2. DI(Dependency Injection, 의존성 주입) 시에 문제가 발생할 수 있음

Spring이 1개의 타입에 대해 불필요하게 여러 개의 빈을 관리해야 할 뿐만 아니라 해당 타입의 빈이 여러 개이므로 의존성 주입 시에도 문제가 발생할 여지가 있는 것이다. (물론 변수 이름이나 지시자 등으로 피할 수 있지만 이는 번거롭다.)

 

 

[ Spring을 통한 JDK dynamic proxy 기반의 자동 Proxy 구현 ]

위와 같은 문제를 해결하기 위해 Spring은 JDK dynamic proxy를 기본으로 하는 자동 프록시 생성기를 통해 직접 프록시 객체를 생성한 후에 특별한 처리를 해준다.

Spring은 프록시를 구현할 때 프록시를 구현한 객체(RateDiscountServiceProxy)를 마치 실제 빈(RateDiscountService)인 것처럼 포장하고, 2개의 빈을 모두 등록하는 것이 아니라 실제 빈을 프록시가 적용된 빈으로 바꿔치기한다.

이러한 방식이 가능한 이유는 프록시 구현 대상(RateDiscountService)이 인터페이스(DiscountService)를 구현하고 있으며, Spring은 프록시 구현체(RateDiscountServiceProxy)를 만들때 프록시 대상과 동일한 인터페이스(DiscountService)를 구현하도록 했기 때문이다.

즉, 프록시 대상(RateDiscountService)과 프록시 구현체(RateDiscountServiceProxy) 모두 동일한 인터페이스(DiscountService) 타입 및 구현체이기 때문에 기존의 RateDiscountService 빈을 RateDiscountServiceProxy로 바꿔치기 하고, 빈 후처리기를 통해 이미 정의된 의존 관계 역시 바꿀 수 있는 것이다.

이를 그림으로 표현하면 다음과 같다.

 

기존에는 왼쪽과 같이 실제 빈과 Proxy 빈 2개가 스프링 컨테이너에 등록되었다면, Spring이 자동 프록시 생성을 통해 생성된 Proxy 빈을 실제 빈처럼 구현하고 대체하도록 하는 것이다.

이러한 방식은 괜찮아 보이지만 만약 실제 빈을 직접 참조하고 있는 경우라면 문제가 발생한다.

위의 예제에서 문제가 발생하지 않은 이유는 실제 빈(RateDiscountService)이 DiscountService라는 인터페이스에 의존하고 있고, DiscountController에서도 DiscountService에 의존하고 있기 때문이다.

하지만 만약 다음과 같이 결제를 담당하는 PaymentService에서 구체 클래스(RateDiscountService)를 주입받고 있다면 어떻게 될까?

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final RateDiscountService rateDiscountService;

}

 

Spring이 새롭게 추가한 RateDiscountServiceProxy는 DiscountService 인터페이스를 구현한 클래스이지 RateDiscountService를 상속받은 클래스가 아니다. 그래서 RateDiscountService 타입의 빈을 찾을 수 없어 에러가 발생하게 된다.

 

 

[ Spring을 통한 CGLib 기반의 자동 Proxy 구현 ]

위와 같은 문제를 해결하기 위해서는 Spring이 구현해주는 Proxy 객체가 인터페이스(DiscountService)를 구현한 객체가 아닌 클래스(RateDiscountService)를 구현한 객체여야 한다. 즉, 클래스를 기반으로 프록시를 구현하도록 강제해야 하는 것이다.

public class RateDiscountServiceProxy extends RateDiscountService {

    ...  
}

 

그러면 이제 Spring은 RateDiscountServiceProxy를 구현할 때 위와 같이 RateDiscountService를 상속받아 구현하는데, 이러한 클래스 기반의 프록시를 구현하기 위해서는 바이트 코드를 조작해야 한다. 그래서 Spring은 CGLib이라는 바이트 조작 라이브러리를 통해 바이트 코드를 조작하여 프록시를 구현하고 있다.

이로써 위에서 발생했던 빈의 의존성 관련 문제를 해결할 수 있도록 도와주고 있다. 그리고 프록시 객체를 기존의 방식처럼 인터페이스를 구현하도록 할 것인지 또는 해당 클래스를 바이트 조작하여 직접 구현하도록 할 것인지에 대한 옵션이 proxyTargetClass이다. 

이에 대한 옵션은 다음과 같이 설정할 수 있다.

@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AtddMembershipApplication {

	public static void main(String[] args) {
		SpringApplication.run(AtddMembershipApplication.class, args);
	}

}

 

 

 

하지만 SpringBoot를 사용하고 있다면 더이상 이러한 옵션을 부여하지 않아도 된다. 왜냐하면 SpringBoot에서는 CGLib 라이브러리가 안정화되었다고 판단하여 proxyTargetClass 옵션의 기본값을 True로 사용하고 있다.

 

[ 정리 및 요약 ]

원래 Spring은 프록시 타깃 객체에 인터페이스가 있다면 그 인터페이스를 구현한 JDK 다이내믹 프록시 방식으로 객체를 생성하고, 인터페이스가 없다면 CGLib을 이용한 클래스 프록시를 만든다. 하지만 이렇게 인터페이스를 구현하면 문제가 발생할 수 있어서 바이트를 조작하여 클래스 기반의 프록시를 강제하는 옵션을 제공하고 있다.

이러한 클래스 기반의 프록시 생성을 강요하는 옵션이 proxyTargetClass이며, 이 값을 true로 지정해주면 Spring은 인터페이스가 있더라도 무시하고 클래스 프록시를 만들게 된다.

 

 

 

 

관련 포스팅

  1. AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 이해 - (1/3)
  2. AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)의 개념 및 사용 방법 예제 코드 - (2/3)
  3. Spring에서 AOP Proxy 구현 방법과 @EnableAspectJAutoProxy의 proxyTargetClass - (3/3)

 

 

 

반응형
댓글
댓글쓰기 폼
반응형
공지사항
Total
1,479,682
Today
3,003
Yesterday
4,488
TAG more
«   2021/09   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
글 보관함