스프링 - 트랜잭션

김주언·2022년 10월 16일
0

Spring

목록 보기
11/15
post-thumbnail

트랜잭션

트랜잭션 : 쪼개질 수 없는 하나의 단위 작업. 즉 한번에 이루어지는 작업의 단위

트랜잭션의 특징 (ACID)

  • 원자성 (Atomicity)
    하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다.

  • 일관성 (Consistency)
    트랜잭션이 성공했다면 DB의 모든 데이터는 일관성을 유지해야한다.

  • 격리성 (Isolation)
    트랜잭션으로 처리되는 중간에 외부에서의 간섭이 없어야한다.

  • 영속성 (Durability)
    트랜잭션이 성공적으로 처리되는 경우 그 결과는 영구적으로 보관되어야 한다.

트랜잭션 예

트랜잭션의 가장 흔한 예제는 계좌이체

계좌이체라는 하나의 행위는 내부적으로 출금과 입금으로 나뉘어진다.

  • 출금 : 이체를 수행하는 계좌에서는 출금이 발생한다.
  • 입금 : 이체의 대상 계좌에서는 입금이 발생한다

즉 계좌 이체는 엄밀히 따지자면 출금과 입금이라는 각각의 거래가 한 단위를 이루는 것이다.


비즈니스에서 하나의 트랜잭션은 데이터베이스 상에서는 하나 혹은 여러개의 작업이 하나의 묶음을 이루는 경우가 많다

위의 1/2번 과정과 3/4번 과정을 트랜잭션으로 관리할 때, 프로그래밍에서는 두 과정의 관계를 AND 연산과 유사하게 본다. ( (1,2) && (3,4) )

영속 계층에서 출금과 입금은 각각 DB와 연결을 맺은 후 처리된다. 출금과 입금을 하나의 트랜잭션으로 처리 할 때, 출금과 입금 둘 중 하나의 작업이 실패하는 경우 이미 성공한 작업 또한 다시 롤백해야 한다.

별도의 프레임워크를 사용하지 않고 순수하게 JDBC를 이용하여 출금과 입금을 처리하는 경우 두 작업 중 어느 하나가 실패할 때를 대비하는 코드가 복잡하게 작성된다.

스프링은 이러한 복잡한 트랜잭션 작업을 간단히 XML설정을 이용하거나 어노테이션을 이용하여 간단하게 처리할 수 있다.


데이터 베이스 설계와 트랜잭션

데이터베이스 설계 시 정규화를 진행하는데, 정규화가 잘 되어있거나 위의 규칙들이 반영되어 있는 데이터베이스 설계에서는 트랜잭션이 많이 발생하지 않는다. 그러나 정규화가 잘 되어 있을수록 테이블은 간결해지지만 쿼리를 이용하여 데이터를 추출하는 것은 조인이나 서브쿼리가 많아지기 때문에 불편해진다.

그런데! 조인이나 서브쿼리를 많이 쓰면 또 성능 이슈가 발생한다~!! 이러면 또 다시 반정규화를 한다 ^^..

반정규화의 가장 흔한 예가 바로 게시물의 댓글이다. 정규화 규칙을 따랐을 경우 게시물 테이블과 게시물의 댓글 테이블은 아래와 같은 구조를 가지게 된다.

게시물 테이블은 게시물에 관한 정보만을 가지고, 댓글 테이블은 댓글에 관한 정보만을 가지는 구조이다. 이러한 구조에서의 문제점은, 목록페이지에서 댓글의 숫자 또한 같이 표시되는 경우 두 개의 테이블에 접근해야 한다는 점이다.

댓글의 숫자를 표시하기 위해서 조인이나 서브쿼리를 이용하기 보다는, 게시물 테이블에 댓글의 숫자를 칼럼으로 처리하는 것이 게시물 목록을 가져올 때 댓글 테이블을 추가적으로 접근할 필요가 없기 때문에 성능상으로 좀 더 이득이 된다.

반정규화를 통해서 쿼리가 단순해지고 성능상으로도 이득이 생겼다.

그러나, 댓글이 추가되는 경우 댓글 테이블에 insert를 하고 댓글의 숫자는 게시물 테이블에 update 해주는 작업이 필요해졌다.
이러한 두 개의 작업은 하나의 트랜잭션으로 관리되어야 하는 작업이다.



트랜잭션 설정 실습

스프링의 트랜잭션 설정은 AOP와 같이 XML 또는 어노테이션 설정이 가능하다.
그 전에 스프링 트랜잭션을 이용하기 위해서는 Transaction Manager가 필요하다.

pom.xml 수정

pom.xml에 spring-jdbc, spring-tx, mybatis, mybatis-spring, hikari 등의 라이브러리를 추가한다.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.22</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>5.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.22</version>
        </dependency>
        <dependency>
            <groupId>org.bgee.log4jdbc-log4j2</groupId>
            <artifactId>log4jdbc-log4j2-jdbc4</artifactId>
            <version>1.16</version>
        </dependency>

root-context.xml 수정

<!--    mlns:tx   자동완성 이용해서 스키마 작성 시 tx로 선택할 것 주의-->

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	   					   http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig" >

        <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"/>
        <property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@//localhost:1521/XEPDB1"/>
        <property name="username" value="[DB유저네임]"/>
        <property name="password" value="[DB패스워드]" />
    </bean>

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
        <constructor-arg ref="hikariConfig"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <tx:annotation-driven/>

    <mybatis-spring:scan base-package="com.ze.mapper" />
    <context:component-scan base-package="com.ze.service"/>
    <context:component-scan base-package="com.ze.aop"/>

    <aop:aspectj-autoproxy/>
</beans>

예제 테이블 생성

create table tbl_sample(col1 varchar2(500));
create table tbl_sample2(col2 varchar2(50));

첫번째 테이블은 길이가 긴 문자열이 들어갈 수 있지만 두번째 테이블은 불가능하다.
하나의 서비스에서 위 두개의 테이블을 모두 사용하는 메서드를 정의하여 이를 테스트해본다,

Mapper 작성

Sample1Mapper

package com.ze.mapper;

import org.apache.ibatis.annotations.Insert;

public interface Sample1Mapper {
// MyBatis의 어노테이션, #{} 를 이용하여 전달받은 매개변수를 사용할 수 있다.
    @Insert("insert into tbl_sample (col1) values (#{data})")
    public int insertCol1(String data);

}

Sample2Mapper

package com.ze.mapper;

import org.apache.ibatis.annotations.Insert;

public interface Sample2Mapper {
    @Insert("insert into tbl_sample2 (col2) values (#{data})")
    public int insertCol2(String data);
}

비즈니스 계층 설정

서비스 인터페이스와 해당 서비스를 구현하는 Impl 클래스를 작성한다.

SampleTxService

package com.ze.service;

public interface SampleTxService {
    public void addData(String value);
}

SampleTxServiceImpl

하나의 메서드 내에서 두 개의 다른 테이블에 접근하는 것을 확인할 수 있다.

package com.ze.service;

// 생략

@Service		// 서비스 역할을 수행하는 빈임을 나타낸다
@Log4j2			// 로그 기록을 위한 어노테이션
public class SampleTxServiceImpl implements SampleTxService{
// Setter 메서드를 통해서 Sample1Mapper 인스턴스를 주입한다. 
    @Setter(onMethod_ = {@Autowired})	
    private Sample1Mapper mapper1;

    @Setter(onMethod_ = {@Autowired})
    private Sample2Mapper mapper2;

    @Override
    public void addData(String value) {
        log.info("-------------------- Mapper1 --------------------");
        mapper1.insertCol1(value);

        log.info("-------------------- Mapper2 --------------------");
        mapper2.insertCol2(value);

        log.info("-------------------- End --------------------");

    }
}

이 상태에서 테스트를 수행해보도록 한다.

SampleTxServiceTests

package com.ze.service;

// import 생략
// 현재 테스트 코드가 스프링을 실행하는 역할을 함을 알린다
@RunWith(SpringJUnit4ClassRunner.class)
// 지정된 문자열을 이용하여 필요한 객체들을 스프링에 빈으로 등록한다
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j2
public class SampleTxServiceTests {
    @Setter(onMethod_ = @Autowired)
    private SampleTxService service;

	//JUnit에서 테스트 대상을 표시하는 어노테이션
    @Test
    public void testLong(){
        String str = "qwertyuiopasdfghjklzxcvbnmqwdfwseqbgdyikfsvhuiqhreiwertyuiopsdfghjklxcvbnmasdadwq";
        log.info(str.getBytes().length);

        service.addData(str);
    }
}

너무 길어서 안들어간다는 오류가 발생한다. 즉 첫번째 테이블에서는 성공적으로 insert 됐으나 두번째 테이블에서는 insert가 실패한 상황.

이를 트랜잭션으로 관리하는 경우였다면, 첫번째 테이블에 insert가 성공했으나, 두번째 테이블에서의 insert 실패로 인해서 첫번째 테이블 또한 insert를 취소해야한다.

이러한 트랜잭션 처리를 위해서 @Transactional 어노테이션을 사용할 수 있다.

@Transactional

SampleTxServiceImpl 수정

package com.ze.service;
// ...

@Service
@Log4j2
public class SampleTxServiceImpl implements SampleTxService{
	// ...
    
    @Transactional		// 추가
    @Override
    public void addData(String value) {
        log.info("-------------------- Mapper1 --------------------");
        mapper1.insertCol1(value);

        log.info("-------------------- Mapper2 --------------------");
        mapper2.insertCol2(value);

        log.info("-------------------- End --------------------");

    }
}

어노테이션 설정 후 다시 테스트를 수행한 결과는 아래와 같다

insert가 실패하고 rollback()이 수행된다

@Transactional 속성

속성 설정 방법

@Transactional(propagation = Propagation.MANDATORY)

1. 전파속성 (propagation)

: 전파옵션이라는 것은 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 것

옵션설명
MANDATORY작업은 반드시 특정한 트랜잭션이 존재한 상태에서만 가능하다.
NESTED해당 메서드가 부모 트랜잭션에서 진행될 경우 별개로 커밋되거나 롤백될 수 있음. 둘러싼 트랜잭션이 없을 경우 REQUIRED와 동일하게 작동
NEVER트랜잭션 상황 하에 실행되면 예외가 발생한다.
NOT_SUPPORTED트랜잭션 있는 경우에 트랜잭션이 끝날 때까지 보류 된 후 실행된다
REQUIRED부모 트랜잭션 내에서 실행하며 부모 트랜잭션이 없을 경우 새로운 트랜잭션을 생성
REQUIRED_NEW부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성. 자신만의 고유한 트랜잭션으로 실행된다.
SUPPORTS트랜잭션을 필요로 하지는 않으나 트랜잭션 상황 하에 있다면 포함되어서 실행된다.



2. 격리 레벨 ( isolation )

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

옵션레벨설명
DEFAULTDB 기본 설정, 기본 격리 수준
SERIALIZABLE3가장 높은 격리 수준. 완벽한 읽기 일관성 모드를 제공
REPEATEABLE_READ2동일 필드에 대해 다중 접근 시 모두 동일한 결과를 보장한다.
트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 shared lock이 걸리므로 다른 사용자는 그 영역에 해당되는 데이터에 대한 수정이 불가능하다.
READ_COMMITED1트랜잭션이 커밋되어 확정된 데이터만을 읽는 것을 허용한다
어떠한 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 해당 데이터에 접근할 수 없다.
READ_UNCOMMITED0커밋되지 않은 데이터에 대한 읽기를 허용한다.
어떤 사용자가 A라는 데이터를 B라는 데이터로 변경하는 동안 다른 사용자는 B라는 아직 완료되지 않은(Uncommitted 혹은 Dirty) 데이터 B를 읽을 수 있다.


3. no-rollback-for - 예외처리 (기본값 : 없음)

특정 예외가 발생하더라도 롤백되지 않도록 설정

4. Rollback-for-예외

특정 예외가 발생하면 강제로 롤백한다.


@Transactional 적용 순서

@Transactional 어노테이션은 메서드에 설정하는 것도 가능하고, 클래스나 인터페이스에 설정하는 것 또한 가능하다.

  • 어노테이션 우선 순위
    1. 메서드의 @Transactional
    2. 클래스의 @Transactional
    3. 인터페이스의 @Transactional
profile
학생 점심을 좀 차리시길 바랍니다

0개의 댓글