본문 바로가기

Spring

AOP와 @Transactional 알아보기

안녕하세요, 오늘은 자바 스프링에서 사용되는 @Transactional 어노테이션이란 무엇인지, 작동 원리에 대해서 살펴보겠습니다. Transactional에 대해 알아보기 전에, 한 번도 다룬적 없었던 AOP에 대해서도 다루어 보겠습니다.

AOP?(Aspect Oriented Programming)


더보기

AOP는 인프라 로직과 비즈니스 로직을 분리해 모듈성을 높이기 위한 목적으로 사용합니다.

위 문장에서 비즈니스 로직이란 무엇일까요?

비즈니스 로직은 실세계의 규칙에 따라 데이터를 생성·표시·저장·변경하는 부분을 일컫는다라고 합니다. 즉, 데이터의 상태 값을 조작하는 로직으로 볼 수 있겠죠. DB에 데이터를 insert하거나 변경하는 로직은 모두 비즈니스 로직에 해당할 것입니다.

 

반대로 인프라 로직은 무엇인가요?

데이터에 대한 저장, 변경은 하지 않으면서 성능을 높이고, 보안을 강화하기 위해 처리해야 하는 로직을 의미합니다. 대표적인 인프라 로직으로 권한 로직, DB 트랜잭션 로직, 데이터 캐싱, 로깅, 성능 측정과 같은 로직을 들 수 있다.

 

하나의 기능을 구현하는데 데이터를 저장하는 부분에 앞 뒤로 로그를 기록하고 성능을 측정하고 데이터의 완전성을 보장하기 위한 트랜젝션 코드가 붙으면 유지보수가 어렵고 재사용성이 떨어질 것입니다. 가독성 측면에서도 보기 어렵고, 로그를 기록하거나 성능을 측정하는 인프라 로직은 많은 기능에 반복적으로 사용되기에 중복 코드도 많겠죠.

 

모듈성을 높인다는 것은 비슷한 일을 하는 것들끼리 모아 유지 보수성과 재사용성을 극대화한다는 의미입니다. AOP와 같이 알아야할 개념으로 바로 OOP, 객체지향 프로그래밍이 있습니다. OOP는 객체지향적 설계를 통해 비즈니스 로직의 중복을 제거하고 유지 보수성을 극대화하는 것이고, AOP는 인프라 로직의 중복을 제거하고 비즈니스 로직과 분리하는 것이라고 이해할 수 있겠습니다. 


AOP의 키워드들


AOP를 이야기할 때 꼭 같이 등장하는 키워드들이 있습니다. 바로, Advice, Pointcut, target, joinpoint입니다. 간단히 알아보겠습니다.

 

Advice는 프로그램의 메소드 또는 함수를 호출하기 전, 후, 또는 주변에 실행되는 코드 조각입니다.


Pointcut은 AOP에서 Advice를 적용할 대상을 결정하는 방법 중 하나입니다. 즉, Pointcut은 어떤 메소드 또는 함수에 Advice를 적용할지를 결정하는 일종의 필터 역할을 합니다. Pointcut은 보통 메소드 이름, 패키지 이름, 클래스 이름, 어노테이션 등과 같은 특정한 규칙에 따라 Advice를 적용할 대상을 선택합니다. 예를 들어, "Before" advice를 적용하려는 모든 메소드의 이름이 "get"으로 시작하는 경우, Pointcut은 "get*"과 같은 규칙으로 지정할 수 있습니다.


Target은 부가 기능을 부여할 대상입니다. 이 객체는 AOP가 적용되는 대상이 되며, AOP를 통해 해당 객체의 기능을 보완하거나 변경할 수 있습니다. AOP에서는 이러한 객체를 "advised object" 또는 "target object"라고도 부르며, 이 객체의 메서드가 호출될 때 AOP가 적용된 어드바이스(advice)들이 실행됩니다. 

 

Join point는 어드바이스(advice)가 적용될 수 있는 위치를 가리킵니다. 보통 join point는 메서드 호출, 예외 발생, 필드 값 변경 등과 같은 애플리케이션 실행 중의 특정 지점을 의미합니다. AOP에서는 이러한 join point들을 기준으로 어드바이스를 적용합니다.

 

Spring AOP


Spring AOP는 프록시 패턴을 사용하여 동작합니다. 프록시 패턴은 객체를 직접 참조하는 대신 해당 객체를 대신하여 대행하는 객체를 사용하여 접근하는 방식입니다. 즉, Spring AOP에서는 Target 객체를 직접 참조하지 않고 프록시 객체를 사용합니다. 이는 Aspect 클래스에 정의된 부가 기능을 사용하기 위해서입니다. 이렇게 함으로써, Target 클래스 안에 부가 기능을 호출하는 로직이 포함되지 않으므로 유지보수성이 향상됩니다. 이렇게 하면 다 좋은데, Proxy 클래스를 계속해서 구현해야하는데, 이렇게 하다보면 프록시의 장점이 희석됩니다.

그래서 여기에 추가로 Spring은 Dynamic Proxy 방식을 사용하는데요, 이는 Proxy 객체를 직접 구현하는 것이 아닌 Runtime에 Interface를 구현하는 Class나 Instance를 만들어 내는 것을 이야기합니다. Java의 Reflection API를 이용해서 클래스 정보에 접근해 런타임에 Proxy 객체를 생성합니다.

 

 

아래는 Dynamic Proxy의 예제 코드입니다.

public interface UserService {
    void addUser(User user);
}

public class UserServiceImpl implements UserService {
    public void addUser(User user) {
        // 사용자 추가 로직
    }
}

public class UserServiceProxy implements InvocationHandler {
    private Object target;

    public UserServiceProxy(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 메소드 호출 전 처리
        Object result = method.invoke(target, args);
        // 메소드 호출 후 처리
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(
                userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                new UserServiceProxy(userService)
        );
        userServiceProxy.addUser(new User("John", "Doe"));
    }
}

이제 AOP에 대해 알아보았으니, Spring 프레임워크에서 AOP의 대표적인 예인 @Transactional에 대해서 알아보겠습니다.


@Transactional이란?


@Transactional 어노테이션으로 메서드나 클래스에 적용할 수 있으며, 트랜잭션 경계를 선언적으로 정의할 수 있습니다. @Transactional 어노테이션을 사용하면 스프링이 자동으로 트랜잭션을 시작하고 커밋 또는 롤백을 수행합니다. 서비스 로직 내에서는 실행 중에 RuntimeException이 발생하는 경우 롤백을 수행하며, 테스트 코드 내에서는 테스트 코드에서는 항상 Roll Back이 호출됩니다. 

 

AOP를 사용하는 것과 마찬가지로, 비즈니스 로직이 트랜잭션 처리를 필요로 할 때 트랜잭션 처리 코드가 비즈니스 로직과 공존한다면 코드 중복이 발생하고 비즈니스 로직에 집중하기 어려워집니다. @Transactional을 명시하면 메서드, 클래스가 제공하는 모든 메서드에 대해 내부적으로 AOP를 통해 트랜잭션 처리 코드가 전 후로 수행되기 때문에, 중복되는 인프라 로직을 분리해 가독성을 높일 수 있습니다.

 

이러한 어노테이션을 코드 뿐만 아니라 테스트에서도 활용할 수 있습니다. 테스트 코드에서는 항상 Roll Back이 호출되기 때문에 @Transactional' 어노테이션을 사용하면 테스트 코드에서 데이터베이스 트랜잭션을 자동으로 시작하고 롤백하는 것이 가능해집니다. 이를 통해 테스트 코드에서도 데이터베이스의 상태를 일관된 상태로 유지할 수 있습니다.

예를 들어, 테스트 코드에서 데이터베이스에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행하는 경우를 생각해보겠습니다. 이때 @Transactional 어노테이션을 사용하면 각각의 테스트 메서드가 실행될 때마다 데이터베이스 트랜잭션이 시작되고, 해당 메서드가 종료될 때마다 롤백됩니다. 이렇게 하면 각각의 테스트 메서드가 독립적으로 실행될 수 있으며, 데이터베이스의 상태가 일관된 상태로 유지됩니다.

 

아래는 간단한 CRUD에 대한 테스트 코드입니다. 간단히 Transactional을 붙이는 것만으로 데이터베이스를 쉽게 롤백할 수 있습니다. 

@SpringBootTest
@Transactional
public class UserControllerTest {

    @Autowired
    private UserController userController;

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testUpdateUser() {
        User user = new User();
        user.setName("jaja");
        user.setEmail("jaja@example.com");
        userRepository.save(user);

        user.setEmail("javavoja@example.com");
        userController.updateUser(user.getId(), user);

        User updatedUser = userRepository.findById(user.getId()).orElse(null);
        assertNotNull(updatedUser);
        assertEquals("javavoja@example.com", updatedUser.getEmail());
    }
}

테스트 작성 시 한가지 삽질 후기는 transactional은 Spring framework에서 제공하는 annotation이므로, 반드시 SpringBootTest와 Spring Bean 등록이 필요하다는 것입니다. 

@SpringBootTest
@Transactional
public class UserControllerTest {

    private UserRepository userRepository = new MemoryUserRepository();
	//이하 생략
    }
}

위와 같이 외부에서 의존성을 주입받지 않는 경우, 테스트 내부의 Repository Bean을 인식하지 못하기 때문에 Advice를 붙일 Target으로 인식되지 않습니다. 이 경우 간단하게 @Autowired로 외부에서 주입받으면 Transactional이 잘 롤백됩니다.


이렇게 AOP의 간단한 개념과 이를 활용한 Transactional 어노테이션의 의미에 대해서 연관지어서 알아보았습니다.