java, spring

[Spring] 스프링에서 트랜잭션 관리

isaac.kim 2021. 8. 22.
728x90
반응형

[Spring] 스프링에서 트랜잭션 관리

 

비즈니스에서 쪼갤 수 없는 하나의 단위 작업을 말할 때 트랜잭션(Transaction)이라는 용어를 사용합니다. 사전적인 의미로 트랜잭션은 '거래'라는 뜻을 갖지만, 현실적으로 '한 번에 이루어지는 작업의 단위'를 트랜잭션으로 간주합니다.

 

트랜잭션의 성격을 'ACID 원칙'으로 설명하곤 합니다.

(데이터베이스 수업으로 돌아간 것 같네요ㅎㅎ)

 

원자성(Atomicity) 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 합니다. 어떤 트랜잭션이 A와 B로 구성된다면 항상 A, B의 처리 결과는 동일한 결과이어야 합니다. 즉 A는 성공했지만, B는 실패할 경우 A, B는 원래 상태로 되돌려져야만 합니다. 어떤 작업이 잘못되는 경우 모든 것은 다시 원점으로 되돌아가야만 합니다.
일관성(Consistency) 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야만 합니다. 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야만 합니다.
격리(Isolation) 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야만 합니다.
영속성(Durability) 트랜잭션이 성공적으로 처리되면, 그 결과는 영속적으로 보관되어야 합니다.

트랜잭션에 가장 흔한 예제는 '계좌 이체'입니다. '계좌 이체'라는 행위가 내부적으로는 하나의 계좌에서는 출금이 이루어져야 하고, 이체의 대상 계좌에서는 입금이 이루어져야만 합니다. '계좌 이체'는 엄밀하게 따져보면 '출금'과 '입금'이라는 각각의 거래가 하나의 단위를 이루게 되는 상황입니다.

 

비즈니스에서 하나의 트랜잭션은 데이터베이스 상에서는 하나 혹은 여러 개의 작업이 같은 묶음을 이루는 경우가 많습니다. 예를 들어 비즈니스 계층에서 '계좌 이체'는 bankTransfer( )라는 메서드로 정의되고, 계좌 내에 입금과 출금은 deposit( )(입금), withdraw( )(출금)이라는 메서드로 정의된다고 가정합니다.

 

deposit( )과 withdraw( )는 각자 고유하게 데이터베이스와 커넥션을 맺고 작업을 처리합니다. 문제는 withdraw( )는 정상적으로 처리되었는데, deposit( )에서 예외가 발생하는 경우입니다. 이미 하나의 계좌에선 돈이 빠져나갔지만, 상대방의 계좌에는 돈이 입금되지 않은 상황이 될 수 있습니다.

 

'트랜잭션으로 관리한다.' 혹은 '트랜잭션으로 묶는다.'는 표현은 프로그래밍에서는 'AND' 연산과 유사합니다.

 

영속 계층에서 withdraw( )와 deposit( )은 각각 DB 커넥션을 맺고 처리하는데 하나의 트랜잭션으로 처리할 경우, 한쪽이 잘못된 경우에는 이미 성공한 작업까지 원상태로 복구되어야 합니다. 별도의 패턴이나 프레임워크를 사용하지 않는 코드라면 어느 한쪽이 실패할 때를 염두에 두는 코드를 복잡하게 만들어야 합니다. 스프링은 이러한 트랜잭션 처리를 간단히 XML 설정을 이용하거나, 어노테이션 처리만으로 할 수 있습니다.


트랜잭션 설정

스프링의 트랜잭션 설정은 AOP와 같이 XML or 어노테이션을 이용해 설정할 수 있습니다. 스프링의 트랜잭션을 이용하기 위해서는 Transaction Manager라는 존재가 필요합니다.

 

pom.xml에는 spring-jdbc, spring-tx 라이브러리를 추가하고, mybatis. mybatis-spring, hikari 등의 라이브러리를 추가합니다.

 

pom.xml

root-context.xml에서는 Namespaces 탭에서 'tx' 항목을 체크합니다.

root-context.xml에는 트랜잭션을 관리하는 빈(객체)를 등록하고, 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 <tx:annotation-driven> 태그를 등록합니다.

 

root-context.xml

 

<bean>으로 등록된 transactionManager와 <tx:annotation-driven /> 설정이 추가된 후에는 트랜잭션이 필요한 상황을 만들어서 어노테이션을 추가하는 방식으로 설정하게 됩니다.


트랜잭션 실습

예제 테이블

 

table : tb_sp1

columns : col varchar(5)

 

table : tb_sp2

columns : col varchar(10)

 

트랜잭션에 사용할 테이블을 두 개 준비했습니다. 한 번에 두 개의 테이블에 insert해야 하는 상황을 재현합니다.

 

tb_sp1.col에는 다섯 글자까지 들어갈 수 있고, tb_sp2.col에는 열 글자까지 들어갈 수 있습니다. 양쪽에 10글자의 데이터를 넣는다면 tb_sp1에서는 실패가 될 것이고, tb_sp2에서는 성공하게 될 것입니다. 트랜잭션 처리로 어떻게 처리가 되는지 확인해보겠습니다.

 

com.isaac.mapper 패키지 아래 SampleMapper, SampleMapper2 클래스를 생성합니다.

SampleMapper는 tb_sp1에 SampleMapper2는 tb_sp2에 데이터를 insert 하게 작성합니다.

 

root-context.xml 에 아래 태그를 추가합니다. (namespace에서 태그 추가 후 적용할 것.)

<mybatis-spring:scan base-package="com.isaac.mapper"/>

비즈니스 계층과 트랜잭션 설정

트랜잭션은 비즈니스 계층에서 이루어지므로, com.isaac.service 계층에서 SampleMapper, SampleMapper2를 사용하는 SampleTxService 인터페이스, SampleTxServiceImpl 클래스를 설계합니다.

 

SampleTxService 인터페이스

SampleTxServiceImpl 클래스

SampleTxServiceImple 클래스는 SampleMapper, SampleMapper2 모두를 이용해서 같은 데이터를 tb_sp1, tb_sp2 테이블에 insert 하도록 작성합니다. 

 

다음 SampleTxService를 테스트하는 테스트 코드를 작성합니다.

 

'src/test/java' 폴더 아래 com.isaac.service 패키지 아래 SampleTxServiceTests 클래스

1. tb_sp1에 다섯 글자 등록이 가능하고, tb_sp2에도 다섯 글자 등록이 가능하니 에러 없이 등록됩니다.

 

글자를 늘려서 HELLOWORLD 열 글자를 넣어보겠습니다. tb_sp1부터 실패할 것입니다.

첫 번째 insert를 하는 것부터 에러가 나타납니다. table도 확인해보면 tb_sp1, tb_sp2 모두 저장되어 있지 않습니다.

그럼 트랜잭션 처리로 인해 rollback 된 것으로 볼 수 있을까요?

 

이번에는 길이에 맞는 tb_sp2에 데이터를 먼저 넣어보겠습니다.

SampleTxServiceImpl

이번에는 다른 결과를 보입니다. 두번째 테이블에 데이터를 먼저 넣으니 데이터 등록이 성공이 되었고, 첫번째 테이블에는 실패하여 데이터가 들어가지 못했습니다.

트랜잭션 처리가 되지 않은 것이라 볼 수 있습니다. 데이터를 모두 삭제하고 트랜잭션을 설정하고 다시 테스트해보겠습니다.

 

@Transactional 어노테이션

아까와 같은 테스트를 진행할 것인데, 트랜잭션을 적용하여 두 테이블 모두 데이터가 들어가지 않도록 합니다.

(한 쪽에서 실패했기 때문에)

 

SampleTxServiceImpl 클래스의 addData( )에 @Transactional 추가

이클립스에서 트랜잭션은 AOP와 마찬가지로 아이콘을 통해 트랜잭션 처리가 된 메서드를 구분해 줍니다.

 

다시 아까 사용했던 테스트 코드로 테스트 합니다.

 

실행 결과 중 rollback( )이 확인됩니다. 실제 테이블 데이터도 적용되었는지 확인합니다.

테이블에서도 확인할 수 있듯이 tb_sp1, tb_sp2 모두 0 rows를 나타내고 있는 것을 확인할 수 있습니다.

 


@Transactional 어노테이션 속성

@Transactional 어노테이션은 중요한 속성을 갖고 있습니다. 경우에 따라 속성들을 조정해서 사용해야 합니다.

 

전파(Propagation) 속성

- PROPAGATION_MADATORY : 작업은 반드시 특정한 트랜잭션이 존재한 상태에서만 가능

- PROPAGATION_NESTED : 기존에 트랜잭션이 있는 경우, 포함되어서 실행

- PROPAGATION_NEVER : 트랜잭션 상황하에 실행되면 예외 발생

- PROPAGATION_NOT_SUPPORTED : 트랜잭션이 있는 경우엔 트랜잭션이 끝날때까지 보류된 후 실행

- PROPAGATION_REQUIRED :트랜잭션이 있으면 그 상황에서 실행, 없으면 새로운 트랜잭션 실행 (기본 설정)

- PROPAGATION_REQUIRED_NEW :대상은 자신만의 고유한 트랜잭션모드로 실행

- PROPAGATION_SUPPORTS : 트랜잭션을 필요로 하지 않으나, 트랜잭션 상황하에 있다면 포함되어서 실행

 

격리(iSOLATION) 레벨

- DEFAULT : DB 설정, 기본 격리 수준(기본 설정)

- SERIALIZABLE : 가장 높은 격리, 성능 저하의 우려가 있음

- READ_UNCOMMITED : 커밋되지 않은 데이터에 대한 읽기를 허용

- READ_COMMITED : 커밋된 데이터에 대해 읽기 허용

- REPEATEABLE_READ : 동일 필드에 대해 다중 접근 시 모두 동일한 결과를 보장

 

Read-only 속성

- true인 경우 insert, update, delete 실행 시 예외 발생, 기본 설정은 false

 

Rollback-for-예외

- 특정 예외가 발생 시 강제로 Rollback

 

No-rollback-for-예외

- 특정 예외의 발생 시에는 Rollback 처리되지 않음

 

위 속성들은 모두 @Transactional을 설정할 때 속성으로 지정할 수 있습니다.


@Transactional 적용 순서

스프링은 간단한 트랜잭션 매니저의 설정과 @Transactional 어노테이션을 이용한 설정만으로 애플리케이션 내의 트랜잭션에 대한 설정을 처리할 수 있습니다.

 

@Transactional 어노테이션의 경우 위와 같이 메서드에 설정하는 것도 가능하지만, 클래스나 인터페이스에 선언하는 것 역시 가능합니다.

 

어노테이션의 우선 순위는 다음과 같습니다.

 

- 메서드의 @Transactional 설정이 가장 우선시 됩니다.

- 클래스의 @Transactional 설정은 메서드보다 우선순위가 낮습니다.

- 인터페이스의 @Transactional 설정이 가장 낮은 우선순위입니다.

 

위 규칙대로 적용되는 것을 기준으로 작성하자면 인터페이스에는 가장 기준이 되는 @Transactional과 같은 설정을 지정하고, 클래스나 메서드에 필요한 어노테이션을 처리하는 것이 좋습니다.


 

 

728x90
반응형