개발놀이터

트랜잭션과 ACID (자바에서 트랜잭션을 다루는 방법에 대한 관점으로) 본문

CS 지식/데이터베이스

트랜잭션과 ACID (자바에서 트랜잭션을 다루는 방법에 대한 관점으로)

마늘냄새폴폴 2023. 2. 27. 16:13

이번 포스팅에서는 트랜잭션과 ACID 그리고 더 나아가서 자바에서 트랜잭션을 어떻게 다루는지에 대해서 알아보도록 하겠습니다. 

 

굉장히 딥하게 들어가기 때문에 트랜잭션과 ACID에 대한 기본적인 내용에 대해서만 알고싶으신 분들은 앞부분만 읽으시고 뒷부분은 알아만 두시는 것을 추천드리겠습니다. 최대한 쉽게 풀어서 쓸거라 찬찬히 읽으시면 이해가 안되지는 않을겁니다!

 

그럼 시작해보죠

 

트랜잭션

트랜잭션을 간단하게 한마디로 설명하자면 

 

"데이터베이스의 상태를 변경시키기 위한 논리적인 단위" 라고 할 수 있습니다. 

 

여기서 상태를 변경시킨다는 것은 우리가 흔히 사용하는 SQL문을 이용한 것입니다. DDL, DML등을 말하는 것이지요.

 

트랜잭션에 대한 개념은 이게 다입니다. 조금 허무하시죠? 

 

이제 트랜잭션의 특징이라고 말할 수 있는 ACID에 대해서 알아보죠

 

 

ACID

ACID는 객체지향의 5대원칙 SOLID처럼 영어의 앞글자만 따서 만든 단어입니다. 저는 ACID(에이씨드)라고 부르긴 하는데 어떤 분은 어씨드 라고도 부르시더라구요. 저는 에이씨드가 편해서 에이씨드라고 말합니다. 

 

아무튼 ACID가 뭐의 앞글자를 딴거냐

 

원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 지속성 혹은 내구성(Durability) 이렇게 네가지입니다. 

 

각각에 대해서 설명하겠지만 이 중 가장 중요한 것은 원자성입니다. 모든 트랜잭션이 반드시 갖춰야할 덕목이죠 (그렇다고 나머지가 안중요하다는 것은 아닙니다)

 

원자성

트랜잭션의 모든 연산은 완벽해야하고, 하나의 연산이라도 실패하는 경우 모든 연산이 전부 실패해야 합니다. 

 

일관성

트랜잭션을 실행하고 있던 데이터베이스가 변경되더라도 변경된 데이터베이스의 트랜잭션으로 마저 실행하는 것이 아닌 기존에 사용하던 트랜잭션의 데이터베이스로 진행되어야 한다는 개념입니다. 

 

독립성

서로 다른 두 트랜잭션이 서로에 대한 연산에 대해 연관되지 않고 독립적으로 실행되어야 한다는 것입니다. 이 독립성은 데이터베이스의 격리수준과 연결되는 내용입니다. 데이터베이스 격리수준에 대한 내용은 아래의 링크를 참조해주세요

https://coding-review.tistory.com/196

 

데이터베이스 격리수준 (isolation level)

우선 데이터베이스 격리수준을 들어가기 전에 우리는 트랜잭션에 대해서 간단한 이해가 필요합니다. 트랜잭션 트랜잭션은 데이터의 정합성을 보장하기 위한 기능입니다. 트랜잭션은 꼭 여러개

coding-review.tistory.com

 

지속성

트랜잭션이 성공적을 완료되었다면, 시스템이 오류로 종료되더라도 성공한(커밋된) 내용이 보장되어야 한다는 것입니다. 

 

 

여기까지 트랜잭션의 기본적인 내용과 ACID에 대한 내용입니다. 이 뒤부터는 조금 어려울 수 있습니다. 

 

Local Transaction VS Global Transaction

트랜잭션에는 크게 Local Transaction(이하 로컬 트랜잭션)과 Global Transaction(이하 글로벌 트랜잭션)이 있습니다. 우선 정말 간단하게 두 개념에 대해서 설명하자면

 

로컬 트랜잭션 = 하나의 데이터베이스를 하나의 트랜잭션으로 여러 연산을 진행하는 것

글로벌 트랜잭션 = 여러개의 데이터베이스를 하나의 트랜잭션으로 여러 연산을 진행하는 것

 

사실 데이터베이스 말고도 프린터 드라이버라던가 메세지 서비스도 있지만 아직 저도 이 두가지에 대해 써본적도 없으며 취직을 하더라도 한동안 안쓸 것 같아서 데이터베이스만 가지고 얘기해보도록 하겠습니다. 

 

우리는 이 뒤에서 자바에서 꽤 자주 사용하는 몇몇개의 라이브러리를 알아보고 어떻게 사용하는지에 대해서도 알아보도록 하겠습니다. 

 

 

Local Transaciton

로컬 트랜잭션에는 우리가 알고있는 것들이 나옵니다. 바로 JDBC, JPA, JMS이죠 JMS는 좀 생소할지 몰라도 JDBC와 JPA는 우리에게 아주 익숙한 API 중 하나입니다. 

 

JDBC, JPA, JMS가 로컬 트랜잭션이라는 것이 아니고 JDBC, JPA, JMS가 가진 API들이 로컬 트랜잭션을 사용할 수 있도록 도와준다 라고 해석해주시면 될 것 같습니다. 

 

JDBC

JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 어떻게 접근할 것인지에대해 정의해놓은 API입니다. 

 

다른 데이터베이스 접근 기술과 다르게 JDBC 드라이버라는 것을 통해 데이터베이스 커넥션을 얻어오는 것이 특징입니다. 

JDBC의 구조는 위 그림과 같습니다. JDBC는 트랜잭션 아래서 여러 상태들을 실행하기 위한 옵션들을 제공합니다. JDBC의 디폴트 실행은 auto-commit(이하 오토-커밋)입니다. 쉽게 말해서 오토-커밋은 SQL문을 날리는 그 단위 하나하나마다 커밋이 들어간다는 의미입니다. 

 

JDBC의 순서는 다음과 같습니다.

 

  1. 드라이버 매니저로 커넥션을 얻고
  2. Statement 객체를 생성한 후에
  3. 쿼리를 날리고
  4. 커밋

쿼리를 한번 날리면 커밋이 됩니다. 

 

그런데 이렇게 하면 트랜잭션의 원자성을 보장할 수 없지 않나요? 

 

맞습니다. 그래서 JDBC에서는 setAutoCommit을 false로 설정해주면 원자성을 보장할 수 있습니다. 

 

Connection connection = DriverManager.getConnection(CONNECTION_URL, USER, PASSWORD);
try {
    connection.setAutoCommit(false);
    PreparedStatement firstStatement = connection .prepareStatement("firstQuery");
    firstStatement.executeUpdate();
    PreparedStatement secondStatement = connection .prepareStatement("secondQuery");
    secondStatement.executeUpdate();
    connection.commit();
} catch (Exception e) {
    connection.rollback();
}

 

 

JPA

JPA(Java Persistence API)는 자바에서 객체지향인 자바(언어)와 관계형 데이터베이스인 RDBMS를 이어주는 다리 역할을 합니다. 그래서 JPA에는 JPA를 구현한 여러개의 구현체를 가지고 있는데 대표적으로 Hibernate와 EclipseLink 그리고 iBatis가 있습니다. 

 

JPA에서는 Entity라는 도메인 클래스를 생성하고 영원히 데이터를 보관하는 영속성컨텍스트의 1차캐시에 이 클래스를 저장해두고 지속적으로 사용합니다. 

 

JPA의 구조는 아래와 같습니다.

 

영속성 컨텍스트에는 두가지 형태가 있는데 transaction-scoped와 extended-scoped입니다. 두 스코프의 차이는 싱글 트랜잭션이냐 멀티 트랜잭션이냐의 차이가 있습니다. 디폴트 설정은 transaction-scoped 즉 싱글 트랜잭션을 기본으로 작동합니다. 

 

JPA는 EntityManager를 통해 트랜잭션의 원자성을 보장합니다. 

 

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-example");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
    entityManager.getTransaction().begin();
    entityManager.persist(firstEntity);
    entityManager.persist(secondEntity);
    entityManager.getTransaction().commit();
} catch (Exception e) {
    entityManager.getTransaction().rollback();
}

 

 

JMS

JMS(Java Messaging Service)는 자바에서 메시지를 사용해서 비동기성이라는 특징을 가지고 애플리케이션끼리 소통하는 특징이 있습니다. 다른 말로는 메세지 큐잉 시스템이라고도 부릅니다. 

 

JMS가 가진 API로는 create, send, receive, read를 통해 메세지를 처리할 수 있습니다. 

 

JMS의 API들은 싱글 트랜잭션에서 여러개의 주고 받는 동작들을 묶어서 지원합니다. 그런식으로 트랜잭션의 원자성을 지원하죠. 

 

JMS는 특정한 vendor인 ConnectionFactory에서 얻은 커넥션을 세션으로 생성해야합니다.

 

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONNECTION_URL);
Connection connection = = connectionFactory.createConnection();
connection.start();
try {
    Session session = connection.createSession(true, 0);
    Destination = destination = session.createTopic("TEST.FOO");
    MessageProducer producer = session.createProducer(destination);
    producer.send(firstMessage);
    producer.send(secondMessage);
    session.commit();
} catch (Exception e) {
    session.rollback();
}

 

이런식으로 사용하면 되는데 사실 처음보는거라... 사용해본적도 없고 빠르게 넘어가도록 하겠습니다. 

 

 

Global Transaction

글로벌 트랜잭션은 이제 진짜 처음보는 것들입니다. 개념만 집고 빠르게 넘어가겠습니다. 

 

글로벌 트랜잭션은 위에서도 설명했듯이 여러개의 데이터베이스에서 하나의 트랜잭션으로 여러개의 연산을 처리하고 싶을 때 사용합니다. 글로벌 트랜잭션은 하나의 트랜잭션으로 여러개의 데이터베이스의 연산을 처리해야 하기 때문에 하나의 트랜잭션을 둘 혹은 그 이상으로 쪼갭니다. 그래서 글로벌 트랜잭션을 "분산 트랜잭션"이라고도 합니다. 

 

분산 트랜잭션에는 XA Architecture라는 것이 있는데 2PC(2 Phase Commit)를 통한 분산 트랜잭션 처리를 위한 X-Open에서 명시한 표준입니다. 뭐... 이런 XA 아키텍쳐의 개념같은건 그냥 넘어가시고 우리가 볼 건 2PC입니다. 

 

트랜잭션의 수행 단계는 1PC와 2PC가 있습니다. 

 

2PC

begin → end  prepare → commit 의 단계를 거치며 글로벌 트랜잭션을 하려면 반드시 2PC를 해야합니다. 1PC에 비해 prepare의 과정이 들어갔기 때문에 성능상으로는 1PC에 비해 큰 이점을 노릴 수는 없으나 글로벌 트랜잭션을 위해서라면 반드시 거쳐야하는 수행 단계입니다. 

 

1PC

begin → end → commit 의 단계를 거치며 트랜잭션에 참여한 리소스들이 서로 같은 리소스라고 확인되었을 경우에는 prepare가 필요없습니다. 이러한 처리 방식을 1PC라고 부릅니다. 

 

이렇게 처리하게 되면 prepare를 안해도 되므로 트랜잭션의 처리 성능 향상에 도움을 줄 수 있습니다. 따라서 1PC는 트랜잭션 튜닝 방식 중 하나입니다. 

 

cf) 1PC라고 하여 로컬 트랜잭션인 것은 아닙니다. 1PC도 엄연한 글로벌 트랜잭션입니다. 

 

즉, 정리하자면 1PC는 글로벌 트랜잭션안에서 트랜잭션에 참여한 리소스들이 서로 같은 리소스라고 확인되었을 경우( = 같은 데이터베이스인 경우) 2PC는 트랜잭션에 참여한 리소스들이 서로 다른 리소스인 경우 사용하는 수행 단계입니다. 

 

 

 

글로벌 트랜잭션을 사용하기 위해서 JTA, JTS의 API를 사용하면 됩니다. 아직 저와는 먼 얘기이니 간단하게 짚고 넘어가도록 하겠습니다.

 

JTA

JTA(Java Transaction API)는 자바 애플리케이션이나 애플리케이션 서버가 XA 리소스를 위해 분산트랜잭션 수행에서 사용합니다. 위에서 XA에 대해서 알아봤으니 넘어가겠습니다. 

 

JTA는 분산트랜잭션안에서 다른 API와 트랜잭션 매니저 사이의 인터페이스중 스탠다드한 아키텍쳐를 가지고 있습니다. 

 

 

 

JTS

JTS(Java Transaction Service)는 OMG OTS 설계를 분류하는 트랜잭션 매니저를 빌딩하기위해 한 분류입니다. JTS는 JTS 트랜잭션 매니저들 사이에서 트랜잭션 상황을 전파하기위한 스탠다드 CORBA ORB/TS 인터페이스와 Internet Inter-ORB Protocol(IIOP)를 사용합니다.

 

음...일단 개념에 대해서 공식문서에 있는걸 그대로 해석했는데 당최 뭔소린지 하나도 모르겠네요 OMG? OTS? CORBA ORB/TS...IIOP?

 

넘어가도록 하겠습니다...

 

공식문서 내용을 보더라도 그리고 구글링을 해보더라도 JTA에 비해서 JTS는 많이 사용되는 것은 아닌듯합니다. 

 

 

 

스프링에서의 트랜잭션

스프링 플랫폼에서는 로컬 트랜잭션과 글로벌 트랜잭션 둘 다 트랜잭션을 깔끔하게 사용하는 방법을 제공합니다. 스프링의 다른 이점들과 함께 트랜잭션을 다루는 방법에 대해 흥미로운 방식인데요. 

 

스프링은 PlatformTransactionManager라는 인터페이스로 다형성을 이용해 여러가지 로컬 트랜잭션과 글로벌 트랜잭션을 구현할 수 있습니다. 

 

우리가 앞서 봤던 JDBCTransactionManager부터 JPATransactionManager, JMSTransactionManager 이런 다양한 구현체들을 만들었습니다. 

 

즉, 위에서 복잡하게 트랜잭션을 구현하지 않아도 스프링은 이미 개발자가 사용하는 데이터 접근 기술에 맞는 적절한 트랜잭션 기술을 가지고 있다는 것입니다. 

 

스프링에서 트랜잭션을 사용하는 두가지 방법에 대해 알아보고 마치도록 하겠습니다. 

 

  • 선언해서 사용하는 방법
  • 프로그램적으로 사용하는 방법

 

선언해서 사용하는 방법

스프링에서 트랜잭션을 가장쉽고 빠르고 간단하게 사용하는 방법은 바로 선언해서 사용하는 방법입니다. 선언해서 사용한다니 뭔가 느낌이 안오지만 이 말은 어노테이션을 이용한 방법이라는 뜻입니다. 

 

@PersistenceContext
EntityManager entityManager;

@Autowired
JmsTemplate jmsTemplate;

@Transactional(propagation = Propagation.REQUIRED)
public void process(ENTITY, MESSAGE) {
   entityManager.persist(ENTITY);
   jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
}

 

사용하고싶은 메서드에 @Transactional을 붙여주면 끝입니다. 해당 어노테이션은 클래스에도 붙일 수 있으며 클래스에 붙이면 클래스에 속한 모든 메서드에 다 어노테이션이 붙습니다. 

 

이 @Transactional을 사용하면 어노테이션이 붙은 메서드에 한해서 트랜잭션의 원자성을 보장해줍니다. 그리고 ACID 모두 지원해주죠 정말 간편합니다. 

 

 

프로그램적으로 사용하는 방법

공식문서에서도 이 방법은 그다지 추천하지는 않습니다. 왜냐면 트랜잭션의 원자성을 위한 바운더리를 컨트롤하는 이점을 포기하고 사용해야 하기 때문이죠. 

 

즉, 트랜잭션의 원자성을 이용할 수 없다는 것입니다. 

 

 

 

마치며

이렇게 트랜잭션과 ACID 그리고 자바 나아가 스프링에서 트랜잭션을 어떤 것을 지원하는지 어떻게 사용하는지까지 확인해봤습니다. 이번 포스팅을 준비하면서 어려운 부분도 물론 있었지만 알게 된 내용이 많아서 정말 뿌듯하고 알찼던 것 같습니다. 로컬 트랜잭션이라던가 글로벌 트랜잭션이나 XA 아키텍쳐, 2PC, 1PC등 알게 된 내용이 정말 많네요

 

이 글을 보시는 여러분들도 도움이 되셨기를 기원하면서 저는 이만 들어가보도록 하겠습니다. 긴 글 읽어주셔서 정말 감사드리구요 오늘도 좋은 하루 보내세요~

 

 

 

출처

https://www.baeldung.com/java-transactions

=> 트랜잭션 in 자바 공식문서

https://heni.tistory.com/10

 

[Database] XA 개념 정리. XA? Non-XA? 분산트랜잭션

XA는 2PC(2 phase commit)을 통한 분산 트렌젝션 처리를 위한 X-Open에서 명시한 표준. 예를 들어 Oracle데이타베이스와 IBM DB2 데이타베이스간에 2단계검증을 통한 2PC를 보장하여 트렌젝션을 보장시켜주

heni.tistory.com