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의 변경사항이 생겼을 때 구체적인 클래스를 사용하다보니 클라이언트도 영향을 받음
- UserService를 인터페이스로 만들고, 구현체를 바꿔끼워가면서 사용하도록 코드 개선하는 방법
- 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
- 프록시 : 타겟 객체의 기능 추가 및 확장, 접근 제어를 할 때 사용되는 객체를 총칭
- 프록시의 단점
- 각각의 타입(인터페이스)마다 프록시를 만들어서 사용해야함(오히려 더 많은 변경점이 생겨버린 것일지도)
- 해당 타입(인터페이스)가 구현해야할 메소드가 많아지면 그만큼 핵심 로직 객체에 위임하는 코드를 작성해야할 일이 많이 생김 : 인터페이스의 변경이 프록시 객체에도 영향이 생김(같은 타입을 구현하고 있는 것이기때문에)
분리 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를 모두 받아서 하나로 합쳐주는 판이 있고 모듈이 존재하는 형태(스프링 그자체…)
- 변경이 각각 된다면 각각 분리해서 사용하는 형태로 코딩
- 방법이 고정된다는 생각을 하지말고, 항상 변경에 유연하게 대처할 수 있도록 짜라는 것을 스프링에서 배우네….