728x90

특징

  • 독립성 : 하나의 트랜젝션은 다른 트랜젝션에 포함될수 없고 독립적이다.
  • 일관성 : 작업 처리의 결과가 항상 일관된다.
  • 원자성 : Database 에 모두반영 (Success) 혹은 모두 반영(Fail)되지 않음을 말한다.
  • 지속성 : 성공적으로 완료된 결과는 영구적용되어야 한다.

트랜젝션 commit & rollback

  • 성공상태이고 success로 반영된 상태
  • 프로세스가 성공아닌 실패로 끝나거나 비정상적으로 종료 되었을때 앞서 성공한 프레세들은 하나로 묶여서 수행전 상태로 되돌아가야 한다.

트랜젝션의 논리적 5가지 상태

  • Active : 현재 실행 중인 상태.
  • Failed : 트랜젝션이 실행중 오류로 인한 중단.
  • Aborted : 트랜잭션이 비정상 종료로 인하여 롤백 된 상태.
  • Partially Committed : 트랜젝션의 연산이 마지막까지 실행 되고 Commit 되기 직전상태.
  • Committed : 트랜젝션이 모두 성공적으로 진행되고 Commit 연산까지 실행된 상태.

그래서 Springboot 에서 트랜젝션은 어떻게 설정하고 사용하는데?

일반적으로 spring-boot-starter-jdbc 와 spring-boot-starter-data-jpa 를 추가하면 springboot 가 알아서 DataSourceTransactionManager 와 JpaTransactionManager 을 주입하기 때문에 사용자는 별도로 @Transactional 어노테이션을 붙일 필요가 없다.

앞서 작성한 spring data jap 다중 datasource 을 기준으로 테스트 코드를 작성해 본다.

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

	@Autowired
	private UserRepository userRepository;

	@Test
	public void test() throws Exception {

		// 10 data 입력 
		userRepository.save(new User("AAA", 10));
		userRepository.save(new User("BBB", 20));
		userRepository.save(new User("CCC", 30));
		userRepository.save(new User("DDD", 40));
		userRepository.save(new User("EEE", 50));
		userRepository.save(new User("FFF", 60));
		userRepository.save(new User("GGG", 70));
		userRepository.save(new User("HHH", 80));
		userRepository.save(new User("III", 90));
		userRepository.save(new User("JJJ", 100));

		// validation 생략
	}

}

User 객체 @Max(50) 이므로 age가 50 보다 큰수입력시 Exception이 발생한다.

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Max(50)
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}

log

2022-12-26 11:55:29.581 ERROR 24424 --- [           main] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Validation failed for classes [com.blake.demo.User] during persist time for groups [javax.validation.groups.Default, ] List of constraint violations:[ ConstraintViolationImpl{interpolatedMessage='최대수치50을 초과할수 없습니다.', propertyPath=age, rootBeanClass=class com.didispace.chapter310.User, messageTemplate='{javax.validation.constraints.Max.message}'} ]]

database 확인

이미지에서 확인할수 있는것처럼 테스트진행중 예외가 발생하여 앞5개 데이터는 정상 입력되었지만 나머지 5개 데이터는 입력실패, 이때 모두 성공 or 실패로 처리하려면 트랜젝션기능을 이용할수 있다.

테스트코드를 살펴보자

@Test
@Transactional
public void test() throws Exception {

    // test 내영은 생략

}

코드에서 처럼 @Transactional 어노테이션 하나만 달아주면 끝!

public interface UserService {
    
    @Transactional
    User update(String name, String password);
    
}

트랜잭션 상세

앞서 작성 글중에서 spring data jap 다중 datasource 사용하는 환경일경우 트랜젝션 선언시 트랜잭션 매니저를 지정해 줘야 한다. 예를 들면

@Transactional(value="transactionManagerPrimary")

트랜젝션 매니저를 지정하는것 외에도 우리는 트랜젝션을 격리,전파 등 컨트롤할수 있다.

격리(Isolation)

개념 : 트랜잭션에서 일관성이 없는 데이터를 허용하도록 하는 수준을 말한다.

위치 : org.springframework.transaction.annotation.Isolation 에서 확인할수 있다.

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
  • DEFAULT : 기본 격리 수준(, DB의 Isolation Level을 따름)
  • READ_UNCOMMITTED : 커밋되지 않는(트랜잭션 처리중인) 데이터에 대한 읽기를 허용
  • READ_COMMITTED : 트랜잭션이 커밋 된 확정 데이터만 읽기 허용
  • REPEATABLE_READ : 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다. 선행 트랜잭션이 읽은 데이터는 트랜잭션이 종료될 때까지 후행 트랜잭션이 갱신/삭제를 보류한다.
  • SERIALIZABLE : 가장 강력한 격리수준이다. 순차적으로 진행해서 여러 트랜잭션이 동시에 같은 테이블 정보를 엑세스하지 못하게 한다.

그럼 어떻게 지정하는데? 지정방법은 아래와 같다!

@Transactional(isolation = Isolation.DEFAULT)

트랜젝션의 전파(propagation)

개념 : propagation 속성을 이용해 호출한 쪽의 트랜잭션을 그대로 사용할 수도 있고, 새롭게 트랜잭션을 생성할 수도 있다. 트랜잭션의 경계를 정의하며 시작 방법을 결정하는 속성이다.

위치 : org.springframework.transaction.annotation.Propagation 에서 확인 할수 있다.

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
  • REQUIRED : 부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성한다.
  • SUPPORTS : 이미 진행중인 부모 트랜잭션이 있으면 참여하고, 없을 경우 Non-transaction으로 실행된다.
  • MANDATORY : 이미 진행중인 트랜잭션이 반드시 있어야만 실행되며, 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.
  • REQUIRES_NEW : 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성, 이미 진행중인 트랜잭션이 있다면 잠깐 보류되고 해당 트랜잭션 경계가 종료 된 후 다시 시작된다.
  • NOT_SUPPORTED : Non-transaction으로 시작하며, 이미 진행중인 트랜잭션이 있으면 끝날 때까지 보류된 후 실행된다.
  • NEVER : Non-transaction으로 시작하며, 실행되며 부모 트랜잭션이 존재한다면 예외가 발생합니다.
  • NESTED : 이미 진행중인 부모 트랜잭션이 있을 경우 중첩 트랜잭션을 생성하여 실행되며, 생성된 중첩 트랜잭션은 부모 트랜잭션이 rollback되면 함께 되지만, 해당 트랜잭션안에서의 Commit/Rollback은 부모 트랜잭션에 영향을 주지 않는다.

propagation 설정을 아래와 같이 할수 있다.

@Transactional(propagation = Propagation.REQUIRED)

끝!

'Springboot2.x 강좌 > DB연결' 카테고리의 다른 글

Flyway 로 DataBase 를 형상관리해보자  (0) 2023.01.05
MyBatis 의 다중 DataSource  (0) 2023.01.05
Spring Data JPA 다중 DataSource  (0) 2023.01.05
JdbcTemplate 다중 DataSource  (0) 2023.01.05
XML 로 Mybatis 설정하기  (0) 2023.01.05
728x90

application.properties 설정

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver

application.yml 설정

spring:
    datasource:
        primary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test1
            password: root
            username: root
        secondary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test2
            password: root
            username: root

단일 datasource 설정과 다르게 다중 datasource 는 spring.datasource 뒤에 접두사 primary 와 secondary 로 구분하고 datasource 를 초기화 할때 사용하게 된다. Springboot 2.x 은 spring.datasource.secondary.jdbc-url 를 사용하고 Springboot 1.x 은 spring.datasource.secondary.url 를 사용하한다. 실행시 java.lang.IllegalArgumentException:jdbcUrl is required with driverClassName 발생하면 해당 버전별 설정이 제대로 되었는지 확인해보면 된다.

 

Config 클래스 작성해보겠다. 기존 JdbcTemplate & Jpa 설정과 동일하다.

@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

Primary 설정

@Configuration
@MapperScan(
        basePackages = "com.blake.demo.p",
        sqlSessionFactoryRef = "sqlSessionFactoryPrimary",
        sqlSessionTemplateRef = "sqlSessionTemplatePrimary")
public class PrimaryConfig {

    private DataSource primaryDataSource;

    public PrimaryConfig(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        this.primaryDataSource = primaryDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryPrimary() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(primaryDataSource);
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplatePrimary() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactoryPrimary());
    }

}

Secondary 설정

@Configuration
@MapperScan(
        basePackages = "com.blake.demo.s",
        sqlSessionFactoryRef = "sqlSessionFactorySecondary",
        sqlSessionTemplateRef = "sqlSessionTemplateSecondary")
public class SecondaryConfig {

    private DataSource secondaryDataSource;

    public SecondaryConfig(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        this.secondaryDataSource = secondaryDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactorySecondary() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(secondaryDataSource);
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplateSecondary() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactorySecondary());
    }

}

User & Repository 는 왜 없냐고 물어보는 분들을 위하여 아래와 같은 소스도 보여준다.

위치는 com.blake.demo.p 패키지 아래에 넣는다.

@Data
@NoArgsConstructor
public class UserPrimary {

    private Long id;

    private String name;
    private Integer age;

    public UserPrimary(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public interface UserMapperPrimary {

    @Select("SELECT * FROM USER WHERE NAME = #{name}")
    UserPrimary findByName(@Param("name") String name);

    @Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

    @Delete("DELETE FROM USER")
    int deleteAll();

}

위치는 com.blake.demo.s 패키지 아래에 넣는다.

@Data
@NoArgsConstructor
public class UserSecondary {

    private Long id;

    private String name;
    private Integer age;

    public UserSecondary(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public interface UserMapperSecondary {

    @Select("SELECT * FROM USER WHERE NAME = #{name}")
    UserSecondary findByName(@Param("name") String name);

    @Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
    int insert(@Param("name") String name, @Param("age") Integer age);

    @Delete("DELETE FROM USER")
    int deleteAll();
}

테스트 코드 작성

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class DemoApplicationTests {

    @Autowired
    private UserMapperPrimary userMapperPrimary;
    @Autowired
    private UserMapperSecondary userMapperSecondary;

    @Before
    public void setUp() {
        userMapperPrimary.deleteAll();
        userMapperSecondary.deleteAll();
    }

    @Test
    public void test() throws Exception {

        userMapperPrimary.insert("AAA", 20);

        UserPrimary userPrimary = userMapperPrimary.findByName("AAA");
        Assert.assertEquals(20, userPrimary.getAge().intValue());

        UserSecondary userSecondary = userMapperSecondary.findByName("AAA");
        Assert.assertNull(userSecondary);

        userMapperSecondary.insert("BBB", 20);

        userPrimary = userMapperPrimary.findByName("BBB");
        Assert.assertNull(userPrimary);

        userSecondary = userMapperSecondary.findByName("BBB");
        Assert.assertEquals(20, userSecondary.getAge().intValue());
    }

}

끝!

728x90

application.properties 설정

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
# SQL Log 출력 여부
spring.jpa.show-sql=true
# Hibernate 의 DDL 수행 전략
spring.jpa.hibernate.ddl-auto=create-drop

application.yml 설정

spring:
    datasource:
        primary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test1
            password: root
            username: root
        secondary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test2
            password: root
            username: root
    jpa:
        hibernate:
            ddl-auto: create-drop
        show-sql: true
단일 datasource 설정과 다르게 다중 datasource 는 spring.datasource 뒤에 접두사 primary 와 secondary 로 구분하고 datasource 를 초기화 할때 사용하게 된다.
Springboot 2.x 은 spring.datasource.secondary.jdbc-url 를 사용하고 Springboot 1.x 은 spring.datasource.secondary.url 를 사용하한다. 실행시 java.lang.IllegalArgumentException:jdbcUrl is required with driverClassName 발생하면 해당 버전별 설정이 제대로 되었는지 확인해보면 된다.
 
JPA 단일 DataSource 시 설정을 먼저 보자
@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

}

일반 JdbcTemplate 설정과 동일하다. 하지만 다중 DataSource 는 두가지로 나누어서 설정한다.

Primary 설정

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactoryPrimary",
        transactionManagerRef="transactionManagerPrimary",
        basePackages= { "com.blake.demo.p" })
public class PrimaryConfig {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Primary
    @Bean(name = "entityManagerPrimary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
    }

    @Primary
    @Bean(name = "entityManagerFactoryPrimary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(primaryDataSource)
                .packages("com.blake.demo.p")
                .persistenceUnit("primaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Primary
    @Bean(name = "transactionManagerPrimary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
    }

}

Secondary 설정

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactorySecondary",
        transactionManagerRef="transactionManagerSecondary",
        basePackages= { "com.blake.demo.s" })
public class SecondaryConfig {

    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    private Map<String, Object> getVendorProperties() {
        return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
    }

    @Bean(name = "entityManagerSecondary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactorySecondary(builder).getObject().createEntityManager();
    }

    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(secondaryDataSource)
                .packages("com.blake.demo.s")
                .persistenceUnit("secondaryPersistenceUnit")
                .properties(getVendorProperties())
                .build();
    }

    @Bean(name = "transactionManagerSecondary")
    PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
    }

}

테스트 코드 작성

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaDataSourceDemoApplicationTests {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MessageRepository messageRepository;

    @Test
    public void test() throws Exception {
        userRepository.save(new User("aaa", 10));
        userRepository.save(new User("bbb", 20));
        userRepository.save(new User("ccc", 30));
        userRepository.save(new User("ddd", 40));
        userRepository.save(new User("eee", 50));

        Assert.assertEquals(5, userRepository.findAll().size());

        messageRepository.save(new Message("o1", "aaaaaaaaaa"));
        messageRepository.save(new Message("o2", "bbbbbbbbbb"));
        messageRepository.save(new Message("o3", "cccccccccc"));

        Assert.assertEquals(3, messageRepository.findAll().size());
    }

}

끝!

'Springboot2.x 강좌 > DB연결' 카테고리의 다른 글

트랜잭션 기본설정 알아보기  (0) 2023.01.05
MyBatis 의 다중 DataSource  (0) 2023.01.05
JdbcTemplate 다중 DataSource  (0) 2023.01.05
XML 로 Mybatis 설정하기  (0) 2023.01.05
Mybatis 로 Mysql 연결하기  (0) 2023.01.05
728x90
application.properties 설정
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=123456
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
 

또는 application.yaml 설정

spring:
    datasource:
        primary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test1
            password: root
            username: root
        secondary:
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/test2
            password: root
            username: root
단일 datasource 설정과 다르게 다중 datasource 는 spring.datasource 뒤에 접두사 primary 와 secondary 로 구분하고 datasource 를 초기화 할때 사용하게 된다. Springboot 2.x 은 spring.datasource.secondary.jdbc-url 를 사용하고 Springboot 1.x 은 spring.datasource.secondary.url 를 사용하한다. 실행시 java.lang.IllegalArgumentException:jdbcUrl is required with driverClassName 발생하면 해당 버전별 설정이 제대로 되었는지 확인해보면 된다.
 
DataSource 초기화 및 JdbcTemplate 설정
@Configuration
public class DataSourceConfiguration {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        return new JdbcTemplate(primaryDataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new JdbcTemplate(secondaryDataSource);
    }

}

테스트 코드 작성

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter37ApplicationTests {

    @Autowired
    protected JdbcTemplate primaryJdbcTemplate;

    @Autowired
    protected JdbcTemplate secondaryJdbcTemplate;

    @Before
    public void setUp() {
        primaryJdbcTemplate.update("DELETE  FROM  USER ");
        secondaryJdbcTemplate.update("DELETE  FROM  USER ");
    }

    @Test
    public void test() throws Exception {

        primaryJdbcTemplate.update("insert into user(name,age) values(?, ?)", "aaa", 20);
        primaryJdbcTemplate.update("insert into user(name,age) values(?, ?)", "bbb", 30);

        secondaryJdbcTemplate.update("insert into user(name,age) values(?, ?)", "ccc", 20);

        Assert.assertEquals("2", primaryJdbcTemplate.queryForObject("select count(1) from user", String.class));

        Assert.assertEquals("1", secondaryJdbcTemplate.queryForObject("select count(1) from user", String.class));
    }

}

'Springboot2.x 강좌 > DB연결' 카테고리의 다른 글

MyBatis 의 다중 DataSource  (0) 2023.01.05
Spring Data JPA 다중 DataSource  (0) 2023.01.05
XML 로 Mybatis 설정하기  (0) 2023.01.05
Mybatis 로 Mysql 연결하기  (0) 2023.01.05
Spring Data JPA  (0) 2023.01.05

+ Recent posts