728x90
트랜잭션 ≒ 거래
트랜잭션 ACID(http://en.wikipedia.org/wiki/ACID)
- 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패해야 한다.
- 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
- 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다.
- 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
트랜잭션 격리 수준 - Isolation level
- READ UNCOMMITED(커밋되지 않은 읽기)
- READ COMMITTED(커밋된 읽기)
- REPEATABLE READ(반복 가능한 읽기)
- SERIALIZABLE(직렬화 가능)
데이터베이스 연결 구조와 DB 세션

- 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다.
- 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다. 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다.
- 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다.
- 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.
트랜잭션 - DB 예제1 - 개념 이해
★ 커밋을 호출하기 전까지는 임시로 데이터를 저장

- 세션1은 select 쿼리를 실행해서 본인이 입력한 신규 회원1, 신규 회원2를 조회할 수 있다.
- 세션2는 select 쿼리를 실행해도 신규 회원들을 조회할 수 없다. (세션1이 아직 커밋을 하지 않았기 때문)
- 세션1 commit 시 세션2도 신규 데이터 조회 가능
- 세션1 rollback 시 모두 트랜잭션 시작하기 직전 상태로 복구
트랜잭션 - DB 예제2 - 자동 커밋, 수동 커밋
★ 자동 커밋
set autocommit true; //자동 커밋 모드 설정
insert into member(member_id, money) values ('data1',10000); //자동 커밋
insert into member(member_id, money) values ('data2',10000); //자동 커밋
★ 수동 커밋 설정
set autocommit false; //수동 커밋 모드 설정
insert into member(member_id, money) values ('data3',10000);
insert into member(member_id, money) values ('data4',10000);
commit; //수동 커밋
- 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다.
- 수동 커밋 설정 시 꼭 commit, rollback 호출 해야 한다.
트랜잭션 - DB 예제3 - 계좌 이체 (문제상황)
- 계좌이체 문제 상황 - commit
- 계좌이체 문제 상황 - rollback

→ 강제 커밋 : 계좌이체는 실패하고 memberA 의 돈만 2000원 줄어드는 아주 심각한 문제가 발생

→ 롤백 : 계좌이체를 실행하기 전 상태로 돌아옴. memberA 의 돈: 10000 원으로 돌아오고, memberB 의 돈: 10000 원으로 유지
DB 락 - 개념 이해
- 세션1이 트랜잭션 중 세션2가 같은 데이터 수정할 경우 원자성이 깨지게 된다.
- 세션1이 트랜잭션을 시작하고 데이터 수정하고 커밋 or 롤백 전까지 다른 세션이 해당 데이터를 수정 할 수 없게 막아야한다.


락 타임아웃
- SET LOCK_TIMEOUT <milliseconds> : 락 타임아웃 시간을 설정한다.
ex) SET LOCK_TIMEOUT 10000 : 10초
세션2에 설정하면 세션2가 10초 동안 대기해도 락을 얻지 못하면 락 타임아웃 오류가 발생한다.
LOCK_TIMEOUT 오류 메시지
Timeout trying to lock table {0}; SQL statement:
update member set money=10000 - 2000 where member_id = 'memberA' [50200-200]
HYT00/50200
DB 락 - 조회
일반적인 조회는 락을 사용하지 않는다
- 조회 시점에 락이 필요한 경우: 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때
ex) 애플리케이션 로직에서 memberA 의 금액을 조회해서 이 금액 정보로 어떤 계산을 수행한다. 이 계산이 돈과 관련된 매우 중요한 계산이어서 계산을 완료할 때 까지 memberA 의 금액을 다른곳에서 변경하면 안된다. 이럴 때 조회 시점에 락을 획득하면 된다.
조회 시 락 획득
set autocommit false;
select * from member where member_id='memberA' for update;
- select for update 구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다.
- 세션1은 트랜잭션을 종료할 때 까지 memberA 의 로우의 락을 보유한다.
트랜잭션 - 적용
문제 : 트랜잭션 없이 비지니스 로직 실행시 롤백 불가능
/**
* 기본 동작, 트랜잭션이 없어서 문제 발생
*/
class MemberServiceV1Test {
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
private MemberRepositoryV1 memberRepository;
private MemberServiceV1 memberService;
@BeforeEach
void before() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV1(dataSource);
memberService = new MemberServiceV1(memberRepository);
}
@AfterEach
void after() throws SQLException {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@Test @DisplayName("정상 이체")
void accountTransfer() throws SQLException {
//given
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
//when
memberService.accountTransfer(memberA.getMemberId(),
memberB.getMemberId(), 2000);
//then
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 {
//given
Member memberA = new Member(MEMBER_A, 10000);
Member memberEx = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberEx);
//when
assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
//then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberEx = memberRepository.findById(memberEx.getMemberId());
//memberA의 돈만 2000원 줄었고, ex의 돈은 10000원 그대로이다.
assertThat(findMemberA.getMoney()).isEqualTo(8000);
assertThat(findMemberEx.getMoney()).isEqualTo(10000);
}
}
테스트 데이터 제거
- @BeforeEach : 각각의 테스트가 수행되기 전에 실행된다.
- @AfterEach : 각각의 테스트가 실행되고 난 이후에 실행된다.

★ 비지니스 로직이 있는 서비스 계층에서 트랜잭션 시작!
★ 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야한다.
→ 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지
◎ 문제
- 애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다.
- 추가로 커넥션을 유지하도록 코드를 변경하는 것도 쉬운 일은 아니다.
반응형
'Spring > DB 1편' 카테고리의 다른 글
자바 예외 이해 (0) | 2023.08.09 |
---|---|
스프링과 문제 해결 - 트랜잭션 (0) | 2023.08.09 |
커넥션풀과 데이터 소스 이해 (0) | 2023.08.09 |
JDBC 이해 (0) | 2023.08.09 |
H2 데이터베이스 설정 (0) | 2023.08.09 |