[Spring Data JPA] 서비스 계층에 @Transactional을 사용하지 않을 때 트랜잭션 실행 여부

Ho Saint·2022년 9월 7일
0

Spring Data JPA를 처음 학습하면서 궁금했던 것은 JPA에서와 다르게 직접 commit을 해주지 않더라도 db에 메소드 결과가 반영되는 것이었습니다.
이후 @Transactional 어노테이션을 통해 트랜잭션을 설정하는 방법을 배우고 다른 교육생의 질문에 답변하면서 찾아본 내용을 공유하고자 합니다.

내용에 앞서 트랜잭션 로그를 확인하기 위해 설정 파일에 옵션을 추가해 줍니다.

/// application.yml
logging:
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG
        transaction: DEBUG
 
 /// application.properties
logging.level.ROOT=INFO
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG

먼저 학습한 JPA에서는 EntityTransaction 객체를 통하여 명시적으로 트랜잭션을 시작하고 commit을 해줬었습니다.

  public void practice() {
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("Transaction test");
      EntityManager em = emf.createEntityManager();
      EntityTransaction tx = em.getTransaction();

      tx.begin();

      /// 작업

      ....

     ///

     tx.commit();
     em.close();
     emf.close();

이와 다르게 Spring Data JPA에서는 JpaRepository를 상속한 Repository 인터페이스를 정의한 후 서비스 계층에서 Repository의 메소드를 호출하는 것만으로 db에 결과가 반영이 됩니다.

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    
    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);
        
        return savedMember;

postman으로 멤버 등록 요청시에 db에 저장이 되어있는 것을 확인할 수 있습니다.

2022-09-07 13:04:54.307 DEBUG 5576 --- [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2022-09-07 13:04:54.493 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(2070283814<open>)] for JPA transaction
2022-09-07 13:04:54.494 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-09-07 13:04:54.498 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3713c44c]
Hibernate: 
    insert 
    into
        member
        (member_id, created_at, last_modified_at, email, member_status, name, phone) 
    values
        (default, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        stamp
        (stamp_id, created_at, last_modified_at, member_id, stamp_count) 
    values
        (default, ?, ?, ?, ?)
2022-09-07 13:04:54.631 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-09-07 13:04:54.631 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(2070283814<open>)]
2022-09-07 13:04:54.646 DEBUG 5576 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
2022-09-07 13:04:54.689 DEBUG 5576 --- [nio-8080-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

로그를 확인해보면 현재 쓰레드에서 트랜잭션을 생성한 후 쿼리를 실행한 뒤 트랜잭션을 commit하는 것을 볼 수 있습니다.
Service 계층을 포함하여 관련 설정을 따로 해준 것이 없는데 어떻게 트랜잭션이 실행되는 것일까요?

그것은 우리가 정의한 Repository의 구현체를 보면 알 수 있습니다.
JpaRepository를 상속할 경우 스프링에서 기본적으로 생성해주는 구현체는 SimpleJpaRepository입니다.

@Repository
@Transactional(readOnly=true)
public class SimpleJpaRepository<T,ID> extends Object implements JpaRepositoryImplementation<T,ID>

Default implementation of the CrudRepository interface. This will offer you a more sophisticated interface than the plain EntityManager .

클래스에 @Transactional 어노테이션이 붙어있는 것을 볼 수 있습니다.
추가적으로 트랜잭션을 설정해주지 않더라도 JpaRepository 메소드 호출 시 메소드 단위로 트랜잭션이 설정되어 실행되는 것입니다.

그렇다면 Service 계층에 @Transactional을 사용하는 것과는 어떤 차이가 있을까요?

@Service
@Transactional(readOnly=true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    
    @Transactional
    public Member createMember(Member member) {
    	verifyExistsEmail(member.getEmail());
        Member savedMember = memberRepository.save(member);
        
        return savedMember;
	
	private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email);
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }

이를 확인해보기 위해 MemberService@Transactional을 설정해주고 createMember 메소드에 이메일 검증 로직을 추가해봤습니다.

2022-09-07 13:48:54.970 DEBUG 5348 --- [nio-8080-exec-2] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2022-09-07 13:48:55.139 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1445793903<open>)] for JPA transaction
2022-09-07 13:48:55.140 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [com.codestates.member.service.MemberService.createMember]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-09-07 13:48:55.143 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2e6c551]
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.created_at as created_2_1_,
        member0_.last_modified_at as last_mod3_1_,
        member0_.email as email4_1_,
        member0_.member_status as member_s5_1_,
        member0_.name as name6_1_,
        member0_.phone as phone7_1_ 
    from
        member member0_ 
    where
        member0_.email=?
2022-09-07 13:48:55.198 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1445793903<open>)] for JPA transaction
2022-09-07 13:48:55.199 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: 
    insert 
    into
        member
        (member_id, created_at, last_modified_at, email, member_status, name, phone) 
    values
        (default, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        stamp
        (stamp_id, created_at, last_modified_at, member_id, stamp_count) 
    values
        (default, ?, ?, ?, ?)
2022-09-07 13:48:55.288 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-09-07 13:48:55.288 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1445793903<open>)]
2022-09-07 13:48:55.298 DEBUG 5348 --- [nio-8080-exec-2] o.s.orm.jpa.JpaTransactionManager        : Not closing pre-bound JPA EntityManager after transaction
2022-09-07 13:48:55.337 DEBUG 5348 --- [nio-8080-exec-2] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor

먼저 현재 쓰레드에 트랜잭션을 생성한 후 verifyExistsEmail 메소드가 호출이 됩니다.
이때 verifyExsitsEmail의 propagation level은 default인 Propagation.REQUIRED가 됩니다.
이미 현재 쓰레드에 createMember 트랜잭션이 존재하므로 verifyExsitsEmail은 새로 트랜잭션을 생성하지 않고 createMember 트랜잭션에 참가하게 됩니다.
이후 조회 쿼리가 실행되어 이메일 검증 로직을 수행한 후 memberRepository.save 을 메소드가 호출이 됩니다.

마찬가지로 memberRepository.save propagation level은 Propagation.REQUIRED이기 때문에 createMember 트랜잭션에 참여하여 작업을 수행합니다.
이후 작업이 종료되고 createMember 트랜잭션은 commit됩니다.

정리하자면 서비스 계층에 트랜잭션 설정을 해주면 트랜잭션 단위가 달라지게 됩니다.
MemberService 클래스에 @Transactional 을 해주지 않았을 경우 트랜잭션 단위는 memberRepository.save가 됩니다.
반대로 MemberService 클래스에 @Transactional 을 해준다면 트랜잭션 단위는 verifyExsitsEmailmemberRepository.save 을 호출한 createMember 메소드가 됩니다.


references. (22.09.07 14:07)

Spring Data JPA - docs.spring.io
SimpleJpaRepository - docs.spring.io
Transactioanl - docs.spring.io
Propagation - docs.spring.io

profile
열정보다 시스템. 실수를 막을 수 있는 프로세스.

0개의 댓글