안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
트랜잭션 처리
- Spring에서의 트랜잭션은 AOP로 처리됨 (보조업무)
- 트랜잭션에 대한 Aspect, Advice은 이미 작성 되어있으므로 별도 작성할 필요 없음
pom.xml
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- #9. AOP관련 의존 추가 -->
<!-- #11 트랜잭션 처리 의존 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
root-context.xml
트랜잭션은 DB와 관련되어있으므로, root-context
<!-- #11.1 트랜잭션 매니저 빈 등록 (트랜잭션 처리) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- @Transactional 어노테이션에 Transaction Advice를 적용 -->
<tx:annotation-driven transaction-manager="transactionManager"/> <!-- 빈을 가져다사용!(기본값) -->
transaction-manager는 등록한 트랜잭션 매니저 빈을 가져다 사용한다는 의미이며, 기본 값으로 설정되어있기 때문에 생략 가능합니다.
다만, 트랜잭션매니저 빈의 id값이 다르다면 반드시 설정해줘야겠죠.
Service
BoardService interface 생략
BoardServiceImpl
@Transactional(rollbackFor = Exception.class)
@Override
public int insertBoard(Board board) {
// insert board
int result = boardDao.insertBoard(board);
log.debug("board#no = ", board.getNo());
// insert Attachment
List<Attachment> attachments = board.getAttachments();
if(!attachments.isEmpty()) {
for(Attachment attach : attachments) {
attach.setBoardNo(board.getNo());
result = boardDao.insertAttachment(attach);
}
}
return result;
}
@Transactional 어노테이션을 이용해 트랜잭션 처리할 메소드임을 명시해줍니다.
기본적으로 RuntimeException에 대해 트랜잭션 처리하므로 rollbackFor 속성을 이용해 Exception에 대해서 트랜잭션 처리하도록 범위를 확장해주었습니다.
※ rollbackFor 속성 - 예외가 던져지면 전체 롤백처리!
Attachment의 테이블명을 잘못 설정하고 게시글 등록 시
트랜잭션 처리가 잘 되어 Board테이블과 Attachment 테이블 모두 데이터가 들어가지 않은 것을 확인할 수 있습니다.
클래스 레벨에 어노테이션을 붙이면 메소드들에 한하여 트랜잭션 처리!
@Transactional(rollbackFor = Exception.class)
@Service
@Slf4j
public class BoardServiceImpl implements BoardService {
트랜잭션 처리가 없는 DQL에 대해선 readOnly 속성을 이용해 DML이 발생하면 예외가 발생되도록 제한할 수 있습니다.
# Transaction
jdbc, mybatis에서 각각 Connection객체, SqlSession객체에 대해서 commit/rollback을 했던 것과 달리
spring에서는 트랜잭션관리자가 처리하게됨.(IOC).
스프링은 다양한 플랫폼(JTA:Java Transaction API, JPA:Java Persistence API, hibernate 등)에서 사용할 수 있도록 일련의 트랜잭션 관리자를 이용함.
그중 jdbc.datasource.DataSourceTransactionManager를 사용함.
다음 두가지 방식이 있음.
1. Programmatic Transaction : `@Transactional` 어노테이션 방식
* @Transactional 에서는 propagation 앨리먼트로 지정
@Transactional(readOnly=...,
isolation=...,
propagation=...,
timeout=...,
rollbackFor=..., rollbackForClassName=...,
noRollbackFor=..., noRollbackForClassName=...)
2. Declarative Transaction : 선언적트랜잭션(xml에 선언)
* <tx:method> 에서는 propagation 애트리뷰트 값으로 지정
<tx:advice>
<tx:attributes>
<tx:method name="..."
read-only="..."
isolation="..."
propatation="..."
timeout="..."
rollback-for="..."
no-rollback-for="..." />
</tx:attributes>
</tx:advice>
## 트랜잭션 전파(propagation)
이제 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성이다.
선언적 트랜잭션 경계설정 방식의 장점은 여러 트랜잭션 적용 범위를 묶어서 커다란 트랜잭션 경계를 만들 수 있다는 점이다.
트랜잭션 경계의 시작 지점에서 트랜잭션 전파 속성을 참조해서 해당 범위의 트랜잭션을 어떤 식으로 진행시킬지 결정할 수 있다.
스프링이 지원하는 트랜잭션 전파 속성은 다음 여섯 가지가 있다. 모든 속성이 모든 종류의 트랜잭션 매니저와 데이터 액세스 기술에서 다 지원되진 않음을 주의해야 한다. 각 트랜잭션 매니저의 API문서에는 사용 가능한 트랜잭션 전파 속성이 설명되어 있으니 사용하기 전에 꼭 참고해 봐야 한다.
propagation 앨리먼트의 enum 값은 org.springframework.transaction.annotation.Propagation 에 정의된 것을 사용한다.
1. REQUIRED
디폴트 속성이다. 모든 트랜잭션 매니저가 지원하며, 대개 이속성이면 충분하다. 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다. 자연스럽고 간단한 트랜잭션 전파 방식이지만 사용해보면 매우 강력하고 유용하다는 사실을 알 수 있다. 하나의 트랜잭션이 시작된 후에 다른 트랜잭션 경계가 설정된 메소드를 호출하면 자연스럽게 같은 트랜잭션으로 묶인다.
2. SUPPORTS
이미 시작된 트랜잭션이 있으면 참여하고 그렇지 않으면 트랜잭션 없이 진행하게 만든다. 트랜잭션이 없긴 하지만 해당 경계 안에서 Connection이나 하이버네이트 Session 등을 공유할 수 있다.
3. MANDATORY
REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다. 반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.
4. REQUIRES_NEW
항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다. JTA 트랜잭션 매니저를 사용한다면 서버의 트랜잭션 매니저에 트랜잭션 보류가 가능하도록 설정되어 있어야 한다.
5. NOT_SUPPORTED
트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.
6. NEVER
트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안된다 있다면 예외를 발생시킨다.
7. NESTED
이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것이다. 하지만 독립적인 트랜잭션을 마드는 REQUIRES_NEW와는 다르다.
중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랝개션에게 영향을 주지 않는다. 예를 들어 어떤 중요한 작업을 진행하는 중에 작업 로그를 DB에 저장해야 한다고 해보자. 그런데 로그를 저장하는 작업이 실패하더라도 메인 작업의 트랜잭션까지는 롤백해서는 안되는 경우가 있다. 힘들게 처리한 시급한 작업을 단지 로그를 남기는 작업에 문제가 있다고 모두 실패로 만들 수는 없기 때문이다. 반면에 로그를 남긴 후에 핵심 작업에서 예외가 발생한다면 이때는 저장한 로그도 제거해야 한다. 바로 이럴 때 로그 작업을 메인 트랜잭션에서 분리해서 중첩 트랜잭션으로 만들어 두면 된다. 메인 트랜잭션이 롤백되면 중첩된 로그 트랜잭션도 같이 롤백되지만, 반대로 중첩된 로그 트랜잭션이 롤백돼도 메인 작업에 이상이 없다면 메인 트랜잭션은 정상적으로 커밋된다.
중첩 트랜잭션은 JDBC 3.0 스펙의 저장포인트(savepoint)를 지원하는 드라이버와 DataSourceTransactionManager 를 이용할 경우에 적용 가능하다. 또는 중첩 트랜잭션을 지원하는 일부 WAS의 JTA 트랜잭션 매니저를 이용할 때도 적용할 수 있다. 유용한 트랜잭션 전파 방식이지만 모든 트랜잭션 매니저에 다 적용 가능한 건 아니므로, 적용하려면 사용할 트랜잭션 매니저와 드라이버, WAS의 문서를 참조해 보고, 미리 학습 테스트를 만들어서 검증해봐야 한다.
## 트랜잭션 격리 수준(isolation)
트랜잭션 격리수준은 동시에 여러 트랜잭션이 진행될 때에 트랜잭션의 작업 결과를 여타 트랜잭션에게 어떻게 노출할 것인지를 결정하는 기준이다. 스프링은 다음 다섯 가지 격리수준 속성을 지원한다.
1. DEFAULT
사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다. 보통 드라이버의 격리수준은 DB의 격리수준을 따르는게 일반적이다. 대부분의 DB는 READ_COMMITTED를 기본 격리수준으로 갖는다. 하지만 일부 DB는 디폴트 값이 다른 경우도 있으므로 DEFAULT를 사용할 경우에는 드라이버와 DB의 문서를 참고해서 디폴트 격리수준을 확인해야 한다.
2. READ_UNCOMMITTED
가장 낮은 격리수준이다. 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있다. 하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용하기도 한다.
3. READ_COMMITTED
실제로 가장 많이 사용되는 격리수준이다. 물론 스프링에서는 DEFAULT로 설정해둬도 DB의 기본 격리수준을 따라서 READ_COMMITTED로 동작하는 경우가 대부분이므로 명시적으로 설정하지 않기도 한다. READ_UNCOMMITTED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다. 이 때문에 처음 트랜잭션이 같은 로우를 읽을 경우 다른 내용이 발견될 수 있다.
4. REPEATABLE_READ
하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다. 따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.
5. SERIALIZABLE
가장 강력한 트랜잭션 격리수준이다. 이름 그대로 트랜잭션을 순차적으로 진행시켜 주기 때문에 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스하지 못한다. 가장 안전한 격리수준이지만 가장 성능이 떨어지기 때문에 극단적인 안전한 작업이 필요한 경우가 아니라면 자주 사용되지 않는다.
## 트랜잭션 제한시간(timeout)
이 속성을 이용하면 트랜잭션에 제한시간을 지정할 수 있다. 값은 초 단위로 지정한다. 디폴트는 트랜잭션 시스템의 제한시간을 따르는 것이다. 트랜잭션 제한시간을 직접 지정하는 경우 이 기능을 지원하지 못하는 일부 트랜잭션 매니저는 예외를 발생시킬 수 있다.
## 읽기전용 트랜잭션(read-only, readOnly)
트랜잭션을 읽기 전용으로 설정할 수 있다. 성능을 최적화하기 위해 사용할 수도 있고 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용할 수도 있다. 트랜잭션을 준비하면서 읽기 전용 속성이 트랜잭션 매니저에게 전달된다. 그에 따라 트랜잭션 매니저가 적절한 작업을 수행한다. 그런데 일부 트랜잭션 매니저의 경우 읽기전용 속성을 무시하고 쓰기 작업을 허용할 수도 있기 때문에 주의해야 한다. 일반적으로 읽기 전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다.
aop/tx 스키마로 트랜잭션 선언을 할 때는 이름 패턴을 이용해 읽기 전용 속성으로 만드는 경우가 많다. 보통 get이나 find 같은 이름의 메소드를 모두 읽기전용으로 만들어 사용하면 편리하다. @Transactional 의 경우는 각 메소드에 일일이 읽기 전용 지정을 해줘야 한다.
## 트랜잭션 롤백 예외(rollback-for, rollbackFor, rollbackForClassName)
* rollback-for
* rollbackFor, rollbackForClassName
선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백한다. 반면에 예외가 전혀 발생하지 않거나 체크 예외가 발생하면 커밋한다. **체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문이다.** 스프링에서는 데이터 액세스 기술의 예외는 런타임 예외로 전환돼서 던져지므로 런타임 예외만 롤백 대상으로 삼은 것이다.
하지만 원한다면 기본 동작방식을 바꿀 수 있다. 체크 예외지만 롤백 대상으로 삼아야 하는 것이 있다면 XML의 rolback-for 애트리뷰트나 애노테이션의 rollbackFor 또는 rollbackForClassName 앨리먼트를 이용해서 예외를 지정하면 된다.
rollback-for 나 rollbackFor은 예외 이름을 넣으면 되고, rollbackForClassName 는 예외 클래스를 직접 넣는다.
@Transactional 에서는 다음과 같이 클래스 이름 대신 클래스를 직접 사용해도 된다.
@Transactional(readOnly=true, rollbackFor=NoSuchMemberException.class)
## 트랜잭션 커밋 예외
* no-rollback-for
* noRollbackFor, noRollbackForClassName
rollback-for 속성과는 반대로 기본적으로는 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정해 준다.
사용 방법은 rollback-for 와 동일하다.
이 여섯가지 트랜잭션 속성은 모든 트랜잭션 경계설정 속성에 사용할 수 있다. 하지만 모든 트랜잭션마다 일일이 트랜잭션 속성을 지정하는 건 매우 번거롭고 불편한 일이다. 세밀하게 튜닝해야 하는 시스템이 아니라면 메소드 이름 패턴을 이용해서 트랜잭션 속성을 한 번에 지정하는 aop/tx 스키마 태그 방식이 편리하다. 보통은 read-only 속성 정도만 사용하고 나머지는 디폴트로 지정하는 경우가 많다. 세밀한 속성은 DB나 WAS의 트랜잭션 매니저의 설정을 이용해도 되기 때문이다.
세밀한 트랜잭션 속성 지정이 필요한 경우에는 @Transactional 을 사용하는 편이 좋다. 대신 트랜잭션 속성이 전체적으로 어떻게 지정되어 있는지 한눈에 보기 힘들다는 단점이 있고, 개발자가 코드를 만들 때 트랜잭션 속성을 실수로 잘못 지정하는 등의 위험이 있기 때문에 사전에 트랜잭션 속성 지정에 관한 정책이나 가이드라인을 잘 만들어 둬야 한다.
'Java > Spring' 카테고리의 다른 글
Spring) 파일 다운로드 (0) | 2022.09.04 |
---|---|
Spring) 2개 이상 테이블 조회(JOIN) - association, collection (0) | 2022.09.02 |
Spring) 파일 업로드 처리 (0) | 2022.08.31 |
Spring) 게시글 작성 - 파일 업로드 처리를 위한 설정 (0) | 2022.08.30 |
Spring) 페이징 처리 (content, pagebar 영역) - RowBounds (0) | 2022.08.30 |