스프링 핵심원리 고급편

jjunhwan.kim·2023년 1월 8일
0

스프링 핵심원리 고급편

로그 추적기 예제

  • 로그를 남기기 위해 TraceId, Trace 객체를 만듬
  • Controller -> Service -> Repository 로 이어지는 흐름에서 HTTP 요청을 구분하고 깊이를 표현하기 위해서 동기화 기능추가
  • TraceId 를 전달하여 레벨을 구분하고, 각 HTTP 요청 단위로 구분
  • Controller, Service, Repository 의 각각의 메서드에서 로그를 남기기위해 TraceId를 파라미터로 전달받아야 하는 단점

쓰레드 로컬

  • TraceId를 파라미터로 전달 -> 필드를 사용하여 Trace 객체에 저장
  • Trace 객체는 싱글톤인데 여러 쓰레드가 접근하여 하나의 필드를 사용하므로 동시성 문제 발생
  • 쓰레드 로컬 도입
  • 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 저장소
  • 쓰레드 풀 환경에서 쓰레드 로컬 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해야함. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있음

템플릿 메서드 패턴과 콜백 패턴

  • 기존 로그 추적기 예제 때문에 핵심 기능 코드에 로그 출력 부가 기능 코드가 추가됨
  • 변하는 부분과 변하지 않는 부분 분리
  • 부모 클래스에 변하지 않는 템플릿 코드를 작성하고 자식 클래스에서 상속과 오버라이딩을 사용하여 변하는 부분을 작성
  • 자식 클래스를 계속 만들어야하는 단점 -> 익명 내부 클래스를 사용하여 보완
  • 제네릭에서 반환 타입이 필요한데, 반환할 내용이 없으면 Void 타입을 사용하고 null 을 반환하면 됨
  • 좋은 설계란 변경이 일어날 때 드러남
  • 로그를 남기는 로직과 비즈니스 로직을 분리하고, 로그를 남기는 로직을 한 곳으로 모듈화 => 로그 로직이 변경될 경우 템플릿 코드만 변경하면 됨
  • 템플릿 메서드 패턴
    • 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있음
    • 상속과 오버라이딩을 사용한 다형성
    • 단점으로 상속을 사용하기 때문에 자식 클래스가 부모 클래스에 컴파일 시점에 강하게 결합됨
    • 자식 클래스에서 부모 클래스의 기능을 사용하지 않음, 즉 자식 클래스는 비즈니스 로직에 관련된 내용이므로 부모 클래스와 관련이 없지만 부모 클래스를 직접 상속받으므로 부모 클래스를 알아야 하는 문제 발생(자식 클래스 코드 에서 extends 로 부모 클래스를 직접 명시함)
  • 전략 패턴
    • 변하지 않는 부분을 Context 에 두고 변하는 부분을 Strategy 인터페이스 구현체에 작성
    • 상속관계가 아닌 위임으로 문제를 해결
    • Context 가 변하지 않는 템플릿 역할, Strategy 가 변하는 알고리즘 역할
    • 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만듬. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있음
    • 전략 패턴의 핵심은 Context 는 Strategy 인터페이스에만 의존함 Strategy 의 구현체를 변경하거나 새로 만들어도 Context 코드에는 영향을 주지 않음
    • 익명 내부 클래스를 자바8부터 제공하는 람다로 변경할 수 있음. 람다로 변경하려면 인터페이스에 메서드가 1개만 있으면 됨(함수형 인터페이스)
    • 전략 패턴은 Context 객체의 필드에 Strategy 객체의 참조를 설정하고 미리 조립하여 실행시점에 호출 하는 방식
    • Context 메서드의 파라미터로 Strategy 객체를 전달 받도록 구조를 수정하면 템플릿 콜백 패턴이 됨
    • 전략, 템플릿 콜백 패턴을 사용해도 예제의 로그 추적기를 적용하기 위해 원본 코드를 수정해야하는 한계점이 있음

프록시 패턴과 데코레이터 패턴

  • 인터페이스와 구현 클래스가 있는 경우 스프링 빈 수동 등록
  • 인터페이스 없는 구체 클래스인 경우 스프링 빈 수동 등록
  • 컴포넌트 스캔으로 자동으로 빈 등록(@Controller, @Service, @Repository 어노테이션 사용)
  • 원본 코드를 수정하지 않고 로그 추적기 도입을 위해 프록시 적용
  • 프록시를 통해 접근제어(권한에 따른 접근 차단 캐싱, 지연 로딩), 부가기능 추가(요청 값이나 응답 값 변형, 로그 등), 프록시 체인 등을 활용할 수 있음
  • 프록시 객체를 사용하려면 클라이언트 변경 없이 서버 객체를 프록시 객체로 변경할 수 있어야 함 -> 클라이언트는 인터페이스에만 의존해야 함 -> 서버 객체와 프록시 객체가 동일한 인터페이스를 구현해야 함
  • 디자인 패턴으로 프록시 패턴(접근 제어), 데코레이터 패턴(새로운 기능 추가) 사용, 사용 의도에 따라 구분
  • 프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
  • 데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
  • 프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴
  • 로그 추적기 예제에 프록시 패턴 도입
    • 인터페이스와 구현 클래스가 있는 경우 스프링 빈 수동 등록
      • 프록시 패턴 사용
      • 인터페이스를 구현한 프록시 객체 생성
      • 프록시 객체를 실제 객체 대신 스프링 빈으로 등록 -> 프록시 객체가 실제 객체를 내부에 참조
    • 인터페이스 없는 구체 클래스인 경우 스프링 빈 수동 등록
      • 구체 클래스를 상속하여 프록시 객체 생성
      • 항상 부모 클래스의 생성자를 호출해야하는 단점이 있음. 프록시 객체는 자식 클래스이긴 하지만 부모 클래스의 기능을 사용하지 않음
    • 컴포넌트 스캔으로 자동으로 빈 등록(@Controller, @Service, @Repository 어노테이션 사용)
  • 프록시를 도입하여 원본 코드를 변경하지 않고 로그 추적기 기능을 적용할 수 있음
  • 클래스 기반 프록시 vs 인터페이스 기반 프록시
    • 클래스 기반 프록시는 해당 클래스에만 적용가능
    • 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용 가능
  • 클래스 기반 프록시는 상속을 사용
    • 부모 클래스의 생성자를 호출해야 함
    • 클래스에 final 키워드가 붙으면 상속 불가능
    • 메서드에 final 키워드가 붙으면 해당 메서드 오버라이딩 불가능
  • 인터페이스 기반 프록시
    • 인터페이스를 도입해야 함 -> 실제로 구현체를 변경할 일이 없는 경우 불필요한 인터페이스 사용
    • 캐스팅 관련 문제
  • 프록시
    • 부가 기능을 적용할 모든 객체의 프록시 객체를 각각 생성해야 함. 각각의 클래스의 프록시 클래스도 작성해야 함

동적 프록시 기술

  • 기존 프록시는 대상 클래스 수 만큼 프록시 클래스를 만들어야 함
  • 자바가 제공하는 JDK 동적 프록시, CGLIB 같은 프록시 생성
  • 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출 가능
  • 리플렉션 기술은 런타임에 동작하므로 컴파일 시점에 오류를 잡을 수 없음 => 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 함
  • JDK 동적 프록시
    • 인터페이스를 기반으로 프록시를 동적으로 만들어줌, 인터페이스가 필수임!!
    • InvocationHandler 인터페이스를 구현하여 프록시 로직을 작성
    • Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler)
    • 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환
    • 프록시 클래스를 각각 작성하지 않고 InvocationHandler 인터페이스 구현체 하나만 작성하면 JDK 동적 프록시가 각각의 객체에 맞는 프록시 객체를 동적으로 생성해줌
  • CGLIB(Code Generator Library)
    • 바이트 코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
    • CGLIB 를 사용하여 인터페이스 없이 구체 클래스만 가지고 동적 프록시 생성 가능
    • CGLIB 는 외부 라이브러리이지만 스프링 프레임워크가 내부 소스 코드에 포함
    • MethodInterceptor 제공
    • JDK 동적 프록시는 인터페이스를 구현해서 프록시를 만들지만, CGLIB 는 구체 클래스를 상속하여 프록시를 만듬
    • 대상클래스$$EnhancerByCGLIB$$임의코드 와 같은 규칙으로 클래스가 생성됨 ex) ConcreteService$$EnhancerByCGLIB$$25d6b0e3
    • 클래스 기반 프록시는 상속을 사용하기 때문에 제약이 있음
      • 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요
      • 클래스에 final 키워드가 붙으면 상속 불가능 불가능. CGLIB에서는 예외 발생
      • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음. CGLIB에서는 프록시 로직이 동작하지 않음

스프링이 지원하는 프록시

  • JDK 동적 프록시는 인터페이스 기반의 프록시 제공

    • InvocationHandler
  • CGLIB 는 클래스 상속 기반의 프록시 제공

    • MethodInterceptor
  • 스프링에서 동적 프록시를 통합하여 프록시 팩토리 라는 기능을 제공

    • 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB 를 사용
    • InvocationHandler, MethodInterceptor 를 각각 중복으로 만들지 않도록 Advice 라는 개념 도입
    • 개발자는 Advice 만 작성하여 부가기능을 만들 수 있음
    • 또한 특정 조건을 만족할 때만 프록시 부가기능이 적용되도록 하는 Pointcut 개념을 도입함
  • Advice 는 기본적으로 MethodInterceptor 인터페이스(org.aopalliance.intercept.MethodInterceptor)를 구현하면 됨 (CGLIB 의 MethodInterceptor 와 다른 인터페이스임)

  • 프록시 팩토리의 기술 선택 방법

    • 대상 객체에 인터페이스가 있으면 JDK 동적 프록시, 인터페이스 기반 프록시 적용
    • 대상 객체에 인터페이스가 없으면 CGLIB, 구체 클래스 기반 프록시 적용
    • proxyTargetClass 옵션을 true로 전달하면 인터페이스 여부와 상관없이 CGLIB, 구체 클래스 기반 프록시 적용
  • 프록시 팩토리의 서비스 추상화로 인해 구체적인 기술(CGLIB, JDK 동적 프록시)에 의존하지 않고 편리하게 동적 프록시를 생성할 수 있음

  • 스프링 부트는 AOP 를 적용할 때 기본적으로 proxyTargetClass 를 true 로 설정하여 항상 CGLIB 를 사용해서 구체 클래스 기반으로 프록시를 생성함

  • 포인트컷, 어드바이스, 어드바이저

    • 포인트컷
      • 어디에 부가 기능을 적용할지 판단하는 필터링 로직
      • 주로 클래스와 메서드 이름으로 필터링
    • 어드바이스
      • 프록시가 호출하는 부가 기능
      • 프록시 로직
    • 어드바이저
      • 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 객체
    • 작동원리
      • 클라이언트가 프록시 객체의 메서드 호출
      • 포인트컷에서 호출한 메서드의 어드바이스를 적용할지 판단
      • 포인트컷이 true 반환시 어드바이스를 호출하여 부가 기능을 적용하고 실제 인스턴스를 호출
  • 스프링이 제공하는 포인트컷

    • NameMatchMethodPointcut
      • 메서드 이름 기반 매칭
    • JdkRegexpMethodPointcut
      • JDK 정규 표현식 기반 매칭
    • TruePointcut
      • 항상 true 반환
    • AnnotationMatchingPointcut
      • 어노테이션으로 매칭
    • AspectJExpressionPointcut
      • aspectJ 표현식으로 매칭
  • 여러 어드바이저 함께 적용

    • 기본적으로 어드바이저는 하나의 포인트컷과 하나의 어드바이스로 구성
    • 부가 기능을 여러 개 적용하려면 프록시 객체를 여러개 만드는 방법 -> 프록시 객체를 어드바이저 수만큼 생성해야하는 단점
    • 프록시 팩토리에 여러 개의 어드바이저를 등록하는 방법
      • 등록한 순서대로 어드바이저가 호출
    • 스프링 AOP 는 하나의 타겟 객체에 여러 AOP 가 동시에 적용되어도 하나의 프록시 객체만 생성함
  • 프록시 팩토리의 한계

    • 프록시 팩토리를 통한 프록시 객체 생성 코드를 프록시 수 만큼 작성해야함
    • 컴포넌트 스캔으로 프록시 적용이 불가능하고 수동으로 빈을 설정해야함

빈 후처리기

  • BeanPostProcessor
    • 스프링 빈으로 등록할 객체를 빈 저장소에 등록하기 직전에 조작할 때 사용
    • 스프링 빈에 등록할 객체 생성(@Bean, 컴포넌트 스캔) -> 빈 후처리기 -> 스프링 빈 저장소 등록
    • 빈 후처리기는 빈으로 등록할 객체를 조작하거나, 다른 객체로 바꿀수도 있음
    • 스프링에서 제공하는 BeanPostProcessor 인터페이스를 구현하고 스프링 빈으로 등록하면 됨
    • postProcessBeforeInitialization 메서드는 객체 생성 이후 @PostConstruct 같은 초기화가 발생하기 전에 호출
    • postProcessAfterInitialization 메서드는 객체 생성 이후 @PostConstruct 같은 초기화가 발생한 후에 호출
    • 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있으므로 빈 객체를 프록시로 교체할 수도 있음
  • CommonAnnotationBeanPostProcessor
    • 스프링이 자동으로 등록하는 빈 후처리기인데, @PostConstruct 어노테이션이 붙은 메서드를 호출함
    • 스프링도 스프링 내부의 기능을 확장하기위해 빈 후처리기를 사용함
  • 빈 후처리기를 적용하여 실제 객체 대신 프록시 객체를 스프링 빈으로 등록
    • 수동으로 등록하는 빈, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있음
    • 수동 빈 등록 설정 파일의 많은 프록시 팩토리를 통한 프록시 생성 코드를 제거할 수 있음
    • 빈 설정파일에는 빈 등록만 작성하면되고, 프록시 생성 코드는 빈 후처리기에만 작성하면 됨
  • 스프링이 제공하는 빈 후처리기
    • 포인트컷은 결과적으로 두 곳에 사용됨
    • 포인트컷을 통해 빈 후처리기에서 프록시를 생성할 객체를 판단할 수 있고, 프록시 메서드가 호출되었을 때 어드바이스를 적용할 지 판단 할 수 있음
    • spring-boot-starter-aop 라이브러리를 추가해야 함
    • AnnotationAwareAspectJAutoProxyCreator 빈 후처리기가 자동으로 등록됨
  • AnnotationAwareAspectJAutoProxyCreator
    • 스프링 빈으로 등록된 어드바이저를 찾아 프록시가 필요한 곳에 자동으로 적용해줌
    • 어드바이저 객체 안에는 포인트컷과 어드바이스가 포함되어 있으므로 포인트컷으로 어떤 스프링 빈에 프록시를 적용할지 판단하고, 어드바이스로 부가 기능을 적용함
    • 작동 원리
      • 스프링 빈 대상이 되는 객체를 생성(@Bean, 컴포넌트 스캔)
      • 빈 저장소에 객체를 등록하기 전 빈 후처리기에 전달
      • 빈 후처리기는 모든 어드바이저를 조회
      • 어드바이저의 포인트컷을 사용하여 해당 객체가 프록시를 적용할 대상인지 판단. 조건이 하나라도 만족하면 프록시 적용 대상이 됨
      • 프록시 적용 대상이면 프록시를 생성하고 반환하여 스프링 빈으로 등록
      • 반환된 객체가 스프링 빈으로 등록됨
  • 프록시와 어드바이저
    • 스프링 빈이 여러 개의 어드바이저의 포인트 컷 조건을 만족해도 프록시를 하나만 생성함
    • 프록시 객체 내부에 여러 개의 어드바이저를 포함할 수 있으므로 하나의 프록시만 생성함

@Aspect AOP

  • 스프링에 자동으로 등록되는 빈 후처리기 덕분에 어드바이저 객체만 빈으로 등록하면 프록시가 자동으로 적용됨
  • 스프링은 @Aspect 어노테이션으로 편리하게 어드바이저를 생성할 수 있는 기능을 지원함
  • @Aspect
    • Aspect 클래스에 선언
  • @Around
    • 값으로 포인트컷 표현식 정의, ApsectJ 표현식 사용
    • @Around가 적용된 메서드는 어드바이스가 됨
  • AnnotationAwareAspectJAutoProxyCreator
    • 어드바이저 객체를 자동으로 찾아 프록시가 필요한 곳에 프록시를 생성하고 적용해줌
    • @Apsect 어노테이션을 찾아 어드바이저로 만들어줌 => @Aspect 기반 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장함(BeanFactoryAspectJAdvisorsBuilder)
      • @Aspect 가 선언된 클래스의 @Around 가 포인트컷이 됨
      • @Around 가 선언된 메서드가 어드바이스가 됨
    • 만들어진 어드바이저를 기반으로 프록시 객체를 생성함
      • @Aspect 기반 어드바이저를 어드바이저 빌더에서 조회함

스프링 AOP 개념

  • 핵심 기능과 부가 기능
    • 핵심 기능은 해당 객체가 제공해야하는 고유 기능 ex) 주문 로직
    • 부가 기능은 핵심 기능을 보조하기 위해 제공하는 기능 ex) 로그, 트랜잭션
    • 부가 기능은 핵심 기능과 함께 사용됨
  • 부가 기능
    • 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용됨
    • ex) 로깅
    • 이러한 부가 기능은 횔단 관심사가 됨
    • 즉 하나의 부가 기능이 여러 곳에 동일하게 사용됨
  • 부가 기능 적용 문제
    • 부가 기능의 특성상 여러 문제가 발생함
    • 부가 기능을 적용할 때 많은 반복이 필요함 -> 적용 되는 모든 클래스에 동일한 코드를 추가해야 함
    • 부가 기능이 여러 곳에 중복 코드를 만들어냄
    • 부가 기능 변경시 많은 수정이 필요함
    • 부가 기능의 적용 대상을 변경할 때 많은 수정이 필요함
  • Aspect
    • 부가 기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 함
    • 부가 기능을 어디에 적용할지 선택하는 기능을 만듬
    • 부가 기능과 부가 기능을 어디에 적용할지 선택하는 기능을 합쳐 하나의 모듈로 만든 것이 애스펙트임
    • 부가 기능과 해당 부가 기능을 어디에 적용할지 정의한 것
    • 애스펙트는 관점이라는 뜻으로 애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사의 관점으로 다르게 보는 것
    • 애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍(AOP) 라고 함
  • AspectJ 프레임워크
  • AOP 적용 방식
    • AOP 를 사용하면 핵심 기능과 부가 기능이 코드상 완전히 분리되어 관리됨
    • AOP 사용시 부가 기능 로직을 실제 로직에 추가하는 방법
      • 컴파일 시점
        • 자바 소스 코드를 컴파일하여 바이트코드를 만드는 시점에 부가 기능 로직 추가
        • AspectJ 가 제공하는 특별한 컴파일러를 사용해야함
        • 부가 기능 코드가 핵심 기능이 있는 컴파일된 코드 주변에 추가
      • 클래스 로딩 시점
        • 자바를 실행하면 .class 파일을 JVM 내부의 클래스 로더에 보관
        • 이 때 .class 를 JVM 에 저장하기 전에 조작
      • 런타임 시점(프록시)
        • 컴파일 이후, 클래스 로더에 .class 도 다 올라가서 자바가 실행되고 난 이후
        • 자바의 main 메서드가 실행된 이후
        • 따라서 자바 언어가 제공하는 범위 내에서 부가 기능을 적용하기 위해 스프링 컨테이너, 프록시, DI, 빈 후처리기 등을 사용하여 스프링 빈에 부가 기능을 적용
  • AOP 적용 위치
    • 적용 가능 지점(조인 포인트)는 생성자, 필드 값 접근, static 메서드 접근, 메서드 실행 등이 있음
    • AspectJ 를 사용하여 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP 는 바이트코드를 조작하므로 모든 지점에 적용할 수 있음
    • 프록시 방식을 사용하는 스프링 AOP 는 메서드 실행 지점에만 AOP 를 적용할 수 있음
      • 프록시는 메서드 오버라이딩 개념으로 동작하므로 생성자, static 메서드, 필드 값 접근에는 사용할 수 없음
      • 프록시를 사용하는 스프링 AOP 의 조인 포인트는 메서드 실행으로 제한됨
    • 프록시 방식을 사용하는 스프링 AOP 는 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP 를 적용할 수 있음
  • 스프링은 AspectJ 의 문법만 사용하고 프록시 방식의 AOP 를 적용함
  • AOP 용어
    • 조인 포인트
      • 어드바이스가 적용될 수 있는 위치
      • ex) 메서드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근
      • AOP 를 적용할 수 있는 모든 지점
      • 스프링 AOP 는 프록시 방식을 사용하므로 항상 메서드 실행 지점으로 제한됨
    • 포인트컷
      • 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
      • 주로 AspectJ 표현식 사용
      • 프록시를 사용하는 스프링 AOP 는 메서드 실행 지점만 포인트컷으로 선별 가능
    • 타겟
      • 어드바이스를 받는 객체, 포인트컷으로 결정
    • 어드바이스
      • 부가 기능
      • 특정 조인 포인트에서 Aspect 에 의해 취해지는 조치
      • Around, Before, After 와 같이 다양한 종류의 어드바이스 존재
    • 애스펙트
      • 어드바이스 + 포인트컷을 모듈화 한 것
      • @Aspect
      • 여러 어드바이스와 포인트컷이 함께 존재
    • 어드바이저
      • 하나의 어드바이스와 하나의 포인트컷으로 구성
      • 스프링 AOP 에만 사용되는 특별한 용어
    • 위빙
      • 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
      • 컴파일 타임, 로드 타임, 런타임
    • AOP 프록시
      • AOP 기능을 구현하기 위해 만든 프록시 객체
      • 스프링 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시

스프링 AOP 구현

  • 스프링 AOP 구현
    • 스프링 AOP 를 구현하는 일반적인 방법은 @Aspect 를 사용하는 방법
    • @Around 어노테이션의 값은 포인트컷이 됨
    • @Around 어노테이션의 메서드는 어드바이스가 됨
    • 포인트컷 표현식으로 AspectJ 포인트컷 표현식 사용
    • @Aspect 는 컴포넌트 스캔이 자동으로 되지 않으므로 빈으로 등록해야함
    • @Around 에 포인트컷 표현식을 직접 넣을수도 있고 @Pointcut 어노테이션을 사용하여 별도로 분리할 수 있음
    • 어드바이스는 기본적으로 순서를 보장하지 않음
    • 하나의 애스펙트에 하나의 어드바이스를 사용해야 함
    • @Order 어노테이션을 사용하여 클래스 단위로 순서를 적용할 수 있음
    • @Around, @Before, @AfterReturning, @AfterThrowing, @After 등 여러가지 종류의 어드바이스 존재
  • 모든 어드바이스는 JoinPoint 를 첫번째 파라미터에 사용할 수 있음

스프링 AOP - 포인트컷

  • AspectJ 는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공함 => AspectJ pointcut expression
  • 포인트컷 지시자
    • 포인트컷 표현식은 포인트컷 지시자로 시작함
    • execution
      • 메서드 실행 조인포인트 매칭
      • execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
        - ? 는 생략 가능
        - 같은 패턴 지정 가능
        - "execution(
        hello.aop.member.MemberServiceImpl.hello(..))"
        - "execution( hello.aop...*(..))"
        - 파라미터에서 .. 은 파라미터의 타입과 파라미터의 수가 상관없다는 뜻
      • 패키지에서 . 은 정확하게 해당 위치의 패키지, .. 은 해당 위치의 패키지와 그 하위 패키지도 포함
        - 타입은 부모 타입을 선언해도 자식 타입은 매칭됨
        - 부모 타입 사용시 메서드는 부모 타입에 선언된 메서드와 일치해야 매칭됨
        - 파라미터
        - (String) : 정확하게 String 타입 파라미터
        - () : 파라미터 없음
        - () : 정확하게 하나의 파라미터, 단 모든 타입
        - (
        , *) : 정확하게 두개의 파라미터, 단 모든 타입
        - (..) : 모든 파라미터, 모든 타입, 파라미터가 없어도 됨
        - (String, ..) : String 타입으로 시작하고 그 이후 모든 파라미터 모든 타입
    • within
      • 특정 타입 내의 조인포인트 매칭
        - 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭됨
        - 단, 타입이 정확하게 맞아야 함 부모타입 지정시 자식타입 매칭 안됨
        - 따라서 인터페이스를 타입으로 지정하면 안됨
    • args
      • 인자가 주어진 타입의 인스턴스인 조인포인트로 매칭
        - execution 은 파라미터 타입이 정확하게 매칭되어야 함 클래스에 선언된 정보를 기반으로 판단
        - args 는 부모 타입을 허용함 실제 넘어온 파라미터 객체의 인스턴스를 보고 판단
    • this
      • 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인포인트
    • target
      • 타겟 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인포인트
    • @target
      • 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인포인트
        - 인스턴스의 모든 메서드를 조인포인트로 적용
        - 부모 클래스의 메서드까지 어드바이스 적용
    • @within
      • 주어진 어노테이션이 있는 타입 내 조인포인트
        - 해당 타입 내에 있는 메서드만 조인포인트로 적용
        - 자기 자신의 클래스에 정의된 메서드에만 어드바이스 적용
    • @annotation
      • 메서드가 주어진 어노테이션을 가지고 있는 조인포인트를 매칭
        - 메서드에 어노테이션이 있으면 매칭
    • @args
      • 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인포인트
    • bean
      • 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷 지정
        - bean(orderService) || bean(*Repository) 등 사용 가능
    • args, @args, @target 은 단독으로 사용하면 안됨!
      • 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있음
        - 따라서 프록시가 있어야 포인트컷 적용 여부를 판단 할 수 있음
      • 단독으로 사용하면 모든 빈에 프록시를 적용하게 되므로 오류가 발생할 수 있음 => 스프링 내부 빈 중 final 로 지정한 것들이 있을 수 있으므로 오류 발생
      • 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야함

스프링 AOP - 실무 주의사항

  • 프록시와 내부 호출

    • 스프링은 프록시 방식의 AOP 사용
    • 따라서 AOP 를 적용하려면 항상 프록시를 통해서 대상 객체를 호출해야 함
      • 프록시에서 어드바이스를 호출하고 이후에 대상 객체를 호출함
    • 프록시를 거치지 않고 대상 객체를 직접 호출하면 AOP 가 적용되지 않고 어드바이스도 호출되지 않음
    • AOP 를 적용하면 스프링은 대상 객체 대신 프록시를 스프링 빈으로 등록 => 의존관계 주입시 항상 프록시 객체를 주입 => 대상객체를 직접 호출하는 문제는 일반적으로 발생 안함
    • 단, 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생함!
  • 프록시 방식의 AOP 한계

    • 스프링은 프록시 방식의 AOP 를 사용
    • 프록시 방식의 AOP 는 메서드 내부 호출에 프록시를 적용할 수 없음
  • 프록시와 내부 호출 대안

    • 자기 자신 주입
      • 수정자를 통해 자기 자신 주입 => 프록시 객체가 주입됨
        - 단 생성자 주입시 순환 참조 오류 발생
    • 지연 조회
      • ObjectProvider(Provider) 또는 ApplicationContext 사용
      • ObjectProvider 는 객체를 스프링 컨테이너에서 조회하는 것을 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연 가능
        - ObjectProvider 의 getObject 메서드를 호출하는 시점에 스프링 컨테이너에서 빈을 조회함
    • 구조 변경
      • 내부 호출이 발생하지 않도록 구조 변경
        - 내부 호출이 발생하는 메서드를 별도의 클래스로 분리
  • 프록시 기술과 한계 - 타입 캐스팅

    • JDK 동적 프록시와 CGLIB 를 사용해서 AOP 프록시를 만드는 방법에 각각 장단점이 있음
    • JDK 동적 프록시는 인터페이스가 필수이고 인터페이스를 기반으로 프록시를 생성
    • CGLIB 는 구체 클래스를 기반으로 프록시 생성
    • 인터페이스가 없고 구체 클래스만 있는 경우 CGLIB 를 사용해야 함
    • 인터페이스가 있는 경우에는 JDK 동적 프록시나 CGLIB 둘 중에 하나를 선택할 수 있음
    • 스프링이 프록시를 만들때 제공하는 프록시 팩토리에 proxyTargetClass 옵션에 따라 JDK 동적 프록시와 CGLIB 를 선택하여 프록시를 만들 수 있음
    • JDK 동적 프록시 한계
      • 인터페이스 기반으로 프록시를 생성
        - 구체 클래스로 타입 캐스팅이 불가능한 한계
        - JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하므로 구체 클래스를 알지 못하므로 구체 클래스로 캐스팅시 예외가 발생함
        - 반면 CGLIB 프록시는 대상 클래스 기반으로 프록시를 생성하므로 구체 클래스로 타입 캐스팅시 성공함, 인터페이스로 타입 캐스팅도 성공함
  • 프록시 기술과 한계 - 의존관계 주입

    • JDK 동적 프록시 의존관계 주입시 인터페이스 타입은 성공하지만 구체 클래스 타입에는 의존관계 주입이 불가능함
    • CGLIB 프록시는 인터페이스, 구체클래스 모두 의존관계 주입이 성공함
  • 프록시 기술과 한계 - CGLIB

    • 스프링에서 CGLIB 는 구체 클래스를 상속 받아서 AOP 프록시를 생성할 때 사용
    • 구체 클래스를 상속받기 떄문에 다음과 같은 문제 존재
      • 대상 클래스에 기본 생성자 필수
        - 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 함 => 생략하면 자식 클래스 생성자 첫줄에 부모 클래스의 기본 생성자를 호출하는 super() 가 자동으로 들어감
        - CGLIB 프록시 사용시 CGLIB 프록시는 대상 클래스를 상속 받고, 생성자에서 대상 클래스의 기본 생성자를 호출함
        - 따라서 대상 클래스에 기본 생성자를 만들어야 함 (기본 생성자는 파라미터가 없는 생성자)
        - 생성자 2번 호출 문제
        - CGLIB 프록시는 구체 클래스를 상속 받으므로 프록시 생성시 생성자가 호출 되고 부모 클래스인 대상 클래스의 생성자를 호출함
        - 실제 대상 객체 생성시 대상 클래스의 생성자를 호출함
        - 따라서 대상 클래스의 생성자가 2번 호출됨
        - final 키워드 클래스, 메서드 사용 불가
        - final 키워드가 클래스에 있으면 상속이 불가능함
        - final 키워드가 메서드에 있으면 오버라이딩이 불가능함
        - CGLIB 프록시는 상속을 기반으로 하기 때문에 두 경우 모두 프록시가 생성되지 않거나 정상 동작하지 않음
  • 프록시 기술과 한계 - 스프링의 해결책

    • 스프링은 CGLIB 라이브러리를 스프링 내부에 함께 패키징하여 별도의 라이브러리 추가 없이 CGLIB 를 사용할 수 있게 함
    • CGLIB 기본 생성자 필수 문제 해결
      • objenesis 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능해짐
    • 생성자 2번 호출 문제
      • objenesis 라이브러리를 사용해서 해결됨 현재는 생성자가 1번 호출됨
    • 스프링 부트 2.0 - CGLIB 기본 사용
      • 스프링 부트 2.0 부터 CGLIB 를 기본으로 사용하도록 함
        - 별다른 설정이 없다면 AOP 적용시 기본적으로 proxyTargetClass=true 로 설정해서 사용함
        - 따라서 인터페이스가 있어도 JDK 동적 프록시를 사용하지 않고 항상 CGLIB 를 사용해서 구체 클래스 기반으로 프록시를 생성함

0개의 댓글