PlatformTransactionManager
DataSourceTransactionManager
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
<bean id = "transactionTemplate"
class = "org.springframework.transaction.support.TransactionTemplate">
<property name = "transactionManager" ref="transactionManager"/>
</bean>
//예시
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.DEFAULT)
@T 아래 insert들이 다 시행되어야 하는데 포인트3 제약조건에 걸렸을 때 여기가 ROLLBACK 되면서 아래도 다 ROLLBACK됨(트랜잭션이 독립적이지 않다. -> REQUIRED)
[insertAndPointUpOfMember]
@T @T
insert() insert()
등록성공 등록성공
포인트2로증가 포인트3으로 증가하다 제약조건 걸림
TEST8 TEST8-2
//트랜잭션 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;
}
@T X
[insertAndPointUpOfMember]
@T NEW @T NEW
insert() insert()
등록성공 등록성공
포인트2로증가 포인트3으로 증가하다 제약조건 걸림
TEST8 TEST8-2
//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를 만들어야 트랜잭션 처리가 편리하다.
@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
void hitUp(String seq);
int getHit(String seq);
@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);
}
@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 속성을 주면 된다.
- 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 속성을 주면 된다.
- Phantom Read
- 조건: 여러 개의 레코드를 한 번에 읽어오는 상황
- 하나의 트랜잭션 안에서 일정 범위 내의 사원 정보 레코드를 두 번 이상 읽는 작업을 반복하는 상황 A와 상황 B 사이에서 누군가 특정 사원의 이름을 변경하거나, 삭제하거나, 추가하였다. 이처럼 여러개의 레코드를 반복적으로 읽어가는 상황 사이에서 누군가가 정보값을 수정한다면, 첫번째 쿼리에서 없던 레코드가 두번째 레코드에서 나타나는 상황을 Phantom Read 상황이라 한다.(다른 트랜잭션에 의한 레코드 수정 및 삽입이 허용되는 것)
- Isolation 속성: 수정 작업을 하려는 사람이 레코드를 읽어가는 반복 작업이 끝날 때까지 기다리게 하려면 SERIALIZABLE 속성을 주면 된다.
Namespaces에서 tx 체크
root-context.xml에 transactionManager가 등록되어 있어야 한다.
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
<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>
<!-- 524 @Transactional 어노테이션 사용가능 -->
<tx:annotation-driven
transaction-manager="transactionManager"
mode="proxy"
proxy-target-class="false"
/>
<!-- 524 @Transactional 어노테이션 사용가능 -->
<tx:annotation-driven
transaction-manager="transactionManager"
mode="proxy"
proxy-target-class="false"
/>
//그냥 애노테이션만 붙여도 되고
@Transactional
//이렇게 설정해주어도 된다(아래 코드는 기본값)
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
ALTER TABLE member
ADD( point number(10) default(0));
ALTER TABLE member
ADD CONSTRAINT ck_notices_point CHECK(point < 3);
ALTER TABLE notices
ADD CONSTRAINT ck_notices_title UNIQUE(title);
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;
}
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;
}
@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);
}
//로그인 인증(세션) 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점 이상 올라갈 수 없도록 설정하였으므로 또 글을 쓰게 되면 다음과 같은 오류가 발생하나, 글은 작성된 것을 확인할 수 있다.(트랜젝션 처리가 없다는 것)
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name = "dataSource" ref="dataSource"/>
</bean>
@Autowired
private DataSourceTransactionManager transactionManager;
//트랜잭션 처리 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);
}
}
<bean id = "transactionTemplate"
class = "org.springframework.transaction.support.TransactionTemplate">
<property name = "transactionManager" ref="transactionManager"/>
</bean>
@Autowired
private TransactionTemplate transactionTemplate;
//트랜잭션 처리 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);
}
});
}