본문 바로가기
책 정리/토비의 스프링

6.1. AOP

by 이석준석이 2021. 2. 2.

1. 메소드 추출

 

현재 upgradeLevels() 메소드는 트랜잭션으로 감싸져있다.

  • 트랜잭션은 전, 후만 수행되면 되므로 로직 부분은 메소드 추출을 통해 바꿔보자
public void upgradeLevels() {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if(canUpgradeLevel(user)) {
                upgradeLevel(user);
            }
        }
        transactionManager.commit(status);
    } catch (RuntimeException e) {
        transactionManager.rollback(status);
        throw e;
    }
}
public void upgradeLevels() {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        upgradeLevelsInternal();
        transactionManager.commit(status);
    } catch (RuntimeException e) {
        transactionManager.rollback(status);
        throw e;
    }
}

2. 인터페이스 도입 및 트랜잭션 전용 구현체 생성

 

기존


변화 1 (인터페이스 적용)

  • 약한 결합을 갖도록 하기


변화 2 (트랜잭션 전용 클래스 적용)

  • 로직을 감싸는 트랜잭션 전용 클래스를 적용하고, 그 내부에서 impl 클래스를 호출하는 방식으로 구현


3. 테스트 개념

 

단위 테스트 : 테스트 대상 클래스를 Mock Object 등으로 의존성을 주입하여 테스트하므로써, 외부의 리소스를 사용하지 않는 테스트

 

통합 테스트 : 외부의 DB / 서비스 리소스가 참여하는 테스트


4. Mockito 프레임워크

 

Mock 객체 생성

  • mock(Class<?>)
UserDao mockUserDao = mock(UserDao.class);

 

Mock 스터빙

  • when(...).thenReturn(...)
when(mockUserDao.getAll()).thenReturn(this.users);

 

Mock 을 통한 메소드 호출 확인

// mockUserDao 의 update 함수가 User.class 타입으로 2회 수행된 적이 있는지 검증
verify(mockUserDao, times(2)).update(any(User.class));

// mockUserDao 의 update 함수에 해당 파라미터를 통해 수행된 적이 있는지 검증
verify(mockUserDao).update(users.get(1));

5. 프록시

 

현재 구조


프록시 구조


프록시란?

  • 클라이언트가 사용하는 대상을 실제대상인것처럼 위장해서 클라이언트의 요청을 받아주는 대리자같은 역할을 한다.

사용목적

  • 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서
  • 타깃에 부가적인 기능을 제공하기 위해서

6. 데코레이터 패턴

 

런타임 시에 다이나믹하게 기능을 부여해주기 위해서 프록시를 사용하는 패턴을 의미한다.

  • 생성자 혹은 Setter를 이용하여 다이나믹하게 대상을 주입한다.
  • 타깃의 코드는 손대지않고 클라이언트가 호출하는 방법도 변경하지 않고 기능을 추가할 때 사용한다.

 

가장 많이 사용되는 예) InputStream, OutputStream

InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));

 

UserServiceTx와 UserServiceImpl의 관계도 데코레이터 패턴이 적용된 예로 볼 수 있다.

@Bean
public UserService userService() {
    UserServiceTx userServiceTx = new UserServiceTx();
    userServiceTx.setUserService(userServiceImpl());
    userServiceTx.setTransactionManager(transactionManager());
    return userServiceTx;
}

7. 프록시 패턴

 

타깃에 대한 접근방법을 제어하기 위해 사용한다. (기능추가, 확장은 하지 않는다.)

 

JPA의 Lazy 로딩처럼

  • 클라이언트가 타깃에 대한 Reference 를 요청
  • 실제로 생성하는 것이 아닌 프록시를 반환
  • 클라이언트가 프록시 메소드를 통해 사용하려하면 프록시가 타깃 오브젝트를 생성하고 요청을 위임한다.

 

요약

  데코레이터 패턴 옵저버 패턴
클라이언트와 타깃사이에 추가되는가 Yes Yes
기능을 변경하는가 Yes No
접근방법을 제어하는가 No Yes

프록시는 두가지 기능으로 구성된다.

  • 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
  • 지정된 요청에 대해서는 부가기능을 수행한다.

 

UserServiceTx

  • 위임과 부가작업을 수행한다.
public class UserServiceTx implements UserService {

    // 타깃 오브젝트
    UserService userService;
    PlatformTransactionManager transactionManager;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    // 타깃과 동일한 메소드 구현
    @Override
    public void add(User user) {
        // 위임
        userService.add(user);
    }

    @Override
    public void upgradeLevels() {
        // 부가기능
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // 위임
            userService.upgradeLevels();

            // 부가기능
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

8. 다이나믹 프록시 적용

 

현재 구조

  • Hello를 구현한 Target 클래스 HelloTarget.class
  • Hello를 구현한 부가기능을 주는 프록시 클래스 HelloUpperCase.class

 

문제점

  • 인터페이스의 모든 메소드를 구현하여 override 해야하는 귀찮음
  • 대문자로 변환시키는 기능이 계속 중복되어서 나타남 (중복)

다이나믹 프록시

  • 런타임시프록시 팩토리에 의해서 다이나믹하게 만들어지는 오브젝트이다.
  • Target이 구현한 인터페이스와 동일한 타입으로 만들어진다.

이후 구조

UpperCaseHandler

public class UppercaseHandler implements InvocationHandler {
    Hello target;

    public UppercaseHandler(Hello target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String ret = (String) method.invoke(target, args);
        return ret.toUpperCase();
    }
}

 

ProxyFactory method

  • ClassLoader를 사용해서 클래스를 로딩한다.
  • Hello Interface를 구현한 형태로
  • 메소드가 수행되면 UppercaseHandler 의 invoke() 를 거쳐서 가도록
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
        getClass().getClassLoader(),
        new Class[] {Hello.class},
        new UppercaseHandler(new HelloTarget()));

문제점

  • 빈으로 등록할 방법이 없다.
  • Proxy 클래스의 newProxyInstance() 를 통해서만 빈으로 등록되며, 클래스이름을 알 수 없다.

해결

  • 팩토리빈을 사용하자.
  • 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈.

Message 클래스와 Message Factory Bean

  • 아래의 MessageFactoryBean을 등록하면 Message 타입으로 사용할 수 있다. [getObjectType() 의 빈]
public class Message {

    String text;

    private Message(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
    public static Message newMessage(String text) {
        return new Message(text);
    }
}

@Component(value = "message")
public class MessageFactoryBean implements FactoryBean<Message> {
    String text;

    // DI 받기
    public void setText(String text) {
        this.text = text;
    }

    // 실제 빈으로 사용될 오브젝트를 직접 생성한다.
    @Override
    public Message getObject() throws Exception {
        return Message.newMessage(this.text);
    }

    @Override
    public Class<?> getObjectType() {
        return Message.class;
    }

    // 요청마다 다른 빈을 리턴하므로 false
    @Override
    public boolean isSingleton() {
        return false;
    }
}

@SpringBootTest
@ContextConfiguration(classes = MessageFactoryBean.class)
public class FactoryBeanTest {
    @Autowired
    Message message;

    @Test
    void test() {
        assertTrue(message instanceof Message);
    }
}

9. 스프링의 프록시 팩토리 빈

 

ProxyFactoryBean

  • 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
  • Advice (위에서 사용했던 Handler) 를 빈으로 등록할 수 있다.
    • Advice 는 MethodInterceptor 를 구현한다.
      • MethodInterceptor은 타깃정보에 상관없이 독립적으로 만들 수 있다. <-> InvocationHandler은 타깃정보를 필드에 명시해줌
      • 콜백오브젝트로서, 템플릿으로 동작하기 때문이다. [proceed()]
@Test
void proxyFactoryBean() {
    ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(new HelloTarget());
    pfBean.addAdvice(new UppercaseAdvice());

    Hello proxiedHello = (Hello) pfBean.getObject();
    assertEquals("HELLO TOBY", proxiedHello.sayHello("Toby"));
    assertEquals("HI TOBY", proxiedHello.sayHi("Toby"));
    assertEquals("THANK YOU TOBY", proxiedHello.sayThankYou("Toby"));
}

10. 어드바이스와 포인트컷

 

Advice

  • 타깃 오브젝트에 종속되지 않는, 순수한 부가기능을 담은 오브젝트

PointCut

  • Advice를 적용할 대상 메소드를 선정하는 방법

 

기존의 다이나믹 프록시는 InvocationHandler 에서 

  • 부가기능 추가
  • Pattern을 기반으로 어떤 함수에서는 수행하고 어떤 함수에서는 수행하지 않을 것인지에 대한것에 대한 2가지 기능을 수행했다.

 

ProxyFactoryBean 은

  • 부가기능 추가 -> Advice(MethodInterceptor)
  • 메소드 선정 -> PointCut 으로 수행한다.

ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*");

pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

Hello proxiedHello = (Hello) pfBean.getObject();

 

addAdvisor을 통해서 Pointcut과 Advice를 추가한다.

  • 어떤 Pointcut에 대해서 어느 Advice를 적용할 지를 명시한다.

Advisior = PointCut + Advice

'책 정리 > 토비의 스프링' 카테고리의 다른 글

7. 스프링 핵심 기술의 응용  (0) 2021.02.14
6.2. 스프링 AOP  (0) 2021.02.10
5. 서비스 추상화  (0) 2021.02.01
4. 예외  (0) 2021.01.30
3. 템플릿  (0) 2021.01.23