TIL

7_2.트랜잭션 전파 옵션과 DB Read/Write 분리

꿀승 2025. 2. 7. 10:49
728x90
반응형
SMALL

학습내용

  1. 트랜잭션 전파 옵션
  2. DB Read/Write 분리

학습정리

1. 트랜잭션 전파 옵션

  • 스프링 프레임워크에서 트랜션이 메서드 간에 어떻게 이어져서(전파) 실행 되는지를 결정하는 설정

  • 옵션은 하나의 트랜잭션 경계 내에서 여러 비즈니스 로직을 실행할 때, 호출하는 메서드와 호출되는 메서드가 동일한 트랜잭션을 공유할지, 아니면 별도의 트랜잭션을 사용할지를 정합

  • 지정 방법

    • 해당 옵션을 적용시키기 위해서는 하위 해당메서드에 전파설정
  • 옵션의 종류

    1. REQUIRED (기본 값)

      • 기존 트랜잭션이 있으면 참여하고, 없으면 새 트랜잭션을 생성함.

      • A트랜잭션이 B트랜잭션을 실행 할 경우 B트랜잭션이 롤백되면 A트랜잭션도 같이 롤백

      • 예시코드

        @Service
        public class ServiceA {
            @Autowired private ServiceB serviceB;
        
            @Transactional(propagation = Propagation.REQUIRED)
            public void methodA() {
                System.out.println("A 트랜잭션 시작");
                serviceB.methodB();
                System.out.println("A 트랜잭션 종료");
            }
        }
        
        @Service
        public class ServiceB {
            @Transactional(propagation = Propagation.REQUIRED)
            public void methodB() {
                System.out.println("B 트랜잭션 실행");
                throw new RuntimeException("B에서 예외 발생!"); // B에서 예외 발생
            }
        }
        // A 트랜잭션 시작 -> B 트랜잭션 시작 -> 예외 발생 -> A 트랜잭션 종료
        //A,B 모두 롤백
  1. REQUIRES_NEW

    • A트랜잭션에 B트랜잭션을 실행 할 경우 항상 새로운 트랜잭션을 생성함.

    • 예제코드

      @Service
      public class ServiceA {
          @Autowired private ServiceB serviceB;
      
          @Transactional(propagation = Propagation.REQUIRED)
          public void methodA() {
              System.out.println("A 트랜잭션 시작");
              serviceB.methodB();
              System.out.println("A 트랜잭션 종료");
          }
      }
      
      @Service
      public class ServiceB {
          @Transactional(propagation = Propagation.REQUIRES_NEW)
          public void methodB() {
              System.out.println("B 트랜잭션 실행");
              throw new RuntimeException("B에서 예외 발생!"); // B에서 예외 발생
          }
      }
      // A 트랜잭션 시작 -> B 트랜잭션 시작 -> 예외 발생 -> A 트랜잭션 종료
      // A: 커밋 ,B: 롤백
  2. NESTED

    • 기존 트랜잭션 안에서 서브 트랜잭션을 생성함.

    • 서브 트랜잭션에서 독립적으로 커밋이나 롤백을 제공.

    • 상위 트랜잭션이 롤백되면 서브트랜잭션도 롤백됨.

    • DB의 종류에 따라서 지원되지 않을 수도 있음.

      @Service
      public class ServiceA {
          @Autowired private ServiceB serviceB;
      
          @Transactional(propagation = Propagation.REQUIRED)
          public void methodA() {
              System.out.println("A 트랜잭션 시작");
              serviceB.methodB();
              System.out.println("A 트랜잭션 종료");
          }
      }
      
      @Service
      public class ServiceB {
          @Transactional(propagation = Propagation.NESTED)
          public void methodB() {
              System.out.println("B 트랜잭션 실행");
              throw new RuntimeException("B에서 예외 발생!"); // B에서 예외 발생
          }
      }
      // A 트랜잭션 시작 -> B 트랜잭션 시작 -> 예외 발생 -> A 트랜잭션 종료
      //B에서 예외 발생 시 B만 롤백, 부분 롤백이 가능
      // A는 커밋 될수도 롤백 될수도 로직에 따라 달라짐
  3. SUPPORTS

    • 상위 트랜잭션이 존재하면 참여하고, 독립적으로 사용 할 경우 트랜잭션 없이 사용됨

    • 주로 조회나 트랜잭션 경계가 반드시 필요하지 않은 작업에서 사용

    • 예시코드

      //단독적, 트랜잭션이 없는 메서드에서 호출되면 트랜잭션이 적용되지 않고 사용됨
        @Transactional(propagation = Propagation.SUPPORTS)
        public User getUser(Long userId) {
          return userRepository.findById(userId)
              .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_USER));
        }
      
        @Transactional(propagation = Propagation.SUPPORTS)
        public Order getOrder(Long orderId) {
          return orderRepository.findById(orderId)
              .orElseThrow(() -> new ServiceException(ServiceExceptionCode.NOT_FOUND_ORDER));
        }
      
        @Transactional(propagation = Propagation.REQUIRED)
        public Order update(Long orderId, Long userId) {
          //여기서 사용시에는 update메서드에 트랜잭션이 걸려있기에 해당 트랜잭션으로 참여
          User orderUser = getUser(userId);
          Order order = getOrder(orderId);
      
          order.setUser(orderUser);
          order.setTotalPrice(BigDecimal.ZERO);
          return orderRepository.save(order);
        }
      
  • 트랜잭션 중 비동기 작업
    A트랜잭션에서 B트랜잭션 호출시에 B트랜잭션 메서드가 비동기일 경우
    비동기는 새로운 쓰레드를 만들기 때문에 전파옵션과 상관없이 새로운 트랜잭션을 구성함.

2. DB Read/Write 분리 개념

  • Master-Slave 구조는 데이터베이스 성능을 최적화 하기 위해 Read/Write 작업을 분리하는 아키텍처

    • Master 노드: 데이터를 쓰기 전용으로 처리 (INSERT,UPDATE,DELETE) , (SELECT가 되긴함)
    • Slave 노드: 데이터를 읽기전용으로 처리, Master로부터 복제된 데이터를 사용 (SELECT)
    • 데이터 동기화 : 마스터에서 변경된 데이터가 슬레이브로 실시간 또는 주기로 복
  • DB 커넥션 설정

    spring:
      datasource:
    # 마스터와 슬레이브로 나눠서 DB연결시에 자동으로 기본설정이 안되기에 파라미터로 기본설정 추가
        master:
          hikari:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/spring_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
            username: root
            password: root
            connectionTimeout: 30000
        slave:
          hikari:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/spring_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
            username: root
            password: root
            connectionTimeout: 30000
    
  • DBConfig 클래스 설정

    @Configuration
    public class DataSourceConfiguration {
        public static final String MASTER_DATASOURCE = "masterDataSource";
        public static final String SLAVE_DATASOURCE = "slaveDataSource";
    
        //MASTER_DATASOURCE(masterDataSource)의 빈 이름으로 저장
        @Bean(name = MASTER_DATASOURCE)
        //application.yml 또는 application.properties에 정의된 값을 Java 객체 필드에 자동으로
        //prefix로 설정하여 관련된 설정값들을 그룹화할 수 있음
        @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
        public DataSource masterDataSource() {
            return DataSourceBuilder.create()
                    .type(HikariDataSource.class)
                    .build();
        }
    
        @Bean(name = SLAVE_DATASOURCE)
        @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
        public DataSource slaveDataSource() {
            return DataSourceBuilder.create()
                    .type(HikariDataSource.class)
                    .build();
        }
    
        @Bean
        @Primary
        //마스터/슬레이브 데이터소스가 모두 초기화된 후 routingDataSource가 동작하도록 함
        @DependsOn({MASTER_DATASOURCE,SLAVE_DATASOURCE})
        public DataSource routingDataSource(
                //특정한 빈 이름을 가진 DataSource를 주입받도록 지정하는 어노테이션.
                @Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
                @Qualifier(MASTER_DATASOURCE) DataSource slaveDataSource
        ) {
            //RoutingDataSource는 AbstractRoutingDataSource을 상속받아 determineCurrentLookupKey 구현
            //트랜잭션이 readOnly면 "slave" / 아니면 "master"
            RoutingDataSource routingDataSource = new RoutingDataSource();
            //Map을 이용해서 RoutingDataSource에서 반환 값과 매핑
            Map<Object, Object> datasourceMap = new HashMap<>();
            datasourceMap.put("master",masterDataSource);
            datasourceMap.put("slave", slaveDataSource);
            //RoutingDataSource 타겟데이터소스를 위에 맵으로 지정
            //RoutingDataSource 부모인 AbstractRoutingDataSource 내부에서 룩업 키(lookupKey)를 DataSource 객체에 매핑함.
            routingDataSource.setTargetDataSources(datasourceMap);
            //기본 설정처리
            routingDataSource.setDefaultTargetDataSource(masterDataSource);
    
            routingDataSource.afterPropertiesSet();
    
            return routingDataSource;
        }
    }
    
  • RoutingDataSource 클래스

    public class RoutingDataSource extends AbstractRoutingDataSource {
        @NotNull
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "slave" : "master";
        }
    }
    
  • 동작 흐름

    1. application.yml 에 Master,Slave 데이터 소스 연결 정보가 설정되고 각각 데이터 소스 Bean에 바인딩
    2. RoutingDataSource 에서 ReadOnly 여부를 통해 해당 Master,Slave 데이터소스를 매핑
      • @Transactional(readOnly=true) 이면 해당 트랜잭션은 slave (Read) 데이터소스 매핑
      • readOnly=true 가 아니면 Master (Write) 데이터 소스로 매핑

ps. 금일에는 트랜잭션 전파옵션과 DB 분리에 대해서 배웠는데
처음 듣는 내용도 있고 유용한 내용이 많아서 재미있게 들었고, 많이 유익한 시간이었습니다.

728x90
반응형
LIST