스프링과 문제 해결 - 트랜잭션
애플리케이션 구조
- 애플리케이션 구조
- 프레젠테이션 계층
- UI 관련된 처리 담당
- 웹 요청과 응답
- 사용자 요청을 검증
- 주 사용 기술 - 서블릿 & HTTP와 같은 웹 기술, 스프링 MVC
- 서비스 계층
- 비지니스 로직을 담당
- 가장 중요한 곳이기에 다른 계층의 기술이 변경되도 서비스 계층은 최대한 변경 없이 유지되어야 한다.
그러기 위해서 특정 기술에 종속되지 않도록 개발해야한다. - 주 사용 기술 - 특정 기술에 의존하지 않고 순수 자바 코드로 작성
- 데이터 접근 계층
- 실제 DB에 접근하는 코드
- 주 사용 기술 - JDBC, JPA, File, Redis, Mongo ...
- 프레젠테이션 계층
문제점
/**
* 트랜잭션 - 파라미터 연동, 풀을 고려한 종료
*/
@RequiredArgsConstructor
@Slf4j
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();
} finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {...}
private void release(Connection con) {...}
private void validation(Member toMember) {...}
}
- 해당 서비스 계층 예제의 문제점
- JDBC 구현 기술이 서비스 계층에 누수되는 문제
- JDBC라는 구현 기술에 종속적이기에 JDBC -> JPA로 구현 기술이 변경되면 서비스 계층에도 영향을 미치게 된다.
- 서비스 계층은 구현 기술에 종속적이지 않고 구현 기술을 변경하더라도 기존 코드를 최대한 유지할 수 있어야한다. => 추상 인터페이스를 사용함으로 특정 기술에 종속적이지 않게 한다.
- 예외 누수
- SQLException은 JDBC 전용 기술이기에 구현 기술이 변경되면 서비스 계층의 코드도 수정해야 한다.
- JDBC 구현 기술이 서비스 계층에 누수되는 문제
- 스프링에서 제공하는 트랜잭션 추상화 인터페이스 - 트랜잭션 매니저
- 트랜잭션 추상화 기능 제공 : 특정 기술에 종속적이지 않도록 해준다.
- 리소스 동기화 기능 제공 - 트랜잭션 동기화 매니저 : 트랜잭션을 유지하기 위해서 같은 DB 커넥션을 사용하도록 도와준다.
- 트랜잭션 매니저 내부에서 트랜잭션 동기화 매니저를 사용하며 동기화 매니저는 쓰레드 로컬을 사용해서 커넥션을 동기화해준다.
- 멀티쓰레드 환경에서도 안전하게 커넥션 동기화 가능하며 동기화 매니저를 통해 커넥션을 획득하면 된다.
- 동작 방식
- 트랜잭션을 시작하기 위해 커넥션이 필요
-> 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성하고 트랜잭션을 시작 - 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
- 레포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용
-> 파라미터로 커넥션을 전달할 필요가 없다. - 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션도 닫는다.
- 트랜잭션을 시작하기 위해 커넥션이 필요
트랜잭션 문제 해결 - 트랜잭션 매니저
- 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
- DataSourceUtils.getConnection() : 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
없는 경우 새로운 커넥션을 생성해서 반환한다. - DataSourceUtils.releaseConnection() : 커넥션을 바로 close() 하지 않고 트랜잭션을 사용하기 위해 동기화된 커넥션은 유지해준다. (서비스 계층에서 닫아야하기 때문)
- 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.
/**
* 트랜잭션 - 트랜잭션 매니저
*/
@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_1 {
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepository;
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();
}
//release는 별도로 해줄 필요 없이 트랜잭션 매니저가 해준다.
}
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);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
- 서비스 계층 예제 코드
- PlatformTransactionManager : 트랜잭션 매니저 사용
- TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()) : 트랜잭션 시작, status는 현 트랜잭션 정보가 담겨있으며 커밋, 롤백할 때 필요하다.
- 트랜잭션 매니저 동작 흐름
- 서비스 계층에서 transactionManager.getTransaction() 을 호출해서 트랜잭션을 시작한다.
- 트랜잭션을 시작하기 위해 DB 커넥션이 필요하기에 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션 션을 생성한다.
- 커넥션을 수동 커밋으로 변경해서 실제 DB 트랜잭션을 시작한다.
- 커넥션을 트랜잭션 동기화 매니저에 보관한다.
- 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다. (멀티쓰레드 환경에서 안전하게 커넥션 보관)
- 서비스 계층은 비지니스 로직을 실행하면서 레포지토리의 메소드들을 호출한다.
- 레포지토리 메소드들은 트랜잭션이 시작된 커넥션이 필요하기에 DataSourceUtils.getConnection()을 사용해서 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.
- 이 과정에서 같은 커넥션을 사용하고 트랜잭션도 유지가 된다.
- 획득한 커넥션을 사용해서 SQL을 DB에 전달해서 실행한다.
- 비지니스 로직이 끝나고 커밋하거나 롤백함으로 트랜잭션을 종료한다.
- 트랜잭션 종료를 위해 동기화된 커넥션이 필요하므로 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
- 획득한 커넥션을 통해 DB에 트랜잭션을 커밋하거나 롤백한다.
- 전체 리소스를 정리한다.
- 동기화 매니저 정리 & 쓰레드 로컬 정리
- 커넥션 풀을 고려해서 커넥션을 자동 커밋으로 되돌린다.
- 커넥션을 종료 / 커넥션 풀에 반환한다.
트랜잭션 문제 해결 - 트랜잭션 템플릿
- 트랜잭션 시작, 비지니스 로직 실행, 성공 시 커밋 / 실패 시 롤백하는 코드가 반복된다.
- 이러한 반복 문제를 템플릿 콜백 패턴을 활용하면 해결할 수 있다.
- 스프링에서 템플릿 콜백 패턴으로 구현된 TransactionTemplate 이라는 템플릿 클래스를 제공한다.
- 트랜잭션 관련 코드는 템플릿을 사용하고 비지니스 로직만 따로 수행할 수 있도록 해주면 된다.
- TransactionTemplate
- execute() : 응답 값이 있을 때 사용
- executeWithoutResult() : 응답 값이 없을 때 사용
/**
* 트랜잭션 - 트랜잭션 템플릿
*/
@Slf4j
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;
}
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();
}
});
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {...}
private void validation(Member toMember) {...}
}
- 서비스 계층 예제
- TransactionTemplate을 사용하려면 transactionManager가 필요하다.
- 트랜잭션 템플릿 기본 동작 방식
- 비지니스 로직이 정상 수행되면 커밋한다.
- 언체크 예외가 발생하면 롤백한다. (체크 예외의 경우 커밋)
- 아직까지 서비스 계층에 비지니스 로직과 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
- 서비스 계층에는 가급적 핵심 비지니스 로직만 존재해야 한다.
AOP 란
- AOP (Aspect Oriented Programming) : 관점 지향 프로그래밍으로 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나눠서 보고 그 관점을 기준으로 각각 모듈화하는 것.
- 여기서 모듈화는 공통된 로직이나 기능을 하나의 단위로 묶는 것
- ex) 핵심적인 관점 - 핵심 비지니스 로직
부가적인 관점 - DB 연결, 로깅, 파일 입출력 .... - 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비지니스 로직에서 분리해서 재사용하는 것이 AOP의 취지
- 흩어진 관심사 (Crosscutting Concerns) : 소스 코드상에서 반복해서 사용하는 코드를 의미한다.
- AOP 주요 개념
- Aspect : 흩어진 관심사를 모듈화 한 것
- 주로 부가기능을 모듈화
- Target : Aspect를 적용하는 곳
- 메소드, 클래스 ...
- Advice : 실질적으로 어떤 일을 해야할지에 대한 것
- 실질적인 부가기능을 담은 구현체
- JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점을 의미한다.
- 메소드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등과 같이 다양한 시점에 적용 가능
- PointCut : JointPoint의 상세한 스펙을 정의한 것
- Aspect : 흩어진 관심사를 모듈화 한 것
- AOP는 로깅, 트랜잭션 관리, 보안, 예외 처리, 모니터링 등에서 사용된다.
- 스프링 AOP 특징
- 스프링 AOP는 기본적으로 프록시 방식으로 동작한다.
- 접근 제어 및 부가기능을 추가하기 위해서 프록시 객체를 사용
- 스프링 빈에만 AOP 적용 가능
- 스프링 AOP는 기본적으로 프록시 방식으로 동작한다.
트랜잭션 문제 해결 - 트랜잭션 AOP
- 스프링 AOP를 사용해 프록시를 도입하면 서비스 계층에서 트랜잭션 처리 로직을 제외하고 핵심 비지니스 로직만 남길 수 있다.
- 프록시를 사용하면 트랜잭션을 처리하는 객체와 비지니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
- 트랜잭션을 처리하는 프록시가 트랜잭션 처리 로직을 모두 가져가고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출함으로 서비스 계층에는 순수 비지니스 로직만 남길 수 있다.
- 스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있다.
- 스프링에서 트랜잭션 AOP를 처리하기 위한 기능도 제공해준다.
- 트랜잭션 처리가 필요한 곳에 @Transactional 어노테이션을 사용하면 스프링의 트랜잭션 AOP가 어노테이션을 인식해서 트랜잭션 프록시를 적용해준다.
- @Transactional을 사용하면 프록시가 서비스 계층을 상속받고 코드를 만들어낸 것이 트랜잭션을 처리하는 프록시이다.
- AOP 프록시가 적용되면 EnhancerBySpringCGLIB가 적용된다.
/**
* 트랜잭션 - @Transactional AOP
*/
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {...}
private void validation(Member toMember) {...}
}
- @Transactional : 트랜잭션 AOP를 적용하여 트랜잭션 프록시를 적용한다.
- 메소드 단위, 클래스 단위에 사용할 수 있다.
- 클래스 단위에 붙이는 경우 외부에서 호출 가능한 public 메소드가 AOP 적용 대상이 된다.
- 스프링 AOP 테스트 코드 작성 시 유의사항
- 스프링 AOP를 적용하려면 스프링 컨테이너가 동작해야하므로 테스트 시 @SpringBootTest를 사용해 스프링 컨테이너를 생성하고 @Autowired로 빈을 주입 받아야한다.
- @TestConfiguration : 테스트 클래스 안에서 내부 설정 클래스를 만들어서 스프링 부트가 자동으로 만들어주는 빈들에 추가로 스프링 빈을 등록할 수 있다.
- AopUtils.isAopProxy().isTrue/False() : Aop 프록시 객체인지 여부를 확인할 수 있다.
- 트랜잭션 AOP 적용 전체 흐름
- 서비스 계층을 호출하면 프록시가 호출된다.
- 스프링 컨테이너에 등록된 트랜잭션 매니저를 획득한다.
- 프록시에서 트랜잭션 매니저로 트랜잭션을 시작한다.
- 데이터소스로 커넥션 생성
- 커넥션을 수동 커밋모드로 설정함으로 트랜잭션을 시작한다.
- 트랜잭션 동기화 매니저에 생성한 커넥션을 보관한다.
- 프록시에서 실제 비지니스 로직을 호출한다.
- 비지니스 로직에서 레포지토리를 호출한다.
- 레포지토리에서 트랜잭션 동기화 커넥션을 획득한다.
- 비지니스 로직, 데이터 접근 로직 수행 후 성공했으면 커밋, 예외 발생 시 롤백을 하며 트랜잭션을 종료한다.
- 선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리
- 선언적 트랜잭션 관리 (Declarative Transaction Management)
- @Transactional 어노테이션 하나만 사용해서 트랜잭션을 적용하는 것
- 해당 로직에 트랜잭션을 적용하겠다라고 어딘가에 선언하면 적용되는 방식
- 프로그래밍 방식의 트랜잭션 관리 (Programmatic Transaction Management)
- 트랜잭션 매니저 / 트랜잭션 탬플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것
- 스프링 컨테이너나 스프링 AOP 없이 사용할 수 있다.
- 선언적 트랜잭션 관리가 프로그래밍 방식에 비해 훨씬 간편하고 실용적이기에 실무에서는 대부분 선언적 트랜잭션 관리 방식을 사용한다.
- 선언적 트랜잭션 관리 (Declarative Transaction Management)
스프링 부트의 자동 리소스 등록
- 데이터소스 자동 등록
- 스프링 부트는 데이터소스(DataSource)를 스프링 빈에 자동으로 등록한다.
- 자동으로 등록되는 스프링 빈 이름 - dataSource
- 개발자가 직접 등록하는 경우 스프링 부트가 자동으로 등록해주지 않는다.
- application.properties 속성을 사용해서 DataSource를 생성한다.
//application.properties 속성 예시
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
- 스프링 부트가 기본으로 생성하는 데이터소스는 커넥션 풀을 제공하는 HikariDataSource 이다.
- spring.datasource.url 속성이 없는 경우 내장 데이터베이스 (메모리 DB)를 생성하려고 시도한다.
- 트랜잭션 매니저 자동 등록
- 스프링 부트는 트랜잭션 매니저 (PlatformTransactionManager)를 자동으로 스프링 빈에 등록해준다.
- 자동으로 등록되는 스프링 빈 이름 : transactionManager
- 직접 트랜잭션 매니저를 빈으로 등록하면 스프링 부트는 트랜잭션 매니저를 자동으로 등록해주지 않는다.
- 스프링 부트가 JDBC, JPA 등 트랜잭션 매니저 구현체를 선택할 때는 현재 등록된 라이브러리를 보고 판단한다.
- JDBC를 사용하면 DataSourceTransactionManager를 빈으로 등록한다.
JPA를 사용하면 JpaTransactionManager를 빈으로 등록한다.
- JDBC를 사용하면 DataSourceTransactionManager를 빈으로 등록한다.
- 트랜잭션 매니저와 데이터소스는 스프링 부트에서 제공하는 자동 빈 등록 기능을 사용하는 것을 권장
정리
- 애플리케이션 구조
- 프레젠테이션 계층
- UI 관련된 처리 담당, 웹 요청과 응답, 사용자 요청을 검증
- 서비스 계층
- 비지니스 로직을 담당
- 가장 중요한 계층으로 다른 계층의 기술이 변경되도 서비스 계층은 최대한 변경 없이 유지되어야 한다.
그러기 위해서 특정 기술에 종속되지 않도록 개발해야한다.
- 데이터 접근 계층
- 실제 DB에 접근하는 코드
- 프레젠테이션 계층
- 트랜잭션 매니저
- 특정 기술에 종속되지 않도록 트랜잭션 추상화 인터페이스를 제공
- 트랜잭션을 유지하기 위해서 같은 DB 커넥션을 사용하도록 해준다.
- 트랜잭션 템플릿
- 스프링에서 제공하는 템플릿 콜백 패턴으로 구현한 클래스로 반복되는 비지니스 로직, 커밋, 롤백등을 해결해준다.
- 스프링 AOP
- 스프링 AOP를 사용하면 프록시를 통해 서비스 계층에서 핵심 비지니스 로직과 트랜잭션 처리 로직을 명확하게 구분할 수 있다.
- 트랜잭션 처리가 필요한 부분에 @Transactional을 사용하면 트랜잭션 프록시를 적용할 수 있다. (메소드 / 클래스 단위에 사용가능)
- 실무에서는 @Transactional 어노테이션을 사용하는 선언적 트랜잭션 관리 방법을 사용한다.
- 트랜잭션 처리 로직에 필요한 데이터소스와 트랜잭션 매니저는 스프링 부트에서 자동으로 등록해주는 빈을 사용할 것.
- 자동 등록을 위해 application.properties 속성 필요
출처 : [인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
스프링 DB 1편 - 데이터 접근 핵심 원리 강의 | 김영한 - 인프런
김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니
www.inflearn.com
'Spring > [인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리]' 카테고리의 다른 글
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.11.19 |
---|---|
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 자바 예외 이해 (2) | 2024.11.18 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 트랜잭션 - 개념 이해 (2) | 2024.11.14 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] 커넥션풀과 데이터소스 이해 (0) | 2024.11.14 |
[인프런 김영한 스프링 DB 1편 - 데이터 접근 핵심 원리] JDBC 이해 (0) | 2024.11.11 |