앞서 살펴본 내용의 주제는 트랜잭션의 개념이었다.
그럼 트랜잭션의 실제 사용한다고 했을 때 시작 시점과 종료 시점에 대해 알아보아야 한다.
시작 시점이라는 말은 살짝 애매모호하다. 해당 로직의 조회, 수정 등과 같은 실제 sql의 실행 시점을 표현하는 것이 아닌
애플리케이션 계층에서의 시작 시점으로 이해해야 한다.
보통인 경우 비즈니스 로직은 서비스 계층에 분리되어 있다. 우리는 트랜잭션의 시작 시점을 비즈니스 로직이 시작되는 서비스 계층에서부터 시작해야 한다.
그 이유는 비즈니스 로직에 관한 문제는 큰 위험성을 가지고 있어 문제가 발생하였을 경우 롤백을 통해 데이터 정합성을 맞춰야 하기 때문이다.
그럼 트랜잭션의 시작은 서비스 계층이다. 여기서 내가 몰랐던 부분은 "커넥션"이라는 문제이다.
애플리케이션과 데이터베이스 구조를 보면 하나의 커넥션을 통해 데이터베이스의 세션을 연결하여 SQL을 실행하는 구조를 취한다.
이 말은 트랜잭션을 정상적으로 시작하고 종료하기 위해선 트랜잭션이 끝날 때까지 커넥션을 유지해야 한다는 것을 의미한다.
그럼 중간에 커넥션이 변경될 수 있다는 것에 의문을 가져보았다.
비즈니스 로직이 데이터베이스 설정에 존재하는 커넥션 타임아웃보다 길어 중간에 끊어질 경우가 존재하나?
아니면 커넥션을 얻는 방법 중 하나인 DriverManager를 통해 모든 비즈니스 로직을 처리한다면 문제가 될 것 같다.
또한 보통 비지니스 로직은 여러 서비스 계층의 메서드를 사용하기 마련인데, 이렇게 공통적인 메서드는 사용이 불가능한가도 싶다.
고민해야 할 것은 이 커넥션을 유지하는 깔끔한 방법이다. (와 여기에 대해 크게 고민해 본 적이 없었는데)
공통 메서드인 경우 결과를 얻고 난 후 커넥션을 반환하기 때문에 비즈니스 로직에 들어가는 부분에 대해선 별도의 작업을 하는 것이 맞는가 싶기도 하다.
커넥션을 유지하는 가장 쉬운 방법은 커넥션을 파라미터로 넘기는 방법이긴 하다.
다만 이 방법은 고려해야 할 부분이 많다.
커넥션 풀을 사용한다고 가정한다면 풀에 반납하기 전 오토 커밋 모드를 다시 true로 돌려주어야 하고, 비즈니스 로직에서 데이터베이스 연결을 사용하는 부분에서는 커넥션을 종료하면 안 된다.
또한 이는 서비스 계층의 코드가 매우 지저분해지고 커넥션을 유지하도록 코드를 변경하는 것도 쉬운 일은 아니다.
서비스 계층은 매우 중요한 곳이다.
서비스 계층은 핵심 비지니스 로직이 들어있기에 의존성도 낮아야 하고 매우 느슨한 결합을 통해 유연하게 확장될 수 있는 구조를 가져야 한다.
사실 어플리케이션의 구조를 나누는 것도 서비스 계층을 최대한 순수하게 유지하기 위한 목적이 크다. 기술에 종속적인 부분은 대부분 다른 계층으로 가져간다.
예를 들어 프레젠테이션 계층은 클라이언트의 접근, UI와 관련된 기술, HTTP와 관련된 부분으로 나누어져 있다.
만약 HTTP API를 사용하다가 GRPC 같은 기술로 변경해도 해당 변경은 서비스 계층에 영향을 미치지 않는다.
데이터 접근 계층은 JDBC, JPA와 같이 구현체들로부터 서비스 계층을 보호해준다.
서비스 계층의 중요도를 알았다면 과거의 서비스 계층에 대해 생각해보자.
트랜잭션의 시발점은 비즈니스 비즈니스 로직의 사용처가 되어야 한다. 그래야 비즈니스 로직 처리 중 문제가 발생하여도 문제가 내부에서만 발생하여 문제점을 찾기 쉽고 롤백하여야 하기 때문이다.
다만 이는 서비스 계층의 의존성을 높이는 문제를 발생시킨다.
public void bizLogicEX(String fromId, String toId, int age) throws SQLException {
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false);
bizLogic(conn, fromId, toId, age);
conn.commit();
} catch (Exception e) {
log.error("db error", e);
conn.rollback();
throw new IllegalStateException(e);
} finally {
release(conn);
}
}
위 과거 코드에는 서비스 계층에 JDBC에 종속된 코드들이 상당히 많이 첨가되어있어 변경에 취약하다.
이는 결국 반복적인 코드의 작성, 예외 처리관련 문제, 트랜잭션을 적용하면서 서비스 계층에 의존성이 높아지는 문제 등이 발생한다.
따라서 스프링에서 좀 더 클린한 방법을 제시해 준다.
첫 번째 문제인 트랜잭션을 추상화하여 서비스 계층에서 분리하는 것을 스프링은 어떠한 방법을 제시했을까?
스프링의 대다수의 기술은 OOP의 장점을 극대화하기 위해 설계되고 그렇게 구현되어 있다.
결국 우리는 트랜잭션 관련 인터페이스를 의존하여 의존성을 낮추고 DI를 통한 방식을 사용하게 될 것이란 걸 예상할 수 있다.
스프링은 데이터 접근 기술들의 인터페이스를 서비스 계층에 주입하여 트랜잭션을 사용한다. 데이터 접근 기술의 인터페이스, 구현체까지 모두 준비해놓았다.
위 그림과 같은 구조를 통해 서비스에 대한 데이터 접근 기술 종속성 문제를 해결할 수 있는 것이다.
그럼 트랜잭션 매니저가 하는 역할은 무엇일까?
스프링이 제공하는 트랜잭션 매니저가 하는 역할은 크게 두 가지로 나뉜다.
위 그림처럼 추상화를 지원하고 두 번째는 리소스를 동기화하는 역할을 한다.
리소스 동기화라는 것은 트랜잭션을 유지하기 위해 커넥션을 유지하는 것을 말한다.
같은 커넥션을 유지하여 비지니스 로직을 처리하는 부분에 있어 매우 복잡한 코드가 작성되었기에 이를 해결하기 위한 방 안으로 보인다.
커넥션을 멀티 쓰레드 환경에서도 안전하게 보관하는 곳을 찾다 보니 트랜잭션 매니저는 ThreadLocal을 사용해 커넥션을 보관한다. (오 생각지도 못했다.)
이를 통해 커넥션이 필요한 데이터 접근 계층에서는 해당 커넥션을 얻어 사용한 후 반환하고 트랜잭션 매니저는 트랜잭션의 종료 이후 해당 커넥션을 닫고 반환하면 되는 것이다.
실제 트랜잭션 동기화하는 클래스는 다음과 같다.
org.springframework.transaction.support.TransactionSynchronizat ionManager
즉 이 클래스는 스레드 당 리소스 및 트랜잭션 동기화를 관리하는 대리자 역할을 한다.
쉽게 동작 방식을 살펴보자.
1. 트랜잭션을 사용하기 위해 트랜잭션 매니저는 커넥션을 얻고 시작
2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션은 트랜잭션 동기화 관리자에게 보관.
3. 실제 사용하는 데이터 접근 계층에서는 동기화 관리자에게 커넥션을 얻어 사용
4. 트랜잭션 종료 시 트랜잭션 매니저는 동기화 관리자에게 보관된 해당 커넥션을 닫는다.
이를 통해 과거처럼 커넥션을 유지하기 위해 중복된 메서드를 작성하고 파라미터로 이리저리 넘기는 코드를 작성하지 않아도 된다.
서비스 계층에서 JDBC 구현체에 의존하는 것이라고 하면 DataSource 객체가 존재한다. 이는 트랜잭션 시작을 위해 커넥션을 얻어와야 하기에 의존하고 있지만 이제는 트랜잭션 매니저에게 관련 내용을 위임시켜 사용하기에 변경된다.
private final PlatformTransactionManager transactionManager;
실제 사용은 다음과 같이 진행된다.
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을 진행하는 순간 커넥션의 동기화는 종료되며 반환된다.
* 트랜잭션 옵션에 관하여 자세히 다음에 다루어 본다.
플랫폼 트랜잭션 매니저의 구현체는 사용하는 데이터 접근 기술에 따라 선택할 수 있다.
만약 JDBC를 이용한다면 DataSourceTransactionManager를 사용하고 JPA를 사용한다면 JpaTransactionManager를 사용하면 된다.
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryImpl(dataSource);
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberService = new MemberServiceImpl(transactionManager, memberRepository);
변경점이 더 존재한다.
레포지토리에서 커넥션을 얻을 때 트랜잭션 동기화 매니저가 관리하고 있는 커넥션을 가져와 사용해야 한다.
그래야 하나의 커넥션에서 트랜잭션이 유지될 수 있기 때문이다.
해당 방법으로 org.springframework.jdbc.datasource.DataSourceUtils를 사용한다.
DataSourceUtils에서 getConnection() 메서드를 호출하면 TransactionSynchronizationManager가 관리하는 커넥션의 유무를 확인해 관리하고 있다면 해당 커넥션을 그렇지 않으면 새로운 커넥션을 얻어 반환한다.
추가적으로 커넥션을 반환하는 것 또한 마찬가지이다.
DataSourceUtils의 releaseConnection() 메서드 통해 리소스 종료를 알릴 수 있는데, 이 과정에서 해당 세션의 커넥션을 종료하는 것이 아닌 트랜잭션 동기화 매니저에게 반납하는 과정을 진행한다.
이를 통해 스프링은 트랜잭션을 서비스 계층에서 시작하지만 데이터 접근 기술에 대한 의존성을 해결했고 커넥션 동기화 문제 또한 트랜잭션 매니저에게 위임하여 해결했다.
스프링은 객체지향의 장점을 가장 잘 살리기 위해 끊임없이 노력하며 이 문제를 해결하려는 구조 또한 객체지향스럽다.
객체에 책임을 부여하고 해당 역할을 할 구현체를 찾고 객체끼리는 아주 느슨한 연결을 하기 위해 여러 가지 기법을 적용한다.
여기까지 강의를 들었을 때 한 가지 의문점이 들었다.
서비스 계층에서 트랜잭션을 시작하고 종료하는 코드는 매우 중복성이 강하다. 이는 핵심 비즈니스 로직에 비하면 관심사가 맞지 않다고 생각된다. 아마 AOP 개념을 조합하여 이를 어떠한 방법으로 떼어냈을지도 앞으로 기대하게 되는 부분이다.
다음 편에는 트랜잭션 AOP에 대해 좀 더 알아보자.
'Spring|Spring-boot' 카테고리의 다른 글
Spring DB (5) (0) | 2022.10.14 |
---|---|
Spring DB(6) (0) | 2022.09.27 |
스프링 DB (3) (0) | 2022.09.05 |
스프링 DB (2) (0) | 2022.08.15 |
스프링 DB (1) (0) | 2022.08.15 |
댓글