본문으로 건너뛰기

Spring

  • 스프링이란 어플리케이션 개발에 필요한, 도움이 되는 기능들을 제공하는 프레임워크이자 생태계다.
  • 스프링의 핵심은 IoC/DI, AOP, PSA다.

Inversion of Control / Dependency Injection

  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.
  • 스프링에서 이야기하는 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 구현을 편리하게 변경할 수 있다.
  • 자바로 OCP, DIP 원칙들을 지키면서 개발을 해보면, 결국 스프링 프레임워크를 만들게 된다.(더 정확히는 DI 컨테이너)
    • DI 컨테이너의 역활이 없다면, 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다. - 구현 객체를 개발자가 직접 넣으니, 변경시 해당 코드가 변경 되어야 함
  • 스프링 DI 컨테이너는 객체의 생성 및 의존 관계 연결 책임을 개발자 코드 밖으로 옮겨, 클라이언트 코드를 변경하지 않고도 구현체를 교체할 수 있게 한다.

Inversion of Control(제어의 역전):

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
  • 스프링에서는 객체의 생성과 생명주기 관리를 개발자가 아닌 **스프링 컨테이너(IoC Container)**가 대신
    • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.

Dependency Injection(의존관계 주입)

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 전달해서 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
  • IoC를 구현하는 방식으로 인터페이스만 선언하면, 스프링 컨테이너가 런타임에 적절한 구현체를 '주입(Injection)'
  • Spring에 특화된 클래스를 요구하지 않으며, POJO(Plain-Old Java Object) 객체를 의존성 주입을 통해 연결할 수 있다.
  • 생성자 주입 (Constructor Injection) — 권장 불변성 유지, 필수 의존성 명확화, 테스트 용이

Spring App Lifecyle

  • 스프링 컨테이너: 설정정보를 바탕으로 스프링 빈를 생성하고 빈의 라이프사이클 관리 및 의존성주입을 담당하는 컴포넌트
  • 스프링 컨테이너의 인터페이스는 ApplicationContext
  • ApplicationContextBeanFactory를 상속받음
    • BeanFactory: 스프링 컨테이너의 최상위 인터페이스로 **스프링 빈(Spring IoC 컨테이너에 의해 관리되는 객체)**을 관리하고 조회하는 역할을 담당한다.
  1. SpringApplication.run()

  2. 애플리케이션 시작 통지 등 초기 이벤트를 처리할 리스너, Environment(application.properties(yml), 시스템 환경 변수, JVM 옵션 등) 준비

  3. Spring Boot는 애플리케이션 환경(Web인지 여부)을 보고 적절한 ApplicationContext 구현체를 자동 선택

    • AnnotationConfigApplicationContext(일반 Java 애플리케이션)
    • AnnotationConfigServletWebServerApplicationContext (Spring Boot Web)
    • AnnotationConfigReactiveWebServerApplicationContext (Spring WebFlux)
  4. Bean Definition(빈 설정 메타정보) 로딩

    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
    • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록
  5. BeanFactoryPostProcessor 실행 - 빈 설정 정보 변경 가능

  6. Bean Instantiation(빈 생성)

    • 스프링 컨테이너는 빈의 메타정보를 기반으로 스프링 빈을 생성
      • 생성자 주입 사용시 객체 생성부터 의존성 빈 주입 보장
      • 반면 다른 방법은 객체 생성 후, @Autowired 붙은 필드나 세터에 의존성이 주입
    • 스프링은 빈이 싱글톤이 되도록 보장해주어야 한다.
    • @Configuration을 붙이면 CGLIB로 프록시 객체를 만들어 싱글톤을 보장
    • 존재하면 빈을 반환, 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드를 동적으로 생성
  7. 빈 후처리기 - BeanPostProcessor

    • @PostConstruct 메서드 실행
    • 빈 후처리기는 모든 Advisor(포인트컷 + 어드바이스)를 확인
    • 타겟 빈이라면 ProxyFactory를 사용해 프록시 객체를 생성해 등록(컨테이너 -> 프록시 객체 -> 빈)
  8. ApplicationRunner / CommandLineRunner 실행: 모든 컨테이너 설정과 빈 생성이 끝나면, 마지막으로 이 러너들을 실행

  9. 애플리케이션 실행

  10. 애플리케이션이 종료될 때, 컨테이너는 관리하던 빈들을 안전하게 소멸

    • @PreDestroy, DisposableBean.destroy() 등이 호출되어 리소스(DB 연결, 파일 스트림 등)를 정리

Spring Bean lifecyle

  1. 스프링 빈 생성
    • 생성자 주입이 필요한 빈이라면, 의존성 주입과 객체 생성이 이 단계에서 함께 일어납니다.
    • 즉, 스프링은 의존성(다른 빈)을 먼저 준비한 뒤, 그 의존성을 인자로 넘겨 생성자를 호출합니다. final 필드도 이 시점에 초기화됩니다.
  2. 의존관계 주입
    • 필드 주입(@Autowired가 붙은 필드)이나 세터 주입(@Autowired가 붙은 세터 메서드) 방식에 해당합니다.
  3. 초기화
    • @PostConstruct 어노테이션이 붙은 메서드나, InitializingBean 인터페이스의 afterPropertiesSet() 메서드가 이 시점에 호출됩니다.
  4. 소멸전 콜백
    • @PreDestroy 어노테이션이 붙은 메서드나, DisposableBean 인터페이스의 destroy() 메서드가 이 시점에 호출됩니다.

객체의 생성과 초기화를 분리하자.

  • 생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다.
  • 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.
  • 생성자에서 무거운 초기화 작업을 하기보다, @PostConstruct에서 하는 것이 의존성 주입이 확실히 끝난 상태임을 보장받기에 권장
  • 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
  • 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다.

Bean Scope

  • 스프링이 해당 빈(Bean)을 어떻게, 얼마나 오래 관리할 것인가를 정의하는 생명주기 정책입니다.
  • 이 정책이 중요한 이유는 '상태(State)' 관리와 직결되기 때문

싱글톤 (Singleton)

  • 스프링 컨테이너(IoC Container) 당 오직 '하나'의 인스턴스만 생성됩니다.
  • 스프링의 기본(Default) 스코프입니다.
  • 모든 요청이 '하나의 객체'를 공유하므로, 싱글톤 빈에 상태를 가지게 해서는 안됌

프로토타입 (Prototype)

  • 빈을 요청(주입 또는 getBean() 호출)할 때마다 '매번 새로운' 인스턴스를 생성
  • 각 객체가 자신만의 '상태(State)'를 가질 수 있습니다.
  • 스프링이 생성만 하고 빈의 소멸(Destroy) 라이프사이클을 관리하지 않습니다.
  • 빈의 소멸은 전적으로 GC(가비지 컬렉터)나 개발자의 몫이 됩니다.

🌐 웹(Web) 스코프

  • 이 스코프들은 웹 환경에서만 (즉, WebApplicationContext에서만) 동작
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리 -> 종료 메서드 호출

request

  • HTTP 요청 하나가 시작되고 끝날 때까지 유지되는 스코프
  • 하나의 HTTP 요청 안에서는 동일한 빈 유지 (즉, 컨트롤러, 서비스에서 같은 빈을 공유)
  • 사용 시점: 요청과 관련된 정보(예: 사용자 IP, 트랜잭션 ID)를 요청 처리 전 과정에서 공유해야 할 때 사용

session

  • HTTP 세션 하나가 유지되는 동안 동일한 빈 인스턴스를 유지
  • 사용자(브라우저)별로 다른 빈이 생성
  • 사용자가 로그아웃하거나 세션이 만료될 때까지 유지

application

  • 사실상 싱글톤 스코프와 거의 동일하게 동작 -> 모든 세션, 모든 요청에서 공유
  • 사실상 싱글톤 빈으로 대체 가능하여 잘 사용하지는 않음

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

  • 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
  • 그런데 싱글톤 빈은 생성시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.
  • 아마 원하는 것이 이런 것은 아닐 것이다. 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하는 것을 원할 것이다.
  • 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
  • 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다.
  • 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.

AOP

핵심 기능과 부가 기능

  • 애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나눌 수 있다.

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능이다.

  • 부가 기능은 핵심 기능을 보조하기 위해 제공되는 기능이다.

    • 예를 들어서 로그 추적 로직, 트랜잭션 기능이 있다.
  • 이러한 부가 기능은 단독으로 사용되지 않고, 핵심 기능과 함께 사용된다.

  • 부가 기능 적용 문제를 정리하면 다음과 같다.

    • 부가 기능을 적용할 때 아주 많은 반복이 필요하다.
    • 부가 기능이 여러 곳에 퍼져서 중복 코드를 만들어낸다.
    • 부가 기능을 변경할 때 중복 때문에 많은 수정이 필요하다.
    • 부가 기능의 적용 대상을 변경할 때 많은 수정이 필요하다.
    • 소프트웨어 개발에서 변경 지점은 하나가 될 수 있도록 잘 모듈화 되어야 한다.
  • 그런데 부가 기능처럼 특정 로직을 애플리케이션 전반에 적용하는 문제는 일반적인 OOP 방식으로는 해결이 어렵다.

  • 그 결과 부가 기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 했다.

  • 이렇게 부가 기능과 부가 기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었는데 이것이 바로 애스펙트(aspect)이다.

  • 스프링이 제공하는 어드바이저도 어드바이스(부가 기능)과 포인트컷(적용 대상)을 가지고 있어서 개념상 하나의 애스펙트이다.

  • 애스펙트는 우리말로 해석하면 관점이라는 뜻인데, 이름 그대로 애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사(cross-cutting concerns) 관점으로 달리 보는 것이다.

  • 이렇게 애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍 AOP(Aspect-Oriented Programming)이라 한다.

  • AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.

  • AOP의 대표적인 구현으로 AspectJ 가 있다.

  • 스프링도 AOP를 지원하지만 대부분 AspectJ의 문법을 차용하고, AspectJ가 제공하는 기능의 일부만 제공한다.

AOP 적용 방식

  • AOP를 사용하면 핵심 기능과 부가 기능이 코드상 완전히 분리되어서 관리된다.
  • AOP를 사용할 때 부가 기능 로직은 어떤 방식으로 실제 로직에 추가될 수 있을까?
  • 크게 3가지 방법이 있다.
    • 컴파일 시점
    • 클래스 로딩 시점
    • 런타임 시점(프록시)

컴파일 시점

  • .java 소스 코드를 컴파일러를 사용해서 .class 를 만드는 시점에 부가 기능 로직을 추가할 수 있다.
  • AspectJ가 제공하는 특별한 컴파일러를 사용해야 한다.
  • 컴파일 된 .class 를 디컴파일 해보면 애스펙트 관련 호출 코드가 들어간다.
  • 참고로 이렇게 원본 로직에 부가 기능 로직이 추가되는 것을 위빙(Weaving)이라 한다.
  • 위빙(Weaving): 옷감을 짜다. 직조하다. 애스펙트와 실제 코드를 연결해서 붙이는 것
  • 단점은 컴파일 시점에 부가 기능을 적용하려면 특별한 컴파일러도 필요하고 복잡하다.

클래스 로딩 시점

  • .class 파일을 JVM 내부의 클래스 로더에 보관한다.
  • 이때 중간에서 .class 파일을 조작한 다음 JVM에 올릴 수 있다.
  • 자바 언어는 .class 를 JVM에 저장하기 전에 조작할 수 있는 기능을 제공한다.
  • 궁금한 분은 java Instrumentation를 검색해보자. 참고로 수 많은 모니터링 툴들이 이 방식을 사용한다.
  • 이 시점에 애스펙트를 적용하는 것을 로드 타임 위빙이라 한다.
  • 단점은 자바를 실행할 때 특별한 옵션( java -javaagent )을 통해 클래스 로더 조작기를 지정해야 하는데, 이 부분이 번거롭고 운영하기 어렵다.

런타임 시점 - 사용되는 방식

  • 런타임 시점은 컴파일도 다 끝나고, 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음을 말한다.
  • 따라서 자바 언어가 제공하는 범위 안에서 부가 기능을 적용해야 한다.
  • 스프링과 같은 컨테이너의 도움을 받고 프록시와 DI, 빈 포스트 프로세서 같은 개념들을 총 동원해야 한다.
  • 이렇게 하면 최종적으로 프록시를 통해 스프링 빈에 부가 기능을 적용할 수 있다. 그렇다.
  • 지금까지 우리가 학습한 것이 바로 프록시 방식의 AOP이다.
  • 프록시를 사용하기 때문에 AOP 기능에 일부 제약이 있다. 하지만 특별한 컴파일러나, 자바를 실행할 때 복잡한 옵션과 클래스 로더 조작기를 설정하지 않아도 된다.

정리

  • AOP는 지금까지 학습한 메서드 실행 위치 뿐만 아니라 다음과 같은 다양한 위치에 적용할 수 있다.
  • AOP를 적용할 수 있는 지점을 조인 포인트(Join point)라 한다.
    • 적용 가능 지점(조인 포인트): 생성자, 필드 값 접근, static 메서드 접근, 메서드 실행
  • AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다.
  • 프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
  • 프록시는 메서드 오버라이딩 개념으로 동작한다.
  • 따라서 생성자나 static 메서드, 필드 값 접근에는 프록시 개념이 적용될 수 없다.
  • 프록시 방식을 사용하는 스프링 AOP는 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다.
  • 스프링은 AspectJ의 문법을 차용하고 프록시 방식의 AOP를 적용한다.
  • 스프링이 제공하는 AOP는 프록시를 사용한다. 따라서 프록시를 통해 메서드를 실행하는 시점에만 AOP가 적용 된다.

AOP 용어 정리

조인 포인트(Join point)

  • 어드바이스가 적용될 수 있는 위치, 메소드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실행 중 지점
  • 조인 포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
  • 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.

포인트컷(Pointcut)

  • 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
  • 주로 AspectJ 표현식을 사용해서 지정

타켓(Target) - 어드바이스를 받는 객체, 포인트컷으로 결정

어드바이스(Advice)

  • 부가 기능
  • 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
  • Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음

애스펙트(Aspect)

  • 어드바이스 + 포인트컷을 모듈화 한 것
  • @Aspect 를 생각하면 됨

어드바이저(Advisor)

  • 하나의 어드바이스와 하나의 포인트 컷으로 구성
  • 스프링 AOP에서만 사용되는 특별한 용어

위빙(Weaving)

  • 포인트컷으로 결정한 타켓의 조인 포인트에 어드바이스를 적용하는 것
  • 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가 할 수 있음
  • 컴파일 타임(AspectJ compiler), 로드 타임, 런타임

AOP 프록시

  • AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.

  • 인터페이스가 있으면 스프링은 JDK 동적 프록시를 사용하여 프록시 객체 생성가능

  • CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.

  • CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다.

  • 프록시 팩토리는 인터페이스 없이 구체 클래스만 있으면 CGLIB를 사용해서 프록시를 적용한다.

  • 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.

    • 이것은 프록시 팩토리가 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice 를 호출하도록 개발해두고, CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다.
  • 프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다.

  • 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용한다.

포인트컷, 어드바이스, 어드바이저 - 소개

포인트컷(Pointcut): 어디에 부가 기능을 적용할지, 적용하지 않을지 판단하는 필터링 로직이다.

  • 주로 클래스와 메서드 이름으로 필터링 한다.
  • 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다.
  • 포인트컷 표현식은 AspectJ pointcut expression 애스펙트J가 제공하는 포인트컷 표현식을 줄여서 말하는 것이다.
  • execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.
  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
  • execution 은 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. 따라서 execution 을 중점적으로 이해하자.

어드바이스( Advice ): 이전에 본 것 처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다.

  • @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능

    • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다.
  • @Before : 조인 포인트 실행 이전에 실행

    • 작업 흐름을 변경할 수는 없다.
  • @AfterReturning : 조인 포인트가 정상 완료후 실행

    • 반환되는 객체를 변경할 수는 없다.
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행

  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

    • 메서드 실행이 종료되면 실행된다.
  • 모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.)

  • @Around 는 ProceedingJoinPoint 을 사용해야 한다.

  • JoinPoint 인터페이스의 주요 기능

    • getArgs() : 메서드 인수를 반환합니다.
    • getThis() : 프록시 객체를 반환합니다.
    • getTarget() : 대상 객체를 반환합니다.
    • getSignature() : 조언되는 메서드에 대한 설명을 반환합니다.
    • toString() : 조언되는 방법에 대한 유용한 설명을 인쇄합니다.
    • ProceedingJoinPoint 인터페이스의 주요 기능
    • proceed() : 다음 어드바이스나 타켓을 호출한다.
  • 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.

  • 실행 순서: @Around , @Before , @After , @AfterReturning , @AfterThrowing

  • 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자.

  • 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다.

  • 이 경우 앞서 배운 것 처럼 @Aspect 를 분리하고 @Order 를 적용하자.

  • @Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.

  • 반면에 @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.

  • 가장 중요한 점은 바로 이 코드를 작성한 의도가 명확하게 드러난다는 점이다.

어드바이저(Advisor): 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.

  • 쉽게 이야기해서 포인트컷1 + 어드바이스1이다.
  • 조언자( Advisor )는 어디( Pointcut )에 조언( Advice )을 해야할지 알고 있다.
  • 이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다.
  • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
  • 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
  • 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다. 둘을 합치면 어드바이저가 된다.

빈 후처리기

  • 우리가 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다.
  • 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다.
  • 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다.
  • 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.

CGLIB 제약

  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
  • 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

프록시, 프록시 패턴, 데코레이터 패턴 - 소개

  • 클라이언트와 서버의 기본 개념을 정의하면 클라이언트는 서버에 필요한 것을 요청하고, 서버는 클라이언트의 요청을 처리하는 것이다.

  • 그런데 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 대신 간접적으로 서버에 요청할 수 있다.

  • 예를 들어서 내가 직접 마트에서 장을 볼 수도 있지만, 누군가에게 대신 장을 봐달라고 부탁할 수도 있다.

  • 여기서 대신 장을 보는 대리자를 영어로 프록시(Proxy)라 한다.

  • 재미있는 점은 직접 호출과 다르게 간접 호출을 하면 대리자가 중간에서 여러가지 일을 할 수 있다는 점이다.

  • 엄마에게 라면을 사달라고 부탁 했는데, 엄마는 그 라면은 이미 집에 있다고 할 수도 있다. 그러면 기대한 것 보다 더 빨리 라면을 먹을 수 있다. (접근 제어, 캐싱)

  • 아버지께 자동차 주유를 부탁했는데, 아버지가 주유 뿐만 아니라 세차까지 하고 왔다. 클라이언트가 기대한 것 외에 세차라는 부가 기능까지 얻게 되었다. (부가 기능 추가)

  • 그리고 대리자가 또 다른 대리자를 부를 수도 있다. 예를 들어서 내가 동생에게 라면을 사달라고 했는데, 동생은 또 다른 누군가에게 라면을 사달라고 다시 요청할 수도 있다.

  • 중요한 점은 클라이언트는 대리자를 통해서 요청했기 때문에 그 이후 과정은 모른다는 점이다. 동생을 통해서 라면이 나에게 도착하기만 하면 된다. (프록시 체인)

  • 객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.

  • 그러므로 서버와 프록시는 같은 인터페이스를 사용해야 한다.

  • 클라이언트 객체에 DI를 사용해서 Client -> Server 에서 Client -> Proxy 로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다.

    • 클라이언트 입장에서는 변경 사실 조차 모른다.
  • DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.

프록시의 주요 기능

프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.

  • 접근 제어

    • 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가

    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    • 예) 요청 값이나, 응답 값을 중간에 변형한다.
    • 예) 실행 시간을 측정해서 추가 로그를 남긴다.
  • 디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 의도에 따라 패턴을 구분한다.

  • 프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공

  • 데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

  • 프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 된다.

PSA(Portable Service Abstraction)

  • 추상화를 통해 기술 독립성을 확보
  • 스프링은 특정 기술(예: DB 트랜잭션, 데이터 접근)에 대해 **표준 인터페이스(Interface)**를 제공
  • @Transactional (스프링 표준)을 사용할 때 씁니다.
    • 실제로는 JPA 트랜잭션이 돌 수도, JDBC 트랜잭션이 돌 수도 있지만, 개발자가 몰라도 된다.
  • Spring Data JPA (스프링 표준)를 사용하면 실제 DB가 MySQL이든, Oracle이든, PostgreSQL이든 비즈니스 코드는 거의 바뀌지 않음