일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 비주얼스튜디오
- 인프런강의
- 자바스크립트
- 자바스크립트함수
- 인프런인강
- slice
- 자바스크립트틱택토
- NPM
- c#
- 이벤트리스너
- 인프런무료강좌
- 객체의비교
- 제로초
- 자바스크립트recude
- 인터넷프로토콜
- 고차함수
- 자바스크립트객체리터럴
- Blazor
- .NET
- 인프런자바스크립트
- 틱택토구현
- sort
- EntityFramework
- 인프런
- 코딩
- 자바스크립트파라미터
- 인프런강좌
- 객체리터럴
- 콜백함수
- HTTP
- Today
- Total
샐님은 개발중
트랜잭션 이해 본문
1. 트랜잭션 개념
db에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다.
커밋: 모든 작업이 성공해서 db에 정상 반영되는 것
롤백 : 작업 중 하나라도 실패해서 거래 이전 상태로 되돌리는 것
원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한 다.
일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정 한 무결성 제약 조건을 항상 만족해야 한다.
격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데 이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.
지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
2. 데이터베이스 연결 구조와 DB 세션
1. 클라이언트가 데이터베이스 서버에 연결 요청을 하고 커넥션을 맺는다. 이때 DB 서버는 내부에 세션이라는 것을 만들고 세션이 SQL을 실행한다. 세션은 트랜잭션을 시작하고 , 커밋또는 롤백을 통해 트랜잭션을 종료한다. 이후에 새로운 트랜잭션을 다시 시작한다.
3. 트랜잭션 DB
커밋을 호출하기 전까지는 임시로 데이터를 저장하고 커밋이 완료 되면 실제 데이터베이스에 반영된다.
4. DB 락
세션 1 트랜잭션이 아직 커밋을 전일때 세션 2에서 동시에 같은 데이터를 수정하면 문제 발생한다.
바로 트랜잭션의 원자성이 깨진다. 여기에 더해 세션1이 중간에 롤백을 하게 되면 세션2는 잘못된 데이터를 수정하는 문제 발생.
이것을 방지하려면 세션이 트랙잰션을 시작하고 데이터 수정하는 동안 커밋이나 롤백전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야 한다.
5. 트랜잭션 적용
비지니스 로직 단위로 트랜잭션을 적용한다.
서비스 계층에서 커넥션 생성 -> 트랜잭션 커밋 이후에 커넥션 종료
애플리케이션에서 DB 트랜잭션 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션 유지.(같은 세션사용)
-> 같은 커넥션을 유지하려면 단순한 방법으로 커넥션을 파라미터로 전달해 같은 커넥션이 사용되도록 유지하는 것이있다.
스프링 트랜잭션 사용 전 흐름
MemberServiceV2Test 에서 accountTransfer 메소드를 실행합니다.
이 메소드에서 Member 데이터를 2개 생성하고 MemberService의 accountTransfer 를 호출합니다.
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import hello.jdbc.repository.MemberRepositoryV2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 트랜잭션 - 커넥션 파라미터 전달 방식 동기화
*/
class MemberServiceV2Test {
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
private MemberRepositoryV2 memberRepository;
private MemberServiceV2 memberService;
@BeforeEach
void before(){
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV2(dataSource);
memberService = new MemberServiceV2(dataSource,memberRepository);
}
@AfterEach
void after() throws SQLException{
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete("ex");
}
@Test
@DisplayName("정상 이체")
void accountTransfer() throws SQLException {
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
memberService.accountTransfer( memberA.getMemberId(),memberB.getMemberId(),2000);
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
@Test
@DisplayName("이체 중 예외 발생")
void accountTransferEx() throws SQLException {
Member memberA = new Member(MEMBER_A, 10000);
Member memberEx = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
assertThatThrownBy(()->memberService.accountTransfer(memberA.getMemberId(),memberEx.getMemberId(),2000))
.isInstanceOf(IllegalArgumentException.class);
Member findMemberA = memberRepository.findById( memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
}
MemberServiceV2.java
- 서비스단에서는 새로운 커넥션을 생성하고 트랜잭션을 시작합니다. 그 후 비지니스 로직인 MemberRepositroyV2 의 update 메소드를 실행시킵니다. 파라미터로 생성한 커넥션을 넘겨서 같은 세션을 유지할 수 있게 합니다. 이과정에서
정상적으로 실행됬다면 커밋을 하고 에러가 발생했다면 롤백을 합니다.
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import javax.swing.*;
import java.sql.Connection;
import java.sql.SQLException;
@Slf4j
@AllArgsConstructor
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try{
con.setAutoCommit(false); //트랜잭션 시작
// 비지니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); //성공시 커밋
}catch (Exception e){
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
}finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con,fromId);
Member toMember = memberRepository.findById(con,toId);
memberRepository.update(con, fromId, fromMember.getMoney() -money);
//validation(toMember);
memberRepository.update(con,toId,toMember.getMoney()+money);
}
private void validation(Member toMember){
if(toMember.getMemberId().equals("ex")){
throw new IllegalStateException("이체중 예외 발생");
}
}
/**
* 커넥션 사용 후 종료. 커넥션 풀을 사용하면 con.close()를 호출했을 때 풀에 반납됨.
* 현재 수동 커밋 모드 동작하기 때문에 풀에 돌려주기 전에 기본값인 자동 커밋 모드로 변경해야함
*
*/
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); //커넥션 풀 고려
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
}
MemberRepositoryV2.java
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JDBC - ConnectionParam
*/
@Slf4j
public class MemberRepositoryV2 {
private final DataSource dataSource;
public MemberRepositoryV2(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
public Member findById(Connection con,String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error FINDBYID", e);
throw e;
} finally {
//connection은 여기서 닫지 않는다.
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
log.info("con ={}",con.isClosed());
// Connection con = null;
PreparedStatement pstmt = null;
try {
// con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error UPDATE", e);
throw e;
} finally {
// close(con, pstmt, null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = DBConnectionUtil.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
'스프링 DB 1편 - 데이터 접근 핵심 원리' 카테고리의 다른 글
ItemController 에서 save 로직 리팩토링 (0) | 2023.07.24 |
---|---|
JPA - Map 데이터를 List로 변환 해서 데이터 저장 (0) | 2023.07.24 |
트랜잭션 문제 해결 - 트랜잭션 템플릿 (0) | 2023.07.21 |
스프링과 문제 해결 - 트랜잭션 - 트랜잭션 매니저 (0) | 2023.07.21 |
커넥션 풀과 데이터 소스 이해 (0) | 2023.07.20 |