Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- spring
- Spring Boot
- pointcut
- Exception
- 스프링 핵심 원리
- java
- Thymeleaf
- 알고리즘
- 스프링
- kotlin
- JPQL
- 자바
- http
- jpa
- 그리디
- 김영한
- 인프런
- transaction
- Greedy
- springdatajpa
- Android
- 백준
- Proxy
- Servlet
- db
- SpringBoot
- QueryDSL
- JDBC
- AOP
- 스프링 핵심 기능
Archives
- Today
- Total
개발자되기 프로젝트
Transaction - 적용 2 본문
- 애플리케이션에서 트랜잭션을 어떤 계층에 걸어야 할까?
- 쉽게 이야기해서 트랜잭션을 어디에서 시작하고, 어디에서 커밋해야할까?
비즈니스 로직과 transaction
- 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다.
- 비즈니스 로직이 잘못되면 해당 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문이다.
- 그런데 트랜잭션을 시작하려면 커넥션이 필요하다.
- 결국 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
- 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야한다.
- 그래야 같은 세션을 사용할 수 있다.
커넥션과 세션
애플리케이션에서 같은 커넥션을 유지하려면 어떻게 해야할까?
가장 단순한 방법은 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지하는 것이다.
MemberRepositoryV2
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();//select는 executeQuery
if (rs.next()){//데이터 있으면 true, 내부 커서를 한번 호출을 해야 데이터가 있는 곳으로 이동함.
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
}else{//데이터 없는 경우
throw new NoSuchElementException("member not fond memberId="+memberId);
}
} catch (SQLException e) {
log.info("db error", e);
throw e;
}finally {//리소스 닫기.
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
//connection은 여기서 닫지 않음.
}
}
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.info("db error", e);
throw e;
}finally {
JdbcUtils.closeStatement(pstmt);
//connection은 여기서 닫지 않음.
}
}
- MemberRepositoryV2 는 기존 코드와 같고 커넥션 유지가 필요한 다음 두 메서드가 추가되었다.
- 참고로 다음 두 메서드는 계좌이체 서비스 로직에서 호출하는 메서드이다.
- findById(Connection con, String memberId)
- update(Connection con, String memberId, int money)
주의 - 코드에서 다음 부분을 주의해서 보자!
- 커넥션 유지가 필요한 두 메서드는 파라미터로 넘어온 커넥션을 사용해야 한다.
따라서 con = getConnection() 코드가 있으면 안된다. - 커넥션 유지가 필요한 두 메서드는 리포지토리에서 커넥션을 닫으면 안된다.
커넥션을 전달 받은 리포지토리 뿐만 아니라 이후에도 커넥션을 계속 이어서 사용하기 때문이다.
이후 서비스 로직이 끝날 때 트랜잭션을 종료하고 닫아야 한다.
ServiceV2
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* transaction - parameter 연동, 풀을 고려한 종료
*/
@Slf4j
@RequiredArgsConstructor
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); //수동커밋 지정 및 transaction 시작
bizLogic(con, fromId, toId, money);
con.commit(); //성공시 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("이체중 예외 발생");
}
}
private void release(Connection con) {
if (con != null){
try{
con.setAutoCommit(true); //autoCommit으로 바꿔놓고
con.close(); //connection pool에 반환.
}catch (Exception e){
log.info("error", e);
}
}
}
}
- Connection con = dataSource.getConnection();
트랜잭션을 시작하려면 커넥션이 필요하다. - con.setAutoCommit(false); //트랜잭션 시작
트랜잭션을 시작하려면 자동 커밋 모드를 꺼야한다.
이렇게 하면 커넥션을 통해 세션에 set autocommit false 가 전달되고, 이후부터는 수동 커밋 모드로 동작한다.
이렇게 자동 커밋 모드를 수동 커밋 모드로 변경하는 것을 트랜잭션을 시작한다고 보통 표현한다. - bizLogic(con, fromId, toId, money);
트랜잭션이 시작된 커넥션을 전달하면서 비즈니스 로직을 수행한다.
이렇게 분리한 이유는 트랜잭션을 관리하는 로직과 실제 비즈니스 로직을 구분하기 위함이다. - memberRepository.update(con..) : 비즈니스 로직을 보면 리포지토리를 호출할 때 커넥션을
전달하는 것을 확인할 수 있다. - con.commit(); //성공시 커밋
비즈니스 로직이 정상 수행되면 트랜잭션을 커밋한다. - con.rollback(); //실패시 롤백
catch(Ex){..} 를 사용해서 비즈니스 로직 수행 도중에 예외가 발생하면 트랜잭션을 롤백한다. - release(con);
finally {..} 를 사용해서 커넥션을 모두 사용하고 나면 안전하게 종료한다.
그런데 커넥션 풀을 사용하면 con.close() 를 호출 했을 때 커넥션이 종료되는 것이 아니라 풀에 반납된다.
현재 수동 커밋 모드로 동작하기 때문에 풀에 돌려주기 전에 기본 값인 자동 커밋 모드로 변경하는 것이 안전하다.
Test V2
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.extern.slf4j.Slf4j;
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;
/**
* Transaction parameter 전달
*/
@Slf4j
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(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
log.info("START TX");
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
log.info("END TX");
//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 - memberEx변경 중 예외.
assertThatThrownBy(() ->
memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
.isInstanceOf(IllegalStateException.class);
//then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberEx = memberRepository.findById(memberEx.getMemberId());
assertThat(findMemberA.getMoney()).isEqualTo(10000);
assertThat(findMemberEx.getMoney()).isEqualTo(10000);
}
}
이체중 예외 발생 - accountTransferEx()
- 다음 데이터를 저장해서 테스트를 준비한다.
- memberA 10000원
- memberEx 10000원
- 계좌이체 로직을 실행한다.
- memberService.accountTransfer() 를 실행한다.
- 커넥션을 생성하고 트랜잭션을 시작한다.
- memberA -> memberEx 로 2000원 계좌이체 한다.
- memberA 의 금액이 2000원 감소한다.
- memberEx 회원의 ID는 ex 이므로 중간에 예외가 발생한다.
- 예외가 발생했으므로 트랜잭션을 롤백한다.
- 계좌이체는 실패했다.
- 롤백을 수행해서 memberA 의 돈이 기존 10000원으로 복구되었다.
- memberA 10000원 - 트랜잭션 롤백으로 복구된다.
- memberB 10000원 - 중간에 실패로 로직이 수행되지 않았다.
- 따라서 그대로 10000원으로 남아있게 된다.
- 트랜잭션 덕분에 계좌이체가 실패할 때 롤백을 수행해서 모든 데이터를 정상적으로 초기화 할 수 있게 되었다.
- 결과적으로 계좌이체를 수행하기 직전으로 돌아가게 된다.
'인프런 > [인프런] 스프링 DB 1편 - 데이터 접근 핵심 원리' 카테고리의 다른 글
Transaction - 적용 1 (0) | 2022.06.07 |
---|---|
DB 락 - 조회 (0) | 2022.06.07 |
DB 락 - 변경 (0) | 2022.06.02 |
DB 락 - 개념 이해 (0) | 2022.06.01 |
트랜잭션 - DB 예제4 - 계좌이체 (0) | 2022.06.01 |
Comments