diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2301.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2301.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2301.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2301.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2302.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2302.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2302.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2302.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2303.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2303.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2303.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2303.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2304.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2304.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2304.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2304.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2305.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2305.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2305.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2305.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2306.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2306.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2306.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2306.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2307.md" "b/\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2307.md" similarity index 100% rename from "\354\230\244\354\210\230\354\247\204/docs/\354\204\271\354\205\2307.md" rename to "\354\230\244\354\210\230\354\247\204/docs/\354\212\244\355\224\204\353\247\201 \354\236\205\353\254\270/\354\204\271\354\205\2307.md" diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/1\354\236\245.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/1\354\236\245.md" new file mode 100644 index 0000000..9c7c7c3 --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/1\354\236\245.md" @@ -0,0 +1,9 @@ +1장 오브젝트와 의존 관계 += + +## 전략 패턴 + +- 자신의 기능 맥락(Context)에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴 +- 컨텍스트 : 자신의 기능을 수행하는데 필요한 기능 중에서 변경 가능한 알고리즘을 인터페이스로 정의하고, 이를 구현한 클래스(전략)을 바꿔가면서 사용한다. +- 이때, 컨텍스트를 사용하는 클라이언트가 컨텍스트가 사용할 전략을 컨텍스트의 생성자 등을 통해서 제공해주면서 DI 구현 + diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.1 \355\212\270\353\236\234\354\236\255\354\205\230 \354\275\224\353\223\234\354\235\230 \353\266\204\353\246\254.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.1 \355\212\270\353\236\234\354\236\255\354\205\230 \354\275\224\353\223\234\354\235\230 \353\266\204\353\246\254.md" new file mode 100644 index 0000000..56f9303 --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.1 \355\212\270\353\236\234\354\236\255\354\205\230 \354\275\224\353\223\234\354\235\230 \353\266\204\353\246\254.md" @@ -0,0 +1,191 @@ +6.1 트랜잭션 코드의 분리 += +```java +public void upgradeLevels() throws Exception { + TransactionStatus status = this.transactionManager + .getTransaction(new DefaultTransactionDefinition()); + try { + List users = userDao.getAll(); + for (User user : users) { + if (canUpgradeLevel(user)) { + upgradeLevel(user); + } + } + this.transactionManager.commit(status); + } catch (Exception e) { + this.transactionManager.rollback(status); + throw e; + } +} +``` + +- 비즈니스 로직 코드와 트랜잭션 경계설정 코드는 완벽하게 독립적인 코드 + - 비즈니스 로직 코드를 사이에 두고 트랜잭션 경계설정 코드가 앞뒤에 위치(분리되어 있음) + - 트랜잭션 경계설정 코드와 비즈니스 로직 코드 사이에 주고 받는 정보가 없음. +
→ 두 코드를 분리할 수 있을 것 + +--- + +### 1. **비즈니스 로직 담당 코드를 메소드로 추출** +```java +public void upgradeLevels() throws Exception { + TransactionStatus status = this.transactionManager + .getTransaction(new DefaultTransactionDefinition()); + try { + upgradeLevelsInternal(); + this.transactionManager.commit(status); + } catch (Exception e) { + this.transactionManager.rollback(status); + throw e; + } +} + +private void upgradeLevelsInternal() { + List users = userDao.getAll(); + for (User user : users) { + if (canUpgradeLevel(user)) { + upgradeLevel(user); + } + } +} +``` +- 가독성 향상, 수정 용이 + +--- + +## 2. 트랜잭션 코드를 클래스 밖으로 뽑아내기 + +### 인터페이스를 통해 간접으로 접근 +- Client → UserService +- Client → UserService(Interface) ← UserServiceImpl + +### 인터페이스 사용 이유 +- 일반적으로 구현 클래스를 바꿔가면서 사용하기 위함 +- 이번 예제에서는 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용하기 위함 + +### 코드 +#### UserService 인터페이스 +```java +public interface UserService { + void add(User user); + void upgradeLevels(); +} +``` + +#### 트랜잭션 코드를 제거한 UserServiceImpl +```java +public class UserServiceImpl implements UserService { + UserDao userDao; + MailSender mailSender; + + public void upgradeLevels() { + List users = userDao.getAll(); + for (User user : users) { + if (canUpgradeLevel(user)) { + upgradeLevel(user); + } + } + } + ... +} +``` +- 기존의 UserService 코드에서 트랜잭션 경계설정 코드 제거 +- 비즈니스 로직에 충실한 코드 + +#### 분리된 트랜잭션 코드 +```java +public class UserServiceTx implements UserService { + UserService userService; + PlatformTransactionManager transactionManager; + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + // UserService 타입의 오브젝트를 DI + public void setUserService(UserService userService) { + this.userService = userService; + } + + // DI 받은 오브젝트에 비즈니스 로직을 위임 + public void add(User user) { + userService.add(user); + } + + public void upgradeLevels() { + TransactionStatus status = this.transactionManager + .getTransaction(new DefaultTransactionDefinition()); + try { + userService.upgradeLevels(); + this.transactionManager.commit(status); + } catch (Exception e) { + this.transactionManager.rollback(status); + throw e; + } + + } +} +``` +- 비즈니스 로직을 구현한 다른 구현 객체를 DI 받는다. +- 비즈니스 로직은 해당 구현 객체에 모두 위임한다. + +### 트랜잭션 적용을 위한 DI 설정 +#### 스프링 설정 파일 +```java +@Configuration +public class UserServiceConfig { + @Bean + public UserService userServiceImpl(UserDao userDao, MailSender mailSender) { + return new UserServiceImpl(userDao, mailSender); + } + + @Bean + public UserService userServiceTx(UserService userServiceImpl, PlatformTransactionManager transactionManager) { + return new UserServiceTx(userServiceImpl, transactionManager); + } +} +``` +- 기존 테스트에서 @Autowired 를 사용해 DI를 구현했는데, UserService가 인터페이스로 바뀌었다. +- @Autowired는 타입이 일치하는 빈을 찾아서 주입해준다. +- 만약 같은 타입의 빈이 여러 개 있으면, 필드 이름을 사용해 빈을 찾는다. +- 단, 위의 Configuration의 경우 UserService 이름을 직접 사용하는 빈이 없기 때문에 의존성 받으려 하면 오류 남 + +#### 해결1 @Primary + +```java + @Configuration + public class UserServiceConfig { + + @Bean + public UserService userServiceImpl(UserDao userDao, MailSender mailSender) { + return new UserServiceImpl(userDao, mailSender); + } + + @Bean + @Primary + public UserService userServiceTx(UserService userServiceImpl, PlatformTransactionManager transactionManager) { + return new UserServiceTx(userServiceImpl, transactionManager); + } +} +``` +#### 해결 2 @Qualifier +```java +@Autowired +@Qualifier("userServiceTx") +private UserService userService; +``` + +```java +private UserService userService; + +public UserServiceTest(@Qualifier("userServiceTx") UserService userService) { + this.userService = userService; +} +``` + +### 트랜잭션 경계설정 코드 분리의 장점 +- 관심사의 분리 + - 비즈니스 로직을 담당하는 구현 객체의 코드를 작성할 때에는 트랜잭션과 같은 기술적인 내용에는 신경 쓰지 않아도 된다. + - DI를 통해 트랜잭션 기능을 가진 객체가 먼저 실행되도록 만들면 된다. +- 테스트 용이성 + - 비즈니스 로직에 대한 테스트를 손쉽게 만들 수 있다. \ No newline at end of file diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.2 \352\263\240\353\246\275\353\220\234 \353\213\250\354\234\204 \355\205\214\354\212\244\355\212\270.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.2 \352\263\240\353\246\275\353\220\234 \353\213\250\354\234\204 \355\205\214\354\212\244\355\212\270.md" new file mode 100644 index 0000000..7dedbd5 --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.2 \352\263\240\353\246\275\353\220\234 \353\213\250\354\234\204 \355\205\214\354\212\244\355\212\270.md" @@ -0,0 +1,6 @@ +6.2 고립된 단위 테스트 += +### 작은 단위의 테스트가 좋은 이유 +- 테스트가 실패했을 때, 원인을 찾기 쉬움 +- 테스트의 의도나 내용이 분명해짐 +- 만들기 쉬워짐 \ No newline at end of file diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.3 \353\213\244\354\235\264\353\202\264\353\257\271 \355\224\204\353\241\235\354\213\234\354\231\200 \355\214\251\355\206\240\353\246\254 \353\271\210.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.3 \353\213\244\354\235\264\353\202\264\353\257\271 \355\224\204\353\241\235\354\213\234\354\231\200 \355\214\251\355\206\240\353\246\254 \353\271\210.md" new file mode 100644 index 0000000..705caed --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.3 \353\213\244\354\235\264\353\202\264\353\257\271 \355\224\204\353\241\235\354\213\234\354\231\200 \355\214\251\355\206\240\353\246\254 \353\271\210.md" @@ -0,0 +1,535 @@ +6.3 다이내믹 프록시와 팩토리 빈 += +## 6.3.1 프록시 +- 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것 +- 대리자, 대리인 + +![image](https://github.com/user-attachments/assets/0463fba4-5327-4190-a622-042a544b2644) +- 부가기능 외의 모든 기능은 원래 핵심 기능을 가진 클래스로 위임한다. +- 따라서, 부가기능이 핵심기능을 사용하는 구조 +- 부가기능은 마치 자신이 핵심 기능을 가진 클래스인 것처럼 꾸며서, 클라이언트가 자신을 거쳐 핵심기능을 사용하게 만들어야 한다. + +### 프록시 특징 +1. 타깃과 같은 인터페이스를 구현 + - 클라이언트는 요청을 보낸 대상이 타깃인지 프록시인지 구별할 수 x + - 낮은 결합도 & OCP +2. 프록시가 타깃을 제어할 수 있는 위치에 있다는 것 + +### 프록시 사용 목적 +1. 클라이언트가 타깃에 접근하는 방법을 제어 → 프록시 패턴 +2. 타깃에 부가적인 기능을 부여 → 데코레이터 패턴 + +--- +## 데코레이터 패턴 +- 타깃에 부가적인 기능을 부여 +- 다이내믹하게 기능을 부여 + - 컴파일 시점에는 프록시와 타깃의 연결 방법과 순서가 정해져 있지 않음 +- 프록시가 한 개로 제한되지 않는다. + - 여러 개의 프록시의 적용 순서를 정하고 단계적으로 위임하는 구조를 설계 + +![image](https://github.com/user-attachments/assets/b6b312a3-9d1f-4acc-9485-e2968df63a9c) +- 각 데코레이터는 인터페이스로 접근하기 때문에 자신이 어느 순서에 위치하는 지 모름 +- 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 설계 +- 스프링을 사용해서 데코레이터 빈의 프로퍼티로 같은 인터페이스를 구현한 다른 데코레이터 또는 타깃 빈을 설정하면 된다. +- 호출하는 쪽의 코드와, 타깃의 코드를 수정하지 않고도 데코레이터를 추가하여 기능을 확장할 수 있다. + +스크린샷 2024-08-24 오후 10 22 25 + +## 프록시 패턴 +- 타깃에 대한 접근 방법을 제어 +- 타깃의 기능을 확장하거나 추가하지 않는다. + +### 사용 예시 +1. 타깃 오브젝트를 생성하기가 복잡하거나 당장 필요하지 않지만, 오브젝트에 대한 레퍼런스가 미리 필요한 경우 +2. 원격 오브젝트를 이용하는 경우 + - 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것처럼 프록시를 사용하고 실제 요청을 받으면 네트워크를 통해 원격의 오브젝트를 실행 +3. 타깃에 대한 접근권한을 제어 + - 특정 레이어로 넘어간 후에는 읽기 전용으로만 동작하게 하고 싶은 경우 + - 프록시의 특정 메소드를 사용하려고 하면 접근 불가 예외 발생 +```java +public class UnmodifiableCollectionExample { + public static void main(String[] args) { + // 원본 리스트 생성 + List originalList = new ArrayList<>(); + originalList.add("Apple"); + originalList.add("Banana"); + originalList.add("Cherry"); + + // unmodifiableCollection을 사용하여 변경 불가능한 컬렉션 생성 + Collection unmodifiableCollection = Collections.unmodifiableCollection(originalList); + + // 읽기 작업은 가능 + System.out.println("Unmodifiable Collection: " + unmodifiableCollection); + + // 변경 시도 (예외 발생) + try { + unmodifiableCollection.add("Date"); + } catch (UnsupportedOperationException e) { + System.out.println("Cannot modify unmodifiable collection: " + e.getMessage()); + } + } +} +``` + +### 데코레이터 패턴, 프록시 패턴 +- 둘 다 인터페이스를 통해 위임하는 코드로 설계 가능 +- 단, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다. + - ex) 생성을 지연하는 프록시의 경우 구체적인 생성 방법을 알아야 하기 때문에 타깃 클래스의 정보를 알아야 한다. + +--- + +## 6.3.2 다이내믹 프록시 + +### 프록시의 두 가지 기능 +1. 타깃과 같은 메소드를 구현하고 메소드가 호출되면 타깃 오브젝트로 위임한다. +2. 지정된 요청에 대해서는 부가기능을 수행한다. + +### 프록시를 만드는 일이 번거로운 이유 +1. 타깃의 인터페이스를 구현하고 위임하는 코드 작성의 번거로움 + - 부가기능이 필요없는 메소드도 구현해서 위임하는 코드를 작성해야 함 + - 메소드가 많아지면 모두 구현해야 하기 때문에 부담 증가 + - 인터페이스의 메소드 추가나 변경 시 프록시 코드도 변경해야 함 +2. 부가기능 코드의 중복 + - 프록시를 활용할 만한 부가기능, 접근제어 기능은 일반적인 성격을 띈다 (횡단 관심사) + - 다양한 타깃 클래스와 메소드에 중복돼서 나타날 가능성이 높다. + +## 리플렉션 +- 자바의 코드 자체를 추상화해서 접근하도록 만든 것 +- 컴파일 시간이 아닌 실행시간에 동적으로 클래스의 정보를 추출할 수 있는 프로그래밍 기법 +- + +[참고] +- 애플리케이션 개발에서보다는 프레임워크, 라이브러리에서 많이 사용된다. + - 사용하는 사람이 어떤 클래스명과 멤버들을 구성할 지 모르는 상황에서, 이러한 사용자 클래스들과 기존의 기능을 동적으로 연결시키기 위해 사용 + - IntelliJ의 자동완성 기능, Spring의 DI, Proxy, ModelMapper에서 사용 + +### 클래스 객체 얻기 +1. Object.getClass() + - Object 클래스에서 제공하는 getClass() 메서드 사용 + - 해당 클래스가 인스턴스화된 상태여야 함 + - ``` java + String str = "aaa"; + str.getClass(); + ``` +2. .class 리터럴 + - 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있는 경우 + - ``` java + String.class; + ``` +3. Class.forName() + - 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 이름만으로 Class 객체 반환 받을 수 있음 + - 단, 클래스 도메인을 상세히 적어주어야 함. + - 해당 객체를 찾지 못하면 ```ClassNotFoundException``` 을 발생시키기 때문에 예외처리가 강제된다. + - ```java + public static void main(String[] args) { + try { + Class cls = Class.forName("java.lang.String"); + System.out.println(cls); + } catch (ClassNotFoundExceptione) {} + } + ``` + +### 메소드 호출하기 +- Class.getMethod(메소드명, 파라미터) 를 사용해 메소드 정보 가져오기 +```java +Method lengthMethod = String.class.getMethod("length"); +``` +
+ +- Method 인터페이스에 정의된 invoke(메소드를 실행시킬 대상, 파라미터 목록)를 사용해 메소드 호출하기 +```java +//public Object invoke(Object obj, Object ... args); + +String name = "Spring"; +int length = lengthMethod.invoke(name); +``` + +### 프록시 클래스 +- 프록시를 적용할 타깃 클래스와 인터페이스 +```java +interface Hello { + String sayHello(String name); + String sayHi(String name); + String sayThankYou(String name); +} + +public class HelloTarget implements Hello { + public String sayHello(String name) { + return "Hello " + name; + } + + public String sayHi(String name) { + return "Hi " + name; + } + + public String sayThankYou(String name) { + return "Thank you " + name; + } +} +``` +
+ +- 대문자로 바꿔주는 프록시 클래스 (데코레이터 패턴) +```java +public class HelloUpperCase { + Hello hello; + + public HelloUpperCase(Hello hello) { + this.hello = hello; + } + + public String sayHello(String name) { + return hello.sayHello(name).toUpperCase(); + } + + public String sayHi(String name) { + return hello.sayHi(name).toUpperCase(); + } + + public String sayThankYou(String name) { + return hello.sayThankYou(name).toUpperCase(); + } +} +``` +- 위의 코드는 프록시를 구성하기 번거로운 두 가지 문제점을 모두 가진다. + 1. 모든 메소드를 구현 및 위임하는 코드 + 2. 부가기능을 적용하는 코드의 중복 +
+ +### 다이내믹 프록시 적용 +- 다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 만들어진다. +- 타깃의 오브젝트와 같은 타입으로 만들어지며, 클라이언트는 타깃 오브젝트를 사용하여 다이내믹 프록시 오브젝트를 사용할 수 있다. +- 덕분에 프록시를 만들 때, 인터페이스를 모두 구현해가면서 클래스를 정의할 필요없이 프록시 팩토리에게 인터페이스 정보만 제공해주면, 자동으로 인터페이스를 구현한 프록시 객체를 만들어준다. +- 단, 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야하는데, InvocationHandler를 구현한 오브젝트에 이 코드를 담아 프록시 팩토리에 전달하면 된다. + +![image](https://github.com/user-attachments/assets/98686d8f-5396-44c1-91a1-c67ab70b22ad) + +### InvocationHandler +![image](https://github.com/user-attachments/assets/0199e1d8-82c4-402c-bc12-ed711d9abe65) +- 프록시 팩토리에게 다이내믹 프록시를 만들어달라고 요청하면 인터페이스의 모든 메소드를 구현한 오브젝트를 생성해준다. +- InvocationHandler 인터페이스를 구현한 오브젝트를 제공하면, 다이내믹 프록시가 받는 모든 요청을 invoke 메소드로 보낸다. + +```java +public Object invoke(Object proxy, Method method, Object[] args); +``` + + +1. InvocationHandler의 invoke는 리플렉션의 Method 인터페이스와 해당 메소드를 호출할 때 필요한 파라미터(args)를 파라미터로 받는다. + - 즉, 타깃 오브젝트 레퍼런스를 가지고 있기 때문에 리플렉션을 이용해 간단히 위임 코드를 만들어 낼 수 있다. +2. 인터페이스의 메소드가 아무리 많더라도 invoke() 하나로 처리될 수 있기 때문에 중복되는 기능을 효과적으로 제공한다. + +
+ +- ``` InvocationHandler``` 구현 클래스 +```java +import java.lang.reflect.InvocationHandler; + +public class UppercaseHandler implements InvocationHandler { + // 타깃 오브젝트로 위임해야하기 때문에, 인터페이스를 통해 타깃 객체 DI + Hello target; + + public UppercaseHandler(Hello target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 타깃으로 위임하는 코드 + String ret = (String) method.invoke(target, args); + return ret.toUpperCase(); // 부가기능 코드 + } +} +``` + +
+ +- 프록시 생성 + +```java +import java.lang.reflect.Proxy; + +Hello proxiedHello = (Hello) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{Hello.class}, + new UppercaseHandler(new HelloTarget())); +``` +1. 다이내믹 프록시가 정의되는 클래스 로더를 지정 +2. 다이내믹 프록시가 구현해야 할 인터페이스 +3. 부가기능과 위임 관련 코드를 담고 있는 InvocationHandler 구현 객체 + +### 다이내믹 프록시의 확장 +#### 장점 +1. 인터페이스에 메소드가 추가돼도 invoke()에서 처리되기 때문에 코드 수정 필요 없음 +2. 타깃의 종류에 상관없이 적용 가능 + - Hello target -> Object target 으로 수정 +
+ +- 다른 타입을 리턴하는 메소드도 처리할 수 있도록 수정 +- 호출하는 메서드 이름, 파라미터 개수와 타입, 리턴 타입 등의 정보를 가지고 부가적인 기능을 적용할 메소드의 범위 지정 +```java +public class UppercaseHandler implements InvocationHandler { + // 타깃 오브젝트로 위임해야하기 때문에, 인터페이스를 통해 타깃 객체 DI + Object target; + + public UppercaseHandler(Object target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 다른 리턴타입의 메소드도 처리 가능 & 적용 메소드 선택 + Object ret = method.invoke(target, args); + if (ret instanceof String && method.getName().startsWith("say")) { + return ((String)ret).toUpperCase(); // + } else return ret; + } +} +``` + +## 6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가기능 +### 기존의 문제점 +1. 인터페이스의 모든 메소드를 구현하고 위임해야 함 +2. 트랜잭션 처리가 필요한 메소드마다 트랜잭션 처리코드가 중복됨 + +```java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; + +public class TransactionHandler implements InvocationHandler { + private Object target; + private PlatformTransactionManager transactionManager; + private String pattern; + + public void setTarget(Object target) { + this.target = target; + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().startsWith(pattern)) { + return invokeInTransaction(method, args); + } else { + return method.invoke(target, args); + } + } + + public Object invokeInTransaction(Method method, Object[] args) throws Throwable { + TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); + try { + Object ret = method.invoke(target, args); + this.transactionManager.commit(status); + return ret; + } catch (InvocationTargetException e) { + this.transactionManager.rollback(status); + throw e.getTargetException(); + } + } +} +``` +[주의] +- 리플랙션 메소드인 Method.invoke()를 이용해 타깃 오브젝트의 메소드를 호출할 때는 예외가 ```InvocationTargetException```으로 한번 포장돼서 전달된다. +- 따라서, 일단 ```InvocationTargetException```으로 받은 다음에 ```e.getTargetException()``` 메소드로 포장된 예외를 가져와야 한다. + +## 6.3.4 다이내믹 프록시를 위한 팩토리 빈 +- 스프링 DI는 지정된 클래스 이름을 가지고 리플렉션을 사용해서 해당 클래스 오브젝트를 만들어 주입한다. +- 다이내믹 프록시는 오브젝트 클래스가 어떤 것인지 정해져 있지 않고 클라이언트 생성 요청 시점에 다이나믹하게 정의해서 사용하기 때문에 이 방법을 사용할 수 없다. + +### 팩토리 빈 +- 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈 + + +``` +[참고] private 생성자 +- 사실 private 생성자를 가져도 리플렉션을 사용해서 오브젝트를 만들어줌 +- 일반적으로 private 생성자를 가진 클래스를 빈으로 등록하는 것은 권장되지 않음 +- 등록하더라도 빈 오브젝트가 바르게 동작하지 않을 가능성이 있다. + +[참고] private 생성자를 사용하는 이유 +- 인스턴스의 생성을 제한하여 클래스의 싱글톤 패턴 구현 +- 유틸리티 클래스의 경우, 인스턴스 생성을 막고 정적 메서드만 제공 +- 상속을 방지하여 클래스의 불변성과 안전성을 보장 +``` +
+ + +#### FactoryBean 인터페이스를 구현하기 +```java +// FactoryBean 인터페이스 +// 세 가지 메소드를 구현하면 된다. +public interface FactoryBean { + T getObject() throws Exception; + Class getObjectType(); + boolean isSingleton(); +} + +// Message의 팩토리 빈 클래스 +public class MessageFactoryBean implements FactoryBean { + String text; + + public void setText(String text) { + this.text = text; + } + + public Message getObject() throws Exception { + // 팩토리 메소드 실행해서 오브젝트 생성 + return Message.newMessage(this.text); + } + + public Class getObjectType() { + return Message.class; + } + // true로 설정하면 스프링이 싱글톤으로 관리 + public boolean isSingleton() { + return false; + } +} +``` +- 팩토리 빈을 빈으로 등록해놓으면 스프링이 자동으로 getObject() 메소드를 호출하여 빈 객체를 생성하고 관리한다. + +```java +@Configuration +public class SpringConfig { + + @Bean(name = "message") + public MessageFactoryBean messageFactoryBean() { + MessageFactoryBean factoryBean = new MessageFactoryBean(); + factoryBean.setText("Factory Bean"); + return factoryBean; + } +} +``` +- 팩토리 빈이 만들어주는 빈 오브젝트가 아니라 팩토리 빈 자체를 가져오고 싶은 경우 빈 이름 앞에 `&`를 붙여준다. +```java +@Test +public void getFactoryBean() throws Exception { + Object factory = context.getBean("&message"); + assertThat(factory, is(MessageFactoryBean.class)); +} +``` + +### 다이내믹 프록시를 만들어주는 팩토리 빈 +- userServiceTx → 팩토리 빈으로 구현 + +```java +import java.lang.reflect.Proxy; + +public class TxProxyFactoryBean implements FactoryBean { + Object target; + PlatformTransactionManager transactionManager; + String pattern; + Class serviceInterface; // General -> 다른 인터페이스에도 적용 가능 + + public void setTarget(Object target) { + this.target = target; + } + + public void setTarget(Object target) { + this.target = target; + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public void setServiceInterface(Class serviceInterface) { + this.serviceInterface = serviceInterface; + } + + // 프록시 객체 생성해서 반환 + public Object getObject() throws Exception { + TransactionHandler txHandler = new TransactionHandler(); + txHandler.setTarget(target); + txHandler.setTransactionManager(transactionManager); + txHandler.setPattern(pattern); + return Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{serviceInterface}, + txHandler); + } + + public Class getObjectType() { + return serviceInterface; + } + + public boolean isSingleton() { + retur false; + } +} +``` +- Config 수정 +```java +@Configuration +public class UserServiceConfig { + + @Bean + public UserService userServiceImpl(UserDao userDao, MailSender mailSender) { + return new UserServiceImpl(userDao, mailSender); + } + + @Bean(name = "userService") + @Primary + public TxProxyFactoryBean txProxyFactoryBean(@Qualifier("userServiceImpl") UserService userService, PlatformTransactionManager transactionManager) { + TxProxyFactoryBean txProxyFactoryBean = new TxProxyFactoryBean(); + txProxyFactoryBean.setTarget(userService); + txProxyFactoryBean.setTransactionManager(transactionManager); + txProxyFactoryBean.setPattern("upgradeLevels"); + txProxyFactoryBean.setServiceInterface(springboot.user.service.UserService.class); + return txProxyFactoryBean; + } +} +``` +
+ +#### [참고] 타깃 바꿔서 테스트 하고 싶을 때, +- 타깃은 InvocationHandler가 가지는데, 이 클래스는 팩토리 빈 내부에서 구현되므로 따로 참조할 수 없다. +- 따라서, 빈을 불러올 때, 프록시를 받지 않고 팩토리 빈을 가져오기 위해서 `&`을 사용한다. +- 팩토리 빈을 가져와서 원하는 타깃 객체를 대입하고 getObject() 메소드를 실행해서 타깃 객체를 바꾼 프록시를 생성한다. +```java +@Test +public void upgradeAllOrNothing() { + TxProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", TxProxyFactoryBean.class); + txProxyFactoryBean.setTarget(testUserService); + UserService txUserService = (UserService) txProxyFactoryBean.getObject(); +} +``` + +--- +## 6.3.5 프록시 팩토리 빈 방식의 장점과 한계 +### 프록시 팩토리 빈의 재사용 +- CoreService 클래스의 메소드에 트랜잭션을 적용해야 하는 경우 +```java +@Bean +public CoreService coreServiceImpl(CoreDao coreDao) { + return new CoreService(coreDao); +} + +@Bean(name = "coreService") +public TxProxyFactoryBean txProxyFactoryBean(CoreService coreServiceImpl, PlatformTransactionManager transactionManager) { + TxProxyFactoryBean txProxyFactoryBean = new TxProxyFactoryBean(); + txProxyFactoryBean.setTarget(coreServiceImpl); + txProxyFactoryBean.setTransactionManager(transactionManager); + txProxyFactoryBean.setServiceInterface(CoreService.class); + return txProxyFactoryBean; +} +``` + +### 프록시 팩토리 빈 방식의 장점 +- 두가지 문제점 해결 +1. 인터페이스를 하나하나 구현할 필요없이 하나의 팩토리 빈을 재사용 +2. 리플랙션을 사용해서 모든 메소드를 동적으로 접근함으로써 부가기능 코드의 중복 삭제 + +### 프록시 팩토리 빈 방식의 한계 +1. 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 것은 불가능 + - 타깃과 인터페이스만 다른, 거의 비슷한 설정이 자꾸 반복됨 +2. InvocationHandler 오브젝트의 중복 생성 + + diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.4 \354\212\244\355\224\204\353\247\201\354\235\230 \355\224\204\353\241\235\354\213\234 \355\214\251\355\206\240\353\246\254 \353\271\210.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.4 \354\212\244\355\224\204\353\247\201\354\235\230 \355\224\204\353\241\235\354\213\234 \355\214\251\355\206\240\353\246\254 \353\271\210.md" new file mode 100644 index 0000000..5d8bc86 --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.4 \354\212\244\355\224\204\353\247\201\354\235\230 \355\224\204\353\241\235\354\213\234 \355\214\251\355\206\240\353\246\254 \353\271\210.md" @@ -0,0 +1,180 @@ +6.4 스프링의 프록시 팩토리 빈 += + +## 6.4.1 ProxyFactoryBean +- 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈 +- 순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해 줄 부가기능은 별도의 빈에 둔다. + - 기존의 팩토리 빈은 팩토리 빈에 InvocationHandler 가 부가기능 제공을 담당했고, InvocationHandler는 팩토리 빈에 의존적으로 생성됐었음(타겟 정보가 필요하기 때문에) +- 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다. + - InvocationHandler의 invoke() 메소드와 달리 MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보를 받는다. + - 이 때문에 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. + - 다른 여러 프록시에서 재사용할 수 있다. + - 싱글톤 빈으로 등록 가능하다. + +```java +public class DynamicProxyTest { + @Test + public void proxyFactoryBean() { + ProxyFactoryBean pfBean = new ProxyFactoryBean(); + pfBean.setTarget(new HelloTarget()); + pfBean.addAdvice(new UppercaseAdvise()); + + Hello proxiedHello = (Hello) pfBean.getObject(); + ... + } + static class UppercaseAdvice implements MethodInterceptor { + public Object invoke(MethodInvocation invocation) { + String ret = (String) invocation.proceed(); + } + } +} +``` +
+ +### 어드바이스 : 타깃이 필요 없는 순수한 부가기능 +- InvocationHandler 구현체와 달리 MethodInterceptor 구현체에는 타깃 오브젝트가 등장하지 않는다. +- 메소드 정보와 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달된다. + - 팩토리 빈 설정정보로 스프링이 자동으로 생성해서 넣어 줌 + + +
+ +#### MethodInvocation +- 일종의 콜백 오브젝트 +- proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해준다. +- 템플릿/콜백 구조 + - 재사용 가능한 부분(MethodInterceptor) 을 만들어두고 바뀌는 부분(콜백 오브젝트 & 메소드 호출 정보)만 외부에서 주입해서 이를 작업 흐름 중에 사용하도록 함 + - MethodInterceptor 는 타깃에 직접 의존하지 않도록 템플릿 구조로 설계됨 + - MethodInterceptor(Advice) 가 부가기능을 부여하는 중에 타깃 메소드의 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출 + +
+ +#### addAdvice() +- 여러 개의 MethodInterceptor 추가 가능 +- 즉, ProxyFactoryBean 하나로도 여러 부가기능을 제공해주는 프록시 생성 가능 +- Advice : 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트 +- MethodInterceptor 는 Advice 인터페이스를 상속하고 있는 서브 인터페이스들 중 하나 + - 다른 서브 인터페이스로는 BeforeAdvice, AfterReturningAdvice, ThrowsAdvice 등이 있다. + +
+ +#### ProxyFactoryBean의 인터페이스 자동검출 기능 +- 타깃 오브젝트가 구현하고 있는 인터페이스의 정보를 자동 검출해서 프록시 생성 + +
+ +### 포인트컷 : 부가기능 적용 대상 메소드 선정 방법 +- MethodInterceptor 오브젝트는 타깃 정보를 가지지 않으며 때문에 싱글톤 빈으로 등록 가능하다. +- 따라서, 여러 프록시가 공유할 수 있는데 이러한 MethodInterceptor에 특정 프록시에만 적용되는 패턴을 넣는 것은 부적절 + +
+ + + +#### 기존의 InvocationHandler vs MethodInterceptor +![image](https://github.com/user-attachments/assets/c4f47688-ce0b-4ae8-b89d-e74e4f5b241a) +- InvocationHandler 는 타깃과 메소드 선정 알고리즘에 의존적 + - 타깃과 메소드 선정 알고리즘이 다르면 재사용 불가능 + - 특정 타깃을 위한 프록시에 한정됨 + - 따로 빈으로 등록할 필요 없었음, 팩토리 빈 내부에서 매번 생성하도록 설계 + - 따라서, 타깃이나 메소드 선정 알고리즘의 변경이 필요하면 프록시 생성코드를 직접 변경해야 했음 + - OCP 원칙 위반 + +![image](https://github.com/user-attachments/assets/e6815a4a-5fb5-468f-b939-ef557423f0c2) +- ProxyFactoryBean 은 부가기능 추가와 메소드 선정 알고리즘을 분리 + - 부가기능 추가 : Advice + - 메소드 선정 알고리즘 : Pointcut + - Advice와 Pointcut + - 모두 프록시에 DI로 주입되어 사용됨 + - 타깃 정보를 따로 가지고 있지 않기 때문에 여러 프록시에 공유 가능 + - 스프링의 싱글톤 빈으로 등록 가능 + + +
+ +#### 전략 패턴 구조 +- 프록시로부터 어드바이스와 포인트컷을 독립시키고 DI를 사용하게 함 +- 부가기능이나 메소드 선정 알고리즘 변경 시, 설정 정보에서 구현 클래스만 바꾸면 됨 +- OCP를 지킴 + + +#### 포인트컷을 적용한 ProxyFactoryBean +``` java +public void pointcutAdvisor() { + ProxyFactoryBean pfBean = new ProxyFactoryBean(); + pfBean.setTarget(new UserServiceImpl()); + + NameMatchMethodPointcut transactionPointcut = new NameMatchMethodPointcut(); + pointcut.setMappedName("sayH*"); + + TransactionAdvice advice = new TransactionAdvice(); + advice.setTransactionManager(new PlatformTransactionManager()); + + pfBean.addAdvisor(new DefaultPointcutAdvisor(transactionPointcut, advice)); + + UserService proxiedUserService = (UserService) pfBean.getObject(); +} +``` +``` java +public class TransactionAdvice implements MethodInterceptor { + PlatformTransactionManager transactionManager; + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public Object invoke(MethodInvocation invocation) throws Throwable { + TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); + try { + Object ret = invocation.proceed(); + this.transactionManager.commit(status); + return ret; + } catch (RuntimeException e) { + this.transactionManager.rollback(status); + throw e; + } + } +} + +``` +- Advisor : advise + pointcut + +#### Config 설정 + +```java +import java.beans.BeanProperty; + +@Configuration +public class SpringConfig { + + @Bean + public TransactionAdvice transactionAdvice(PlatformTransactionManager transactionManager) { + return new TransactionAdvice(transactionManager); + } + + @Bean + public NameMatchMethodPointcut transactionPointcut() { + NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); + pointcut.setMappedName("upgrade*"); + return pointcut; + } + + @Bean + public DefaultPointcutAdvisor transactionAdvisor(TransactionAdvice advice, NameMatchMethodPointcut pointcut) { + return new DefaultPointcutAdvisor(advice, pointcut); + } + + @Bean + @Primary + public ProxyFactoryBean userService(UserService userServiceImpl, DefaultPointcutAdvisor[] advisorList) { + ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean(); + proxyFactoryBean.setTarget(userServiceImpl); + proxyFactoryBean.setInterceptorNames("transactionAdvisor"); + return proxyFactoryBean; + } +} +``` +- Advisor과 Advice를 혼합해서 사용하기 위해 interceptor 이름으로 주입 +- advice와 pointcut은 재사용이 가능 + +![image](https://github.com/user-attachments/assets/f15876df-34ef-4cc4-9f91-227f500d0288) + diff --git "a/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.5 \354\212\244\355\224\204\353\247\201 AOP.md" "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.5 \354\212\244\355\224\204\353\247\201 AOP.md" new file mode 100644 index 0000000..276661d --- /dev/null +++ "b/\354\230\244\354\210\230\354\247\204/docs/\355\206\240\353\271\204/6\354\236\245_AOP/6.5 \354\212\244\355\224\204\353\247\201 AOP.md" @@ -0,0 +1,146 @@ +6.5 스프링 AOP += +## 자동 프록시 생성 +### 프록시 팩토리 빈 방식의 한계 (2) +1. 부가기능(InvocationHandler) 가 타깃 오브젝트마다 새로 만들어지는 문제 + - ProxyFactoryBean 의 어드바이스를 분리해서 해결 +2. 부가기능의 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해줘야 하는 문제 + - 자동 프록시 생성기를 사용해 해결 + +### 빈 후처리기를 이용한 자동 프록시 생성기 +- 빈 후처리기를 빈으로 등록 +- 스프링은 빈 오프젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다. +- 빈 후처리기의 수행 + - 빈 오프젝트의 프로퍼티 강제 수정 + - 별도의 초기화 작업 수행 + - 빈 오브젝트를 바꿔치기 → 프록시로 포장하고 빈으로 대신 등록 + +
+ +### DefaultAdvisorAutoProxyCreator +- 어드바이저를 이용한 자동 프록시 생성기 +- 빈 오브젝트를 생성할 때마다 동작하는 빈 후처리기 +- 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인 +- 프록시 적용 대상이면 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결 +- 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에게 돌려주고 컨테이너는 프록시를 빈으로 등록하고 사용한다. +- 즉, 포인트컷이 담긴 어드바이저와 빈후처리기만 등록하면, ProxyFactoryBean을 등록하지 않아도 자동으로 프록시 적용 + +### 확장된 포인트컷 +- 포인트컷은 클래스 필터와 메소드 매처 두 가지를 돌려주는 메소드를 가진다. +```java +public interface Pointcut { + ClassFilter getClassFilter(); // 프록시 적용 클래스인지 확인 + MethodMatcher getMethodMatcher(); // 어드바이스 적용 메소드인지 확인 +} +``` +- ProxyFactoryBean 에서 포인트컷을 사용할 때는 타깃이 정해져 있었기 때문에 ClassFilter가 필요없었다. +- DefaultAdvisorAutoProxyCreator 에서는 모든 빈에 대해 프록시 자동 적용 대상을 선별해야 하기 때문에 클래스와 메소드 선정 알고리즘을 모두 가져야 함 + +## 6.5.3 포인트컷 표현식 +- AspectJExpressionPointcut 클래스 사용 +- 포인트컷 표현식을 사용하면 클래스와 메소드 선정 알고리즘을 한번에 지정 가능 +- AspectJ 프레임워크에서 제공하는 것을 가져와 확장한 표현식이라는 의미로 AspectJ 표현식이라고도 부름 + +### 포인트컷 표현식 문법 +- 포인트컷 지시자 중 대표적인 execution() +```java +execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴(타입패턴 | "..", ...)) +``` +- bean() : 빈의 이름으로 비교 +- @annotation() : 애노테이션이 적용된 메소드를 선정 + +### 설정 정보 수정 +```java + @Bean + public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { + return new DefaultAdvisorAutoProxyCreator(); + } + + @Bean + public AspectJExpressionPointcut pointcut() { + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression("execution(* *.userService.*(..))"); + return pointcut; + } + + @Bean + public TransactionAdvice transactionAdvice(PlatformTransactionManager transactionManager) { + return new TransactionAdvice(transactionManager); + } + + + @Bean + public DefaultPointcutAdvisor transactionAdvisor(TransactionAdvice advice, AspectJExpressionPointcut pointcut) { + return new DefaultPointcutAdvisor(advice, pointcut); + } +``` + +## 6.5.4 AOP 란 무엇인가? +### 부가기능의 모듈화 +- 모듈화 : 관심사가 같은 코드를 객체지향 설계 원칙에 따라 분리하고, 한 곳에 모으는 것 +- 트랜잭션 코드는 모듈화가 어려웠다 + - 다른 모듈에 부가적으로 부여되는 기능 + - 타깃 코드 안에 들어있고 독립적으로 존재할 수 없음 +- DI, 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷과 같은 기법을 사용해 부가기능을 Advice로 모듈화 + +### AOP : Aspect-oriented Programming +- 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 특성 +- 부가기능 모듈을 오브젝트와는 다른 이름의 `Aspect`라고 부르기로 함 +- 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 모듈로 만들어 설계하고 개발하는 기법을 AOP(Aspect Oriented Programming) + +## 6.5.5 AOP 적용 기술 +### 프록시를 이용한 AOP +### 바이트코드 생성과 조작을 통한 AOP +- AspectJ 는 프록시를 사용하지 않고 타깃 오브젝트를 수정해서 부가기능을 직접 넣어주는 방식을 사용 +- 컴파일된 타깃 클래스 파일 자체나 JVM 로딩 시점의 바이트코드를 조작하는 방식 +- 소스코드를 수정하지는 않기 때문에 개발자는 비즈니스 로직 코드에 충실한 타깃 객체 개발 +- 장점 + - 스프링과 같은 DI 컨테이너의 도움을 받지 않아도 됨(스프링이 아닌 환경에서도 AOP 적용 가능) + - 강력하고 유연한 AOP 가능 + - 프록시 방식은 부가기능 부여 대상이 메소드로 한정 + - 바이트 코드는 오브젝트 생성, 필드 값의 조회와 조작, private 메소드 호출, 스태틱 초기화 등의 가능 +- 기본적으로 스프링 AOP를 사용하고 추가적으로 AspectJ 사용하는 방식 채택 + - 대부분의 부가 기능은 프록시 방식으로 충분 + - AspectJ 는 JVM 실행 옵션 변경, 별도의 바이트코드 컴파일러, 클래스 로더 설정 등 번거로운 작업이 필요 + +## 6.5.6 AOP 용어 +### 타깃 +- 부가기능을 부여할 대상 +- 핵심 비즈니스 로직을 담은 클래스 or 다른 부가기능을 제공하는 프록시 오브젝트 + +### 어드바이스 +- 타깃에게 제공할 부가기능을 담은 모듈 + +### 조인 포인트 +- 어드바이스가 적용될 수 있는 위치 +- 스프링 AOP에서 조인 포인트는 메소드의 실행 단계 +- 타깃 오브젝트가 구현한 인터페이스의 모든 메소드 + +### 포인트컷 +- 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈 +- 스프링에서 포인트컷은 메소드를 선정하는 기능 + - 메소드의 실행이라는 의미 execution() + +### 프록시 +- 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트 +- DI를 통해 타깃 대신 클라이언트에게 주입됨 + +### 어드바이저 +- 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트(둘의 조합) +- 부가기능을 어디에 전달할 것인가를 알고 있는 *스프링 AOP의 가장 기본이 되는 모듈* +- 스프링에서는 자동 프록시 생성기가 어드바이저를 작업 정보로 활용하여 프록시를 적용 +- 스프링 AOP에서만 사용되는 특별한 용어 + +### 애스팩트 +- AOP의 기본 모듈 +- 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어진다. +- 보통 싱글톤 형태의 오브젝트로 존재 +- 스프링의 어드바이저는 아주 단순한 애스팩트라고 볼 수 있다. + +--- + +### 빈 등록 +- 자동 프록시 생성기 +- 어드바이스 +- 포인트컷 +- 어드바이저