어플리케이션 구조
현재 가장 많이 사용하는 어플리케이션의 구조는 역할에 따라 3가지 계층으로 나누는 것
- 프리젠테이션 계층
- UI와 관련된 처리 담당
- 웹 요청과 응답
- 사용자 요청을 검증
- 주 사용 기술 : 서블릿, HTTP와 같은 웹 기술, 스프링 MVC
- 서비스 계층
- 비지니스 로직 담당
- 가급적이면 특정 기술에 의존하지 않고 순수 자바코드로 작성하는 것이 좋음
- 데이터 접근 계층
- 실제 데이터베이스에 접근하는 코드
- 주요 사용 기술 : JDBC, JPA, File, Redis, Mongo DB 등
순수한 서비스 계층
보통 핵심 비지니스 로직을 수행하는 서비스 계층이 다른 계층보다 우선시 되곤 함. 시간이 흘러 UI, 웹과 같은 기술이 변하고 데이터 저장 기술 또한 다른 기술로 변한다고 하더라도 비지니스 로직은 최대한 변경 없이 유지되어야 하기 떄문.
서비스 계층을 최대한 변경없이 유지하려면 특정 기술에 종속적이지 않게 개발해야 함. 그래야 비지니스 로직을 유지보수 하거나 테스트 및 비지니스 로직 변경등에 있어서 수정을 최소화 할 수 있음.
트랜잭션 추상화
구현 기술에 따른 트랜잭션 사용번
- 트랜잭션은 원자적 단위의 비지니스 로직을 처리하기 위해 사용됨
- 구현 기술마다 트랜잭션을 사용하는 방법이 다름
- JDBC : connection.setAudoCommit(false)
- JPA : transaction.begin()
트랜잭션을 사용하는 코드는 데이터 접근 기술마다 다름. 만약 JDBC 기술을 사용해 JDBC 트랜잭션에 의존하다가 JPA 기술로 변경하게 되면 서비스 계층의 트랜잭션을 처리하는 코드를 모두 수정해야함.
- JDBC 트랜잭션에 의존
- JDBC 기술을 사용하다가 JPA 기술로 변경할 시 서비스 계층의 코드를 JPA 기술을 사용하도록 수정해야 함
위와 같은 문제를 해결하기 위한 방법중 하나는 아래와 같이 트랜잭션 기능을 추상화 하는 것.
1
2
3
4
5
public interface TxManager {
public void begin();
public void commit();
public void rollback();
}
서비스는 특정 트랜잭션 기술에 직접 의존하는 것이 아닌 TxManager 라는 추상화된 인터페이스에 의존하게 함 이후 원하는 구현체를 DI를 통해 주입받아 사용하면 됨. 이렇게 되면 클라이언트인 서비스는 인터페이스에 의존하고 DI를 사용한 덕분에 OCP 원칙을 지킬 수 있음
스프링의 트랜잭션 추상화 및 동기화
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 함
- 트랜잭션 추상화
- 트랜잭션 동기화
스프링의 트랜잭션 추상화
스프링은 이미 위와 같은 트랜잭션 추상화 기능을 제공하고 있음. 또한 각 데이터 접근 기술에 대한 트랜잭션 구현체도 대부분 구현되어 있어 개발자는 단순히 사용만 하면 됨.
- 스프링의 트랜잭션 추상화의 핵심은 PlatformTransactionManager 인터페이스
- org.springframework.transaction.PlatformTransactionManager
스프링의 트랜잭션 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지하여 세션을 유지해야 함. 결국 같은 커넥션을 동기화하기 위해서 이전에는 파라미터로 커넥션을 전달하는 방법을 사용했지만 해당 방법은 코드의 가독성과 중복 코드등의 여러 단점이 존재 함.
- 스프링은 트랜잭션 동기화 매니저를 제공함
- 트랜잭션 동기화 매니저는 쓰레드 로컬(ThreadLocal)을 사용하여 커넥션을 동기화 해줌
- 트랜잭션 매니저는 내부에서 해당 트랜잭션 동기화 매니저를 사용함
- 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 떄문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있게 해줌. 그러므로 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 됨
다음 트랜잭션 동기화 매니저 클래스를 열어서 확인해보면 쓰레드 로컬을 사용하는 것을 알 수 있음
- org.springframework.transaction.support.TransactionSynchronizationManager
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여되어 쓰레드는 자기 자신의 저장소의 데이터에만 접근할 수 있음
트랜잭션 동기화 매니저의 동작방식
- 트랜잭션의 시작하려면 커넥션이 필요하므로 트랜잭션 매니저는 DataSource를 통해 커넥션을 만들고 트랜잭션을 시작함
- 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
- Repository는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용 함
- 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫음
트랜잭션 매니저의 동작 흐름
- 클라이언트 요청으로 서비스 로직을 실행함
- 서비스계층에서 transactionManager.getTransaction()을 호출하여 트랜잭션 시작
- 트랜잭셕을 시작하려면 데이터베이스 커넥션이 필요하므로 트랜잭션 매니저는 내부에서 데이터 소스를 사용해 커넥션을 생성
- 커넥션을 수동 커밋 모드로 변경하여 실제 데이터베이스 트랜잭션을 시작
- 커넥션을 트랜잭션 동기화 매니저에 보관
- 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관하여 멀티 쓰레드 환경에 안전하게 커넥션을 보관함
- 서비스는 비지니스 로직을 실행하면서 리포지토리의 메서드를 호출
- 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하므로 리포지토리는 DataSourceUtils.GetConnection()을 호출하여 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내어 사용. 이 과정을 통해 같은 커넥션을 사용하고, 트랜잭션도 유지됨
- 획득한 커넥션을 사용하여 SQL을 데이터베이스에 전달하여 실행
- 비지니스 로직이 끝나고 트랜잭션을 종료. 트랜잭션은 커밋하거나 롤백하면 종료됨
- 트랜잭션을 종료하려면 동기화된 커넥션이 필요하므로 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득
- 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백함
- 전체 리소스 정리
- 트랜잭션 동기화 매니저를 정리. 쓰레트 로컬은 사용후 반드시 정리해야함
- connection.setAutoCommit(true) 로 되돌림. 커넥션 풀을 고려해야함
- connection.clost() 를 호출하여 커넥션을 종료함. 커넥션 풀을 사용하는 경우 connection.clost()를 호출하면 커넥션 풀에 반환됨
정리
트랜잭션 추상화 덕분에 서비스 코드를 이제 JDBC 기술에 의존하지 않음
- 이후 JDBC에서 JPA로 변경해도 서비스 코드를 그대로 유지 가능
- 기술 변경시 의존관계 주입만 DataSourceTransactionManager에서 JpaTransactionManager로 변경해주면 됨
트랜잭션 템플릿
트랜잭션을 사용할때 getTransaction() -> logic -> commit, rollback 과 같이 logic 이외에 반복되는 코드가 존재함 이러한 반복코드를 제거하기 위해 스프링은 템플릿 콜백 패턴을 적용한 클래스를 제공함
1
2
3
4
5
6
7
8
9
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
// return 값이 있을 때 사용
public <T> T execute(TransactionCallback<T> action){..}
// return 값이 없을 때 사용
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 사용 예시
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
transactionTemplate.executeWithoutResult((status) -> {
// 코드에서 예외를 처리하기 위해 try~catch 가 들어갔는데, bizLogic() 메서드를 호출하면
// SQLException 체크 예외를 넘겨줌.
// 해당 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크 예외로 바꾸어 던지도록 예외를 전환했다
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
- 트랜잭션 템플릿의 기본동작은 다음과 같음
- 비지니스 로직이 정상수행되면 커밋
- 언체크 예외가 발생하면 롤백. 그외의 경우 커밋
- 체크 예외의 경우에는 커밋
트랜잭션 AOP
트랜잭션을 편리하게 처리하기 위해 트랜잭션 추상화와 반복적인 트랜잭션 로직을 해결하기 위해 트랜잭션 템플릿을 도입해봤으나, 서비스 계층에 순수한 비지니스 로직만 남지 않고 트랜잭션 관련 코드가 남는 문제가 존재
이를 해결하기 위해 스프링 AOP를 통해 프록시를 도입.
프록시 도입 전
1
2
3
4
5
6
7
8
9
10
11
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
프록시 도입 후
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// proxy 코드
public class TransactionProxy {
private MemberService target;
public void logic() {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(..);
try {
//실제 대상 호출
target.logic();
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
}
// service 코드
public class Service {
public void logic() {
//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
bizLogic(fromId, toId, money);
}
}
- 스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있음
- 스프링 AOP를 직접 사용해서 트랜잭션을 처리해도 되지만 스프링은 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공함.
- 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해줌
- 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 됨
- 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용함
@Transactional
1
org.springframework.transaction.annotation.Transactional
참고 스프링 AOP를 적용하려면 어드바이저, 포인트컷, 어드바이스가 필요함. 스프링은 트랜잭션 AOP 처리를 위해 다음 클래스를 제공함. 스프링 부트를 사용하면 해당 빈들은 스프링 컨테이너에 자동으로 등록됨
어드바이저: BeanFactoryTransactionAttributeSourceAdvisor 포인트컷: TransactionAttributeSourcePointcut 어드바이스: TransactionInterceptor
트랜잭션 AOP 적용 전체 흐름
선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리
- 선언적 트랜잭션 관리(Declarative Transaction Management)
- @Transactional 어노테이션 하나만 선언하여 편리하게 트랜잭션을 적용하는 방식
- 선언적 트랜잭션 관리가 프로그래밍 방식에 비해 간편하고 실용적이므로 실무에선 주로 선언적 프로그래밍 방식을 사용
- 프로그래밍 방식 트랜잭션 관리(Programmatic transaction management)
- 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용하여 트랜잭션 관련 코드를 직접 작성하는 방식
- 스프링 컨테이너나 스프링 AOP 기술 없이 간단히 사용할 수 있지만 실무에서는 대부분 스프링 컨테이너나 스프링 AOP 기술을 사용하기 떄문에 거의 사용되지 않음
- 가끔 테스트시에 사용되기도 함
스프링 부트의 자동 리소스 등록
스프링 부트가 등작하기 이전에는 데이터소스와 트랜잭션 매니저를 개발자가 직접 스프링 빈으로 등록하여 사용 했음. 이후 스프링 부트에서 많은 부분이 자동화 되면서 데이터 소스와 트랜잭션 매니저 또한 자동으로 등록해줌
- 데이터소스 자동등록
- 스프링 부트는 데이터소스(DataSource)를 스프링 빈에 자동으로 등록함
- 자동으로 등록된 스프링 빈 이름은 dataSource
- 트랮잭션 매니저 자동등록
- 스프링 부트는 적절한 트랜잭션 매니저(PlatformTransctionManager)를 자동으로 스프링 빈에 등록함
- 자동으로 등록된 스프링 빈 이름은 transactionManager
- 어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단
- JDBC를 기술을 사용하면 DataSourceTransactionManager를 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager를 빈으로 등록함. 둘다 사용하는 경우 JpaTransactionManager를 등록
개발자가 직접 데이터소스나 트랜잭션 매니저를 등록하면 스프링 부트는 자동으로 등록하지 않음
데이터 소스와 트랜잭션 매니저 자동등록
1
2
3
4
# application.properties
pring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@TestConfiguration
static class TestConfig {
// 스프링 부트에서 자동으로 생성된 DataSource를 주입
private final DataSource dataSource;
public TestConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
MemberRepositoryV3 memberRepository() {
return new MemberRepositoryV3(dataSource);
}
@Bean
MemberServiceV3_3 memberService() {
return new MemberService(memberRepository());
}
}
참고
- 스프링 DB - 데이터 접근 핵심 원리(김영한)