19. 스프링에서 트랙잭션 관리
한 번에 이루어지는 작업의 단위, 쪼개 질 수 없는 하나의 단위 작업.
트랜잭션의 성격- 'ACID 원칙'
- 원자성 (Atomicty): 항상 A, B의 처리 결과는 동일해야 함. 한 쪽이 실패하면 다른 한 쪽도 원래 상태로 되돌려져야 함.
- 일관성 (Consistency): 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야만 함.
- 격리 (isolation): 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야 함.
- 영속성 (Durability): 트랜잭션이 성공적으로 처리되면, 그 결과는 영속적으로 보관되어야 함.
계좌이체를 생각해 보면, 내부적으로는 하나의 계좌에서는 출금이 이루어져야 하고 이체 대상 계좌에서는 입금이 이루어져야 한다. '출금'과 '입금'이라는 각각의 거래가 하나의 단위를 이루게 되는 것.
19.1 데이터베이스 설계와 트랜잭션
데이터베이스의 저장 구조를 효율적으로 관리하기 위해 흔히 '정규화'를 하는데 정규화의 가장 기본은 '중복된 데이터를 제거'해서 '저장의 효율을 올리는 것'이다.
정규화를 하면서 원칙적으로 칼럼으로 처리되지 않는 데이터들이 있는데,
- 시간이 흐르면 변경되는 데이터들(생년월일은 기록하지만, 현재 나이는 기록하지 않음.)
- 계산이 가능한 데이터들. (주문과 주문 상세가 별도의 테이블로 분리되어 있다면 사용자가 한 번에 몇 개의 상품을 주문했는지 등은 칼럼으로 기록하지 않음.)
- 누구에게나 정해진 값을 이용하는 경우
정규화가 진행될수록 테이블은 점점 더 순수한 형태가 되고 그럴수록 '트랜잭션'이 많이 일어나지 않는다. 테이블은 간결해지지만 반대로 쿼리문은 불편해 지는데, 단순 조회를 하는 게 아니라 조인이나 서브쿼리를 이용해야 현재 상황을 알 수 있기 때문이다.
그래서 '반정규화(역정규화)'를 하게 되는데 중복이나 계산되는 값을 데이터베이스 상에 보관하고 대신에 조인이나 서브쿼리의 사용을 줄이는 방식이다.
반정규화의 가장 흔한 예는 '게시물의 댓글'인데 게시물 목록에서 댓글의 숫자도 같이 표시할 때, 댓글의 숫자를 칼럼으로 처리하게 되면 게시물의 목록을 가져올 경우 tbl_reply 테이블을 이용해야 하는 일이 없기 때문에 성능상으로 이득을 볼 수 있다.
- 실습을 위한 테이블 생성
CREATE TABLE TBL_SAMPLE1 (COL1 VARCHAR2(500));
CREATE TABLE TBL_SAMPLE2 (COL1 VARCHAR2(50));
Sample1Mapper인터페이스
package site.levinni.mapper;
import org.apache.ibatis.annotations.Insert;
public interface Sample1Mapper {
@Insert("INSERT INTO TBL_SAMPLE1 VALUES(#{data})")
public int insertCol1(String data);
}
Sample2Mapper 인터페이스
package site.levinni.mapper;
import org.apache.ibatis.annotations.Insert;
public interface Sample2Mapper {
@Insert("INSERT INTO TBL_SAMPLE2 VALUES(#{data})")
public int insertCol2(String data);
}
- 비즈니스 계층과 트랜잭션 설정
SampleTxService 인터페이스
package site.levinni.service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public interface SampleTxService {
void addData(String value);
}
🔶 @Transactional은 구현한 클래스뿐 아니라 인터페이스에도 붙일 수 있다.
SampleTxServiceImpl 클래스
package site.levinni.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;
import site.levinni.mapper.Sample1Mapper;
import site.levinni.mapper.Sample2Mapper;
@Service
@AllArgsConstructor
@Log4j
@Transactional(isolation=Isolation.DEFAULT, propagation=Propagation.REQUIRED )
public class SampleTxServiceImpl implements SampleTxService{
private Sample1Mapper sample1Mapper;
private Sample2Mapper sample2Mapper;
@Override
public void addData(String value) {
log.info("mapper1 ........");
sample1Mapper.insertCol1(value);
log.info("mapper2 ........");
sample2Mapper.insertCol2(value);
log.info("end........");
}
}
🔶 인터페이스를 구현한 클래스에서 각 메서드마다 @Transactional을 붙여도 되고, 클래스 위에 붙여도 된다. 괄호 안은 기본값을 적어본 것.
SampleTxServiceTests.java
package site.levinni.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.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class SampleTxServiceTests {
@Autowired
private SampleTxService service;
@Test
public void testLong() {
String str = "0123456789 0123456789 0123456789 0123456789 0123456789";
log.info(str.getBytes().length);
service.addData(str);
}
}
🔶 @Transactional 처리를 하지 않았을 땐 tbl_sample1에만 데이터가 추가되고 tbl_sample2에는 길이 제한 때문에 추가되지 않는데, 트랜잭션 어노테이션을 하나 붙이면 tbl_sample1에도 데이터가 들어가지 않는다.
둘 다 들어가거나 둘 다 안 들어가야 함.
- @Transactional 어노테이션 속성들- 어노테이션을 설정할 때 속성으로 지정할 수 있다.
전파(Propagation) 속성
격리 레벨
Read-only 속성
Rollback-for-예외
No-Rollback-for-예외
http://wiki.gurubee.net/pages/viewpage.action?pageId=21200923
20. 댓글과 댓글 수에 대한 처리
아래 예제는 단순히 댓글을 추가하면 tbl_reply 테이블에 insert 하고, tbl_board 테이블에는 댓글 수를 의미하는 replyCnt라는 칼럼을 추가해서 해당 게시물 댓글 수를 update할 것이고 tbl_board 테이블에는 replyCnt 칼럼을 추가한다.
기존에 있던 댓글들도 replyCnt에 반영하기 위한 쿼리.
UPDATE TBL_BOARD SET
REPLYCNT = (
SELECT COUNT(*)
FROM TBL_REPLY
GROUP BY BNO
)
WHERE T.BNO = R.BNO;
MERGE INTO TBL_BOARD B
USING (
SELECT BNO, COUNT(*) CNT
FROM TBL_REPLY
GROUP BY BNO
) R
ON (B.BNO = R.BNO)
WHEN MATCHED THEN
UPDATE SET B.REPLYCNT = CNT;
20.1 프로젝트 수정
BoardVO에 replyCnt 추가
package site.levinni.domain;
import java.util.Date;
import lombok.Data;
@Data
public class BoardVO {
private Long bno;
private String title;
private String content;
private String writer;
private Date regdate;
private Date updateDate;
private int replyCnt;
}
BoardMapper 인터페이스에 추가
@Update("UPDATE TBL_BOARD SET REPLYCNT = REPLYCNT + #{amount} WHERE BNO = #{bno}")
void updateReplyCnt(@Param("bno") Long bno, @Param("amount") int amount);
🔶 쿼리가 간단하기 때문에 xml 대신 어노테이션으로 처리.
🔶 MyBatis의 SQL을 처리하기 위해서는 기본적으로 하나의 파라미터 타입을 사용하기 때문에 2개 이상의 데이터를 전달하려면 @Param이라는 어노테이션을 이용해야 한다.
BoardMapper.xml 수정
<select id="getListWithPaging" resultType="site.levinni.domain.BoardVO">
<![CDATA[
WITH TMP AS (
SELECT
/*+ INDEX_DESC(TBL_BOARD PK_BOARD)*/
ROWNUM RN, TBL_BOARD.*
FROM TBL_BOARD
WHERE
]]>
<include refid="cri"/>
<![CDATA[
BNO > 0
AND ROWNUM <= #{pageNum} * #{amount}
)
SELECT BNO, TITLE, CONTENT, WRITER, REGDATE, UPDATEDATE, REPLYCNT FROM TMP
WHERE RN > (#{pageNum} - 1) * #{amount}
]]>
</select>
🔶 새로 추가된 replycnt 칼럼을 가져오도록 쿼리 수정.
- ReplyServiceImpl에 트랜잭션 처리
기존에는 ReplyMapper만을 이용했지만, 반정규화 처리가 되면서 BoardMapper를 같이 이용해야 하는 상황이다. ReplyServiceImpl에서 새로운 댓글이 추가되거나 삭제되는 상황이 되면 BoardMapper와 ReplyMapper를 같이 이용해서 처리하고, 이 작업은 트랜잭션으로 처리되어야 한다.
ReplyServiceImpl.java 일부
package site.levinni.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;
import site.levinni.domain.Criteria;
import site.levinni.domain.ReplyPageDTO;
import site.levinni.domain.ReplyVO;
import site.levinni.mapper.BoardMapper;
import site.levinni.mapper.ReplyMapper;
@Service
@AllArgsConstructor
@Log4j
@Transactional
public class ReplyServiceImpl implements ReplyService{
private BoardMapper boardMapper;
private ReplyMapper replymapper;
@Override
public int register(ReplyVO vo) {
log.info("register :: " + vo);
boardMapper.updateReplyCnt(vo.getBno(), 1);
return replymapper.insert(vo);
}
@Override
public int remove(Long rno) {
// TODO Auto-generated method stub
log.info("remove :: " + rno);
boardMapper.updateReplyCnt(get(rno).getBno(), -1);
return replymapper.delete(rno);
}
}
🔶 필요한 메서드에만 @Transactional을 해도 됨.
🔶 ReplyVO에서 글번호를 가져올 수 있다!!
list.jsp 일부
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>수정일</th>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="board">
<tr>
<td>${board.bno}</td>
<td><a href="get${pageMaker.cri.listLink}&bno=${board.bno}"><c:out value="${board.title}" /> <span class="text-muted small">[${board.replyCnt}]</span></a></td>
<td><c:out value="${board.writer}" /></td>
<td><fmt:formatDate value="${board.regdate}" /></td>
<td><fmt:formatDate value="${board.updateDate}" /></td>
</tr>
</c:forEach>
</tbody>
</table>
📜 오류 날적이
'스프링 Spring' 카테고리의 다른 글
Part 6. 파일 업로드 처리 21. 04. 14. (0) | 2021.04.15 |
---|---|
댓글 더보기 처리 이어서 21. 04. 14. (0) | 2021.04.15 |
21. 04. 13. (0) | 2021.04.14 |
21. 04. 12. (0) | 2021.04.13 |
21. 04. 07. (0) | 2021.04.08 |