일반적인 웹 애플리케이션 구조는 다음과 같다.

  • 프레젠테이션 계층 (@Controller)
  • 서비스 계층 (@Service)
  • 데이터 접근 계층 (@Repository)

 

여기서 가장 중요한 곳은 핵심 비즈니스 로직이 있는 서비스 계층으로 비즈니스 로직은 최대한 변경 없이 순수하게 유지되도록 특정 기술에 종속적이지 않게 개발하는 것이 좋다. 서비스 계층을 특정 기술에 종속적이지 않게 개발하기 위해 다른 계층에서 기술에 종속적인 부분을 가지고 가는데 프레젠테이션 계층UI와 관련된 기술인 웹, 서블릿, HTTP와 관련된 부분을 담당하고 데이터 접근 계층JDBC, JPA와 같은 데이터 접근 기술을 담당한다. 서비스 계층은 데이터 접근 계층에 직접 접근하지 않고 인터페이스를 통해 접근하면 비즈니스 로직을 유지보수, 테스트 하기 쉽게 관리할 수 있다.

 

JDBC로 서비스 계층에서 트랜젝션을 시작하면 DataSource, Connection 같은 JDBC 기술에 의존하게 되는데 JDBC 코드를 데이터 계층으로 옮기고 인터페이스로 제공한다 해도 데이터 계층에서 발생한 JDBC 예외가 서비스 계층으로 전파되고  try, catch 코드가 반복되는 등 여러 문제가 발생한다.

 


 

스프링 트랜잭션

 

스프링은 서비스 계층을 순수하게 유지하면서 위의 문제들을 해결할 수 있는 기술을 제공한다.

 

트랜잭션 매니저(TransactionManager)

 

스프링은 PlatformTransactionManager 라는 인터페이스를 제공하는데 이는 트랜잭션 추상화를 통해 데이터 접근 기술이 바뀌면 트랜젝션 코드를 수정해야 하는 등의 문제를 해결해주고 각각의 기술에 맞는 구현체(JpaTransactionManage..)도 직접 만들어 제공한다.

 

트랜잭션 매니저는 내부적으로 트랜잭션 동기화 매니저를 사용하는데 이는 쓰레드 로컬을 사용해서 트랜젝션의 시작부터 끝까지 같은 커넥션을 유지할 수 있도록 커넥션을 동기화 해주고 쓰레드 로컬을 사용하기 때문에 멀티 쓰레드 상황에서 안전하게 커넥션을 동기화 할 수 있다.

 

트랜젝션 매니저는 트랜젝션을 시작하면 트랜젝션 동기화 매니저를 통해 쓰레드 로컬에 커넥션을 보관하는데 리포지토리는 트랜젝션 동기화 매니저를 통해 동기화된 커넥션을 꺼내서 획득한 커넥션으로 SQL을 데이터베이스에 전달해서 실행한다.

트랜잭션은 커밋하거나 롤백하면 종료가 되는데 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득하고 데이터베이스에 트랜잭션을 커밋하거나 롤백한 뒤에 전체 리소스를 정리한다. 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 사용 후 반드시 리소스를 정리해야 한다.

 

추가로 스프링은 TransactionTemplate이라는 템플릿 클래스를 제공하는데 트랜잭션 템플릿을 사용하면 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드를 전부 제거할 수 있다. (비즈니스 로직이 정상 수행되거나 체크 예외가 발생하면 커밋하고 언체크 예외는 발생 시 롤백)

 

하지만 트랜잭션 템플릿을 사용해도 비즈니스 로직과 트랜잭션 로직을 완전히 분리할 수는 없는데 스프링 AOP를 통해 프록시를 도입하면 해당 문제를 깔끔하게 해결할 수 있다.

 


 

스프링 트랜잭션 AOP

 

스프링 AOP를 통해 트랜잭션 프록시를 사용하면 서비스 계층에서 트랜잭션을 생성했던 것과 달리 트랜잭션 프록시 객체에서 트랜잭션을 생성해서 비즈니스 로직을 호출을 하는 방식으로 바뀌어 트랜잭션과 비즈니스 로직을 처리하는 객체를 명확하게 분리할 수 있다.

스프링이 제공하는 트랜잭션 AOP를 사용하면 프록시를 매우 편리하게 적용할 수 있는데 스프링 부트는 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들(어드바이저, 포인트컷, 어드바이스)을 자동으로 등록해준다. 

 

스프링이 제공하는 트랜잭션 AOP를 사용하려면 메서드나 클래스에 @Transactional 애노테이션을 추가하면 되는데 클래스에 붙이면 외부에서 호출 가능한 public 메서드에 AOP 적용이 된다.

 

 

간단한 흐름

 

1. 트랜잭션 AOP 프록시 트랜잭션 시작

2. 스프링 컨테이너에 등록된 트랜잭션 매니저 획득

3. transactionManager.getTransaction()

4. DataSource를 통해 커넥션 조회, 없으면 생성

5. setAutoCommit(false)

6. 트랜잭션 동기화 매니저에 커넥션 보관

7. 트랜잭션 프록시에서 실제 객체의 서비스 로직 호출

8. 리포지토리에서 트랜잭션 동기화 매니저로부터 커넥션 획득

 

스프링 부트는 커넥션 풀을 제공하는 HikariDataSource를 생성해서 스프링 빈으로 등록하고 현재 등록된 라이브러리를 확인하여 데이터 접근 기술에 맞는 PlatformTransactionManager 구현체를 스프링 빈으로 등록(빈 이름 : transactionManager)한다.

트랜젝션

 

데이터를 저장할 때 단순 파일이 아닌 데이터베이스에 저장하는 가장 큰 이유는 데이터베이스가 트랜젝션이라는 개념을 지원하기 때문이다.

트랜젝션은 데이터베이스에서 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻하는데 트랜젝션 기능을 사용하면 중간에 문제가 생길 경우 시작 이전으로 되돌릴 수 있다. 작업이 완료되고 데이터베이스에 정상 반영되는 것을 커밋(Commit)이라 하고, 문제가 생겨서 작업 이전으로 되돌리는 것을 롤백(Rollback)이라 한다.

 

 

트랜젝션 ACID 

 

트랜젝션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)를 보장해야 한다.

  • 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업처럼 모두 성공 하거나 모두 실패해야 한다.
  • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
  • 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(JPA  16.1 트랜잭션과 락 참고)을 선택할 수 있다.
  • 지속성: 성공적으로 트랜젝션이 끝나면 그 결과가 항상 기록되어야 한다중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

 


 

데이터베이스 연결 구조와 DB 세션

 

데이터베이스는 커넥션을 연결할 때 내부에 DB 세션을 생성(커넥션 풀이 10개의 커넥션을 생성하면 세션도 10개 생성)하는데 트랜젝션을 시작하고, SQL을 실행하고, 커밋, 롤백, 트랜젝션 종료 등의 모든 요청은 DB 세션을 통해 실행이 된다. 

 

트랜젝션 내부 동작

 

데이터베이스는 커밋을 호출하기 전까지 데이터를 임시로 저장하는데 해당 트랜젝션을 시작한 세션만 변경 데이터(등록, 수정, 삭제)가 보이고 다른 세션은 변경중인 데이터가 보이지 않는데 이는 문제가 생겨서 트랜젝션 롤백이 되는 경우 데이터 정합성에 큰 문제가 생기기 때문이다.

 

트랜젝션에는 자동 커밋과 수동 커밋이 있는데 자동 커밋의 경우 말 그대로 쿼리를 하나 하나 실행할 때마다 자동으로 커밋을 해줘서 편리하지만 트랜젝션 개념이랑 맞지 않기 때문에 수동 커밋 모드(set autocommit false)로 바꾸어 사용하는 것이 트랜젝션의 시작이라고 볼 수 있다.

 

 

DB 락

 

한 세션에서 트랜젝션을 시작하고 데이터를 수정하는 동안 다른 세션이 같은 데이터를 수정하게 되면 트랜젝션의 원자성이 깨지는데 이런 문제를 방지하려면 트랜젝션이 커밋이나 롤백을 하기 전까지 다른 세션에서 해당 데이터를 수정할 수 없도록 막아야 한다.

 

데이터베이스는 이런 문제를 해결하기 위해 락(Lock)이라는 개념을 제공하는데 세션은 트랜젝션을 시작하고 값을 변경하려는 데이터의 row에 대해 먼저 lock을 얻는데 락을 갖고 있는 동안 다른 세션은 해당 row의 데이터를 변경할 수 없다. 락을 획득한 세션이 트랜젝션을 종료하면 락이 반납되고 다른 세션은 대기하다가 락을 획득한 뒤에 데이터를 변경할 수 있으며 락 대기 시간이 넘어가면 타임아웃 오류가 발생한다.

 

데이터베이스마다 다르지만, 락이 걸린 동안에는 다른 세션에서 데이터를 변경하지 못하고 지연이 걸리기 때문에 일반적인 데이터베이스는 조회를 할 때는 락을 사용하지 않고 바로 데이터를 조회할 수 있도록 한다.  조회를 할 때도 락이 필요한 경우  'select for update' 구문을 사용하면 되는데 조회를 통해 중요한 계산을 수행하는 경우 조회 시점에 락을 획득하면 된다.

 


 

트랜젝션 적용

 

트랜젝션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다. 비즈니스 로직이 잘못되면 문제가 되는 부분을 함께 롤백해야 하기 때문이다. 서비스 로직에서 트랜젝션을 시작하려면 먼저 커넥션을 획득하고 유지해야 하는데 애플리케이션에서 같은 커넥션을 유지하려면 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지해야 한다.

 

하지만 이러한 방법은 서비스 계층이 매우 복잡해지고 커넥션을 유지하기도 어려워지는데 스프링을 사용해서 트랜젝션을 편리하게 사용할 수 있다.

 

 

[참고]  인프런 김영한님 강의를 공부한 내용입니다.

+ Recent posts