[Spring] 토비의 스프링 Vol.1 6장 AOP (상)

Shiba·2023년 9월 12일
0

🍀 스프링 정리

목록 보기
7/21
post-thumbnail

6장은 내용이 많아 두개의 글로 분리
(상편 : AOP , 하편 : 스프링의 AOP)

📗 AOP

❗ 토비의 스프링 3.1 vol 1 정리입니다.
책을 읽지 않으셨다면 이해가 어려울 수 있습니다!

이번 장에서는 스프링의 3대 기술중 하나인 AOP에 대해 알아보도록 하자
- 1편에서는 AOP를 직접 구현해볼 것이다.

📖 트랜잭션 코드의 분리

트랜젝션 코드를 분리하기위해 많은 노력을 했지만, 여전히 경계설정에 관한 코드가 찜찜하게 남아있다. 논리적으로 존재해야하지만, 우리는 깔끔한 코드를 위해 트랜젝션 코드를 분리할 것이다.

📝 메소드 분리

먼저 메소드로 분리해보자

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<User> users = userDao.getAll();
    for(User user : users) {
    	if(canUpgradeLevel(user)){
        	upgradeLevel(user);
        }
    }
}

📝 DI를 통한 클래스의 분리

메소드로 분리하여 한결 깔끔해졌지만 여전히 경계설정 코드가 존재하고 있음. 이를 클래스로 뽑아내어 분리하도록 할 것임

보통은 DI를 통해 하나의 구현 클래스를 선택해서 받는 형태이지만, 꼭 그럴 필요는 없다. 한번에 두개의 구현 클래스를 동시에 사용하여 트랜잭션을 적용시킬 것이다.

//UserService인터페이스 생성
public interface UserService {
	void add(User user);
    void upgradeLevels();
}

//트랜잭션 코드를 제거한 구현클래스 생성
public class UserServiceImpl implements UserService {
	UserDao userDao;
    MailSender mailSender;
    
    private void upgradeLevelsInternal() {
	List<User> users = userDao.getAll();
    for(User user : users) {
    	if(canUpgradeLevel(user)){
        	upgradeLevel(user);
        }
    }
}

//트랜잭션을 가진 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) {
		userService.add(user);
	}

	public void upgradeLevels() {
    	TransactionStatus status = this.transactionManager
        		.getTransaction(new DefaultTransactionDeifinition());
        try {
			userService.upgradeLevels();
            
            this.transactionManager.commit(status);
       } catch (RuntimeException e) {
       		this.transactionManager.rollback(status);
            throw e;
       }
	}
}

📖 고립된 단위 테스트

테스트는 작은 단위로 하는 것이 좋다. 하지만 작은 단위로 테스트하고 싶어도 다른 오브젝트와 환경에 의존하고 있다면 그럴 수 없다.

📝 복잡한 의존관계 속의 테스트

UserService를 테스트 스텁, 목 오브젝트 등을 사용하여 분리하지 않았을 때, 테스트가 한번 실행될 때마다 아래의 모든 요소들이 동작해야했다.

  세 가지의 의존관계를 갖는 오브젝트들 또한 자신의 코드만 실행하는 것이 아닌, 또 다른 요소들에 의존하고 있으므로 테스트 한번에 많은 오브젝트, 서버, 네트워크까지 테스트하고 있는 셈이다. 이렇게 되면 오류가 발생했을 때, 무엇이 오류를 발생시키는지 발견하는데 불필요한 시간을 낭비해야할 수도 있다.
  따라서, 우리는 이러한 문제들을 해결하기 위해 5장에서 DummyMailSender라는 테스트 스텁과 MockMailSender라는 목 오브젝트를 사용해보았던 것이다. 이를 UserDao에도 적용해볼 것이다.

  이미 트랜잭션 코드를 독립시켰기 때문에 고립된 테스트가 가능하도록 UserService를 재구성해보면 다음과 같은 구조가 될 것이다.

UserDao는 upgradeLevels()의 테스트 결과를 검증하기위해 목 오브젝트로 만들었다.
UserServiceImpl의 upgradeLevels() 메소드는 리턴값이 없으므로 UserDao의 update()메소드 호출을 확인하여 결과에 반영하도록 하였다.

위의 고립된 단위 테스트 방법을 직접 적용해보자

@Test
public void upgradeLevels() throws Exception {
	userDao.deleteAll();
    for(User user : users) userDao.add(user); //DB데이터 받기
    
    MockMailSender mockMailSender = new MockMailSender();
    userServiceImpl.setMailSender(mockMailSender); // 목 오브젝트 DI
    
    userService.upgradeLevels();
    
    //UserDao를 이용해 DB반영 확인
    checkLevelUpgraded(users.get(0), false);
    checkLevelUpgraded(users.get(1), true);
    checkLevelUpgraded(users.get(2), false);
    checkLevelUpgraded(users.get(3), true);
    checkLevelUpgraded(users.get(4), false);
    
    //목 오브젝트를 통한 메일발송 요청 확인
    List<String> request = mockMailSender.getRequests(); //요청왔는지 확인
    assertThat(request.size(), is(2));
    assertThat(request.get(0), is(users.get(1), getEmail()));
    assertThat(request.get(1), is(users.get(3), getEmail()));
}

테스트는 다섯 단계의 작업으로 구성된다


1. 테스트 실행 중에 UserDao를 통해 가져올 테스트용 정보를 DB에 넣음
2. 메일 발송 여부를 확인하기 위해 MailSender 목 오브젝트를 DI해줌
3. 실제 테스트 대상userService의 메소드를 실행
4. UserDao를 이용해 DB에서 결과를 가져와 확인
5. 목 오브젝트를 통해 UserService에 의한 메일 발송이 있었는지 확인

◼ UserDao 목 오브젝트

UserDao를 의존하고 있는 다른 테스트 방식도 목 오브젝트를 만들어 적용해보자.

//업그레이드 후보가 되는 사용자 목록을 가져오는 getAll()은 리턴값이 없으므로 빈 메소드로 만듦
//update()는 업그레이드를 통해 DB에 반영이 되어야 하므로 목 오브젝트로 만듦

// 위 두가지 형태로 동작하는 테스트 대역 MockUserDao를 생성
static class MockUserDao implements UserDao {
	private List<User> user;
    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;
    }
    
    //목 오브젝트 기능 제공
    public void update(User user) {
    	updated.add(user);
    }
    
    //테스트에 사용하지않지만 구현해야하는 클래스들
    public void add(User user) { throw new UnsupportedOperationException(); }
    public void deleteAll() { throw new UnsupportedOperationException(); }
    public User get(String id) { throw new UnsupportedOperationException(); }
    public int getCount() { throw new UnsupportedOperationException(); }
}
//MockUserDao를 사용해서 만든 고립된 테스트
@Test
public void upgradeLevels() throws Exception {
	UserServiceImpl userServiceImpl = new UserServiceImpl();
    
    MockUserDao mockUserDao = new MockUserDao(this.users);
    userServiceImpl.setUserDao(mockUserDao); //수동 DI
    
    MockMailSender mockMailSender = new MockMailSender();
    userServiceImpl.setMailSender(mockMailSender); //DI
    
    userServiceImpl.upgradeLevels(); // 레벨업그레이드 실행
    
    //update()확인
    List<User> updated = mockUserDao.getUpdated();
    assertThat(updated.size(), is(2));
    checkUserAndLevel(updated.get(0), "joytouch", Level.SILVER);
    checkUserAndLevel(updated.get(1), "madnite1", Level.GOLD);
    
    //메일 발송 확인
    List<String> request = mockMailSender.getRequests();
    assertThat(request.size(), is(2));
    assertThat(request.get(0), is(users.get(1).getEmail()));
    assertThat(request.get(1), is(users.get(3).getEmail()));
} 

private void checkUserAndLevel(User updated, String expectedId, 
		Level expectedLevel) {
	assertThat(updated.getId(), is(expectedId));
    assertThat(updated.getLevel(), is(expectedLevel));
}

📝 단위테스트와 통합테스트

단위가 사람마다 기준이 달라 경계가 애매모호함. 책에서는 다음과 같이 명시

  • 단위 테스트 : 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트 하는 것

  • 통합 테스트 : 두 개 이상의 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나, 또는 외부의 DB나 파일, 서비스 등의 리소스가 참여하는 테스트

📝 목 프레임워크

단위 테스트가 많은 장점이 존재하지만 이를 위해 번거롭게 목 오브젝트를 작성해주어야 한다. - 이를 편리하게 작성하도록 도와주는 다양한 목 오브젝트 지원 프레임워크가 있다.

◼ Mockito 프레임워크

목 오브젝트를 위해 목 클래스를 일일히 준비해둘 필요없이 간단한 메소드 호출만으로 특정 인터페이스를 구현한 테스트용 목 오브젝트를 만들 수 있다.

// Mockito 프레임워크는 다음과 같이 스태틱메소드를 한 번 호출해주면 만들어진다.
UserDao mockUserDao = mock(UserDao.class);

// 사용자 목록을 리턴하도록 스텁 기능을 추가하기 위해 다음과 같이 코드를 작성하면 된다
when(mockUserDao.getAll()).thenReturn(this.users);

// 테스트 진행중 메소드가 호출되었는지 확인하고 싶다면 다음과 같은 검증 코드를 넣어주면 된다
verify(mockUserDao, times(2)).update(any(User.class)); //2는 예상 호출 횟수

Mockito 프레임워크를 이용해 upgradeLevels() 테스트 코드를 다시 작성해보자

@Test
public void mockUpgradeLevels() throws Exception {
	UserServiceImpl userServiceImpl = new UserServiceImpl();
    
    //Mockito 프레임워크를 이용해 오브젝트 생성 후 사용자목록 리턴 테스트 스텁 생성
    UserDao mockUserDao = mock(UserDao.class); //오브젝트 생성
    when(mockUserDao.getAll()).thenReturn(this.users);//테스트 스텁 생성
    userServiceImpl.setUserDao(mockUserDao): //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()));
}

📖 다이내믹 프록시와 팩토리 빈

📝 프록시와 프록시 패턴, 데코레이터 패턴

앞에서 트랜잭션 코드와 비즈니스 로직을 분리했던 기법을 다시 검토해보도록 하자.
- 부가기능과 핵심기능을 분리하여 각각 클래스로 생성
- 부가기능 외의 나머지 모든 기능은 핵심기능을 가진 클래스로 위임.
이때, 핵심기능은 부가기능을 가진 클래스의 존재를 모름(의존하지 않는 상태)

◼ 프록시와 타깃

위 그림에서 부가기능은 자신이 마치 핵심기능을 가진 클래스인 것처럼 꾸며서, 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 한다.

  • 프록시(Proxy) : 자신이 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것
  • 타깃(Target), 실체(Read Subject) : 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트

◼ 프록시의 특징과 사용목적

  • 특징
    • 타깃과 같은 인터페이스를 구현
    • 프록시가 타깃을 제어할 수 있는 위치에 있음
  • 사용목적
    • 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서 - 프록시 패턴
    • 타깃에 부가적인 기능을 부여해주기 위해서 - 데코레이터 패턴

◼ 데코레이터 패턴

타깃부가적인 기능런타임 시 다이나믹하게 부여해주기 위해 프록시를 사용하는 패턴
- 어떤 방법과 순서프록시와 타깃이 연결되어 사용되는지 정해져있지 않음
- 프록시가 꼭 한 개로 제한되지않고 직접 타깃을 사용하도록 고정시킬 필요도 없음
  - 프록시를 단계적으로 위임하는 구조를 만들어 여러 개의 부가기능 사용 가능

타깃의 코드와 클라이언트가 호출하는 방법을 변경하지 않고 새로운 기능을 추가 가능

◼ 프록시 패턴

프록시와 프록시 패턴은 구분할 필요가 있다!

  • 프록시 : 클라이언트와 사용 대상 사이 대리 역할을 맡은 오브젝트를 두는 방법
  • 프록시 패턴 : 타깃에 대한 접근방법을 제어하려는 목적을 가진 경우

타깃 오브젝트를 생성하기가 복잡하거나 당장 필요하지 않은 경우, 꼭 필요한 시점까지 생성하지 않는 편이 좋은데, 이때 프록시 패턴을 적용하여 실제 타깃 오브젝트 대신 프록시를 넘겨주는 것
- 타깃의 기능 자체에는 관여하지 않으면서 접근 방식을 제어

📝 다이나믹 프록시

프록시의 기능은 좋으나 부가기능을 추가하고 싶을 때 마다 프록시를 만드는건 상당히 번거롭다. (마치 목 오브젝트를 위해 목 클래스를 만들어야 하는 것)
- 자바에는 프록시를 손쉽게 만들 수 있도록 지원해주는 클래스가 존재

◼ 프록시 작성의 문제점

먼저 다음 프록시 코드를 보자

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;
        }
    }
}

위 코드에서 우리는 다음과 같은 문제점을 발견할 수 있다

  • 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거로움
    - 메소드가 많을수록 더욱 번거로운 작업이 될 것
  • 부가기능 코드가 중복될 가능성이 높음


    => 이러한 문제를 해결하기위해 다이나믹 프록시를 사용하는 것!

◼ 리플렉션

자바의 코드 자체를 추상화해서 접근하도록 만든 것
- 리플렉션 기능을 이용다이나믹 프록시를 만듦
- '클래스이름.class' 혹은 getClass()를 호출하여 Class타입의 오브젝트를 가져옴

Method lengthMethod = String.class.getMethod("length");
// String이 가진 메소드중 "length"라는 이름을 가진 파라미터가 없는 메소드의 정보를 가져옴

//invoke()로 메소드를 실행시킬 수 있음
int length = lengthMethod.invoke(name); //int length = name.length();

리플렉션 학습 테스트

package springbook.learningtest.jdk;
...
public class ReflectionTest {
	@Test
    public void invokeMethod() throws Exception {
    	String name = "Spring";
        
        //length()
        assertThat(name.length(), is(6));
        
        //String클래스에서 "length"라는 이름을 가진 파라미터가 없는 메소드의 정보를 가져옴
        Method lengthMethod = String.class.getMethod("length");
        //lengthMethod.invoke(name) == name.length()
        assertThat((Integer)lengthMethod.invoke(name), is(6));
        
        //charAt()
        assertThat(name.charAt(0), is('S'));
        
        //String클래스에서 "charAt"이라는 이름을 가진 int타입 변수를 파라미터로 가지는 메소드를 가져옴
        Method charAtMethod = String.class.getMethod("charAt",int.class);
        //(Character)charAtMethod.invoke(name, 0) == name.charAt(0)
        assertThat((Character)charAtMethod.invoke(name, 0), is('S'));
    }
}

이제 프록시를 만들어보자 먼저 간단한 타깃클래스와 인터페이스를 정의하자

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;
    }
}

Hello 인터페이스를 통해 HelloTarget 오브젝트를 사용하는 클라이언트 역할을 하는 간단한 테스트를 만들어보자

@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"));
}

이제 인터페이스를 구현한 프록시를 만들어보자

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 프록시 테스트
HelloporxiedHello = new HelloUppercase(new HelloTarget());
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby") is("THANK YOU TOBY"));

이제 위의 프록시를 다이나믹 프록시를 이용해 만들어보자
먼저, 다이나믹 프록시는 아래의 구조로 동작한다

  • 다이나믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어짐
  • 부가기능 제공 코드프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다.
    - InvocationHandler는 우리가 위에서 사용해본 invoke()만 가진 인터페이스
  • 프록시 팩토리에 프록시 요청 -> 프록시 생성 -> 프록시 처리요청 -> 타깃에 위임(invoke()로 타깃의 기능 사용) -> 결과 리턴

다이나믹 프록시 생성

//InvocationHandlder 구현 클래스
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(); //부가기능 제공
    }
}

//프록시 생성
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
		getClass().getClassLoader(), //동적으로 생성되는 다이나믹 프록시 클래스의 로딩에 사용할 클래스 로더
        new Class[] { Hello.class }, //구현할 인터페이스
        new UppercaseHandler(new HelloTarget())); //부가기능과 위임코드를 담은 InvocationHandler

작성한 코드를 보면 앞서만든 프록시 클래스보다 코드의 양도 줄지 않은 것 같고....
오히려 더 복잡하게 보인다. 다이나믹 프록시를 굳이 만들어야할 필요가 있을까?

◼ 다이나믹 프록시의 확장

Hello 인터페이스의 메소드가 3개가 아닌 30개로 늘어나면 어떻게 될까?

  • 기존의 클래스로 직접 구현한 프록시는 코드를 그만큼 추가해야한다
  • 다이나믹 프록시로 생성하는 코드수정이 필요가 없다!
    - invoke()에서 모두 처리를 하기 때문


    지금까지의 메소드와 다른 리턴타입을 갖는 메소드가 생성된다면 어떨까?
  • 기존의 클래스는 강제로 캐스팅을 하여 메소드를 처리하므로 런타임 시 캐스팅 오류 발생
  • 다이나믹 프록시리턴타입을 확인해서 받을 수 있음
    - InvocationHandler는 타깃의 종류와 상관없이 적용이 가능

타입에 상관없이 처리하는 다이나믹 프록시로 확장

//InvocationHandlder 구현 클래스
public class UppercaseHandler implements InvocationHandler {
	Object target; //모든 타입을 받을 수 있도록 Object로 선언
    
    private 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) { //String인 경우에만 대문자로 변경하도록 수정
        	return ((String)ret).toUpperCase();
        }
        else {
        	return ret;
        }
    }
}

📝 다이나믹 프록시를 이용한 트랜잭션 부가기능

이제 UserServiceTx를 다이나믹 프록시 방식으로 변경해보자

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 
    		tranactionManager) {
    	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().startWith(pattern)) { //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();
        }
    }
}

//다이나믹 프록시를 이용한 트랜잭션 테스트
@Test
public void upgradeAllOrNothing() throws Exception {
	...
    TransactionHandler txHandler = new TransactionHandler();
    
    //트랜잭션 핸들러가 필요한 정보와 오브젝트 DI
    txHandler.setTarget(testUserService);
    txHandler.setTransactionManager(transactionManager);
    txHandler.setPattern("upgradeLevels");
    
    //UserService 인터페이스 타입의 다이나믹 프록시 생성
    UserService txUserService = (UserService)Proxy.newProxyInstance(
    	getClass().getClassLoader().new Class[] { UserService.class }, txHandler);
    
}

📝 다이나믹 프록시를 위한 팩토리 빈

다이나믹 프록시는 빈으로 등록할 방법이 없음 -> DI불가
=> 팩토리 빈을 통해 다이나믹 프록시를 스프링 빈으로 등록해보자!

◼ 팩토리 빈

스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈

//팩토리 빈 인터페이스
package org.springframework.beans.factory;

public interface FactoryBean<T> {
	T getObject() throws Exception; // 빈 오브젝트 생성후 반환
    Class<? extends T> getObjectType(); //생성되는 오브젝트 타입을 알려줌
    boolean isSingleton(); //getObject()가 반환하는 오브젝트가 항상 같은 싱글톤인지 확인
}

팩토리 빈을 이용하여 다이나믹 프록시를 빈으로 등록해 사용해보자
스프링 빈에는 팩토리 빈과 UserServiceImpl만 빈으로 등록한다. 팩토리 빈은 다이나믹 프록시와 함께 생성할 TransactionHandler에 타깃을 전달해주어야 하기 때문에 다이나믹 프록시가 위임할 타깃인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI받아두어야 한다.

위의 구조대로 코드를 작성해보도록하자

package springbook.user.service;

public class TxProxyFactoryBean implements FactoryBean<Object> {
	Object target;
    PlatformTransactionManager transactionManager;
    String pattern;
    //다이나믹 프록시를 생성할 때 필요. UserService 외의 인터페이스를 가진 타깃에도 적용가능
    Class<?> serviceInterface; 
    
    public void setTransactionManager(PlatformTransactionManager
    		transactionManager) {
    	this.tranactionManager = transactionManager;        
    }
    
    public void setPattern(String pattern) {
    	this.pattern = pattern;
    }
    
    //Factory Bean
    public Object getObject() throws Exception { //DI받은 정보로 다이나믹 프록시 생성
    	TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTarget(target);
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern(pattern);
        return Proxy.newProxyInstance(
        	getClass().getClassLoader(),new Class[] { serviceInterface },
            txHandler);
    }
    
    //팩토리 빈이 생성하는 오브젝트 타입은 DI받는 인터페이스 타입에 따라 변함
    //재사용이 가능
    public Class<?> getObjectType() {
    	return serviceInterface;
    }
    
    
    public boolean isSingleton() {
    	return false; //getObject()가 매번 같은 오브젝트를 리턴하지 않음
    }
}

📝 프록시 팩토리 빈 방식의 장점과 한계

지금까지 적용했던 방법의 장점을 정리해보고 한계점도 생각해보도록하자

◼ 프록시 팩토리 빈의 재사용

TxProxyFactoryBean코드의 수정없이 다양한 클래스에 적용가능
- 타깃 인터페이스의 타입을 빈이 따라가는 형태로 되어있기 때문

◼ 프록시 팩토리 빈의 장점

앞서 살펴본 기존의 프록시 생성 시 문제점 두가지를 모두 해결해줌

  • 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움이 사라진다.
  • 부가기능의 코드 중복문제가 해소된다.
    - 하나의 핸들러 메소드를 구현하는 것으로 수많은 메소드에 부가기능 부여가능
    - DI까지 더한다면, 다이나믹 프록시 생성 코드도 삭제 가능

◼ 프록시 팩토리 빈의 한계

  • 한 번에 여러 개의 클래스공통적인 부가기능을 제공하는 것은 불가능
  • 하나의 타깃여러 개의 부가기능을 적용하려고 할 때도 문제
  • TransactionHandler프록시 팩토리 빈 개수만큼 만들어짐
profile
모르는 것 정리하기

0개의 댓글