TIL - 0620

AOP

  • 핵심 로직과 부가 기능을 분리하기 : 중복 제거도 되면서 핵심 로직 개발에 더 집중할 수 있도록
    • 완전한 분리 : 핵심 로직 부분에서 부가 기능이 적용되는지 모르도록(의존하지않도록 - 의존 관계 설정해두면 로직에서 사용되어야 부가기능이 적용됨)

분리 1단계 : 메소드 분리

@Service("userService")
public class UserService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Autowired
    private UserRepository userRepository;

    public void upgradeLevels() throws Exception {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            upgradeLevelsInternal(userRepository.findAll());
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }

    private void upgradeLevelsInternal(List<User> users) {
        for (User user : users) {
            user.upgradeLevel();
        }
    }
}
  • 단순히 메소드만 분리해냄
  • 비즈니스 로직과 트랜잭션 경계(필요하지만 핵심은 아닌) 코드가 혼재되어있음
    • 다른 곳에서 트랜잭션이 필요할 때 중복으로 구현해야함
    • 핵심 로직에만 관심을 가질 수가 없음
  • 클라이언트는 UserService를 사용함 : UserService의 변경사항이 생겼을 때 구체적인 클래스를 사용하다보니 클라이언트도 영향을 받음
    1. UserService를 인터페이스로 만들고, 구현체를 바꿔끼워가면서 사용하도록 코드 개선하는 방법
    2. UserService를 인터페이스로 만들고, 기본 구현 클래스와 추가된 기능을 구현한 클래스를 추가하는 형태로 코드 개선을 하는 방법

분리 2단계 : 프록시 객체 만들기 - 디자인 패턴으로 해결

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    public void upgradeLevels() throws Exception {
        for (User user : userRepository.findAll()) {
            user.upgradeLevel();
        }
    }
}

public class UserServiceTx implements UserService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    private UserService userService;

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

    @Override
    public void upgradeLevels() throws Exception {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            userService.upgradeLevels();
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
}
  • 클라이언트는 UserServiceTx에 의존하고, UserServiceTx는 실제로 요청을 UserServiceImpl에 위임(핵심 로직)
  • 디자인패턴(데코레이터 패턴)을 통해서 핵심로직과 부가 로직을 분리해냄 : 대리 객체(프록시)
    • 전략 패턴을 사용하지않는 이유 : 단순 확장성을 고려한다면 전략 패턴을 사용하면 되지만, 전략을 사용한다는 사싫은 남아있기때문에 개발자가 개발할 때 고려해야함, 진짜 목표는 핵심 로직과 부가 로직을 완벽하게 분리해내서 개발하는 방법을 찾는 것(개발자가 핵심 로직 구현에만 집중할 수 있도록)
    • 데코레이터 패턴 : 꾸며주는(기능 추가) 객체가 있고(프록시 객체), 핵심 로직을 담당하는 객체가 있음(타겟 객체)
  • 단점이라면 UserService 메소드가 많아졌을 때 모두 구현해두고 실제 로직을 가진 객체에 위임하는 코드를 모두 작성해줘야함
    • 변경이 생겼을 때 프록시 객체에도 변경이 생겨버리는 단점이 있음
  • 추가 지식 : 프록시 페턴 - 위의 프록시와는 다름, 기능을 추가(확장)하지 않음, 클라이언트가 대상 객체를 사용할 때 접근 방식을 제어
    • 예1 : unmodifiableCollection - 콜렉션에 대해 어떠한 수정 작업을 하지못하도록 막는 프록시 객체
    • 예2 : lazy-loading을 위해 겉면만 감싸둔 객체 - 꼭 필요할 때 진짜 로직을 가진 객체의 메소드를 실행시켜서 loading
  • 프록시 : 타겟 객체의 기능 추가 및 확장, 접근 제어를 할 때 사용되는 객체를 총칭
  • 프록시의 단점
    1. 각각의 타입(인터페이스)마다 프록시를 만들어서 사용해야함(오히려 더 많은 변경점이 생겨버린 것일지도)
    2. 해당 타입(인터페이스)가 구현해야할 메소드가 많아지면 그만큼 핵심 로직 객체에 위임하는 코드를 작성해야할 일이 많이 생김 : 인터페이스의 변경이 프록시 객체에도 영향이 생김(같은 타입을 구현하고 있는 것이기때문에)

분리 3단계 : API 사용해서 다이내믹 프록시 객체(JDK API 사용) 만들기

public class TransactionHandler implements InvocationHandler {
    
    private Object target;
    private PlatformTransactionManager transactionManager;
    private String pattern;
    
    public TransactionHandler setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
        return this;
    }

    public TransactionHandler setTarget(Object target) {
        this.target = target;
        return this;
    }

    public TransactionHandler setPattern(String pattern) {
        this.pattern = pattern;
        return this;
    }
        
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /* 메소드 이름 패턴 매칭 시켜봤을 때 프록시 적용이 되어야하는 메소드라면 트랜잭션 처리 기능을 덧붙여줌 */
        if (method.getName().startWith(pattern)) {
            return applyTransaction(method, args);
        }        
        return method.invoke(target, args);
    }
    
    private Object applyTransaction(Method method, Object[] args) throws Throwable {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        
        try {
            Object result = method.invoke(target, args);
            transactionManager.commit(status);
            return result;
        } catch (InvocationTargetException e) {
            transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

@Test
public void apply_tx_handler() {
    TransactionHandler txHandler = new TransactionManager().setTarget(userService).setTransactionManager(transactionManager).setPattern("input_method_name");
    UserService proxiedUserService = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{UserService.class}, txHandler);
    proxiedUserService.upgradeLevels();
}
  • 이름에서부터 프록시 객체의 단점을 채우기위함 : 공통적으로 사용할 수 있는 프록시를 만들겠다는 것
    • 프록시의 역할은 그대로 : 부가 기능을 덧씌우되, 핵심 기능은 핵심 로직을 담은 객체에 위임
  • 스태틱 팩토리 메소드 - 다이내믹 프록시 생성: Proxy.newInstance(ClassLoader, interfaces, InvocationHandler)
    • InvocationHandler : 프록시 기능(부가 기능 - 덧붙이는 의미)을 가진 객체, 인터페이스이므로 구현해야함(원하는 기능에 맞게)
    • 내부적으로 리플렉션 API를 사용함 : API 문서
    • 스태틱 팩토리 메소드를 통해서 만들어진(프록시가 적용된 타겟) 객체를 다이내믹 프록시(객체)라고 함
  • 디자인 패턴을 통해서 프록시와 타겟을 구분 구현하던 것과는 달리 확장성이 생김 : 타겟 마다 프록시를 만들 필요가 없음
    • 런타임 시에 다이내믹 프록시를 만들어주면 됨 : 원하는 핸들러를 만들어뒀다가 핸들러와 타겟을 결합시킨 다이내믹 프록시를 만들면 됨
  • 문제점 : 다이내믹 프록시를 만들려면 Proxy.newInstance()를 사용해야하는데, 스프링은 빈으로 설정해두면 해당클래스.newInstance() 기본생성자로 생성 후 상태 설정
    • 스프링에서 스프링빈을 초기화하는 방식과는 다르기때문에 자동으로 생성되는 어노테이션으로는 다이내믹 프록시를 만들 수 없음
    • 해결책 : 스프링에서는 팩토리빈 인터페이스를 제공함 - 수동으로 빈을 생성할 수 있는 팩토리빈 만드는 표준 방법을 제공함, 팩토리빈을 구현하고 빈으로 등록해야함

스프링에서 다이내믹 프록시(스프링에서 JDK API 사용) 만들기 - 팩토리빈

스프링빈을 수동으로 만들어서 등록해줘야할 때 - Factory 구현

/* 생성 대상 */
public class Message {
    private 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> {

    private String text;

    @Override
    public Message getObject() throws Exception {
        return Message.newMessage(text);
    }

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

    @Override
    public boolean isSingleton() {
        return true;
    }
}

/* 테스트로 확인하기 */
@Autowired
private ApplicationContext context;

@Test
public void testFactoryBean() {
    Object messageBean = context.getBean("message");
    Object messageFactoryBean = context.getBean("&message);
}
  • 팩토리빈 : 스프링을 대신해서 오브젝트를 생성하는 로직을 가지는 빈, **팩토리빈을 만들고(빈으로 등록) getObject를 구현해두면 getBean으로 생성 **
    • 3개 메소드를 가짐 : 인스턴스 생성, 생성되는 인스턴스의 타입, 싱글톤 관리하고 있는지 여부
  • 팩토리빈을 통해서 빈이 생성되었는지 확인 : ApplicationContext(스프링빈 컨테이너) 주입받고 getBean으로 확인하기
    • getBean(“팩토리빈 빈 설정 시 설정한 name”) : 팩토리빈에 의해 컨테이너에 없을 경우 생성한 후 빈을 가져옴, 빈 어노테이션에 따로 이름 설정하지않으면 해당 클래스명이 설정됨(카멜케이스 - messageFactoryBean)
    • getBean(“&팩토리빈 빈 설정 시 설정한 name”) : 해당 빈의 팩토리빈이 리턴됨

실습 : 트랜잭션 핸들러 - 다이내믹 프록시 객체 생성

@Component("transaction")
public class TransactionProxyFactoryBean implements FactoryBean<Object> {

    private PlatformTransactionManager transactionManager;
    private Object target;
    private String pattern;
    private Class<?> serviceType;

    public TransactionProxyFactoryBean setTarget(Object target) {
        this.target = target;
        return this;
    }

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

    public TransactionProxyFactoryBean setPattern(String pattern) {
        this.pattern = pattern;
        return this;
    }

    public TransactionProxyFactoryBean setServiceType(Class<?> serviceType) {
        this.serviceType = serviceType;
        return this;
    }

    @Override
    public Object getObject() throws Exception {
        TransactionHandler txHandler = new TransactionHandler().setTarget(target).setTransactionManager(transactionManager).setPattern(pattern);
        return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{serviceType}, txHandler);
    }

    @Override
    public Class<?> getObjectType() {
        return serviceType;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

@Configuration
public class TransactionProxyFactoryBeanConfig {

    @Bean(value = "transaction")
    public TransactionProxyFactoryBean createFactoryBean() {
        return new TransactionProxyFactoryBean().setPattern("upgradeLevels").setTarget(new HelloTarget()).setServiceType(Hello.class).setTransactionManager(new JpaTransactionManager());
    }
}
  • 팩토리빈 생성에 필요한 설정을 해줘야함 : @Configuration, @Bean을 사용해서 수동으로 빈을 생성하면 됨 - streotype가 달려있는 빈을 생성할 때 여기를 통해서 생성
    • Application Context 컨테이너는 @Configuration을 참고해서 스프링빈을 생성함
  • 장점 : 디자인패턴에 비해 유연하고(기존 코드 수정하지않아도 됨), 팩토리빈을 통해서 스프링에서도 다이내믹 프록시를 만들 수 있게됨
  • 단점을 꼽는다면?
    • 핸들러가 팩토리빈 개수만큼 만들어짐 - 적용시킬 타겟이 있다면 팩토리를 사용해야하는데 팩토리는 내부적으로 다이내믹 프록시(타겟 타입 인터페이스 구현)를 만들 때 핸들러 생성 후 적용(핸들러를 스프링빈으로 만들면 적용 타겟의 타입마다 핸들러를 만들어두고 빈으로 초기화해야함….)
    • 한번에 여러 대상에 부가 기능을 적용시킬 수 없음
  • 위의 장점 흡수, 단점을 해결해서 스프링은 서비스로 만들어냄(서비스 추상화) : ProxyFactoryBean

스프링의 ProxyFactoryBean

  • 일관되게 프록시를 생성할 수 있도록 추상화 : 부가 기능은 없지만, 부가 기능을 DI 받아서 타겟에 부가기능을 더해주는 프록시 객체
  • 부가기능이 없는 프록시 : 부가기능은 따로 만들어서 빈으로 생성해둬야함 - MethodInterceptor 구현(Advice 라고 함)
/* Advice - 부가기능 */
@Component
public class UpperCaseAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result = invocation.proceed(); // method.invoke(target, args)와 같음(추상화)
        if (result instanceof String) {
            return String.valueOf(result).toUpperCase();
        }
        return result;
    }
}

/* ProxyFactoryBean - 부가기능, 포인트컷까지 DI 받음 */
@Test
public void proxyBeanFactoryTest() {
    ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
    proxyFactoryBean.setTarget(new HelloTarget());

    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedName("sayH*");
    proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UpperCaseAdvice()));

    Hello proxiedHello = (Hello) proxyFactoryBean.getObject();
}
  • 일관된 방법으로 프록시 객체를 만들 수 있음
  • 용어 정리
    • 어드바이스 : 순수 부가기능, 프록시가 아닌 부가기능만 가지는 객체
    • 포인트 컷 : 부가기능이 적용될 것인지 알고리즘/전략 객체(직접 만들어본 프록시에서 pattern과 같은 역할), 전략이 계속해서 변경될 수 있으므로 DI로
    • 어드바이저 : 부가기능마다 포인트컷이 다를 수 있기때문에 하나의 별도 객체로 묶어서 add하도록 함 - 타겟 객체의 특정 메소드마다 어드바이스를 적용하는 것임

ProxyFactoryBean 트랜잭션 프록시 생성

@Service("userService")
public class UserServiceImpl implements UserService

/* 어드바이스(순수 부가기능) - 스프링빈으로 등록해서 재사용 */
@Component
public class TransactionAdvice implements MethodInterceptor {

    private PlatformTransactionManager transactionManager;

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

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            Object result = invocation.proceed();
            transactionManager.commit(status);
            return result;
        } catch (InvocationTargetException e) {
            transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

/* 사용 테스트 */
@Resource(name = "userService")
private UserService userService;

@Autowired
private TransactionAdvice transactionAdvice;

@Test
public void transactionAdviceTest() {
    ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
    proxyFactoryBean.setTarget(userService);

    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedName("upgradeLevels");

    proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, transactionAdvice));
    UserService userService = (UserService) proxyFactoryBean.getObject();
}
  • 결국 스프링은 DI로 어드바이스를 재활용 할 수 있게 함 : 어드바이스에서 상태가 들어가는 부분을 아예 객체 분리해서 하나의 판(ProxyFactoryBean)을 만들어 모두 DI하는 형태로 프록시를 만들어버림 - 관심사 분리 스프링 1장에 나오는 그 내용이 계속해서 스프링 내부에 적용됨(대단…)
    • 포인트컷 또한 일정한 규칙만 만들어두면 스프링빈으로 등록해두고 재사용할 수 있겠음, 어드바이저 또한 오버라이딩해서 트랜잭션 어드바이저로 만들고 빈 등록 후 재사용도…
    • 스프링빈(싱글톤 기준)으로 등록해서 관리하려면 객체는 변할 수 있는 상태값을 가지면 안됨
  • @Resource : 빈 컨테이너에서 빈을 찾을 때 설정된 이름으로 찾음(어노테이션 name 메소드 리턴값)
  • @Autowired : 빈 컨테이너에서 빈을 찾을 때 타입으로 찾음

AOP 프록시 적용 타겟을 만들면서 배운 것

  • Application Context 스프링빈 생성 과정에서 어떤 것을 쓰나 : newInstance() 사용, @Configuration - @Bean 참고해서 인스턴스 생성
  • 빈을 만드는 팩토리빈을 생성하기위해서 Factory를 구현함 : 빈으로 등록해서 사용할 것이라 streotype을 달아줘야함
  • JDK 다이내믹 프록시 API로 프록시가 적용된 타겟 객체 구현을 할 수 있음
  • 스프링은 서비스 추상화를 해서 스프링 사용자에게 제공함 : ProxyFactoryBean
    • DI를 통해서 확장성을 가져감 : 프록시 객체와 프록시 기능(Advice - MethodInterceptor 구현)을 따로 만들고 프록시 객체에 DI할 수 있도록해서 일관된 방법으로 프록시 객체를 생성할 수 있도록 함
    • Advice(스프링에서 부가기능을 가진 아이를 Advice라 부름)는 타겟을 가지고 있지않기때문에 빈 등록해서 싱글톤으로 관리해도 됨
  • 결국 DI가 핵심 : 재사용을 위해서 아예 역할을 잘게잘게 분리해서 DI 해주는 것으로, DI를 모두 받아서 하나로 합쳐주는 판이 있고 모듈이 존재하는 형태(스프링 그자체…)
    • 변경이 각각 된다면 각각 분리해서 사용하는 형태로 코딩
    • 방법이 고정된다는 생각을 하지말고, 항상 변경에 유연하게 대처할 수 있도록 짜라는 것을 스프링에서 배우네….