
프로그램의 핵심 기능(Core Concerns)과 부가적인 기능(Cross-cutting Concerns)을 분리하여 관리하는 프로그래밍 패러다임의 일종이다. 프로그램 전반에 걸쳐 공통적으로 사용되는 기능을 모듈화하여 코드의 복잡도를 줄이고 유지보수를 쉽게 만든다.
프로그램에는 다양한 관심사가 존재하는데, 특정 서비스에서 집중하는 각각의 기능들이 관심사에 해당한다. 이 기능이 프로그램의 주요 기능을 담당하면 핵심 관심사, 그렇지 않을 때는 횡단 관심사라고 한다.
주 관심사 (Core Concerns)
프로그램의 핵심 기능을 담당한다. 애플리케이션이 수행해야 하는 본연의 역할과 직접적으로 관련된 기능을 의미한다. 즉, 해당 관심사가 목적으로 하고 있는 것 자체를 의미한다고 보면 된다.
횡단 관심사 (Cross-cutting Concerns)
애플리케이션의 여러 핵심 관심사에 걸쳐 공통적으로 적용되는 기능을 의미한다. 대표적으로 로깅 (Logging), 트랜잭션 (Transaction) 관리, 성능 모니터링 (Performance Monitoring) 등이 이에 포함된다. 이 기능들은 특정한 비즈니스 로직과 직접적인 관련이 있는 것은 아니지만, 시스템이 안정적으로 동작하기 위해 핵심 기능과 동시에 필수적으로 적용해야 한다.
예시
은행을 예시로 들었을 때, 관심사는 계좌이체, 계좌개설 등의 은행에서 제공하는 서비스
핵심 관심사는 계좌 이체의 기능에서 하나의 계좌에서 다른 계좌로 돈을 옮기는 것
횡단관심사(Cross-cutting Concern)를 모듈화하여 관리하는 단위이다. 애플리케이션에서 반복적으로 사용되는 기능을 핵심 로직과 분리하여 하나의 독립적인 단위로 정의한 것이다.
Aspect가 수행해야 하는 구체적인 동작을 정의하는 부분을 의미한다. 즉, 특정 횡단 관심사를 실행할 실제 코드를 의미한다.
실행 시점에 따라 여러가지 유형으로 구분할 수 있다.
- Before : 대상 기능이 실행되기 이전에 수행되는 코드
- After Returning : 대상 기능이 정상적으로 실행된 다음, 예외가 없이 반환될 때 수행되는 코드
- After Throwing : 대상 기능이 수행되는 도중 예외가 발생되었을 때 수행되는 코드
- After : 대상 기능의 수행 완료 후 실행되는 코드 (기능이 완벽히 수행된 다음의 시점)
- Around : 대상 기능의 실행 전 (Before)과 실행 후 (After)를 전부 포함하여 실행되는 코드
기존 클래스에 새로운 메서드나 필드를 동적으로 추가하는 기능을 제공한다. 이 개념은 주로 인터페이스를 기반으로 동작하며, 특정 클래스가 원래 구현(implement)하지 않았던 인터페이스를 런타임 또는 컴파일 타임에 동적으로 구현하도록 만들 수 있다.
어디에 AOP를 적용할지 정하는 것이다. 코드의 실행 시점을 정의하는 개념으로, 어떤 메서드가 실행되거나 어떤 이벤트가 발생할 때, 그 지점에 관심사를 적용하기 위한 조건을 지정하는 역할을 한다. 즉, Pointcut은 관심사(Aspect)를 어디에 적용할 것인지 결정하는 기준이 되는 부분이다.
AOP의 Advice나 다른 Aspect가 적용되는 실제 대상 객체를 의미한다. 즉, AOP에서 우리가 적용하고자 하는 기능이 실제로 실행될 대상이 되는 객체이다. 이 객체는 일반적으로 비즈니스 로직을 처리하는 클래스의 인스턴스로, 관심사를 주입하거나 부가적인 작업을 추가하는 대상이 된다.
프로그램 실행 중, Advice가 적용될 수 있는 지점이나 타이밍을 의미한다. 즉, Join Point는 관심사(Aspect)를 코드에 삽입할 수 있는 구체적인 지점으로, 일반적으로 메서드 호출, 메서드 호출, 메서드 실행 등 다양한 프로그램의 실행 시점을 포함한다.
Point cut과 Join Point 의 차이점 ❓
🔹 Point cut
🔹 Join Point
🔹 비유적으로 나타내자면,,
✅ 정리
예시
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethodExecution() {
System.out.println("메서드 실행 전에 로깅");
}
}
execution(* com.example.service.*.*(..)) → com.example.service 패키지의 모든 메서드 실행이 Join Point가 된다.@Before)가 적용된다.pointcut은 따로 명시적인 코드가 있는 것이 아니라 Join Point를 특정 메서드 앞에 지정해준 것이 필터링해준 것이기 때문에 그 자체가 pointcut이라고 보면 된다.실제 대상 객체 (Target Object) 앞에 위치하여, 그 객체의 메서드 호출을 가로채고 추가적인 동작을 수행하는 중간 객체이다. AOP Proxy는 실제 비즈니스 로직을 처리하는 객체에 횡단 관심사(Aspect)를 동적으로 적용하는 역할을 하며, 이 과정에서 어드바이스(Advice)를 실행하게 된다.
관심사(Aspect)를 실제 비즈니스 로직에 적용하는 과정이다. Advice나 기타 관심사를 코드의 특정 지점에 삽입하는 과정을 의미한다.
크게 세 가지 시점에 AOP를 적용한다.
컴파일 타임
컴파일이 되는 시점에 Advice를 적용하는 방식
로드 타임
클래스 파일이 JVM에 로드될 때 Advice를 적용하는 방식
주로 바이트코드를 변환하는 방식으로 동작
런타임
프로그램 실행 중에, 즉 메서드가 호출될 때 Advice가 적용되는 방식
Spring AOP에서는 이 방식이 가장 일반적으로 사용된다.
이 경우, AOP 프록시가 생성되어, 실제 메서드 호출을 가로채고 그 전후에 Advice를 적용한다. 런타임 Weaving은 주로 동적 프록시 또는 CGLIB를 통해 이루어진다.
Spring AOP는 프록시 패턴을 기반으로 동작한다.
객체에 대한 접근을 제어하기 위해 대리 객체를 사용하는 디자인 패턴이며, 실제 객체를 대신하여 그 객체에 접근할 수 있는 중간 객체를 두는 구조이다. 이 중간 객체를 프록시라고 부른다.
목적 : 접근 제어
자바 언어 자체에 들어있는 기능으로, 런타임 시에 인터페이스를 기반으로 객체의 프록시를 동적으로 생성할 수 있는 방법이다. 이 기능은 자바 리플렉션(Reflection)을 사용하여, 지정된 인터페이스를 구현하는 클래스의 실제 객체를 대신할 수 있는 프록시 객체를 생성한다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
...
}
Proxy 클래스와 InvocationHandler 인터페이스를 사용하여 구현된다. InvocationHandler 는 프록시 객체에서 호출되는 메서드를 처리하는 역할을 한다.인터페이스 기반
InvocationHandler의 invoke() 메서드에서 처리할 수 있다.자바에서 클래스 기반의 동적 프록시를 생성할 수 있는 라이브러리이다. CGLIB는 클래스 기반으로 프록시를 생성할 수 있기 때문에 인터페이스가 없는 클래스에 대해서도 프록시를 만들 수 있는 장점이 있다.
클래스 기반
Spring에서는 CGLIB 프록시를 자동으로 선택하여 사용한다. 기본적으로 Spring은 대상 객체가 인터페이스를 구현하고 있으면 JDK 동적 프록시를 사용하고, 그렇지 않으면 CGLIB 프록시를 사용한다.
@Slf4j
로그를 기록할 수 있도록 해준다.
@Getter
클래스 필드에 대한 getter가 다 만들어진다.
@Setter
아래의 필드에 대한 setter가 다 만들어진다.
@AllArgsConstructor
이 모든 필드를 다 가지고 있는 생성자를 하나 만들어준다.
@NoArgsConstructor
기본 생성자를 추가해준다.
@RequiredArgsConstructor
private final로 선언해놓은 변수 등 초기화가 꼭 필요한 것의 초기화를 해줄 생성자를 만든다.
@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();
}
}
RealSubject가 Subject를 구현했다.Proxy도 Subject를 구현하였고, realSubject를 직접 호출하는 것이 아닌, proxy.operation()을 통해 프록시 객체가 중간 객체가 되어준다.💡 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번 코드
handler 는 targetObject를 이용해서 메서드 실행을 중간에 가로채는 InvocationHandler이다.2번 코드
Proxy.newProxyInstance()는 MockService 인터페이스를 구현하는 프록시 객체를 생성한다. MockService.class.getClassLoader()를 사용해서 새로운 클래스를 생성한다. 인터페이스를 기반으로 동적 프록시 클래스가 생성된다. (아직 객체 X)2 프록시 객체가 인터페이스를 구현
new Class[]{MockService.class} 를 통해 프록시 객체가 MockService 인터페이스를 구현하도록 한다. 내부적으로 생성된 프록시 클래스를 기반으로 인터페이스를 구현하는 객체를 만드는 것이다.
InvocationHandler.invoke()로 위임proxy.logic1()을 실행하면 프록시 객체는 직접 실행하지 않고, InvocationHandler의 invoke()를 호출한다.💡 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
아무튼,, ! 그렇게 어지러운 상태로 스크럼을 하는데, 팀원 분들 다 어려웠다고 얘기해주셔서.. 너무 마음이 놓였다. 하하하 ㅠㅠ 다들 어려웠던 거라고 해줘. 제발.
결국엔 이해하고 넘어가서 뿌듯하다..! 알고나니 재밌어.. 하.. 스프링 이거야? 날 괴롭혀놓고, 이해하면 흐뭇하고 재미있게 만드는 게.. 너의 매력이냐고.
이래놓고 또 내일 아침되면 그냥 날 화나게 하겠지만 ㅋㅎㅋㅎ 하..
오늘도 하루를 버텨냈어요.. 매번 어려워해도 .. 오늘 하루를 버텨냈다는 게 중요한 거 아니겠냐며
내일도.. 파이팅... 하