4번째 필수 강좌 Part 1. ch3. 1~5강 요약
앞서 DMakerService에서 다음과 같은 코드를 작성한 적이 있다.
@Transactional
public void createDeveloper(){
Developer developer = Developer.builder()
.developerLevel(DeveloperLevel.JUNIOR)
.developerSkillType(DeveloperSkillType.FRONT_END)
.experienceYears(2)
.name("T")
.age(25)
.build();
developerRepository.save(developer);
}
이 때 @Transactional이라고 붙인 애너테이션을 보자. Transaction은 일반적으로 ACID(Atomic Consistency Isolation Durability)특성을 가진다.
- Atomic: 원자성. 짝을 이루는 두 작업이 하나의 트랜잭션으로 묶여서 두 작업이 모두 성공하거나 둘 다 실패하도록(=하나만 성공하고 다른 것이 실패하면 다시 처음으로 롤백) 하는 특성
- Consistency: 일관성. 모든 DB테이블의 자료들은 항상 정해진 규칙에 따라 저장되며, 트랜잭션 도중에는 일관성이 흐트러지더라도 하나의 트랜잭션이 종료되는 시점에 일관성이 유지되고 있어야 한다.
- Isolation: 고립성. 성능을 상쇄시키는 특성으로, DB의 고립성이 높으면 서버에서 많은 요청을 받을 수 없고, 반대로 낮으면 서버에서 많은 요청을 받을 수 있어 성능이 향상되지만 데이터의 정합성이 떨어지게 된다. Isolation 레벨과 성능을 적절히 저울질하여 로직을 구상하는 것이 중요하다.
- Durability: 지속성. 모든 DB 작업 이력이 기록되어야 하는 특성을 말한다. 데이터가 DB에 커밋되는 것도, DB에서 삭제되는 것도 전부 기록되어야 한다.
- DB에 데이터를 저장할 때, 바로 디스크에 쓰기보다는 로그를 먼저 남기고 로그에 남은 데이터를 기반으로 쓰게 된다. 디스크에 쓰는 과정에서 오류가 발생하면, 로그는 남아있지만 디스크에는 내용이 반영되지 않는다. 이 상태에서는 그 다음 작업이 들어오더라도 문제가 발생했던 로그 내용이 저장되지 않는 이상 디스크에는 더이상 반영되지 않는다.
트랜잭션을 하려면 먼저 트랜잭션을 열고, 작업을 수행하고 커밋해서 예외가 발생하면 롤백하는 등 공통적인 작업이 필요하다. 이럴 때 AOP나 인터셉터를 사용할 수 있는데, 후자는 전역적으로 활용되는 경우에 사용하는 데 반해 트랜잭션은 지정된 몇 개의 작업에서만 쓰이므로 적절하지 않다.
AOP: Aspect Oriented Programming
특정 로직에 대해서만 공통적으으로 처리하거나 적용하고 싶은 관심사가 있을 때 사용한다.
- Aspect: 여러 클래스나 기능에 걸쳐서 있는 관심사를 모듈화한 것 (주로 @Transactional)
- Advice: AOP에서 실제로 적용하는 기능, 액션 (로깅, 트랜잭션, 인증 등)
- Pointcut: join point 중에서 해당 Aspect를 적용할 대상을 뽑을 조건식
- Join point: 모듈화된 특정 기능이 실행될 수 있는 연결 포인트. Aspect를 넣어줄 수 있는 포인트. 연결 가능한 지점
- Target Obejct: Advice가 적용될 대상 오브젝트
- AOP Proxy: 대상 오브젝트에 Aspect를 적용하는 경우 Advice를 덧붙이기 위해 하는 작업. 주로 CGLIB(Code Generation Library, 실행 중 실시간으로 코드를 생성하는 라이브러리) 프록시를 사용해 처리한다.
- Weaving: Advice를 비즈니스 로직 코드에 삽입하는 것
- AOP를 제대로 사용하기 위해서는 스프링에서 기본적으로 제공하는 Spring AOP에 추가로 AspectJ 라이브러리를 사용한다.
트랜잭션 실행 전에 선행되어야 하는 트랜잭션을 여는 작업이나, 트랜잭션을 마치고 커밋을 했을 때 성공 여부에 따라 롤백하는 작업을 직접 작성해줄 수도 있지만, AOP 애너테이션 @Transactional을 사용하면 해당 코드를 작성하지 않더라도 스프링이 알아서 처리해주기 때문에 편리하다.
@Transactional 애너테이션은 TransactionInterceptor라는 구현체를 통해 해당 애너테이션이 붙은 메서드의 앞뒤에 Transaction 시작, Commit 혹은 Rollback을 작성해준다.
// TransactionInterceptor.java
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
@Override
@Nullable
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
@Override
public Object getTarget() {
return invocation.getThis();
}
@Override
public Object[] getArguments() {
return invocation.getArguments();
}
});
}
MethodInvocation은 Invocation을 상속하는 인터페이스이다. Invoke는 누군가를 부르다 라는 뜻으로, 설명을 보면 조인 포인트라고 나와 있다.
Description of an invocation to a method, given to an interceptor upon method-call.
A method invocation is a joinpoint and can be intercepted by a method interceptor.
더 자세히는 메서드를 중간에 인터셉트해서 앞쪽에서 대신 작업을 해주고, 실제 메서드를 돌려준 다음, 해당 동작이 끝난 후 작업을 처리해야 할 때 MethodInvocation을 구현하여 사용할 수 있다. 함수를 호출하는 것 자체가 하나의 조인 포인트가 될 수 있는 것이다.
DMakerService에서 createDeveloper라는 함수에 @Transactional이 붙었다. 이 때 createDeveloper가 invoke되면서 TransactionInterceptor는 이 함수와 함수가 속한 클래스인 DMakerService에 대한 정보를 알게 된다. createDeveloper를 호출하면 invokeWithinTransaction로 트랜잭션 안에서 invocation method를 실행한다. 이미 열려있는 트랜잭션이 있는지 확인하고, 필요하면 하위 트랜잭션을 생성하는 등 트랜잭션끼리의 계층구조를 따라 내용을 실행한다. 예외가 발생하면 설정에 따라 롤백하거나 롤백하지 않고 커밋하기도 한다.