6장 AOP
6.1 트랜잭션 코드의 분리
AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술의 하나다.
스프링에 적용된 가장 인기 있는 AOP의 적용 대상은 바로 선언적 트랜잭션 기능이다!
서비스 추상화를 통해 많은 근본적인 문제를 해결했던 트랜잭션 경계설정 기능을 AOP를 이용해 바꿔보자.
🍃 메소드 분리
비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치함.
=> 비즈니스 로직을 담당하는 코드를 메소드로 추출해서 독립시키자.
🍃 DI를 이용한 클래스의 분리
DI 적용을 이용한 트랜잭션 분리
UserService를 구현한 또 다른 구현 클래스를 만들자.
UserService를 구현한 UserServiceTx는 트랜잭션 경계설정이라는 책임을 맡고 있을 뿐이다.
UserService 인터페이스 도입
public interface UserService {
void add(User user);
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
UserDao userDao;
MailSender mailSender;
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user: users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
...
}
분리된 트랜잭션 기능
비즈니스 트랜잭션 처리를 담은 UserServiceTx를 만들어보자.
같은 인터페이스를 구현한 다른 오브젝트에게 작업을 위임하게 되면 된다.
그리고 여기에 트랜잭션의 경계설정이라는 부가적인 작업을 부여해보자!
- 트랜잭션이 적용된 UserServiceTx
public class UserServiceTx implements UserService {
UserService userService;
PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager){
this.transactionManager = transactionManager;
}
public void setUserService(UserService userService){
this.userService = userService;
}
public void add(User user){
this.userService.add(user);
}
public void upgradeLevels() {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userService.upgradeLevels();
this.transactionManager.commit(status);
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
트랜잭션 적용을 위한 DI 설정
기존에 userService 빈이 의존하고 있던 transactionManager는 UserServiceTx의 빈이,
userDao와 mailSender는 UserServiceImpl 빈이 각각 의존하도록 프로퍼티 정보를 분리한다.
트랜잭션 경계설정 코드 분리의 장점
- 비즈니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 신경쓰지 않아도 됨.
- 비즈니스 로직에 대한 테스트를 손쉽게 만들어 낼 수 있음.
6.2 고립된 단위 테스트
테스트는 작은 단위로 하면 좋다.
테스트 대상이 다른 오브젝트와 환경에 의존하고 있다면 작은 단위의 테스트가 주는 장점을 얻기 힘들다.
🍃 복잡한 의존관계 속의 테스트
UserService라는 테스트 대상이 테스트 단위인 것처럼 보인다.
하지만, 그 뒤의 의존관계를 따라 등장하는 오브젝트와 서비스, 환경 등이 모두 합쳐서 테스트 대상이 된다.
=> 이 경우 테스트 준비가 힘들고, 환경이 조금이라도 달라지면 동일한 테스트 결과를 내지 못할 수 있다.
🍃 테스트 대상 오브젝트 고립시키기
테스트를 위한 UserServiceImpl 고립
UserServiceImpl 과의 사이에서 주고받은 정보를 저장해뒀다가,
테스트의 검증에 사용할 수 있게 하는 목오브젝트를 만들자.
고립된 단위 테스트 활용
- 테스트 실행중에 UserDao를 통해 가져올 테스트용 정보를 db에 넣는다.
- 메일 발송 여부를 확인하기 위해 MailSender 목 오브젝트를 di 해준다.
- 실제 테스트 대상인 userService 메소드를 실행한다.
- 결과가 db에 반영됐는지 확인하기 위해서 UserDao 를 이용해 db에서 데이터를 가져와 결과를 확인하다.
- 목 오브젝트를 통해 UserService에 의한 메일 발송이 있었는지를 확인하면 된다.
- UserDao 오브젝트
static class MockUserDao implements UserDao{
private List<User> users;
private List<User> updated = new ArrayList();
private MockUserDao(List<User> users) {
this.users = users;
}
public List<User> getUpdated() {
return this.updated;
}
//스텁 기능 제공
public List<User> getAll() {
return this.users;
}
//목 오브젝트 기능 제공
pubilc void update(User user) {
updated.add(user);
}
// 테스트에 사용되지 않는 메소드
...
}
=> DB에 직접 가지 않아도 미리 준비된 테스트용 리스트를 메모리에 갖고 있다가 돌려주는 역할을 MockUserDao가 수행해준다.
테스트 수행 성능의 향상
고립된 테스트를 하면 테스트가 다른 의존 대상에 영향을 받을 경우를 대비해 복잡하게 준비할 필요가 없을 뿐만 아니라, 테스트 수행 성능도 크게 향상됨.
🍃 단위 테스트와 통합 테스트
단위 테스트의 단위는 정하기 나름이다!
중요한 것은 하나의 단위에 초점을 맞춘 테스트라는 점이다.
단위 테스트 : 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트하는 것
통합 테스트 : 두 개 이상의 단위가 결합해서 동작하면서 테스트가 수행되는 것. 스프링의 테스트 컨텍스트 프레임워크를 이용해서 컨텍스트에서 생성되고 DI된 오브젝트를 테스트하는 것도 통합 테스트.
🍃 목 프레임워크
단위 테스트를 만들기 위해서는 스텁이나 목 오브젝트의 사용이 필수적이다.
Mockito 프레임워크
Mockito 와 같은 목 프레임워크의 특징은 목클래스를 일일이 준비해둘 필요가 없다!
- UserDao 인터페이스를 구현한 테스트용 목 오브젝트
UserDao mockUserDao = mock(UserDao.class);
- getAll() 메소드가 불려올 때 사용자 목록을 리턴하도록 스텁 기능 추가
when(mockUserDao.getAll()).thenReturn(this.users);
- mockUserDao의 update() 메소드가 두 번 호출됐는지 검증하는 방법
verify(mockUserDao, times(2)).update(any(User.class));
Mockito 목 오브젝트는 다음의 4단계를 거쳐 사용하면 된다.
- 인터페이스를 이용해 목 오브젝트를 만든다.
- 목 오브젝트가 리턴할 값이 있으면 이를 지정해준다. 메소드가 호출되면 예외를 강제로 던지게 만들 수도 있다.
- 테스트 대상 오브젝트에 DI해서 목 오브젝트가 테스트 중에 사용되도록 만든다.
- 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다.
- Mockito를 적용한 테스트 코드
@Test
public void mockUpgradeLevels() throws Exception {
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 다이내믹한 목 오브젝트 생성 & 메소드의 리턴 값 설정 & DI
UserDao mockUserDao = mock(UserDao.class);
when(mockUserDao.getAll()).thenReturn(this.users);
userServiceImpl.setUserDao(mockUserDao);
//MialSender도 간단히 목오브젝트를 생성하고, DI해준다~!
MailSender mockMailSender = mock(MailSender.class);
userServiceImpl.setMailSender(mockMailSender);
userServiceImpl.upgradeLevels();
//목오브젝트가 제공하는 검증 기능을 통해서 어떤 메소드가 몇 번 호출됐는지, 파라미터는 무엇인지 확인할 수 있다.
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao).update(users.get(1));
assertThat(users.get(1).getLevel(), is(Level.SILVER));
verify(mockUserDao).update(users.get(3));
assertThat(users.get(3).getLevel(), is(Level.GOLD));
ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class);
verify(mockMailSender, times(2)).send(mailMessageArg.capture()); //파라미터를 정밀하게 검사하기 위해 캡쳐할 수도 있다.
List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
assertThat(mailMessages.get(0).getTo()[0], is(users.get(1).getEmail()));
assertThat(mailMessages.get(1).getTo()[0], is(users.get(3).getEmail()));
}
6.3 다이내믹 프록시와 팩토리 빈
🍃 프록시와 프록시 패턴, 데코레이터 패턴
프록시
자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리인과 같은 역할을 한다고 해서 프록시라고 부른다.
타킷 또는 실체
프록시를 통해 최종적으로 요청을위임받아 처리하는 실제 오브젝트
클라이언트 -> 프록시 -> 타깃 (사용하는 구조)
프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것
프록시를 사용목적에 따라 2가지로 구분가능
- 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서
- 타깃에 부가적인 기능을 부여해주기 위해서 => 두가지 모두 대리 오브젝트라는 개념의 프록시를 두고 사용한다는 점은 동일,
but, 목적에 따라서 디자인 패턴에서는 다른 패턴으로 구분한다.
데코레이터 패턴
타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
💥 다이내믹하게 기능을 부가한다?
컴파일 시점, 즉 코드상에서는 어떤 방법과 순서로 타깃이 연결되어 사용되는지 정해져 있지 않다.
데코레이터 패턴 사용 예시
1️. 자바 IO 패키지의 InputStream과 OutputStream 구현 클래스
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
- UserService 인터페이스를 구현한 타깃인 UserServiceImpl에 트랜잭션 부가기능을 제공해주는 UserServiceTx를 추가한 것 => 수정자 메소드를 이용해 데코레이터인 UserServiceTx에 위임할 타깃인 UserServiceImpl을 주입해준다.
데코레이터 패턴은 인터페이스를 통해 위임하는 방식이기 때문에 어느 데코레이터에서 타깃으로 연결될지 코드레벨에선 미리 알 수 없다.
데코레이터 패턴은 타깃의 코드를 손대지 ㅇ낳고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법이다.
프록시 패턴
일반적으로 사용하는 프록시라는 용어와 디자인 패턴에서 말하는 프록시패턴은 구분해야함.
전자 : 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트를 두는 방법을 총칭
후자 : 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
프록시 패턴의 프록시
타깃의 기능을 확장하거나 추가하지 않는다. 대신 클라이언트가 타깃에 접근하는 방식을 변경해준다.
프록시 패턴을 적용하는 경우?
- 타깃 오브젝트에 대한 레퍼런스가 미리 필용할 때.
- 클라이언트에게 타깃에 대한 레퍼런스를 넘겨야하는데, 실제 타깃 오브젝트는 만드는 대신 프록시를 넘겨줌.
- 원격 오브젝트를 이용하는 경우에도 프록시를 사용하면 편함.
- RMI나, EJB , 각종 리모팅 기술을 이용해 다른 서버에 존재하는 오브젝트를 사용해야 한다면 원격 오브젝트에 대한 프록시를 만들어두고 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것처럼 프록시를 사용하게 할 수 있다.
- 특별한 상황에서 타깃에 대한 접근권한을 제어하기 위해 프록시 패턴을 사용할 수 있음
- ex. Collections의 unmodifiableCollection()을 통해 만들어지는 오브젝트가 전형적인 접근권한 제어용 프록시
프록시 패턴 정리
타깃의 기능자체에는 관여하지 않으면서 접근하는 방법을 제어해주는 프록시를 이용하는 것
구조적으로 보자면 프록시와 데코레이터는 유사하다.
하지만, 차이점!!!
- 프록시 : 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다. 생성을 지연하는 프록시라면 구체적인 생성 방법을 알아야 하기 때문에 타깃 클래스에 대한 직접적인 정보를 알아야 한다.
- 물론 프록시 패턴이라고 하더라도 인터페이스를 통해 위임하도록 만들 수도 있다.
앞으로는 타깃과 동일한 인터페이스를 구현하고,
클라이언트와 타깃사이에 존재하면서 기능의 부가 또는 접근 제어를 담당하는 오브젝트를 모두 프록시라고 부르자!!
- 기능의 부가 => 데코레이터 패턴
- 접근 제어 => 프록시 패턴
🍃 다이내믹 프록시
자바에는 java.lang.reflect 패키지 안에 프록시를 손쉽게 만들 수 있도록 지원해주는 클래스들이 있음!
기본적인 아이디어는 목 프레임워크와 비슷하다.
프록시 구성과 프록시 작성의 문제점
프록시의 구성
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
- 지정된 요청에 대해서는 부가기능을 수행한다.
트랜잭션 부가기능을 위해 만든 UserTx는 기능 부가를 위한 프록시!
- UserServiceTx 프록시의 기능 구분
public class UserServiceTx implements UserService{
UserService userService; // 타깃 오브젝트
...
// 메소드 구현과 위임
public void add(User user){
this.userService.add(user);
}
// 메소드 구현
public void upgradeLevels() {
// 부가기능 수행
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
userService.upgradeLevels(); //-> 위임
// 부가기능 수행
this.transactionManager.commit(status);
} catch (RuntimeException e){
this.transactionManager.rollback(status);
throw e;
}
}
}
UserServiceTx 코드는 UserService 인터페이스를 구현하고 타깃으로 요청을 위임하는 트랜잭션 부가기능을 수행하는 코드로 구분할 수 있다!
프록시의 역할 2가지
- 위임
- 부가작업
프록시를 만들기 번거로운 이유 2가지
- 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거로움
- 부가기능 코드가 중복될 가능성이 많다.
✅ 다이내믹 프록시를 통해 첫번째 문제를 해결할 수 있다!
리플렉션
다이내믹 프록시는 리플랙션 기능을 이용해서 프록시를 만들어준다.
리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것이다.
- Method 인터페이스 이용해 메소드 호출
Method lengthMethod = String.class.getMethod("length");
=> 스트링이 가진 메소드 중에서 "length"라는 이름을 갖고 있고, 파라미터는 없는 메소드의 정보를 가져옴
- Method 인터페이스에 정의된 invoke() 메소드 이용해 메소드 실행
public Object invoke(Object obj, Object... args)
invoke() 메소드
메소드를 실행시킬 대상 오브젝트(obj)와 파라미터 목록(args)을 받아서 메소드를 호출한 뒤에 그 결과를 Object 타입으로 돌려줌
int length = lengthMethod.invoke(name); // int length = name.length();
=> 이를 이용해 length()메소드를 다음과 같이 실행할 수 있다.
프록시 클래스
다이내믹 프록시를 이용한 프록시를 만들어보자.
- Hello 인터페이스
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;
}
}
- 클라이언트 역할의 테스트
@Test
public void simpleProxy() {
Hello hello = new HelloTarget(); // 타깃은 인터페이스를 통해 접근하는 습관을 들이자.
assertThat(hello.sayHello("Toby"), is("Hello Toby"));
assertThat(hello.sayHi("Toby"), is("Hi Toby"));
assertThat(hello.sayThankYou("Toby"), is("Thank You Toby"));
}
이제 Hello인터페이스를 구현한 프록시를 만들어보자!
프록시에는 데코레이터 패턴을 적용해서 타깃인 HelloTarget에 부가기능을 추가하겠다.
- 추가할 기능은 리턴하는 문자를 모두 대문자로 바꿔주는 것이다.
- 프록시 클래스
public class HelloUppercase implements Hello{
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();
}
}
- HelloUppercase 프록시 테스트
@Test
public void simpleProxy() {
Hello hello = new HelloUppercase(new HelloTarget()); //프록시를 통해 타깃 오브젝트에 접근하도록 구성
assertThat(hello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(hello.sayHi("Toby"), is("HI TOBY"));
assertThat(hello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
but, 이 프록시는 프록시 적용의 일반적인 문제점 2가지를 모두 가지고 있음
- 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야함.
- 부가기능인 리턴 값을 대문자로 바꾸는 기능이 모든 메소드에 중복되어서 나타남.
다이내믹 프록시 적용
HelloUppercase 를 다이내믹 프록시를 이용해 만들어보자~
- 다이내믹 프록시 : 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트
- 타깃의 인터페이스와 같은 타입으로 만들어짐
- 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용 가능
- 인터페이스 모두 구현해가면서 클래스 정의하지 않아도 됨
- 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야 함
- 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담음
public Object invoke(Object proxy, Method method, Object[] args)
- invoke() 메소드는 리플렉션의 Method 인터페이스를 파라미터로 받음.
- 메소드를 호출할 때 전달되는 파라미터도 args로 받음
- 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것
- 타깃 인터페이스의 모든 메소드 요청이 하나의 메소드로 집중되기 때문에 중복되는 기능을 효과적으로 제공
InvocationHandler 를 통한 요청 처리 구조
- Hello 인터페이스 제공하면서 프록시 팩토리에게 다이내믹 프록시 만들어달라고 요청하면
- Hello 인터페이스의 모든 메소드를 구현한 오브젝트 생성
- InvocationHandler 인터페이스를 구현한 오브젝트를 제공해주면
- 다이내믹 프록시가 받는 모든 요청을 InvocationHandler의 invoke() 메소드로 보내줌. => Hello 인터페이스의 메소드가 아무리 많더라도 invoke() 메소드 하나로 처리 가능!!
다이내믹 프록시로부터 메소드 호출 정보를 받아서 처리하는 InvocationHandler를 만들어보자~
- InvocationHandler 구현 클래스
public class UppercaseHandler implements InvocationHandler {
// 다이내믹 프록시로부터 전달받은 요청을 다시 타깃 오브젝트에 위임해야하므로 타깃 오브젝트 주입받음
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(); // 부가기능 제공
}
}
이제 이 InvocationHandler를 사용하고 Hello 인터페이스를 구현하는 프록시를 만들어보자!
다이내믹 프록시 생성은 Proxy 클래스의 newProxyInstance() 스태틱 팩토리 메소드 이용!!!
- 프록시 생성
Hello proxiedHello = (Hello)Proxy.newProxyInstance( //생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 hELLO타입으로 캐스팅해도 안전하다.
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더
new Class[] { Hello.class }, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임코드를 담은 InvocationHandler
- 파라미터1 => 클래스 로더를 제공
- 파라미터2 => 다미내믹 프록시가 구현해야할 인터페이스
- 파라미터3 => 부가기능과 위임 관련 코드를 담고 있는 InvocationHandler 구현 오브젝트를 제공해야한다.
다이내믹 프록시의 확장
- 확장된 UppercaseHandler
public class UppercaseHandler implements InvocationHandler{
// 어떤 종류의 인터페이스를 구현한 타깃에도 적용 가능하도록 Object 타입으로 수정
Object target;
private UppercaseHandler(Object target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 호출한 메소드의 리턴 타입이 String인 경우만 대문자 변경 기능 적용하도록 수정
Object ret = method.invoke(target, args);
if(ret instanceof String){
return ((String)ret).toUpperCase();
}
else{
return ret;
}
}
}
위의 코드와 같이 리턴타입 등의 정보를 가지고 부가적인 기능을 적용할 메소드를 선택할 수 있다.
🍃 다이내믹 프록시를 이용한 트랜잭션 부가기능
다이내믹 프록시 방식으로 UserServiceTx를 변경해보자.
트랜잭션 InvocationHandler
트랜잭션 부가기능을 가진 핸들러의 코드
@Setter
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private String 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);
}
}
private 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();
}
}
}
TransactionHandler와 다이내믹 프록시를 이용하는 테스트
@Test
public void upgradeAllOrNothing() throws Exception {
...
TransactionHandler txHandler = new TransactionHandler();
//트랜잭션 핸들러가 필요한 정보와 오브젝트를 DI해준다.
txHandler.setTarget(testUserService);
txHandler.setTransactionManager(trnasactionManager);
txHandler.setPattern("upgradeLevels");
//UserService 인터페이스 타입의 다이내믹 프록시 생성
UserService txUserService = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] { UserService.class }, txHandler);
...
}
🍃 다이내믹 프록시를 위한 팩토리 빈
앞에서는 어떤 타깃에도 적용 가능한 트랜잭션 부가기능을 담은 TransactionHandler.
이를 이용하는 다이내믹 프록시를 UserService에 적용하는 테스트를 만듦.
이제 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록 만들자!
스프링은 내부적으로 리플렉션 api를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 빈오브젝트를 생성한다.
문제는 다이내믹 프록시 오브젝트는 이런식으로 프록시 오브젝트가 생성 x.
사전에 프록시 오브젝트의 클래스 정보를 미리 알아내서 스프링의 빈에 정의할 방법이 없음.
팩토리 빈
- 이해를 위해 Message 클래스의 오브젝트를 생성해주는 팩토리 빈 클래스를 만들자
public class MessageFactoryBean implements FactoryBean<Message> {
String text;
public void setText(String text){
this.text = text;
}
public Message getObject() throws Exception {
return Message.newMessage(this.text);
}
public Class<? extends Message> getObjectType(){
return Message.class;
}
public boolean isSigleton(){
return false;
}
}
팩토리빈은 전형적인 팩토리 메소드를 가진 오브젝트다.
스프링은 FactoryBean 인터페이스를 구현한 클래스가 빈의 클래스로 지정되면,
팩토리 빈 클래스의 오브젝트인 getObject() 메소드를 이용해 오브젝트를 가져오고, 이를 빈 오브젝트로 사용한다.
빈의 클래스로 등록된 팩토리 빈은 빈 오브젝트를 생성하는 과정에서만 사용된다.
팩토리 빈의 설정 방법
id와 class 애트리뷰트를 사용해 빈의 아이디와 클래스를 지정
그동안의 빈 설정과 다른 점은 message 빈 오브젝트의 타입이 class 애트리뷰트에 정의된 MessageFactoryBean이 아니라 Message 타입이라는 것.
- 팩토리 빈을 가져오는 기능 테스트
@Test
public void getFactoryBean() throws Exception {
Object factory = context.getBean("&message");
assertThat(factory, is(MessageFactoryBean.class));
}
다이내믹 프록시를 만들어주는 팩토리 빈
팩토리 빈 을 이용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어 줄 수 있다.
- 스프링빈 : 팩토리 빈, UserServiceImpl 만 빈으로 등록
트랜잭션 프록시 팩토리 빈
@Setter
public class TxProxyFactoryBean implements FactoryBean<Object> {
//TransactionHandler를 생성할 때 필요
Object target;
PlatformTransactionManager transactionManager;
String pattern;
Class<?> serviceInterface; //다이내믹 프록시를 생성할 때 필요함
//FactoryBean 인터페이스 구현 메소드
public Object getObject() throws Exception {
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(target);
txHandler.setTransactionManager(trnasactionManager);
txHandler.setPattern(pattern);
return Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] { serviceInterface }, txHandler);
}
public Class<?> getObjectType() {
return serviceInterface;
}
public boolean isSingleton() {
return false; //싱글톤빈이 아니라는 뜻이 아니라 getObejct가 매번 같은 오브젝트를 리턴하지 않는다는 의미
}
}
=> 트랜잭션 처리가 필요한 비즈니스 로직 객체에 다이내믹 프록시를 적용. => 특정 메서드 호출시 자동으로 트랜잭션을 시작, 커밋/롤백 처리하도록 설정하는 팩토리 빈
트랜잭션 프록시 팩토리빈 테스트
@Autowired ApplicationContext context;
@Test
@DirtiesContext
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(userDao);
testUserService.setMailSender(mailSender);
TxProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", TxProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
UserService txUserService = (UserService) txProxyFactoryBean.getObject();
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
txUserService.upgradeLevels();
fail("TestUserServiceException expected");
} catch (TestUserServiceException e){
}
checkLevelUpgraded(users.get(1), false);
}
프록시 팩토리 빈 방식의 장점과 한계
장점
- 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움을 제거 가능
- 코드의 중복 문제도 해결 가능
- 번거로운 다이내믹 프록시 생성 코드도 제거 가능
한계
- 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 것은 불가능하다.
- 한 타깃에 여러 개의 부가기능을 적용하기 문제
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어진다.
'Group Study (2024-2025) > Spring 심화' 카테고리의 다른 글
[ Spring 심화 ] 8주차 - AOP(3) (0) | 2024.12.01 |
---|---|
[ Spring 심화 ] 7주차 - AOP(2) (0) | 2024.12.01 |
[ Spring 심화 ] 5주차 - 서비스 추상화 (2) | 2024.11.06 |
[ Spring 심화 ] 4주차 - 예외처리 (1) | 2024.10.29 |
[ Spring 심화 ] 3주차 - 템플릿 (0) | 2024.10.29 |