ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
트랜잭션이란
정의
트랜잭션(Transaction)이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위를 말한다.
하나의 트랜잭션에는 하나 이상의 SQL문을 포함한다.
하나 이상이라는 말은 여러 개도 가능하다는 말이다.
예를 들어, 계좌 이체 기능을 수행한다고 하면
1. A유저의 계좌 DB의 돈을 빼고 2. B유저의 계좌 DB의 돈을 증가시켜야한다.
이는 하나의 트랜잭션으로 정의될 수 있다.
모든 작업이 성공해서 데이터베이스에 영구적으로 반영시키는 것을 커밋(Commit), 작업 중 일부가 실패해서 작업 실행 이전으로 되돌리는 것을 롤백(Rollback)이라고 한다.
ACID 특성
트랜잭션은 4가지의 특성을 가진다. 이 4가지의 앞글자를 따 흔히 ACID라고 한다.
- Atomicity(원자성): 트랜잭션의 일련의 작업들은 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
절대로 일부만 성공해선 안된다. - Consistency(일관성): 일관성은 트랜잭션이 데이터베이스를 항상 일관된 상태로 유지해야 한다.
모든 트랜잭션이 일관된 데이터베이스 제약을 가져야 한다. - Isolation(고립성): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
동시에 같은 데이터 를 수정하지 못하도록 해야 한다. - Durability(지속성): 트랜잭션이 성공하면 그 정보는 데이터베이스에 영구적으로 저장되어야 한다.
자동 커밋 & 수동 커밋
자동커밋: sql문을 날릴 때마다 자동으로 그 변경사항이 데이터베이스에 반영(이후 commit이나 rollback을 할 필요 없음)
set autocommit true; //자동 커밋 모드 설정
수동커밋: sql문을 작성한 뒤 한꺼번에 그 변경사항을 데이터베이스에 반영(이후 commit 혹은 rollback을 해야 함)
set autocommit false; //수동 커밋 모드 설정
DB락
DB락이란 여러 사람이 동시에 같은 데이터를 수정하려고 할 때 발생하는 문제를 방지하기 위해 한쪽이 데이터를 수정하는 동안(Commit이 발생하기 전까지) 다른 쪽은 그 데이터를 수정할 수 없게 하는 것
JDBC의 트랜잭션
JDBC를 통해 트랜잭션을 사용하는 방법에 대해 알아보자.
시나리오: 계좌이체
- 트랜잭션은 서비스 계층에서 시작되어야 한다. 비즈니스 로직을 실행하는 도중에 잘못되면 다시 롤백시켜야 하기 때문이다.
- 그러기 위해선 커넥션을 서비스 계층에서 커넥션을 생성해야 한다.
트랜잭션에는 여러 개의 SQL이 실행되므로 그 커넥션이 유지되어야 한다.
따라서 리포지토리에 접근할 때 그 커넥션을 넘겨줘야 한다. - 트랜잭션을 종료(Commit or Rollback)된 후 커넥션을 종료해주는 작업도 서비스 계층에서 일어난다.
즉, 기존의 리포지토리에서 해줬던 작업들도 서비스 계층에서 해주어야 한다.
코드로 보기
1. 서비스 코드
주요부분1
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 {
if(con != null) {
try {
release(con);
} catch (Exception e) {
log.info("error", e);
}
}
}
- 여기서 주목할 부분은 커넥션을 생성하고 트랜잭션을 수행한 뒤 커넥션을 닫아주는 작업까지 서비스에서 일어난다는 점이다.
주요부분2
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = repository.findById(con, fromId);
Member toMember = repository.findById(con, toId);
repository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
repository.update(con, toId, toMember.getMoney() + money);
}
- 리포지토리를 호출할 때 커넥션을 함께 넘겨준다.
2. 리포지토리 코드
주요부분1 - findById
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
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", e);
throw e;
} finally {
// connection은 여기서 닫지 않음
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
- 커넥션을 직접 여는 것이 아닌 파라미터로 받는다.
- 커넥션을 직접 닫지 않고 그 책임을 서비스 단으로 넘긴다.
주요부분2 - update
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
PreparedStatement pstmt = null;
try {
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", e);
throw e;
} finally {
JdbcUtils.closeStatement(pstmt);
}
}
- 커넥션을 직접 여는 것이 아닌 파라미터로 받는다.
- 커넥션을 직접 닫지 않고 그 책임을 서비스 단으로 넘긴다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] SQL Mapper VS ORM (0) | 2024.06.05 |
---|---|
[Spring Data] 예외로 인한 의존성과 스프링의 예외 추상화 (0) | 2024.04.30 |
[Spring Data] Spring Transaction을 통한 문제해결 (0) | 2024.04.29 |
[Spring Data] 커넥션 풀과 데이터 소스 (0) | 2024.04.29 |
[Spring Data] JDBC 이해하기 (0) | 2024.04.29 |