트랜잭션의 성격을 'ACID 원칙'으로 설명하곤 하는데 다음과 같다.
원자성(Atomicity) | 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다. 좀 더 쉽게 말하자면 어떤 트랜잭션이 A와 B로 구성된다면 항상 A,B의 처리 결과는 동일한 결과이어야 한다. 즉 A는 성공했지만, B는 실패할 경우 A,B는 원래 상태로 되돌려져야만 한다. 어떤 작업이 잘못되는 경우 모든 것은 다시 원점으로 되돌아가야만 한다. |
일관성(Consistency) | 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야만 한다. 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야만 한다. |
격리(Isolation) | 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야만 한다. |
영속성(Durability) | 트랜잭션이 성공적으로 처리되면, 그 결과는 영속적으로 보관되어야 한다. |
1. 데이터 베이스 설계와 트랜젝션
데이터베이스의 저장 구조를 효율적으로 관리하기 위해서 흔히 '정규화'라는 작업을 한다. '정규화'의 가장 기본은 '중복된 데이터를 제거'해서 데이터 저장의 효율을 올리자는 것이다. 정규화를 진행하면 1) 테이블은 늘어나고, 2) 각 테이블의 데이터 양은 줄어드는 것이 일반적이다.
정규화가 잘 된 데이터베이스의 설계에서는 '트랜잭션'이 많이 일어나지는 않는다. 정규화가 진행될수록 테이블은 점점 더 순수한 형태가 되어가는데, 순수한 형태가 될수록 '트랜잭션 처리'의 대상에서 멀어진다. 정규화를 진행할 수록 테이블은 더욱 간결해지지만 반대로 쿼리 등을 이용해서 필요한 데이터를 가져오는 입장에서는 점점 불편해 진다. 현재 상황을 알기 위해서는 단순히 조회를 하는 것이 아니라 직접 조인이나 서브쿼리를 이용해서 처리해야 하기 때문이다.
조인이나 서브쿼리를 이용하게 되면 다시 성능의 이슈가 발생할 수 있다. 매번 계산이 발생하도록 만들어지는 쿼리의 경우 성능이 저하되기 때문에 많은 양의 데이터를 처리해야 하는 상황에서는 바람직하지 않을 수 있다. 이런 상황에서는 흔히 '반정규화' (혹은 역정규화)를 하게 된다. 정규화의 반대이므로 중복이나 계산되는 값을 데이터베이스 상에 보관하고, 대신에 조인이나 서브쿼리의 사용을 줄이는 방식이다.
반정규화의 가장 흔한 예가 '게시물의 댓글'의 경우이다. 게시물의 목록 페이지에서 일반적으로 댓글의 숫자도 같이 표시한다. 이때, 댓글의 숫자를 칼럼으로 처리하게 되면 게시물의 목록을 가져올 경우에는 tbl_reply 테이블을 이용해야 하는 일이 없기 때문에 성능상으로 좀 더 이득을 볼 수 있게 된다!
반정규화는 이처럼 중복이나 계산의 결과를 미리 보관해서 좀 더 빠른 결과를 얻기 위한 노력이다. 반정규화를 하게 되면 쿼리가 단순해지고 성능상으로도 얻을 수 있는 이득이 있지만, 대신에 댓글이 추가될 때에는 댓글을 의미하는 tbl_reply 테이블에 insert하고, 댓글의 숫자는 tbl_board 테이블에 update를 시켜주는 작업이 필요하다. 이 두 작업이 하나의 트랜잭션으로 관리되어야 하는 작업이다.!
2. 트랜잭션 설정 실습
스프링의 트랜잭션 설정은 AOP와 같이 XML을 이용해서 설정하거나 어노테잇녀을 이용해서 설정이 가능하다. 우선 스프링의 트랜잭션을 이용하기 위해서는 Transaction Manager라는 존재가 필요하다.
1) pom.xml에는 spring-jdbc, spring-tx 라이브러리를 추가하고, mybatis, mybatis-spring, hikari 등의 라이브러리를 추가한다.
2) root-context.xml에서는 Namespaces 탭에서 'tx' 항목을 체크한다.
3) root-context.xml 에는 트랜잭션을 관리하는 빈을 등록하고, 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 <tx:annotation-driven> 태그를 등록한다.
<bean>으로 등록된 transactionManager와 <tx:annotation-driven> 설정이 추가된 후에는 트랜잭션이 필요한 상황을 만들어서 어노테이션을 추가하는 방식으로 설정하게 된다.
2-2. 예제 테이블 생성
트랜잭션의 실습은 간단히 2개의 테이블을 생성하고, 한 번에 두개의 테이블에 insert 해야하는 상황을 재현하도록 한다.
1) 예제로 사용할 테이블을 아래와 같이 생성한다. (sqldeveloper)
tbl_sample1 테이블의 col1의 경우는 varchar2(500)으로 설정된 반면에 tbl_sample2는 varchar2(50)으로 설정되었다. 만일 50바이트 이상의 데이터를 넣는 상황이라면 tbl_sample1에는 정상적으로 insert 되지만, tbl_sample2에는 insert 시 칼럼의 최대 길이보다 크기 때문에 문제가 있게 된다.
2) org.zerock.mapper 패키지에 Sample1Mapper 인터페이스와 Sample2Mapper 인터페이스를 추가한다.
- Sample1Mapper 인터페이스
package org.zerock.mapper;
import org.apache.ibatis.annotations.Insert;
public interface Sample1Mapper {
@Insert("insert into tbl_sample1 (col1) values (#{data})")
public int insertCol1(String data);
}
- Sample2Mapper 인터페이스
package org.zerock.mapper;
import org.apache.ibatis.annotations.Insert;
public interface Sample2Mapper {
@Insert("insert into tbl_sample2 (col2) values (#{data})")
public int insertCol2(String data);
}
2-3. 비즈니스 계층과 트랜잭션 설정
1) 트랜잭션은 비즈니스 계층에서 이루어지므로, org.zerock.service 계층에서 Sample1Mapper, Sample2Mapper를 사용하는 SampleTxService 인터페이스, SampleTxServiceImpl 클래스를 설계한다.
트랜잭션의 설정이 안되어 있는 상태를 먼저 테스트하기 위해서 기존 방식처럼 코드를 작성한다.
- SampleTxService 인터페이스
package org.zerock.service;
public interface SampleTxService {
public void addData(String value);
}
- SampleTxServiceImple 클래스
package org.zerock.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zerock.mapper.Sample1Mapper;
import org.zerock.mapper.Sample2Mapper;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@Service
@Log4j
public class SampleTxServiceImpl implements SampleTxService{
@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.........................");
}
}
SampleTxService는 addData() 라는 메서드를 통해서 데이터를 추가한다. SampleTxServiceImpl 클래스는 Sample1Mapper와 Sample2Mapper 모두를 이용해서 같은 데이터를 tbl_sample2과 tbl_sample2 테이블에 insert하도록 작성한다.
2) src/test/java에 SampleTxService를 테스트 하는 SampleTxServiceTests 클래스를 작성한다.
package org.zerock.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class SampleTxServiceTests {
@Setter(onMethod_ = {@Autowired})
private SampleTxService service;
@Test
public void testLong() {
String str = "Starry\r\n" + "Starry night\r\n" + "Paint your palette blue and grey\r\n"
+ "Look out on a summer's day";
log.info(str.getBytes().length);
service.addData(str);
}
}
testLong()은 50bytes가 넘고 500bytes를 넘지 않는 길이의 어떤 문자열을 이용해서 tbl_sample1, tbl_sample2 테이블에 insert를 시도한다. testLong()을 실행하면 tbl_sample1에는 데이터가 추가되지만, tbl_sample2에는 길이의 제한으로 인해서 insert가 실패하게 된다. 테스트 코드를 실행했을 때에는 아래와 같은 결과를 보게 된다.
테스트에 사용한 문자열은 82bytes 였으므로 tbl_sample1에는 아래와 같이 정상적으로 insert가 되고, tbl_sample2에는 insert가 실패한다.
2-4. @Transactional 어노테이션
위의 결과를 보면 트랜잭션 처리가 되지 않았기 때문에 하나의 테이블에만 insert가 성공한 것을 볼 수 있다. 만일 트랜잭션 처리가 되었다면 tbl_sample1과 tbl_sample2 테이블 모두에 insert가 되지 않았어야 하므로,
1) 트랜잭션 처리가 될 수 있도록 SampleTxServiceImpl의 addData()에 @Transactional을 추가한다.
기존의 코드에서 달라지는 부분은 @Transactional 어노테이션이 추가된 것뿐이다. 이클립스에서 트랜잭션은 AOP와 마찬가지로 아이콘을 통해서 트랜잭션 처리가 된 메서드를 구분해 준다.
정확한 테스트를 위해서 이전에 성공한 tbl_sample1의 데이터를 삭제하고, commit한다.
양쪽 테이블에 모든 데이터가 없는 것을 확인한 후에 다시 테스트 코드를 실행한다. 동일한 코드였지만 @Transactional이 추가된 후에는 실행 시 rollback() 되는 것을 확인할 수 있다.
'Spring' 카테고리의 다른 글
[29] AOP와 트랜잭션 - 댓글과 댓글 수에 대한 처리 (0) | 2020.01.05 |
---|---|
ERROR: org.springframework.test.context.TestContextManager - Caught exception while allowing (3) | 2020.01.05 |
[27] AOP와 트랜잭션 - AOP 이론과 예제 실습 (0) | 2020.01.05 |
[26] REST 방식과 Ajax를 이용하는 댓글처리 - 이벤트 처리와 HTML 처리 (0) | 2020.01.04 |
[25] REST 방식과 Ajax를 이용하는 댓글 - Ajax 댓글 처리 3 (0) | 2019.12.31 |