dev-course day18

2rlokr·2025년 3월 27일

dev-course

목록 보기
18/43
post-thumbnail

오늘 배운 것

Spring AOP

관점 지향 프로그래밍 (Aspect-Oriented Programming, AOP)

프로그램의 핵심 기능(Core Concerns)부가적인 기능(Cross-cutting Concerns)을 분리하여 관리하는 프로그래밍 패러다임의 일종이다. 프로그램 전반에 걸쳐 공통적으로 사용되는 기능을 모듈화하여 코드의 복잡도를 줄이고 유지보수를 쉽게 만든다.

관심사 (Concern)

프로그램에는 다양한 관심사가 존재하는데, 특정 서비스에서 집중하는 각각의 기능들이 관심사에 해당한다. 이 기능이 프로그램의 주요 기능을 담당하면 핵심 관심사, 그렇지 않을 때는 횡단 관심사라고 한다.

주 관심사 (Core Concerns)
프로그램의 핵심 기능을 담당한다. 애플리케이션이 수행해야 하는 본연의 역할과 직접적으로 관련된 기능을 의미한다. 즉, 해당 관심사가 목적으로 하고 있는 것 자체를 의미한다고 보면 된다.

횡단 관심사 (Cross-cutting Concerns)
애플리케이션의 여러 핵심 관심사에 걸쳐 공통적으로 적용되는 기능을 의미한다. 대표적으로 로깅 (Logging), 트랜잭션 (Transaction) 관리, 성능 모니터링 (Performance Monitoring) 등이 이에 포함된다. 이 기능들은 특정한 비즈니스 로직과 직접적인 관련이 있는 것은 아니지만, 시스템이 안정적으로 동작하기 위해 핵심 기능과 동시에 필수적으로 적용해야 한다.

예시
은행을 예시로 들었을 때, 관심사는 계좌이체, 계좌개설 등의 은행에서 제공하는 서비스
핵심 관심사는 계좌 이체의 기능에서 하나의 계좌에서 다른 계좌로 돈을 옮기는 것

핵심 개념

📌 Aspect

횡단관심사(Cross-cutting Concern)를 모듈화하여 관리하는 단위이다. 애플리케이션에서 반복적으로 사용되는 기능을 핵심 로직과 분리하여 하나의 독립적인 단위로 정의한 것이다.

  • 예를 들어, '로깅'이 하나의 Aspect가 될 수 있는 것이다.

📌 Advice

Aspect가 수행해야 하는 구체적인 동작을 정의하는 부분을 의미한다. 즉, 특정 횡단 관심사를 실행할 실제 코드를 의미한다.

  • '로깅'을 어떻게 실행해야 하는지 실질적으로 구현하는 부분

실행 시점에 따라 여러가지 유형으로 구분할 수 있다.

  • Before : 대상 기능이 실행되기 이전에 수행되는 코드
  • After Returning : 대상 기능이 정상적으로 실행된 다음, 예외가 없이 반환될 때 수행되는 코드
  • After Throwing : 대상 기능이 수행되는 도중 예외가 발생되었을 때 수행되는 코드
  • After : 대상 기능의 수행 완료 후 실행되는 코드 (기능이 완벽히 수행된 다음의 시점)
  • Around : 대상 기능의 실행 전 (Before)과 실행 후 (After)를 전부 포함하여 실행되는 코드

📌 Introduction

기존 클래스에 새로운 메서드나 필드를 동적으로 추가하는 기능을 제공한다. 이 개념은 주로 인터페이스를 기반으로 동작하며, 특정 클래스가 원래 구현(implement)하지 않았던 인터페이스를 런타임 또는 컴파일 타임에 동적으로 구현하도록 만들 수 있다.

📌 Point-cut

어디에 AOP를 적용할지 정하는 것이다. 코드의 실행 시점을 정의하는 개념으로, 어떤 메서드가 실행되거나 어떤 이벤트가 발생할 때, 그 지점에 관심사를 적용하기 위한 조건을 지정하는 역할을 한다. 즉, Pointcut은 관심사(Aspect)를 어디에 적용할 것인지 결정하는 기준이 되는 부분이다.

📌 Target Object

AOP의 Advice나 다른 Aspect가 적용되는 실제 대상 객체를 의미한다. 즉, AOP에서 우리가 적용하고자 하는 기능이 실제로 실행될 대상이 되는 객체이다. 이 객체는 일반적으로 비즈니스 로직을 처리하는 클래스의 인스턴스로, 관심사를 주입하거나 부가적인 작업을 추가하는 대상이 된다.

📌 Join point

프로그램 실행 중, Advice가 적용될 수 있는 지점이나 타이밍을 의미한다. 즉, Join Point는 관심사(Aspect)를 코드에 삽입할 수 있는 구체적인 지점으로, 일반적으로 메서드 호출, 메서드 호출, 메서드 실행 등 다양한 프로그램의 실행 시점을 포함한다.

Point cut과 Join Point 의 차이점 ❓

🔹 Point cut

  • Joint point 중에서 실제로 Advice를 적용할 지점을 선별하는 것
  • 특정 메서드만 골라서 Advice를 실행하고 싶다면 Pointcut을 정의해서 필터링
  • 즉, "어디에 적용할 것인가?"를 결정하는 필터 역할

🔹 Join Point

  • 프로그램 실행 중에서 Aspect를 적용할 수 있는 모든 지점
  • 예를 들어, 메서드 호출, 생성자 호출, 예외 발생, 필드 접근 등이 Join Point
  • 즉, "어디에 적용할 수 있는가?"에 대한 개념

🔹 비유적으로 나타내자면,,

  • Join Point는 전체 지도에서 도로(적용 가능 지점)를 표시하는 것.
  • Pointcut은 그중에서 특정 도로(실제 적용할 지점)를 선택하는 필터.

✅ 정리

  • 즉, Join Point는 가능성의 집합이고, Pointcut은 그중에서 선택하는 기준
  • Join point는 포괄적인 개념이고, Pointcut은 필터링하는 개념
  • Join Point는 그저 "적용할 수 있는 지점"이고, Pointcut을 통해 선택되면, 그 지점에서 Advice가 실행된다.

예시

public class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBeforeMethodExecution() {
        System.out.println("메서드 실행 전에 로깅");
    }
}
  • execution(* com.example.service.*.*(..))com.example.service 패키지의 모든 메서드 실행이 Join Point가 된다.
  • 메서드 실행 전으로 Advice(@Before)가 적용된다.
  • pointcut은 따로 명시적인 코드가 있는 것이 아니라 Join Point를 특정 메서드 앞에 지정해준 것이 필터링해준 것이기 때문에 그 자체가 pointcut이라고 보면 된다.

📌 AOP Proxy

실제 대상 객체 (Target Object) 앞에 위치하여, 그 객체의 메서드 호출을 가로채고 추가적인 동작을 수행하는 중간 객체이다. AOP Proxy는 실제 비즈니스 로직을 처리하는 객체에 횡단 관심사(Aspect)를 동적으로 적용하는 역할을 하며, 이 과정에서 어드바이스(Advice)를 실행하게 된다.

📌 Weaving

관심사(Aspect)를 실제 비즈니스 로직에 적용하는 과정이다. Advice나 기타 관심사를 코드의 특정 지점에 삽입하는 과정을 의미한다.

크게 세 가지 시점에 AOP를 적용한다.

컴파일 타임
컴파일이 되는 시점에 Advice를 적용하는 방식

로드 타임
클래스 파일이 JVM에 로드될 때 Advice를 적용하는 방식
주로 바이트코드를 변환하는 방식으로 동작

런타임
프로그램 실행 중에, 즉 메서드가 호출될 때 Advice가 적용되는 방식
Spring AOP에서는 이 방식이 가장 일반적으로 사용된다.
이 경우, AOP 프록시가 생성되어, 실제 메서드 호출을 가로채고 그 전후에 Advice를 적용한다. 런타임 Weaving은 주로 동적 프록시 또는 CGLIB를 통해 이루어진다.

구성 요소

Spring AOP는 프록시 패턴을 기반으로 동작한다.

프록시 패턴 (Proxy Pattern)

객체에 대한 접근을 제어하기 위해 대리 객체를 사용하는 디자인 패턴이며, 실제 객체를 대신하여 그 객체에 접근할 수 있는 중간 객체를 두는 구조이다. 이 중간 객체를 프록시라고 부른다.

목적 : 접근 제어

  1. 직접적인 접근을 차단
  2. 그 접근을 제어하여 특정 조건을 만족할 때만 실제 객체가 동작하도록 하는 역할

JDK 동적 프록시 (JDK Dynamic Proxy, 인터페이스 기반)

자바 언어 자체에 들어있는 기능으로, 런타임 시에 인터페이스를 기반으로 객체의 프록시를 동적으로 생성할 수 있는 방법이다. 이 기능은 자바 리플렉션(Reflection)을 사용하여, 지정된 인터페이스를 구현하는 클래스의 실제 객체를 대신할 수 있는 프록시 객체를 생성한다.

public interface InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

	...

}
  • Proxy 클래스와 InvocationHandler 인터페이스를 사용하여 구현된다.
  • InvocationHandler 는 프록시 객체에서 호출되는 메서드를 처리하는 역할을 한다.

인터페이스 기반

  • 프록시 객체는 반드시 인터페이스를 구현해야 하며, 이를 통해 생성된 프록시는 해당 인터페이스의 모든 메서드를 덮어쓰고, InvocationHandlerinvoke() 메서드에서 처리할 수 있다.

CGLIB (Code Generator Library) Proxy (클래스 기반)

자바에서 클래스 기반의 동적 프록시를 생성할 수 있는 라이브러리이다. CGLIB는 클래스 기반으로 프록시를 생성할 수 있기 때문에 인터페이스가 없는 클래스에 대해서도 프록시를 만들 수 있는 장점이 있다.

클래스 기반

  • 클래스를 상속받아 프록시 객체를 생성한다.
  • 프록시 객체는 실제 객체를 상속하여, 해당 객체의 메서드를 오버라이드하여 처리한다.

Spring에서는?

Spring에서는 CGLIB 프록시를 자동으로 선택하여 사용한다. 기본적으로 Spring은 대상 객체가 인터페이스를 구현하고 있으면 JDK 동적 프록시를 사용하고, 그렇지 않으면 CGLIB 프록시를 사용한다.

실습

Lombok

@Slf4j
로그를 기록할 수 있도록 해준다.

@Getter
클래스 필드에 대한 getter가 다 만들어진다.

@Setter
아래의 필드에 대한 setter가 다 만들어진다.

@AllArgsConstructor
이 모든 필드를 다 가지고 있는 생성자를 하나 만들어준다.

@NoArgsConstructor
기본 생성자를 추가해준다.

@RequiredArgsConstructor
private final로 선언해놓은 변수 등 초기화가 꼭 필요한 것의 초기화를 해줄 생성자를 만든다.

Proxy

수동 프록시

@Slf4j
public class Proxy implements Subject{

    private Subject targetObject;

    public Proxy(Subject subject) {
        this.targetObject = subject;
    }

    @Override
    public void operation() {
        //전처리 로직
        log.info("[prefix] Proxy.operaton()");
        targetObject.operation();
        log.info("[postfix] Proxy.operaton()");
        //후처리 로직
    }
}

@Slf4j
public class ProxyPatternBasicTests {

    @Test
    @DisplayName("프록시 기본 1")
    void test1() throws Exception {

        RealSubject realSubject = new RealSubject();
        Proxy proxy = new Proxy(realSubject);
        proxy.operation();

    }
}
  • 실제 대상이 되는 객체 RealSubjectSubject를 구현했다.
  • ProxySubject를 구현하였고, realSubject를 직접 호출하는 것이 아닌, proxy.operation()을 통해 프록시 객체가 중간 객체가 되어준다.

JDK 동적 프록시

💡 InvocationHandlere 인터페이스란?

  • 프록시 객체에서 메서드가 호출될 때 실행되는 로직을 정의하는 인터페이스
  • 즉, 프록시 객체가 메서드를 호출하면, 실제 객체(타겟 객체)의 메서드를 실행하기 전에 invoke()가 먼저 호출된다.
@Slf4j
public class SubLogicInvocationHandler implements InvocationHandler {

    private final Object target;

    public SubLogicInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        log.info("[BEFORE] 로직 실행 전!");
        Object result = method.invoke(target, args); // 원래 객체 (타겟 객체)의 메서드 실행
        log.info("[AFTER] 로직 실행 후!");

        return result;
    }
}
  • InvocationHandler를 구현한 클래스이다.
  • method : 호출된 메서드 정보
  • method.invoke(target, args); 가 호출되면 실제 객체의 메서드가 실행된다.

@Slf4j
public class JdkDynamicProxyTest {

    @Test
    @DisplayName("언어차원(JDK, reflection)에서 제공하는 프록시 패턴 테스트")
    void T1() throws Exception {

        MockServiceImpl targetObject = new MockServiceImpl();
		
        // 1번 !
        SubLogicInvocationHandler handler = new SubLogicInvocationHandler(targetObject);
		
        
        //2번 !
        MockService proxy = (MockService) Proxy.newProxyInstance(
                MockService.class.getClassLoader(),
                new Class[]{MockService.class},
                handler
        );

        proxy.logic1();
        proxy.logic2();


    }

}

1번 코드

  • 프록시 객체가 실제 메서드를 실행할 때 사용할 대상 객체를 지정해준다.
  • handlertargetObject를 이용해서 메서드 실행을 중간에 가로채는 InvocationHandler이다.

2번 코드

  • Proxy.newProxyInstance()MockService 인터페이스를 구현하는 프록시 객체를 생성한다.
  1. 클래스 로더를 통해 인터페이스 기반 프록시 클래스 생성
    MockService.class.getClassLoader()를 사용해서 새로운 클래스를 생성한다. 인터페이스를 기반으로 동적 프록시 클래스가 생성된다. (아직 객체 X)

2 프록시 객체가 인터페이스를 구현
new Class[]{MockService.class} 를 통해 프록시 객체가 MockService 인터페이스를 구현하도록 한다. 내부적으로 생성된 프록시 클래스를 기반으로 인터페이스를 구현하는 객체를 만드는 것이다.

  1. 프록시 객체의 모든 메서드 호출이 InvocationHandler.invoke()로 위임
    proxy.logic1()을 실행하면 프록시 객체는 직접 실행하지 않고, InvocationHandlerinvoke()를 호출한다.

CGLIB 프록시

Enhancer, MethodInterceptor 사용

💡 Enhancer란?
Enhancer는 CGLIB에서 프록시 객체를 생성하는 핵심 클래스이다.
즉, 클래스를 상속하는 새로운 프록시 클래스를 동적으로 만들어주는 역할을 한다.

@Slf4j
@RequiredArgsConstructor
public class SubLogicInterceptor implements MethodInterceptor {

    private final Object target;

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        log.info("[BEFORE] 호출 전 적용!");
        Object result = proxy.invoke(target, args); // 원본 객체의 메서드 실행
        log.info("[AFTER] 호출 후 적용!");

        return result;
    }
}
  • MethodInterceptor를 구현하여 프록시 객체의 메서드 호출을 가로챈다.
  • intercept()는 프록시가 실행할 메서드를 감싸서 호출하는 역할을 한다.
@Slf4j
public class CglibTests {

    @Test
    @DisplayName("CGLIB 방식 프록시 테스트")
    void test1() throws Exception {

        MockService mockService = new MockService();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(MockService.class); // 프록시가 상속받을 클래스 설정
        enhancer.setCallback(new SubLogicInterceptor(mockService)); // 실행될 인터셉터 설정

        MockService proxy = (MockService) enhancer.create(); // 프록시 객체 생성

        proxy.logic1();

    }


}
  • Enhancer 를 이용해 타겟 객체 (MockService)를 상속하는 프록시 클래스를 동적으로 생성한다.
  • enhancer.setSuperclass(MockService.class); → 프록시 객체가 대상 객체(MockService)를 상속받도록 설정한다.
  • enhancer.setCallback(new SubLogicInterceptor(mockService)); → 메서드 호출을 가로챌 인터셉터를 설정한다.
  • enhancer.create() → 새로운 동적 프록시 객체를 생성한다.
  • proxy.logic1(); → 프록시 객체를 통해 메서드를 실행하면, 인터셉터가 실행된다.

느낀 점

우와... 정말 .. 어려웠다. 결론은 지금은 다 이해된다. 내일 수업에서 복습해주실 때, 내가 이해한 게 맞는지 확인하면서 들으면서 보충하면 되겠다.. ㅎ

그치만 결론 가기 전에, 일단 내 수업 전반의 표정은 넋 나감 + 이해가 안되서 너무 (속상 + 화남) 이었다. ㅋ큐ㅠㅠ 옆에 살짝씩 보이는 파워 끄덕임을 하시는 분들 보면서 '와.. 이게 이해가 된다고?' 라고 생각했다. 사실 수업이 오늘은 일찍 끝나주길 바랐다. 왜냐? 내가 모르는 내용 그만.. 나 길 잃었어요. 더 나가면 나 지금 1도 이해안되는데 10 이해해야 해요. HELP 그 자체였다는 것 하하하 마지막에 실습 시간 때,, 일단 계속 하다보면 감이 오겠지, 코드 보면서 이해해야 하니까 계속 실습 따라해보자. 하면서 따라했는데,, 진심 타자 연습 하는 줄 알았다. ㅠ
지금 코드 흐름도 이해가 안가는데, 막 Enhancer 나오고, InvocationHandler 나오고 내가 모르는 거 파티였다. 그래서 더 길을 잃었던 것 같다. 응? Proxy 허덕이면서 따라가고 있는데 또 뭐라고?? 응? 응... 그만.. 그만.. 수업 그만... 이런 너낌 하하하하

수업 들으면서 또 생각했다. 와.. 앞으로 계속 이 지경으로 수업 듣는 건 아니겠지? 다 처음 듣는 개념일 텐데 매 수업이 이러면 나 어떡해..? ㅎ... 제발 .. 오늘이 어려운 거라 말해줘요 PLZ

아무튼,, ! 그렇게 어지러운 상태로 스크럼을 하는데, 팀원 분들 다 어려웠다고 얘기해주셔서.. 너무 마음이 놓였다. 하하하 ㅠㅠ 다들 어려웠던 거라고 해줘. 제발.

결국엔 이해하고 넘어가서 뿌듯하다..! 알고나니 재밌어.. 하.. 스프링 이거야? 날 괴롭혀놓고, 이해하면 흐뭇하고 재미있게 만드는 게.. 너의 매력이냐고.
이래놓고 또 내일 아침되면 그냥 날 화나게 하겠지만 ㅋㅎㅋㅎ 하..

오늘도 하루를 버텨냈어요.. 매번 어려워해도 .. 오늘 하루를 버텨냈다는 게 중요한 거 아니겠냐며

내일도.. 파이팅... 하

0개의 댓글