티스토리 뷰

SpringBoot

@Transactional 이해하기

eello 2025. 4. 24. 16:13

먼저, 스프링부트와 JPA에서 데이터를 저장하는 다음과 같은 코드를 보자.

@Service  
@RequiredArgsConstructor  
public class UserService {  

    private final UserRepository userRepository;  

    public User externalSave(User user) {  
        return internalSave(user);  
    }  

    @Transactional  
    public User internalSave(User user) {  
        return userRepository.save(user);  
    }  
}

만약 외부에서 UserService.externalSave(user); 를 실행한다면 user는 정상적으로 저장이 될까? 정답은 Yes가 될 수도 있고 No가 될 수도 있다. 그렇다면 언제 Yes이고 언제 No일까?

@Transactional의 동작 방식

일단 @Transactional의 동작 방식을 간략하게 알아보자. @Transactional은 AOP를 활용한 기술이기 때문에 이 어노테이션이 클래스 또는 클래스 내의 메소드에 적용되어 있다면 해당 클래스는 프록시로 동작한다. 예를들어, UserService 클래스 내에 @Transactional이 적용된 save() 메소드가 선언되어있고 UserController에서 UserService의 save() 메소드를 실행시킨다 가정해보자.

 

save() 메소드에 @Transactional이 적용되어 있으니 스프링은 UserService에 대한 빈을 프록시 객체로 생성해 등록하게 된다.

 

실제로 @Autowired로 주입받은 UserServicegetClass()를 출력해봤을 때 클래스명 뒤에 $$SpringCGLIB$$이 붙는다는 것은 CGLIB을 사용해 프록시 객체가 생성되었다는 의미이다. 프록시 객체는 내부에 실제 객체에 대한 참조(target)를 포함하며 실제 객체의 메소드를 실행하기 전에 AOP 로직을 수행한다. @Transactional의 경우는 이 때 트랜잭션을 만들게 된다.

 

[10분 테코톡] 카피의 @Transactional 을 참고하면 더 자세한 동작 방식을 이해할 수 있다.

위와 같이 UserController에서 UserServicesave() 메소드를 호출하면 프록시 객체의 save() 메소드가 실행되고 내부에서 target.save()를 통해 실제 UserServicesave() 메소드가 실행되는 것이다.

프록시 패턴의 문제점

위와 같이 프록시 객체를 활용하는 패턴을 프록시 패턴이라고 한다. 하지만 이 패턴에는 내부 호출 문제(Self-Invocation)이라는 중요한 문제점이 하나 있다.

 

프록시 객체는 외부 호출에 대해서만 적용된다. UserController 등과 같은 외부에서 UserService의 프록시 객체를 호출할 때 @Transactional에 대한 AOP가 동작한한다는 것이다. 맨 처음 예시로 들었던 UserService를 생각해보자. externalSave() 내에서 똑같이 UserService 내에 존재하는 internalSave() 메소드를 호출했다. externalSave() 내에서 internalSave()를 호출한 것처럼 동일 클래스에 존재하는 메소드를 실행할 때 AOP가 동작하지 않는다는 의미이다. 이러한 문제가 내부 호출 문제이다.

 

이제 처음에 제시했던 문제를 생각해보자. UserServiceexternalSave(user)를 호출하는 외부 클래스를 UserController라고 하자. 여전히 internalSave()에는 @Transactional이 적용되어 있으므로 UserService는 프록시 객체이며 내부에서 실제 객체에 대한 참조인 target을 통해 externalSave(user)를 호출하게 된다.

externalSave(user) 내부에서 internalSave(user)를 호출하는 것은 동일한 target 객체 내에서 일어나는 일이다. 즉, 외부에서 일어난 상황이 아니기 때문에 internalSave()@Transactional이 적용되어 있다고 하더라도 AOP 로직이 실행되지 않는다. AOP 로직이 실행되지 않으므로 트랜잭션이 존재하지 않게 된다. JPA에서는 데이터 저장이나 수정, 삭제는 트랜잭션 내에서만 가능하므로 UserRepositorysave()가 호출되더라도 데이터베이스에 반영이 되지 않는다.

 

즉, UserController에서 externalSave(user)를 호출했을 때 user는 정상적으로 저장되지 않는다. 이것이 No가 되는 경우이다.

프록시 패턴의 문제점 해결

정답이 Yes가 되는 경우를 알아보기 전에 프록시 패턴의 문제점인 내부 호출 문제에 대한 해결 방법을 간단히 알아보자.

내부 호출 문제를 해결하는 방법은 크게 2가지로 쉽게 해결가능하다.

  1. externalSave()@Transactional 적용
  2. internalSave()를 다른 클래스로 분리

1번으로 해결한다면 externalSave()@Transactional이 적용 되었기 때문에 프록시 객체에서 target.externalSave(user)를 실행하기 전에 트랜잭션을 생성하는 AOP 로직을 실행하게 된다. 때문에 UserRepositorysave()는 트랜잭션 내에서 실행되는 것이 되므로 데이터베이스에 데이터가 저장된다.

 

2번 해결방법은 새로운 클래스 UserServiceB*를 생성해 internalSave() 메소드를 해당 클래스로 옮겨주는 것이다. 그렇게 된다면 externalSave() 내에서 분리된 *UserServiceB 내에 있는 internalSave()를 호출하게 된다. 이때 internalSave() 메소드에는 @Transactional이 적용되어 있기 때문에 마찬가지로 프록시 객체로 등록될 것이고 externalSave()에서 호출하는 것은 UserServiceB의 프록시 객체가 될 것이다.

 

이 말은 UserController에서 UserService를 호출하는 것과 같다는 것으로 internalSave()를 호출하기 전에 트랜잭션이 만들어질 것이고 결국 UserRepositorysave()는 트랜잭션 내에서 동작하므로 데이터가 데이터베이스에 정상적으로 저장된다는 의미이다.

 

이제 정답이 Yes가 되는 경우를 알아보자. 답은 간단하다. Spring Boot에서 JPA를 사용할 때 일반적으로 많이 Spring Data Jpa 의존성을 추가해 사용하고 레포지토리는 아래와 같이 인터페이스로 JpaRepository를 상속해 사용한다.

 

public interface UserRepository extends JpaRepository<User, Long> {}

 

JpaRepository의 기본적인 구현체는 SimpleJpaRepository이며 UserRepositorysave()는 실제로 SimpleJpaRepositorysave() 함수가 실행되는 것이다. 이 때 SimpleJpaRepositorysave()에는 @Transactional이 적용되어 있다.

때문에 내부 호출 문제의 2번째 해결방법과 같은 원리로 save()가 실행되기 전에 트랜잭션이 생성되므로 정상적으로 데이터가 저장될 수 있는 것이다.

결론

앞에서 제시한 문제는 내부 호출 문제 때문에 데이터가 정상적으로 저장되지 않는게 맞다. Spring Data Jpa의 JpaRepository를 상속해 레포지토리를 만들었다면 내부적으로 @Transactional이 적용되어 있어 데이터가 정상적으로 저장될 수 있다.

'SpringBoot' 카테고리의 다른 글

DLQ(SpringBoot + RabbitMQ 메시지 실패 처리)  (1) 2024.11.20
SpringBoot + RabbitMQ  (0) 2024.11.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2025/10   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함