본문 바로가기
Spring/SpringBoot

[SpringBoot] @Transactional 사용 방식 정리

by J4J 2023. 9. 25.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 @Transactional 사용 방식에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

@Transactional이란?

 

@Transactional은 Spring에서 사용 가능한 어노테이션 중 하나로 트랜잭션의 원칙이 지켜질 수 있도록 도와줍니다.

 

조금 더 단순한 기능에 대해서 말해보면, 비즈니스 로직이 동작되는 과정에서 에러가 발생되면 DB 작업을 모두 롤백을 시켜주고 에러가 발생되지 않으면 DB 작업을 모두 커밋하도록 도와줍니다.

 

 

 

비즈니스 로직에서 트랜잭션 처리가 이루어지는 계층은 Service Layer로 정의되고 있습니다.

 

즉, Spring 개발을 하다 보면 Controller - Service - Repository의 형태가 구성되기 마련인데 이중 Service 쪽에 트랜잭션 처리가 이루어지도록 @Transactional 어노테이션을 활용해 볼 수 있습니다.

 

 

반응형

 

 

Method별 적용

 

@Transactional을 적용할 때 가장 기본적인 방법은 Method별 적용하는 것입니다.

 

메서드 위에 @Transactional 어노테이션을 다음과 같이 사용하면 메서드에서 실행되는 비즈니스 로직은 트랜잭션 원칙이 지켜지게 됩니다.

 

package com.transactional.service;

import com.transactional.entity.LogEntity;
import com.transactional.repository.LogJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class TransactionService {

    private final LogJpaRepository logJpaRepository;

    /**
     * 데이터 생성
     */
    @Transactional
    public void create() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("create")
                        .build()
        );
    }

    /**
     * 데이터 생성 (에러 발생)
     */
    @Transactional
    public void createError() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("createError")
                        .build()
        );

        throw new RuntimeException("createError");
    }
}

 

 

 

위의 예제에서 첫 번째 메서드 같은 경우는 다른 부가적인 이슈가 발생되지 않는 다면 DB에 데이터가 올바르게 적재가 될 것입니다.

 

하지만 두 번째 메서드 같은 경우는 DB에 데이터를 적재하는 로직이 있지만 RuntimeException이 발생되도록 설정되어 있기 때문에 최종적으로 DB에 데이터가 적재되지 않습니다.

 

왜냐하면 트랜잭션 원칙을 지키기 위해 모든 쿼리가 롤백되기 때문입니다.

 

 

 

 

Class별 적용

 

@Transactional은 Class에서도 적용을 할 수 있습니다.

 

Class에 적용하는 것은 Method 때와 마찬가지로 Class 내부에 존재하는 모든 비즈니스 로직은 트랜잭션 원칙이 지켜지게 됩니다.

 

그래서 위의 Method별 예제를 다음과 같이 변경해도 동일하게 동작하는 것을 확인할 수 있습니다.

 

package com.transactional.service;

import com.transactional.entity.LogEntity;
import com.transactional.repository.LogJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class TransactionService {

    private final LogJpaRepository logJpaRepository;

    /**
     * 데이터 생성
     */
    public void create() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("create")
                        .build()
        );
    }

    /**
     * 데이터 생성 (에러 발생)
     */
    public void createError() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("createError")
                        .build()
        );

        throw new RuntimeException("createError");
    }
}

 

 

 

 

AOP 적용

 

만약 모든 Service Layer에 존재하는 클래스 및 메서드들마다 @Transactional 어노테이션을 추가할 것이라는 설계를 하고 프로젝트를 진행한다면 매번 어노테이션을 추가하는 것이 생각보다 쉽지 않을 수 있습니다.

그래서 한 번의 설정만으로 @Transactional 어노테이션을 매번 추가하는 것과 동일한 결과를 만들 수 있도록 AOP를 적용해볼 수 있습니다.

 

AOP를 이용하여 위의 예제들과 동일한 결과를 만들기 위해서는 다음과 같이 해줄 수 있습니다.

 

 

 

[ 1. AOP 클래스 추가 ]

 

package com.transactional.aop;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Aspect
@Component
@RequiredArgsConstructor
public class TransactionalAop {

    private final PlatformTransactionManager transactionManager;

    @Around("execution(* *..service.*.*(..))") // service 패키지에 있는 모든 메서드에 적용
    public Object serviceTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
        // 필요시 @Transactional에서 제공해주는 속성들 설정
        // defaultTransactionDefinition.setReadOnly(true);

        TransactionStatus transactionStatus = transactionManager.getTransaction(defaultTransactionDefinition);
        try {
            Object object = joinPoint.proceed();
            transactionManager.commit(transactionStatus);
            return object;
        } catch (Throwable e) {
            transactionManager.rollback(transactionStatus);
            throw e;
        }
    }
}

 

 

 

[ 2. Service Layer에 @Transactional 제거 ]

 

package com.transactional.service;

import com.transactional.entity.LogEntity;
import com.transactional.repository.LogJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class TransactionService {

    private final LogJpaRepository logJpaRepository;

    /**
     * 데이터 생성
     */
    public void create() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("create")
                        .build()
        );
    }

    /**
     * 데이터 생성 (에러 발생)
     */
    public void createError() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("createError")
                        .build()
        );

        throw new RuntimeException("createError");
    }
}

 

 

 

 

ReadOnly

 

만약 다음과 같은 단순 조회만을 위한 기능이 추가되었다고 가정해보겠습니다.

 

package com.transactional.service;

import com.transactional.entity.LogEntity;
import com.transactional.repository.LogJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TransactionService {

    private final LogJpaRepository logJpaRepository;

    /**
     * 모든 contents 데이터를 조회한다.
     *
     * @return 모든 contents 데이터
     */
    public List<String> getContents() {
        return logJpaRepository
                .findAll()
                .stream()
                .map(logEntity -> logEntity.getContents())
                .collect(Collectors.toList());
    }
}

 

 

 

위에서 AOP를 설정한 상황으로 따지면 getContents Method에 대해서도 데이터 수정 작업이 발생되는 Method들과 모두 동일하게 @Transactional이 적용되어 있습니다.

 

하지만 단순 조회를 하는 비즈니스 로직에 대해서는 readOnly 옵션을 넣어주는 것을 추천하고 있습니다.

 

readOnly 옵션을 넣을 경우 다음과 같은 효과를 얻을 수 있습니다.

 

  • 생성, 수정, 삭제 로직이 있더라도 실제로 적용되지 않음
  • JPA를 사용할 경우 변경 감지를 위한 스냅샷을 만들지 않기 때문에 메모리 낭비를 줄일 수 있음
  • 데이터 조회만을 가지고 있는 비즈니스 로직이라는 것을 직관적으로 확인할 수 있음
  • DB Replication을 사용할 경우 데이터 조회를 할 땐 Slave DB에 접근하도록 설정할 수 있음

 

 

 

최종적으로 단순 조회를 할 땐 readOnly 옵션을 넣어주고, 나머지 Method 들에 대해서는 AOP에서 설정한 것을 그대로 적용하기 위해 다음과 같은 Service Layer 코드가 작성될 수 있습니다.

 

package com.transactional.service;

import com.transactional.entity.LogEntity;
import com.transactional.repository.LogJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TransactionService {

    private final LogJpaRepository logJpaRepository;

    /**
     * 데이터 생성
     */
    public void create() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("create")
                        .build()
        );
    }

    /**
     * 데이터 생성 (에러 발생)
     */
    public void createError() {
        logJpaRepository.save(
                LogEntity
                        .builder()
                        .contents("createError")
                        .build()
        );

        throw new RuntimeException("createError");
    }

    /**
     * 모든 contents 데이터를 조회한다.
     *
     * @return 모든 contents 데이터
     */
    @Transactional(readOnly = true)
    public List<String> getContents() {
        return logJpaRepository
                .findAll()
                .stream()
                .map(logEntity -> logEntity.getContents())
                .collect(Collectors.toList());
    }
}

 

 

 

 

 

 

 

 

이상으로 @Transactional 사용 방식에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글