Spring @Transactional, @Caching 그리고 AspectJ — 2편 : AspectJ

Brant Hwang
QueryPie
Published in
6 min readAug 28, 2015

1편 내용을 다시 정리해보겠습니다.

  1. Self Invocation ( 동일 클래스에 있는 메서드를 호출 ) 일때는 호출하는 메서드 (invoked method) 에 @Transactional을 선언해주어야 합니다. ( 혹은 @Transactional 을 선언하지 않으려면 AspectJ를 사용하면 됩니다. )
  2. public 이외의 메서드에 트랜잭션을 처리 하고 싶을 경우는 AspectJ를 사용하면 됩니다.

사실 Self-invocation 문제는 트랜잭션 뿐만 아니라 캐시 (@Caching)일때도 동일하게 발생하는데 간단하게 살펴보겠습니다.

alphaService의 findAll 메서드를 이용해 alpha 테이블의 모든 데이터를 가져오는 테스트 케이스를 하나 작성합니다.

@Test
public void selectTest() {
List<Alpha> alphaList = alphaService.findAll();
Assert.assertEquals(alphaList.size(), 1);
}

그리고 alphaService에 findAll을 다음과 같이 구현합니다.

public List<Alpha> findAll() {
return alphaRepository.findAll();
}

테스트 케이스를 돌려보면 성공합니다.

현재는 데이터가 1건만 있으므로 성능이슈가 크게 없지만, 데이터가 많거나 혹은 성능 이슈가 있는 대용량 시스템이라면 캐시가 필요할 것 같습니다.

Spring Cache 기능을 이용해 간단한 캐시를 처리해 보겠습니다.

1) Cache 설정을 위한 Configuration Bean을 생성합니다.

@Configuration
@EnableCaching(proxyTargetClass = true, mode = AdviceMode.PROXY)
public class RedisCacheConfiguration extends JCacheConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer());
return redisTemplate;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
final RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
return redisCacheManager;
}
}
2) Redis에 Cache 데이터를 넣을 때 Serialization 된 Byte코드를 저장하게 되므로, Alpha Entity Class에 Serializable 인터페이스를 연결하고, Entity가 변경 되었을 때에도 Serialization / Deseriailization 이슈가 없도록 serialVersionUID도 생성해줍니다. ( 사실serialVersionUID는 굉장히 중요한 요소입니다. 자세한 내용은 http://www.mkyong.com/java-best-practices/understand-the-serialversionuid/ 링크를 읽어보세요! )
@Entity
public class Alpha implements Serializable {
private static final long serialVersionUID = 968146958609275352L;@Id
@Column(name = "id", precision = 11)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "value1", length = 10)
private String value1;
@Column(name = "value2", length = 10)
private String value2;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getValue1() {
return value1;
}
public void setValue1(String value1) {
this.value1 = value1;
}
public String getValue2() {
return value2;
}
public void setValue2(String value2) {
this.value2 = value2;
}
}

3) 마지막으로 findAll 메서드에 @Cacheable을 설정해줍니다.
@Cacheable(value = "alphaAllData")
public List<Alpha> findAll() {
return alphaRepository.findAll();
}
테스트 케이스를 실행 한 후, Redis에 캐시 데이터가 정상적으로 들어갔는지 확인해봅니다.
Screen Shot 2015-08-28 at 2.23.51 PM
여기까지는 정상적으로 동작한다. 이제 Self-Invocation 상황을 만든다음 캐시가 생성되는지 한번더 확인해보겠습니다.public List<Alpha> findAll() {
return findAllWithCache();
}
@Cacheable(value = "alphaAllData")
public List<Alpha> findAllWithCache() {
return alphaRepository.findAll();
}
트랜잭션 때와 같은 방식으로 코드를 수정 한 다음 테스트 케이스를 다시 실행해봅니다.
테스트케이스는 정상적으로 수행 되었지만, Redis에서 데이터를 확인해보면 캐시 데이터가 보이지 않습니다. @Cacheable이 우리가 의도한대로 동작하지 않았습니다.
Screen Shot 2015-08-28 at 2.30.49 PM
이제 1편에서 했던 동일한 작업을 해봅니다.@EnableCaching(proxyTargetClass = true, mode = AdviceMode.ASPECTJ)
@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED)
두개의 Annotation을 추가하고 실행합니다. 물론 1편에서 했던 것 처럼 VM Option으로 AspectJ javaagent도 추가 해야 합니다.테스트 케이스를 실행하고 Redis에 캐시 데이터가 저장되었는지 확인해봅니다.
Screen Shot 2015-08-28 at 2.30.35 PM
예상했던대로 Self-Invocation 상황에서 @Cacheable이 우리가 의도한 대로 동작했습니다.두가지 테스트를 통해 이런 질문을 생각해볼 수 있습니다." 왜 Self-Invocation 상황일 때 트랜잭션이나 캐시가 정상적으로 동작지 않을까? "이제부터 Spring AOP와 Proxy에 대한 내용들을 살펴보겠습니다.AOPAspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns such as transaction management that cut across multiple types and objects. (Such concerns are often termed crosscutting concerns in AOP literature.)SpringFramework의 문서에는 AOP를 이렇게 이야기 하고 있습니다.1) 프로그램 구조를 새로운 방식으로 생각하게 해서, OOP를 보완한다.2) OOP의 모듈화 단위는 Class이지만, AOP에서의 모듈화 단위는 관점 (Aspect) 라고 이야기한다.트랜잭션이나 캐시를 적용 하려 할때 @Transactional, @Cacheable과 같은 Annotation 처리만으로 우리는 트랜잭션, 캐시 기능들을 사용 할 수 있었습니다. 어떠한 추가적인 코드도 없이 말이죠.그렇다면 @Transactional, @Cacheable Annotation을 Spring Framework이 적절한 시점에 가로챈 다음 트랜잭션 작동에 필요한 코드, 혹은 캐시 작동에 필요한 코드들을 실행 했다는 의미겠죠.이런일이 가능한 배후에는 Proxy라는 녀석이 존재합니다.간단한 코드를 통해서 Proxy가 어떤 모습인지, 어떤 일을 할 수 있는지 살펴보겠습니다calculate() 라는 메서드가 있는데, 이 메서드가 실행에 얼마나 걸렸는지 추적하고 싶어서 다음과 같은 코드를 작성했습니다.public class CalculateService {

public void calculate() throws InterruptedException {
long start = System.nanoTime();
// 임의로 Sleep
Thread.sleep(1000);
long end = System.nanoTime();System.out.println("ElapsedTime : " + (end - start));
}
}
하지만 시간을 측정하는 로직은 calculate() 메소드 밖으로 제거하고, calculate()에는 순수한 비즈니스 로직만을 넣어 보겠습니다.
프록시를 통해서 calculate() 로직과, 시간 측정로직을 분리합니다.1) Java의 Proxy 생성은 Interface 기반이므로 인터페이스와 구현체로 분리를 합니다.public interface CalculateService {
void calculate() throws InterruptedException;
}
public class CalcuateServiceImpl implements CalculateService {@Override
public void calculate() throws InterruptedException {
// 임의로 Sleep
Thread.sleep(1000);
}
}
시간 측정 로직이 제거되서 이제 순수한 calcuate() 로직만 존재합니다.
2) MethodExecutionTimeInvocationHandler 클래스를 생성합니다.public class MethodExecutionTimeInvocationHandler implements InvocationHandler {private Object target;public void setTarget(Object target) {
this.target = target;
}
public Object getTarget() {
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.nanoTime();
try {
return method.invoke(getTarget(), args);
} catch (Exception e) {
throw (e);
} finally {
long end = System.nanoTime();
System.out.println("ElapsedTime : " + (end - start));
}
}
}
해당 클래스는 InvocationHandler 라는 인터페이스를 구현하고 있고, 오버라이드 한 invoke 안에 시간측정 로직을 작성했습니다.
3) 테스트 코드 작성@Test
public void calculate() throws InterruptedException {
MethodExecutionTimeInvocationHandler methodExecutionTimeInvocationHandler = new MethodExecutionTimeInvocationHandler();
methodExecutionTimeInvocationHandler.setTarget(new CalcuateServiceImpl());
CalculateService calculateService =
(CalculateService) Proxy.newProxyInstance(
CalculateService.class.getClassLoader(),
new Class[]{CalculateService.class},
methodExecutionTimeInvocationHandler);
calculateService.calculate();
}
MethodExecutionTimeInvocationHandler에 CalucateServiceImpl( 구현체 ) 를 주입해주고, 프록시를 통해 생성한 프록시 인터페이스로 메소드를 호출했습니다.
Screen Shot 2015-08-28 at 3.16.58 PM
ElapsedTime이 표시됩니다.이제 calucate() 메소드는 순수한 비즈니스 로직을 가지게 되었고, 메소드 실행에 대한 시간 계산은 MethodExecutionTimeInvocationHandler가 책임지게 되었습니다.이것이 바로 Proxy이고, 이 개념을 더 확장 한것이 AOP라고 생각하면 이해가 조금 쉬울것 같습니다. 결국 Spring의 트랜잭션 처리, 캐시 처리 또한 더 복잡한 코드가 있을 뿐이지 결국은 Proxy 라고 하는 큰 틀안에서 작동되고 있는 것이지요. 또 Spring에서는 Proxy를 생성하는 방식이 JDK Proxy vs CGLIB 2가지 방식이 있는데, 이내용도 한번 꼭 살펴보면 많은 도움이 됩니다. ( http://wiki.javajigi.net/pages/viewpage.action?pageId=1065 )다시 정리를 해보면,1) Spring에서 트랜잭션과 캐시 처리는 Proxy를 통해서 이루어 집니다.( https://spring.io/blog/2012/05/23/transactions-caching-and-aop-understanding-proxy-usage-in-spring )2) 하지만 Spring AOP에서는 Self-Invocation시 트랜잭션, 캐시 처리를 할 수 없습니다. 그래서 AspectJ를 쓰라고 한다. ( 내부 클래스에서의 동일 메소드를 호출 할 때에는 Proxy 객체가 아닌 this 를 이용해 메소드를 호출하기 때문이죠. )하지만 StackOverflow를 살펴보니AspectJ를 쓰지 않고 Self-Invocation을 할 수 있는 몇몇 꼼수가 있긴 하네요.@Service
public class UserService implements Service {
@Autowired
private ApplicationContext applicationContext;
private Service self;@PostConstruct
private void init() {
self = applicationContext.getBean(UserService);
}
}
스스로 자기 객체 복사본을 가지고 있는 방식이다. 즉 this.Method() 가 아닌 self.Method() 호출 방식으로 마치 Proxy를 통해 호출 하는 효과를 만드는 방법입니다. 개인적으로는 이런 코드를 좋아하지 않아서 베스트 솔루션은 아닌 것 같다는 생각이 드네요.
다시 AspectJ로 돌아가보면,AspectJ는 ajc (https://eclipse.org/aspectj/doc/next/devguide/ajc-ref.html)라고 하는 컴파일러를 통해 bytecode를 Weaving 하게 됩니다.Weaving이란 뭘까요? Spring 문서에서는 Weaving을 다음과 같이 설명하고 있습니다.

In Spring AOP makes it possible to modularize and separate logging, transaction like services and apply them declaratively to the components Hence programmer can focus on specific concerns. Aspects are wired into objects in the spring XML file in the way as JavaBean. This process is known as ‘Weaving’.

단순한 단어 뜻으로는 엮기( A와 B를 엮는다 ) 라는 의미고, AOP 관점에서 보았을 때는 모듈화되어 분리된 코드를 핵심 관심사 ( i.e 비즈니스로직 )과 엮는 것 (섞는 것) 라는 뜻이네요.즉, AspectJ는 개발자가 만든 AOP 코드 와 Business Code의 bytecode를 Weaving (엮어) 해서 AOP를 구현 할 수 있도록 해준다 정도로 이해하면 될것같습니다.AspectJ는 3가지의 Weaving 방식이 있습니다.(https://eclipse.org/aspectj/doc/released/devguide/ltw.html )
  • Compile-time weaving : ajc를 이용해서 소스코드를 컴파일 할때 Weaving
  • Post-compile weaving : 이미 컴파일된 바이너리 클래스에 Weaving
  • Load-time weaving (LTW) : Class Loader가 클래스를 로딩할 때 Weaving (Weaving Agent 필요합니다)
순수한 Spring AOP에서 사용하는  Weaving은 Compile-time weaving 이라고 할 수 있는데, 그러다보니 Self-Invocation 상황의 트랜잭션 처리나, 캐시, 또는 @Configurable (http://egloos.zum.com/springmvc/v/548980) 과 같은 Annotation 기능을 사용 할 수가 없습니다.그래서 1편 내용에서 AspectJ를 이용해 Spring에 LTW를 활성화 했고, LTW를 이용해 Self-Invocation 상황에서 트랜잭션과 캐시를 처리 할 수 있었습니다.즉 Self-Invocation 상황이지만, 내부적인 동작은 this로 하는 것이 아니라,  bytecode를 Weaving해서 Proxy 처럼 동작하도록 AspectJ가 해주는 것이지요.1,2편을 쓰면서 참고한 URL들입니다. 좋은 글들이 아주 많으니 꼭 참고하세요~

--

--