[Spring] 5. AOP
AOP
1. AOP란?
관점 지향 프로그래밍(Aspect Oriented Programming)의 약자로,
중복되는 공통 코드를 분리하고 코드 실행 전이나 후의 시점에 해당 코드를 삽입함으로써 소스 코드의 중복을 줄이고,
필요할 때마다 가져다 쓸 수 있게 객체화하는 기술을 말한다.
2. AOP 핵심 용어
용어 | 설명 |
Aspect | 핵심 비즈니스 로직과는 별도로 수행되는 횡단 관심사. Advice + Pointcut |
Advice | Aspect의 기능 자체. (분리해놓은) 부가 코드가 작성되어 있는 것 |
Join point | Advice가 적용될 수 있는 위치. 부가 코드를 삽입하려는 주요 코드의 위치 (한 지점) |
Pointcut | Join point 중에서 Advice가 적용될 가능성이 있는 부분을 선별한 것. |
Weaving | Advice를 핵심 비즈니스 로직에 적용하는 것. (주요 코드에 삽입하는 동작) |
업무로직: 로그인, 검색, 검색 게시판 등 핵심 관점에 해당하는 것
부가 코드: logging, security, 트랜젝션 등 횡단 관점에 해당하는 것
3. Advice의 종류
시점을 기준으로 5개의 종류가 있다.
종류 | 설명 |
Before | 대상 메소드가 실행되기 이전에 실행되는 어드바이스 |
After-returning | 대상 메소드가 정상적으로 실행된 이후에 실행되는 어드바이스 |
After-throwing | 예외가 발생했을 때 실행되는 어드바이스 |
After | 대상 메소드가 실행된 이후에(정상, 예외 관계없이) 실행되는 어드바이스 |
Around | 대상 메소드 실행 전/후에 적용되는 어드바이스 |
4. Spring AOP의 특징
(1) 프록시 기반의 AOP 구현체
대상 객체(Target Object)에 대한 프록시를 만들어 제공하며, 타겟을 감싸는 프록시는 서버 runtime 시에 생성된다.
즉, 수행하고 싶은 동작(Target Object, 핵심동작)을 proxy가 감싸고 있다.
(2) 메서드 조인 포인트만 제공
Spring에서는 메서드가 호출되는 시점만 join point로 잡을 수 있다.
Spring AOP 구현하기
* 디렉토리 구조
1. 라이브러리 의존성 추가
aspectjweaver 라이브러리가 있어야 AOP 기능이 동작할 수 있으므로
build.gradle 파일에 aspectjweaver 라이브러리를 추가해준다.
MVN REPOSITORY에서 "AspectJ Weaver"를 검색하면 아래 코드를 얻을 수 있다.
dependencies {
// https://mvnrepository.com/artifact/org.aspectj/aspectjweaver
implementation 'org.aspectj:aspectjweaver:1.9.22.1'
}
2. 로직을 포함하는 코드 작성
package org.example.section01.aop;
@Getter
@Setter
@ToString
@AllArgsConstructor
public class MemberDTO {
private Long id;
private String name;
}
package org.example.section01.aop;
@Repository
public class MemberDAO {
private final Map<Long, MemberDTO> memberMap;
public MemberDAO(){
memberMap = new HashMap<>();
memberMap.put(1L, new MemberDTO(1L, "유관순"));
memberMap.put(2L, new MemberDTO(2L, "홍길동"));
}
public Map<Long, MemberDTO> selectMembers(){
return memberMap;
};
public MemberDTO selectMember(Long id) {
MemberDTO returnMember = memberMap.get(id);
if(returnMember == null) throw new RuntimeException("해당하는 id의 회원이 없습니다.");
return returnMember;
}
}
package org.example.section01.aop;
@Service
public class MemberService {
private final MemberDAO memberDAO;
public MemberService(MemberDAO memberDAO) {
this.memberDAO = memberDAO;
}
public Map<Long, MemberDTO> selectMembers(){
System.out.println("selectMembers 메소드 실행");
return memberDAO.selectMembers();
}
public MemberDTO selectMember(Long id) {
System.out.println("selectMember 메소드 실행");
return memberDAO.selectMember(id);
}
}
package org.example.section01.aop;
public class Application {
public static void main(String[] args) {
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext("org.example.section01.aop");
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
System.out.println("========== selectMembers ==========");
System.out.println(memberService.selectMembers());
System.out.println("========== selectMember ==========");
System.out.println(memberService.selectMember(3L));
}
}
3. AutoProxy 설정
package org.example.section01.aop;
@Configuration
/* AspectJ의 AutoProxy 기능을 활성화 시킨다.
(aspectj의 autoProxy 사용에 관한 설정을 해주어야 advice가 동작한다.)*/
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLib 방식을 통해 proxy 생성
public class ContextConfiguration {
}
4. Aspect 생성
LoggingAspect 클래스를 생성하고 빈 스캐닝을 통해 빈 등록을 한다.
(1) @Aspect
pointcut과 advice를 하나의 클래스 단위로 정의하기 위한 어노테이션
@Aspect
@Component
public class LoggingAspect {}
(2) Pointcut
LoggingAspect 클래스에 pointcut을 정의한다.
여러 join point을 매치하기 위해 지정한 표현식이다.
@Pointcut("execution(* org.example.section01.aop.*Service.*(..))")
public void logPointcut() {}
/*logPointcut()은 빈 메소드이지만, 포인트컷 표현식을 정의하고 이를 Advice에서 참조한다.
ex) @Before("logPointcut()")*/
1) execution
AOP에서 가장 많이 사용되는 pointcut 표현식 중 하나로,
execution 표현식은 메서드 실행 시점에 일치하는 join point을 정의하는 데 사용된다.
execution 표현식의 기본 구성은 아래와 같다.
execution([접근제한자패턴] [리턴타입패턴] [클래스이름패턴] [메서드이름패턴]([파라미터타입패턴]))
예시 1)
org.example.* 패키지 내의 클래스에서 반환값이 void인 메소드 중,
메소드명이 "get*"으로 시작하는 메소드를 포함하는 표현식은 아래와 같다.
execution(void org.example.*.*.get*(..))
- org.example.*.*: 클래스 이름 패턴으로 org.example 패키지 내의 모든 클래스를 나타낸다.
- get*: 메소드 이름 패턴으로 "get*"으로 시작하는 모든 메소드를 나타낸다.
- ..: 파라미터 타입 패턴으로 모든 파라미터를 나타낸다.
예시 2)
org.example 패키지 내의 클래스에서 메소드명이 "set*"으로 시작하는 메소드 중,
인자로 java.lang.String 타입의 인자를 갖는 메소드를 포함하는 표현식은 아래와 같다.
execution(* org.example..set*(java.lang.String))
- *: 리턴타입 패턴으로 모든 반환값을 나타낸다.
- org.example..: 클래스 이름 패턴으로 org.example 패키지 내의 모든 클래스를 나타낸다.
- set*: 메소드 이름 패턴으로 "set" 으로 시작하는 모든 메소드를 나타낸다.
- java.lang.String: 파라미터 타입 패턴으로 인자로 java.lang.String 타입 하나만을 나타낸다.
(3) Before
대상 메소드가 실행되기 이전에 실행되는 advice로, 미리 작성한 포인트컷을 설정한다.
@Before("LoggingAspect.logPointcut()")
public void logBefore(JoinPoint joinPoint) { // JoinPoint : 포인트 컷으로 패치한 실행 시점
// JoinPoint 객체를 통해 현재 조인포인트의 메소드명, 인수 값 등 자세한 정보를 엑세스 할 수 있다.
System.out.println("before joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("before joinPoint.getSignature() : " + joinPoint.getSignature());
if(joinPoint.getArgs().length > 0) {
System.out.println("before joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]);
}
}
<실행 결과>
MemberService 클래스의 selectMembers 메소드와 selectMember 메소드가 실행되기 전
Before 어드바이스의 실행 내용이 삽입되어 동작하는 것을 확인할 수 있다.
(4) After
대상 메소드가 실행된 이후에(정상, 예외 관계없이) 실행되는 어드바이스로, 미리 작성한 포인트 컷을 설정한다.
포인트컷을 동일한 클래스 내에서 사용하는 것이면 클래스명은 생략 가능하지만,
패키지가 다르면 패키지를 포함한 클래스명을 기술해야 한다.
Before 어드바이스와 동일하게 매개변수로 JoinPoint 객체를 전달 받을 수 있다.
@After("logPointcut()") // 포인트 컷을 클래스 내에서 사용할 경우 클래스명 생략 가능, 패키지가 다르면 패키지명까지 기술.
public void logAfter(JoinPoint joinPoint) {
// JoinPoint 객체를 통해 현재 조인포인트의 메소드명, 인수 값 등 자세한 정보를 엑세스 할 수 있다.
System.out.println("after joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("after joinPoint.getSignature() : " + joinPoint.getSignature());
if(joinPoint.getArgs().length > 0) {
System.out.println("after joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]);
}
}
MemberService 클래스의 selectMembers 메소드와 selectMember 메소드가 실행된 후에
After 어드바이스의 실행 내용이 삽입되어 동작하는 것을 확인할 수 있다.
(5) AfterReturning
대상 메소드가 정상적으로 실행된 이후에 실행되는 어드바이스로, 미리 작성한 포인트컷을 설정한다.
returning 속성은 리턴값으로 받아올 오브젝트의 매개변수 이름과 동일해야 하며,
joinPoint는 반드시 첫번째 매개변수로 선언해야 한다. 이 어드바이스에서는 반환값을 가공할 수 있다.
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void logAfterReturn(JoinPoint joinPoint, Object result) {
System.out.println("after returning result : " + result);
if(result != null && result instanceof Map) {
((Map<Long, MemberDTO>) result).put(100L, new MemberDTO(100L, "반환 값 가공"));
}
}
(6) AfterThrowing
예외가 발생했을 때 실행되는 어드바이스로, 미리 작성한 포인트 컷을 설정한다.
throwing 속성의 이름과 매개변수 이름이 동일해야 하며, 이 어드바이스에서는 exception에 따른 처리를 할 수 있다.
@AfterThrowing(pointcut = "logPointcut()", throwing = "exception")
public void logAfterThrow(JoinPoint joinPoint, Exception exception) {
System.out.println("after throwing exception : " + exception);
}
(7) Around
대상 메소드 실행 전/후에 적용되는 어드바이스로, 미리 작성한 포인트컷을 설정한다.
Around Advice는 가장 강력한 어드바이스로,
이 어드바이스는 조인포인트를 완전 장악하기 때문에
앞에서 살펴본 어드바이스는 모두 Around 어드바이스로 조합할 수 있다.
AroundAdvice의 조인포인트 매개변수는 JoinPoint의 하위 인터페이스인 ProceedingJoinPoint로 고정되어 있고,
원본 조인포인트의 진행 시점을 제어할 수 있다.
조인포인트 진행하는 호출(proceed())을 잊는 경우가 자주 발생하기 때문에 주의해햐 하며,
최소한의 요건을 충족하면서도 가장 기능이 약한 어드바이스를 쓰는 게 바람직하다.
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before : " + joinPoint.getSignature());
Object result = joinPoint.proceed(); // 원본 조인 포인트를 실행
System.out.println("around after : " + joinPoint.getSignature());
return result;
}
위에서 작성한 코드들을 합쳐 LoggingAspect 클래스를 아래와 같이 작성했다.
@Component
@Aspect // pointcut과 advice를 하나의 클래스 단위로 정의하기 위한 어노테이션
public class LoggingAspect {
// Pointcut : 여러 Join Point 를 매치하기 위해 지정한 표현식
@Pointcut("execution(* org.example.section01.aop.*Service.*(..))")
public void logPointcut() {}
// Advice : 부가 코드
// Before : 핵심 기능 수행 전 동작
@Before("LoggingAspect.logPointcut()")
public void logBefore(JoinPoint joinPoint) { // JoinPoint : 포인트 컷으로 패치한 실행 시점
// JoinPoint 객체를 통해 현재 조인포인트의 메소드명, 인수 값 등 자세한 정보를 엑세스 할 수 있다.
System.out.println("before joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("before joinPoint.getSignature() : " + joinPoint.getSignature());
if(joinPoint.getArgs().length > 0) {
System.out.println("before joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]);
}
}
// After : 핵심 기능 수행 후 동작 (정상 수행 또는 오류 발생 무관)
@After("logPointcut()") // 포인트 컷을 클래스 내에서 사용할 경우 클래스명 생략 가능, 패키지가 다르면 패키지명까지 기술.
public void logAfter(JoinPoint joinPoint) {
// JoinPoint 객체를 통해 현재 조인포인트의 메소드명, 인수 값 등 자세한 정보를 엑세스 할 수 있다.
System.out.println("after joinPoint.getTarget() : " + joinPoint.getTarget());
System.out.println("after joinPoint.getSignature() : " + joinPoint.getSignature());
if(joinPoint.getArgs().length > 0) {
System.out.println("after joinPoint.getArgs()[0] : " + joinPoint.getArgs()[0]);
}
}
// AfterReturning -> 정상 수행 시
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void logAfterReturn(JoinPoint joinPoint, Object result) {
System.out.println("after returning result : " + result);
if(result != null && result instanceof Map) {
((Map<Long, MemberDTO>) result).put(100L, new MemberDTO(100L, "반환 값 가공"));
}
}
// AfterThrowing -> exception 발생 시
@AfterThrowing(pointcut = "logPointcut()", throwing = "exception")
public void logAfterThrow(JoinPoint joinPoint, Exception exception) {
System.out.println("after throwing exception : " + exception);
}
// Around : 핵심 기능 시작과 끝을 감싸고 동작
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before : " + joinPoint.getSignature());
Object result = joinPoint.proceed(); // 원본 조인 포인트를 실행
System.out.println("around after : " + joinPoint.getSignature());
return result;
}
}
Reflection
실행 중인 자바 프로그램 내부의 클래스, 메소드, 필드 등의 정보를 분석하여 다루는 기법으로,
이를 통해 프로그램의 동적인 특성을 구현할 수 있다.
예를 들어 실행 중인 객체의 클래스 정보를 얻어오거나, 클래스 내부의 필드나 메소드에 접근할 수 있다.
이러한 기능들은 프레임워크, 라이브러리, 테스트 코드 등에서 유용하게 활용된다.
Spring에서는 Reflection을 사용해서 런타임 시 등록한 빈을 애플리케이션 내에서 사용할 수 있게 한다.
1. 리플렉션 테스트 대상이 될 Account 클래스 생성
public class Account {
private String backCode;
private String accNo;
private String accPwd;
private int balance;
public Account() {}
public Account(String bankCode, String accNo, String accPwd) {
this.backCode = bankCode;
this.accNo = accNo;
this.accPwd = accPwd;
}
public Account(String bankCode, String accNo, String accPwd, int balance) {
this(bankCode, accNo, accPwd);
this.balance = balance;
}
/* 현재 잔액을 출력해주는 메소드 */
public String getBalance() {
return this.accNo + " 계좌의 현재 잔액은 " + this.balance + "원 입니다.";
}
/* 금액을 매개변수로 전달 받아 잔액을 증가(입금) 시켜주는 메소드 */
public String deposit(int money) {
String str = "";
if(money >= 0) {
this.balance += money;
str = money + "원이 입급되었습니다.";
}else {
str = "금액을 잘못 입력하셨습니다.";
}
return str;
}
/* 금액을 매개변수로 받아 잔액을 감소(출금) 시켜주는 메소드 */
public String withDraw(int money) {
String str = "";
if(this.balance >= money) {
this.balance -= money;
str = money + "원이 출금되었습니다.";
}else {
str = "잔액이 부족합니다. 잔액을 확인해주세요.";
}
return str;
}
}
2. 리플렉션 테스트
(1) Class
Class 타입의 인스턴스는 해당 클래스의 메타정보를 가지고 있는 클래스이다.
// .class 문법을 이용하여 Class 타입의 인스턴스를 생성(클래스를 알고 있을 경우)
Class class1 = Account.class;
System.out.println("class1 : " + class1); // Class 타입의 인스턴스는 해당 클래스의 메타 정보를 가짐
// Object 클래스의 getClass() 메소드를 이용해서도 Class 타입의 인스턴스 생성 가능
Class class2 = new Account().getClass();
System.out.println("class2 : " + class2);
try {
// 클래스 이름만 알고 있을 경우
Class class3 = Class.forName("com.ohgiraffers.section02.reflection.Account");
// [: 배열, D: double -> double 배열 타입을 나타내는 Class 객체
Class class4 = Class.forName("[D");
Class class5 = double[].class;
System.out.println("class3 : " + class3);
System.out.println("class4 : " + class4);
System.out.println("class5 : " + class5);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// 원시 자료형을 사용하면 컴파일 에러가 발생한다.
// double d = 1.0;
// Class class6 = d.getClass();
// 원시형 클래스는 TYPE 필드로 반환
Class class6 = Double.TYPE;
System.out.println("class6 : " + class6);
/* 클래스 메타 정보를 이용하여 여러 정보를 반환 받는 메소드가 제공 된다. */
Class superClass = class1.getSuperclass();
System.out.println("superClass : " + superClass);
(2) field
field 정보에 접근할 수 있다
// getDeclaredFields(): 클래스에 선언된 모든 필드를 배열로 반환
Field[] fields = class1.getDeclaredFields();
for(Field field : fields) {
System.out.println("modifiers : " + Modifier.toString(field.getModifiers()));
System.out.println("type : " + field.getType());
System.out.println("name : " + field.getName());
}
(3) 생성자
생성자 정보에 접근할 수 있다.
Constructor[] constructors = class1.getConstructors();
// 생성자 정보에 접근할 수 있다.
for(Constructor constructor : constructors) {
System.out.println("name : " + constructor.getName());
Class[] params = constructor.getParameterTypes();
for(Class param : params) {
System.out.println("paramType : " + param.getTypeName());
}
}
// 생성자를 이용하여 인스턴스를 생성할 수 있다.
try {
Account acc = (Account) constructors[0].newInstance("20", "110-234-567890", "1234", 10000);
System.out.println(acc.getBalance());
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
(4) 메소드
메소드 정보에 접근할 수 있다.
// 메소드 정보에 접근할 수 있다.
Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for(Method method : methods) {
System.out.print(Modifier.toString(method.getModifiers()) + " ");
System.out.print(method.getReturnType().getSimpleName() + " ");
System.out.println(method.getName());
if("getBalance".equals(method.getName()))
getBalanceMethod = method;
}
// invoke 메소드로 메소드를 호출할 수 있다.
try {
System.out.println(getBalanceMethod.invoke(constructors[2].newInstance()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
wait, equals 등의 메소드는 Object 클래스에서 상속받은 메서드이다.
위에서 작성한 코드들을 합쳐 Application 클래스를 아래와 같이 작성했다.
package org.example.section02.reflection;
public class Application {
public static void main(String[] args) {
// .class 문법을 이용하여 Class 타입의 인스턴스를 생성
Class class1 = Account.class;
System.out.println("class1 : " + class1); // Class 타입의 인스턴스는 해당 클래스의 메타 정보를 가짐
// Object 클래스의 getClass() 메소드를 이용해서도 Class 타입의 인스턴스 생성 가능
Class class2 = new Account().getClass();
System.out.println("class2 : " + class2);
try {
Class class3 = Class.forName("org.example.section02.reflection.Account");
Class class4 = Class.forName("[D");
Class class5 = double[].class;
System.out.println("class3 : " + class3);
System.out.println("class4 : " + class4);
System.out.println("class5 : " + class5);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// 원시 자료형의 경우
// double d = 1.0;
// Class class6 = d.getClass();
// 원시형 클래스는 TYPE 필드로 반환
Class class6 = Double.TYPE;
System.out.println("class6 : " + class6);
/* 클래스 메타 정보를 이용하여 여러 정보를 반환 받는 메소드가 제공 된다. */
Class superClass = class1.getSuperclass();
System.out.println("superClass : " + superClass);
Field[] fields = class1.getDeclaredFields();
for(Field field : fields) {
System.out.println("modifiers : " + Modifier.toString(field.getModifiers()));
System.out.println("type : " + field.getType());
System.out.println("name : " + field.getName());
}
Constructor[] constructors = class1.getConstructors();
for(Constructor constructor : constructors) {
System.out.println("name : " + constructor.getName());
Class[] params = constructor.getParameterTypes();
for(Class param : params) {
System.out.println("paramType : " + param.getTypeName());
}
}
try {
Account acc = (Account) constructors[0].newInstance("20", "110-234-567890", "1234", 10000);
System.out.println(acc.getBalance());
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for(Method method : methods) {
System.out.print(Modifier.toString(method.getModifiers()) + " ");
System.out.print(method.getReturnType().getSimpleName() + " ");
System.out.println(method.getName());
if("getBalance".equals(method.getName())) getBalanceMethod = method;
}
try {
System.out.println(getBalanceMethod.invoke(constructors[2].newInstance()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
}
Proxy
프록시는 기존의 객체(Target Object)를 감싸서 그 객체의 기능을 확장하거나 변경할 수 있게 해주는 실제 객체이다.(대리인)
예를 들어,
프록시 객체를 사용하면 객체에 대한 접근을 제어하거나,
객체의 메소드 호출 전후에 로깅 작업 등을 수행할 수 있으며,
프록시 객체를 사용하여 원격으로 실행되는 객체를 호출할 수도 있다.
프록시는 주로 AOP에서 사용된다.
* 공통 클래스
package org.example.section03.proxy.common;
public interface Student {
void study(int hours);
}
public class OhgiraffersStudent implements Student {
@Override
public void study(int hours) {
System.out.println(hours + "시간 동안 열심히 공부합니다.");
}
}
1. 프록시를 생성하는 두가지 방식
(1) JDK Dynamic Proxy 방식
리플렉션을 이용해서 proxy 클래스를 동적으로 생성해주는 방식으로, 타겟의 인터페이스를 기준으로 proxy를 생성해준다.
Target Object의 타입이 반드시 inteface여야 하고,
리플렉션을 통해 매번 프록시와 target object 사이를 동적으로 연결하므로 처리가 다소 느릴 수 있다.
예전에 주로 쓰이던 방식이다.
* 핵심 구성 요소
1) Proxy Interface
프록시가 구현할 인터페이스를 정의하며, 프록시 객체는 이 인터페이스를 구현해야 한다.
현재 다루는 예제의 경우 Student 인터페이스가 이에 해당한다.
2) InvocationHandler
InvocationHandler 인터페이스를 구현하여 메서드 호출을 처리한다.
invoke 메서드를 구현하여 실제 메소드 호출을 가로채고 필요한 작업을 수행한다
3) Proxy 클래스
Proxy 클래스의 newProxyInstance 메소드를 사용하여 프로시 객체를 생성한다.
이 메서드는 인터페이스와 InvocationHandler를 입력받아 프록시 객체를 반환한다.
package org.example.section03.proxy.subsection01.dynamic;
// OhgiraggersStudent 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.
public class Handler implements InvocationHandler {
// 메소드 호출을 위한 타겟 인스턴스
private final Student student;
public Handler(Student student) {
this.student = student;
}
// 생성된 proxy 인스턴스와 타겟 메소드, 전달받은 인자를 매개변수로 한다.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("====공부가 너무 하고 싶습니다 ====");
System.out.println("호출 대상 메소드: " + method);
for(Object arg : args) {
System.out.println("전달 인자: " + arg);
}
// 타겟 메소드를 호출한다. 타겟 Object와 인자를 매개변수로 전달한다.
// 여기서 프록시를 전달하면 다시 타겟을 호출할 때 다시 프록시를 생성하고 다시 또 전달하는 무한 루프에 빠지게 된다.
method.invoke(student, args);
System.out.println("====공부를 마치고 수면 학습을 시작합니다====");
return proxy;
}
}
package org.example.section03.proxy.subsection01.dynamic;
public class Application {
public static void main(String[] args) {
Student student = new OhgiraffersStudent();
Handler handler = new Handler(student);
/* proxy 객체 생성.
인터페이스와 InvocationHandler을 입력받아 해당 인터페이스를 구현하는 프록시 객체를 생성한다.*/
Student proxy
= (Student) Proxy.newProxyInstance(Student.class.getClassLoader(), new Class[]{Student.class}, handler);
proxy.study(16);
}
}
(2) CGLib 방식
동적으로 proxy를 생성하지만 바이트코드를 조작하여 프록시를 생성해주는 방식으로,
Target Object가 인터페이스 뿐 아니라 클래스여도 가능하다.
바이트 코드를 조작하여 초기 설정 비용은 있지만, 재호출 시 빠르게 동작하며 성능 면에서 더 우수하여 요즘에 쓰이는 방식이다.
package org.example.section03.proxy.subsection02.cglib;
public class Handler implements InvocationHandler {
/* Target Object로 class 타입 사용 가능 */
private final OhgiraffersStudent ohgiraffersStudent;
public Handler(OhgiraffersStudent ohgiraffersStudent) {
this.ohgiraffersStudent = ohgiraffersStudent;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===== 공부가 너무 하고 싶습니다 =====");
System.out.println("호출 대상 메소드 : " + method);
for(Object arg : args) {
System.out.println("전달 인자 : " + arg);
}
method.invoke(ohgiraffersStudent, args); // 타겟 메소드 호출
System.out.println("===== 공부를 마치고 수면 학습을 시작합니다 =====");
return proxy;
}
}
package org.example.section03.proxy.subsection02.cglib;
public class Application {
public static void main(String[] args) {
OhgiraffersStudent ohgiraffersStudent = new OhgiraffersStudent();
Handler handler = new Handler(ohgiraffersStudent);
/* Enhancer 클래스의 create static 메소드는 타겟 클래스의 메타정보와 핸들러를 전달하면 proxy를 생성해서 반환해준다. */
OhgiraffersStudent proxy
= (OhgiraffersStudent) Enhancer.create(OhgiraffersStudent.class, handler);
proxy.study(20);
}
}