2023. 1. 27. 17:45ㆍspring/db
1/27
* 기본 애플리케이션 구조
- 프레젠테이션 계층 (UI 관련, 웹 요청과 응답)
- 서비스 계층(비즈니스 로직)
- 데이터 접근 계층
* 서비스 계층은 순수 자바코드로 작성되어야 한다.
-> 컨트롤러나 레포지토리가 변경되도 서비스는 상관없어야한다. (유지보수)
- 기존 JDBC 문제점
이와 같은 패턴을 트랜잭션 사용 로직의 개수에 따라서 반복해야 한다.
throws SQLException 은 JDBC 전용 예외이다. 따라서 JPA 등으로 변경시 예외 코드도 모두 변경해줘야 한다.
* 스프링의 해결
* 레포지토리 부분 수정
JdbcUtils.closeConnection(con);
DataSourceUtils.releaseConnection(con, dataSource);
- 트랜잭션 사용시 DataSourceUtils.releaseConnections은 바로 닫지 않는다.
* 서비스 부분 수정
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);
}
}
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
//비지니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch(Exception e){
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
- 트랜잭션 연결 부분이 모두 빠지고 commit, rollback만 할 수 있게끔 바뀌었다.
private final PlatformTransactionManager transactionManager;
서비스에서 트랜잭션 매니저를 주입 받는다. (JDBC, JPA 등)
* 트랜잭션 동작 순서
* 트랜잭션 시작
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV3(dataSource);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberService = new MemberServiceV3_1(transactionManager, memberRepository);
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 500);
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
transaction.getTransaction() -> 커넥션을 얻기 위해 데이터 소스를 사용해서 커넥션을 생성
커넥션을 수동 커밋 모드로 변경하며 실제 데이터베이스 트랜잭션을 시작
* 비즈니스 로직 시작
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById( toId);
memberRepository.update( fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update( toId, toMember.getMoney() + money);
}
각각 리포지토리의 함수에서 데이터베이스와 연결하는 로직이 포함되어 있다.
리포지토리에서 데이터베이스와 연결하는 함수를 호출할 때 < DataSourceUtils.getConnection() >
트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
private Connection getConnection() throws SQLException {
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
* 트랜잭션 종료
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch(Exception e){
transactionManager.rollback(status); //실패시 롤백
비즈니스 로직이 끝나고 트랜잭션이 커밋하거나 롤백을 호출하며 종료한다.
그러면 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
획득한 커넥션을 통해 실제 데이터베이스에 커밋하거나 롤백한다.
DataSourceUtils.releaseConnection(con, dataSource);
커넥션을 자동 커밋으로 변경하고, 커넥션을 종료한다.
==============================================================================
트랜잭션 매니저를 사용함에 불구하고 비즈니스 로직을 사용할때마다 성공하면 commit 실패하면 rollback 이라는
반복적인 코드를 제거하지 못했다.
반복적인 코드를 제거하기 위해서 트랜잭션 탬플릿을 알아보자.
* 트랜잭션 매니저 -> 트랜잭션 탬플릿 (탬플릿 콜백 패턴)
@RequiredArgsConstructor
public class MemberServiceV3_1 {
// private final DataSource dataSource;
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepository;
public class MemberServiceV3_2 {
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
TransactionTemplate 는 생성자로 (PlatformTransactionManager) 를 받는다.
* 비즈니스 로직 변경
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
//비지니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch(Exception e){
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status)->{
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
- 트랜잭션 탬플릿은 비즈니스 로직이 정상 수행되면 자동 커밋한다.
- 언체크 예외가 발생하면 롤백한다. (체크 예외는 커밋한다.)
==============================================================================
반복적인 코드 문제는 해결했다.
하지만 서비스 계층의 클래스에서 비즈니스 로직만 남기기 위해서 스프링 AOP를 통해 프록시를 도입해야 한다.
* @Transactional -> 스프링 AOP
* 트랜잭션 등록 방법 변경
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status)->{
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
비즈니스 로직에서 트랜잭션과 관련된 로직을 전부 지우고 스프링이 제공하는
@Transactional을 등록한다.
하지만 이것만 바꿔서는 테스트가 정상적으로 실행되지 않는다.
해당 클래스는 @Transactional 이라는 어노테이션을 썻는지 모르기 때문에
* SpringBootTest 등록 (스프링 컨테이너 생성)
@SpringBootTest
class MemberServiceV3_3Test {
* @TestConfiguration(Test 내부 설정 클래스)
@TestConfiguration
static class TestConfig{
@Bean
DataSource dataSource(){
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
MemberRepositoryV3 memberRepositoryV3(){
return new MemberRepositoryV3(dataSource());
}
@Bean
PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
@Bean
MemberServiceV3_3 memberServiceV3_3(){
return new MemberServiceV3_3(memberRepositoryV3());
}
}
- 기본으로 사용할 DataSource (DriverManagerDataSource) 등록
- 트랜잭션을 관리해줄 PlatformTransactionManager (DataSourceTransactionManager) 등록
- 사용할 리포지토리, 서비스 등록
* 스프링 부트는 자동으로 DataSource와 TransactionManager 를 등록해준다.
(application.properties에 속성 추가 )
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
* 트랜잭션 전체 구조
'spring > db' 카테고리의 다른 글
스프링 db2 - MyBatis (0) | 2023.02.01 |
---|---|
스프링 db2 - JdbcTemplate 사용 정보 (0) | 2023.01.31 |
스프링 db1 - JdbcTemplate (0) | 2023.01.30 |
스프링 db - 예외 처리 반복 해결 (0) | 2023.01.30 |
스프링 DB1 - JDBC 구조(DataSource, 커넥션풀, 트랜잭션 등) (0) | 2023.01.26 |