Spring 트랜잭션

tabi·2023년 6월 29일
0

Spring

목록 보기
5/15
post-thumbnail

1. 트랜잭션이란?

  • 두 개 이상의 업무가 하나의 논리적인 작업 단위를 이루고 있는 상태
  • 예시) 계좌이체를 한다고 할 때 A의 통장에서는 출금, B의 통장에는 입금 처리가 되지 않고 출금 작업만 된다면 모든 작업이 취소(ROLLBACK)되어야 한다.

1. ACID

  1. Atomicity(원자성)
  2. Consistency(일관성)
  3. Isolation(고립성)
  4. Durability(지속성)

2. 스프링의 트랜잭션 지원

  1. PlatformTransactionManager

  2. DataSourceTransactionManager

  • Spring JDBC 또는 mybatis DB에서 트랜잭션 처리를 위해 반드시 필요하므로 DataSourceTransactionManager를 트랜잭션 관리자로 등록해준다.
  • root-context.xml에 다음과 같은 코드 추가해 등록 필요
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
  1. TransactionTemplate
  • root-context.xml에 다음과 같은 코드 추가해 등록 필요
<bean id = "transactionTemplate"
class = "org.springframework.transaction.support.TransactionTemplate">
<property name = "transactionManager" ref="transactionManager"/>
</bean>

3. 트랜잭션 전파(propagation)와 격리(isolation) 레벨

  • 현재 진행중인 트랜잭션이 있는 상태에서 새로운 트랜잭션을 시작하고자 하는 경우
  • 스프링은 새로운 트랜잭션 생성, 기존 트랜잭션 사용, 기존 트랜잭션이 진행중인 상태에서 현재 코드를 실행하는 등 트랜잭션 전파와 관련된 부분을 설정으로 지정할 수 있도록 지원한다.

propagation

  • 전파: 첫번째 트랜잭션과 두번째 트랜잭션이 하나의 트랜잭션으로 처리되어야 하는지, 각각 독립적으로 처리되어야 하는지를 결정하는 것이 전파 방식(Propagation)의 설정이다.
//예시
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT)

propagation 처리 예시

@T 아래 insert들이 다 시행되어야 하는데 포인트3 제약조건에 걸렸을 때 여기가 ROLLBACK 되면서 아래도 다 ROLLBACK됨(트랜잭션이 독립적이지 않다. -> REQUIRED)
[insertAndPointUpOfMember]
@T				@T
insert()		insert()
등록성공		등록성공
포인트2로증가		포인트3으로 증가하다 제약조건 걸림
TEST8			TEST8-2
  1. NoticeDaoImpl.java
  • 아래와 같이 트랜잭션을 설정하고 실행하면 트랜잭션끼리 독립적이지 않아서 제약조건을 위반하면 전부 ROLLBACK한다.
	 //트랜잭션 propagation 설명 위한 수정메서드
	 @Override
	 @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
	 public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException { //A.공지사항 쓰기
	
		 insert(notice);
		 notice.setTitle(notice.getTitle()+"-2");
		 insert(notice);
	  }
	
	 //트랜잭션 propagation 위한 메서드
	 @Override
	 @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
	 public int insert(NoticeVO notice) throws ClassNotFoundException, SQLException { //A.공지사항 쓰기
	String sql = "INSERT INTO NOTICES(SEQ, TITLE, CONTENT, WRITER, REGDATE, HIT, FILESRC) VALUES( (SELECT MAX(TO_NUMBER(SEQ))+1 FROM NOTICES), :title, :content, :writer, SYSDATE, 0, :filesrc)";
	 
	  SqlParameterSource parameterSource = new
	  BeanPropertySqlParameterSource(notice); int insertCount =
	  this.namedParameterJdbcTemplate.update(sql, parameterSource);
	  
	  //B.포인트 증가
	  sql = " UPDATE member " + " SET point = point +1 " + " WHERE id = :id ";
	  
	  MapSqlParameterSource paramSource = new MapSqlParameterSource();
	  paramSource.addValue("id", notice.getWriter());
	  
	  int updateCount = this.namedParameterJdbcTemplate.update(sql, paramSource);
	  
	  return updateCount;
	  
	  }
  1. 다른 경우
  • 아래와 같이 트랜잭션이 걸려있는 코드를 실행한다면 당연히 TEST8만 들어가고 포인트는 2로 오른 뒤 우측의 메서드는 ROLLBACK될 것이라고 예상할 것이다.
@T X
[insertAndPointUpOfMember]
@T	NEW			@T  NEW
insert()		insert()
등록성공		등록성공
포인트2로증가		포인트3으로 증가하다 제약조건 걸림
TEST8			TEST8-2
  • 그런데 예상한대로 결과가 나오지 않는다.
  • 이유는? 같은 클래스 내에서 다른 메서드를 호출하는 구조는 AOP 방식이므로 트랜잭션 처리가 되지않기 때문!
  • 해결: iapu의 insert 분리 : 별도의 서비스 클래스를 추가해야 한다.
    • org.doit.ik.service 패키지에 MemberShipService 인터페이스와 MemberShipServiceImpl 클래스를 추가하자
    • 기존의 NoticeDao와 NoticeDaoImpl에서 insertAndPointUpOfMember 주석처리 하고 이를 MemberShipService와 MemberShipServiceImpl에 추가
    • CustomerController에 autowired 추가
//MemberShipService
@Repository
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT)
public interface MemberShipService {

	//트랜젝션 처리를 하기 위한 메서드 추가
	public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException;
	
}

//MemberShipServiceImpl
public class MemberShipServiceImpl implements MemberShipService{

	@Autowired
	private NoticeDao noticeDao;
	
	 @Override
	// @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
	 public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException { //A.공지사항 쓰기
	
		 noticeDao.insert(notice);
		 notice.setTitle(notice.getTitle()+"-2");
		 noticeDao.insert(notice);
	  }

}

//CustomerController
	@Autowired
	private MemberShipService memberShipService;

=> 따라서 모든 DAO의 경우 중간에 무조건 Service를 만들어야 트랜잭션 처리가 편리하다.

isolation

  • 격리: 트랜잭션을 처리하는 과정 속에서 두개 이상의 트랜잭션이 서로 동시에 같은 자원에 접근하게 된다면?
  • 동시에 트랜잭션이 실행되더라도 서로 영향을 받지 않게 격리 레벨(수준)을 설정할 수 있다.
  • isolation의 종류
@Transactional(isolation = Isolation.DEFAULT)
//DBMS(오라클)에 설정되어 있는 격리수준을 따라감

@Transactional(isolation = Isolation.READ_COMMITTED)
//커밋을 한 후에 처리

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
//커밋을 하지 않은 것 처리

@Transactional(isolation = Isolation.REPEATABLE_READ)
//

@Transactional(isolation = Isolation.SERIALIZABLE)
//팬텀리더 상황 X

isolation 처리 예시

  1. NoticeDao.java 인터페이스에 조회수 증가 메서드 추가
	void hitUp(String seq);
	
	int getHit(String seq);
  1. NoticeDaoImpl.java에 구현
	@Override
	@Transactional
	public void hitUp(String seq) {
		String sql = "UPDATE notices "
				+ " SET hit = hit + 1 "
				+ " WHERE seq = :seq ";
		
		  MapSqlParameterSource paramSource = new MapSqlParameterSource();
		  paramSource.addValue("seq", seq);
		  this.namedParameterJdbcTemplate.update(sql, paramSource);
		
	}

	@Override
	//@Transactional(isolation = Isolation.DEFAULT)
	public int getHit(String seq) {
		String sql = "SELECT hit "
				+ " FROM notices "
				+ " WHERE seq = :seq ";
		
		Map<String, Object> paramMap = new HashMap<String, Object>();
		paramMap.put("seq", seq);
		
		return this.namedParameterJdbcTemplate.queryForObject(sql, paramMap, Integer.class);
	}
  1. CustomerController.java 코드 수정
  • 조회수 증가 작업(코드 수정)
	@GetMapping("/noticeDetail.htm")
		public String noticeDetail(
				@RequestParam("seq") String seq
				, Model model
				) throws Exception{
		//조회수 1 증가
		this.noticeDao.hitUp(seq);
		NoticeVO notice = this.noticeDao.getNotice(seq);
	     model.addAttribute("notice", notice);

	            return "noticeDetail.jsp";
	         }

Isolation을 적용하는 여러가지 상황
1. Dirty Read

  • 조회수 증가(hit+1)는 일어났으나 commit은 되지 않은 상황에서 누군가가 해당 게시글의 조회수를 읽어(getHit(seq))갔다. 그런데 어떤 오류가 발생해서 조회수 증가가 ROLLBACK 된다면 잘못된 정보를 읽어간 것이 된다. 이것이 Dirty Read 상황이다.
  • Isolation 속성: Dirty Read 허용 시 READ_UNCOMMITTED 속성, 비허용 시 READ_COMMITTED 속성을 주면 된다.
  1. Non-Repeatable Read
  • 조건: 최소 2번 이상의 반복되는 활동이 있는 경우
  • 하나의 트랜잭션 안에서 1번 게시글(1개)의 조회수를 읽어가는(getHit(seq)) 반복적인 상황 A와 상황 B, 총 2번의 상황이 발생하였다. 이 때 A와 B 사이에 누군가가 게시글을 읽어 A는 조회수 10을, B는 조회수 11을 읽어가게 되었다. 이처럼 반복적인 상황인데 서로 다른 값을 읽어가게 되는 상황을 Non-Repeatable Read 상황이라고 한다.
  • Isolation 속성: Non-Repeatable Read 허용 시 다른 모든(REPEATABLE_READ 제외) 속성, 비허용 시에는 REPEATABLE_READ 속성을 주면 된다.
  1. Phantom Read
  • 조건: 여러 개의 레코드를 한 번에 읽어오는 상황
  • 하나의 트랜잭션 안에서 일정 범위 내의 사원 정보 레코드를 두 번 이상 읽는 작업을 반복하는 상황 A와 상황 B 사이에서 누군가 특정 사원의 이름을 변경하거나, 삭제하거나, 추가하였다. 이처럼 여러개의 레코드를 반복적으로 읽어가는 상황 사이에서 누군가가 정보값을 수정한다면, 첫번째 쿼리에서 없던 레코드가 두번째 레코드에서 나타나는 상황을 Phantom Read 상황이라 한다.(다른 트랜잭션에 의한 레코드 수정 및 삽입이 허용되는 것)
  • Isolation 속성: 수정 작업을 하려는 사람이 레코드를 읽어가는 반복 작업이 끝날 때까지 기다리게 하려면 SERIALIZABLE 속성을 주면 된다.

2. 스프링의 트랜잭션 처리

1. 코드 기반 트랜잭션 처리

2. 선언적 트랜잭션 처리(Declarative Transaction)

  • Transaction Template과 달리 트랜잭션 처리를 코드에서 직접 수행하지 않고, 설정 파일이나 애노테이션을 이용해 트랜잭션의 범위, 롤백 규칙 등의 정의함
  • 선언적 트랜잭션 처리는 두 가지 방식으로 정의된다.
    • <tx:advice> 태그 이용한 트랜잭션 처리
    • @Transactional 애노테이션을 이용한 트랜잭션 설정
  1. tx 네임스페이스를 이용한 트랜잭션 설정
  • Namespaces에서 tx 체크

  • root-context.xml에 transactionManager가 등록되어 있어야 한다.

<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
  • tx:advice 태그로 Advisor 생성, tx:attributes와 tx:method 태그를 이용해 트랜잭션 속성 정의 및 AOP를 통해 트랜잭션 적용(servlet-context.xml에 트랜잭션 설정)
<tx:advice id="txAdvice" transaction-manager = "transactionManager">
  <tx:attributes>
    <tx:method name="insertAndPointUpOfMember"
    isolation="DEFAULT"
    propagation="REQUIRED"
    read-only="true"
    timeout= "-1"/>
  </tx:attributes>
</tx:advice>
    
<aop:config>
  <aop:pointcut expression="execution(public void insertAndPointUpOfMember(*,*) )" id="insertOrM"/>
<!-- insertAndPointUpOfMember 메서드에 tx:advice로 설정한 트랜잭션 Advisor를 적용하도록 처리 -->
     <aop:advisor advice-ref="txAdvice"  pointcut-ref="insertOrM"/>
</aop:config>
  • tx:method 태그 속성

3. 애노테이션 기반 트랜잭션 처리

  • 보통 프로젝트는 애노테이션 기반으로 진행한다.
  • @Transactional 애노테이션을 사용해 트랜잭션을 설정할 수도 있다.
  • 메서드나 클래스에 애노테이션을 붙이면 적용되며 관련 트랜잭션 속성을 설정한다.
  • @Transactional 애노테이션이 적용된 스프링 빈에 트랜잭션을 적용하려면 <tx:annotation-driven> 태그를 설정해주어야 한다.
<!-- 524 @Transactional 어노테이션 사용가능 -->
<tx:annotation-driven
   	transaction-manager="transactionManager"
   	mode="proxy"
   	proxy-target-class="false"
/>

  • 자바 코드 설정 사용 시 @EnableTransactionManagemen 클래스를 이용해 @Transactional 애노테이션을 적용할 수도 있다.
  1. servlet-context.xml 파일에 <tx:annotation-driven> 태그 설정(위에서 설정한 다른 방식의 트랜잭션 처리 방식들은 지워주거나 주석처리)
<!-- 524 @Transactional 어노테이션 사용가능 -->
<tx:annotation-driven
   	transaction-manager="transactionManager"
   	mode="proxy"
   	proxy-target-class="false"
/>
  1. 트랜잭션을 적용하고자 하는 클래스나 메서드 위에 @Transactional 애노테이션을 붙여준다.
//그냥 애노테이션만 붙여도 되고
@Transactional
//이렇게 설정해주어도 된다(아래 코드는 기본값)
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)

2. 트랜잭션 처리 실습

  • 게시글을 쓰면(INSERT) 작성자의 포인트가 1증가하는 트랜잭션을 만들어보자

1. Setting

  1. 테이블 수정
  • member 테이블에 point 컬럼 추가
ALTER TABLE member
ADD( point number(10) default(0));
  • point 컬럼값은 3점 이상 올라가지 않도록 제약조건 설정
ALTER TABLE member
ADD CONSTRAINT ck_notices_point CHECK(point < 3);
  • notice 테이블에 유일성 제약조건 추가
ALTER TABLE notices
ADD CONSTRAINT ck_notices_title UNIQUE(title);
  1. MemberVO에 point 필드 추가
package org.doit.ik.domain;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {
	//member 테이블의 컬럼과 이름을 일치
	//fields
	private String id;
	private String pwd;
	private String name;
	private String gender;
	private String birth;
	private String is_lunar;
	private String cphone;
	private String email;
	private String habit;
	private Date regdate;
	
	//트랜젝션 처리 테스트 위해 point 컬럼 추가 -> 필드 추가, getter setter 추가
	private int point;
}
  1. NoticeDao 인터페이스, NoticeDaoImpl 코드 수정
  • NoticeDao 추상 메서드 추가: 공지사항 INSERT + 포인트 UPDATE를 동시에 하는 메서드
package org.doit.ik.persistence;

import java.sql.SQLException;
import java.util.List;

import org.doit.ik.domain.NoticeVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public interface NoticeDao {
	
	//검색한 결과의 총 레코드 수를 반환하는 메서드
	public int getCount(String field, String query) throws ClassNotFoundException, SQLException;

	//페이징+공지사항 목록
	public List<NoticeVO> getNotices(int page, String field, String query) throws ClassNotFoundException, SQLException;
	
	//공지사항 삭제
	public int delete(String seq) throws ClassNotFoundException, SQLException;
	
	//공지사항 수정
	public int update(NoticeVO notice) throws SQLException;
	 
	//공지사항 상세보기
	public NoticeVO getNotice(String seq) throws ClassNotFoundException, SQLException;

	//공지사항 작성
	public int insert(NoticeVO notice) throws ClassNotFoundException, SQLException;
	
	//트랜젝션 처리를 하기 위한 메서드 추가
	public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException;
	
}
  • NoticeDaoImpl.java 에 코드 추가
	@Override
	public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException {
		//A.공지사항 쓰기
		String sql = "INSERT INTO NOTICES(SEQ, TITLE, CONTENT, WRITER, REGDATE, HIT, FILESRC) VALUES( (SELECT MAX(TO_NUMBER(SEQ))+1 FROM NOTICES), :title, :content, :writer, SYSDATE, 0, :filesrc)";
		
	    SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(notice);
	    int insertCount = this.namedParameterJdbcTemplate.update(sql, parameterSource);
	    
	    //B.포인트 증가
	    sql = " UPDATE member "
	    		+ " SET point = point +1 "
	    		+ " WHERE id = :id ";
	    
		MapSqlParameterSource paramSource = new MapSqlParameterSource();
		paramSource.addValue("id", id);
	    
	    int updateCount = this.namedParameterJdbcTemplate.update(sql, paramSource);
	    
	}
  1. CustomerController.java 수정
		//로그인 인증(세션) notice.setWriter("kenik");
		notice.setWriter("hj"); //일단 작성자 저장해둠(나중에 세션 받아올 시 수정 필요)
		//int insertcount = this.noticeDao.insert(notice);
		this.noticeDao.insertAndPointUpOfMember(notice, "hj");
		
		int insertcount = 1;
		if(insertcount ==1) {
			return "redirect:notice.htm"; //redirect: == response.sendRedirect()
		}else
		return "noticeReg.jsp?error";
	}
  • 이제 게시글을 작성해보면 사용자의 포인트가 증가한 것을 테이블에서 확인할 수 있다.

  • 우리는 포인트가 3점 이상 올라갈 수 없도록 설정하였으므로 또 글을 쓰게 되면 다음과 같은 오류가 발생하나, 글은 작성된 것을 확인할 수 있다.(트랜젝션 처리가 없다는 것)

2. Transaction Manager 사용

  1. root-context.xml에 DataSourceTransactionManager 등록
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
  1. NoticeDaoImpl.java에 DataSourceTransactionManager @Autowired 해준다.
@Autowired
private DataSourceTransactionManager transactionManager;
  1. NoticeDaoImpl.java에 트랜젝션 매니저 사용한 코드 추가
	//트랜잭션 처리 O
	@Override
	public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException {
		//A.공지사항 쓰기
		String sql = "INSERT INTO NOTICES(SEQ, TITLE, CONTENT, WRITER, REGDATE, HIT, FILESRC) VALUES( (SELECT MAX(TO_NUMBER(SEQ))+1 FROM NOTICES), :title, :content, :writer, SYSDATE, 0, :filesrc)";
	    //B.포인트 증가
	    String sql2 = " UPDATE member "
	    		+ " SET point = point +1 "
	    		+ " WHERE id = :id ";
		
	    TransactionDefinition definition = new DefaultTransactionDefinition();
		TransactionStatus status = this.transactionManager.getTransaction(definition);
		//status: 다른 트랜잭션과 중첩되거나 충돌이 나는 경우 어떻게 트랜젝션을 처리할지 => 격리성 관련 설정 상태
	    
		try { //A, B 작업을 처리하는데 오류가 나지 않으면 COMMIT
			//A 작업 처리
		    SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(notice);
		    int insertCount = this.namedParameterJdbcTemplate.update(sql, parameterSource);

		    //B 작업 처리
			MapSqlParameterSource paramSource = new MapSqlParameterSource();
			paramSource.addValue("id", id);
		    int updateCount = this.namedParameterJdbcTemplate.update(sql2, paramSource);
		    
		    //COMMIT
		    this.transactionManager.commit(status);
		} catch (Exception e) {
			//ROLLBACK
			this.transactionManager.rollback(status);
		}  
	}

3. Transaction Template 사용

  1. root-context.xml에 TransactionTemplate 등록
<bean id = "transactionTemplate"
class = "org.springframework.transaction.support.TransactionTemplate">
<property name = "transactionManager" ref="transactionManager"/>
</bean>
  1. NoticeDaoImpl.java에 TransactionTemplate @Autowired 해주기
@Autowired
private TransactionTemplate transactionTemplate;
  1. NoticeDaoImpl.java에 트랜젝션 매니저 사용한 코드 추가
	//트랜잭션 처리 O - 트랜잭션 템플릿
	@Override
	public void insertAndPointUpOfMember(NoticeVO notice, String id) throws ClassNotFoundException, SQLException {
		//A.공지사항 쓰기
		String sql = "INSERT INTO NOTICES(SEQ, TITLE, CONTENT, WRITER, REGDATE, HIT, FILESRC) VALUES( (SELECT MAX(TO_NUMBER(SEQ))+1 FROM NOTICES), :title, :content, :writer, SYSDATE, 0, :filesrc)";
	    //B.포인트 증가
	    String sql2 = " UPDATE member "
	    		+ " SET point = point +1 "
	    		+ " WHERE id = :id ";
		
	    //교재 517p
	    this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
			
			@Override
			protected void doInTransactionWithoutResult(TransactionStatus status) {
				//A 작업 처리
			    SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(notice);
			    int insertCount = namedParameterJdbcTemplate.update(sql, parameterSource);

			    //B 작업 처리
				MapSqlParameterSource paramSource = new MapSqlParameterSource();
				paramSource.addValue("id", id);
			    int updateCount = namedParameterJdbcTemplate.update(sql2, paramSource);
			}
		});
	}
  • 그러면 이제 포인트가 2인 경우에는 ORA-02290: check constraint (SCOTT.CK_NOTICES_POINT) violated 오류가 발생하고, 리스트로 돌아가보면 새 글도 작성되지 않는다.(ROLLBACK)
profile
개발 공부중

0개의 댓글