본문 바로가기

Study

트랜잭션의 개념과 사용

트랜잭션

트랜잭션은 사전적으로는 거래라는 뜻을 가지고 있다. 데이터베이스 관점에서 트랜잭션은 데이터베이스의 논리적인 작업 단위를 뜻한다. 개발자들에게 트랜잭션이라는 용어는 대개 db 트랜잭션의 개념을 떠올리게 한다. 실제로 트랜잭션을 적용해야 하는 사례는 대부분 데이터베이스의 변경과 관련되어 있다. 그러나 요구사항과 서버, 인프라의 구조에 따라 데이터베이스 상태가 관여되지 않아도 작업 단위를 트랜잭션으로 처리해야 하는 경우가 있기 때문에, 트랜잭션 == db 트랜잭션 으로 생각해선 안되고 트랜잭션 안에 db트랜잭션이라는 개념이 포함되어 있다고 봐야 한다. 이 글은 db 트랜잭션에 대해 작성되었고 db 트랜잭션을 트랜잭션으로 표현하겠지만(그렇다고 표현을 나눠야 할 필요도 없다. db 트랜잭션은 단지 db에 가해지는 트랜잭션일 뿐이다.), ACID 와 같은 트랜잭션의 기본적인 속성이 db 트랜잭션에 국한되는 개념이 아니라는 것을 유의하면서 읽었으면 좋겠다.

 

db 트랜잭션(이하 트랜잭션)

트랜잭션에서 작업의 단위란 설계자가 정하는 것으로, 단일 질의일 수도 있고 다수의 질의를 묶은 단위일 수도 있다. 트랜잭션은 데이터베이스 작업에 있어서 모든 작업들이 성공적으로 완료될 경우에만 변경사항을 영구적으로 반영(Commit)하고, 문제가 생겼을 경우 작업의 모든 변경을 취소하고 종료한다(Rollback). 트랜잭션의 기본 속성과 트랜잭션을 적용하는 방법에 대해 알아보자.

 

AutoCommit

Autocommit 은 하나의 쿼리 실행에 대해 쿼리가 성공적으로 실행될 경우 바로 commit 하여 변경사항을 데이터베이스에 영구적으로 반영하고, 쿼리 실행 중 문제가 생길 경우 자동으로 Rollback 해 주는 DBMS의 옵션이다. 즉 각각의 쿼리 실행에 대해 트랜잭션 처리를 해 주는 것이다. 대부분의 데이터베이스 관리 시스템에서는 Autocommit 이 기본적으로 활성화되어 있다.

그러나 작업 하나에 여러 쿼리가 묶어서 실행될 경우, Autocommit 이 활성화되어 있다면 작업 진행 도중 문제가 생겼을때 이전에 실행된 쿼리를 Rollback 시킬 수 없다. 따라서 작업 전체를 어떤 단위로 묶어서, 작업 내에서의 각각 쿼리를 언제든지 Rollback시킬 수 있도록 하고 전체 작업이 문제없이 완료될 경우 변경사항을 commit 하여 데이터베이스에 영구적으로 반영하도록 한다. 이것이 트랜잭션이다. 즉 의도에 따라서 단일 명령이 작업 단위가 될 수도 있고, 여러 명령의 묶음이 작업 단위가 될 수 있다. 설계자가 의도한 대로 데이터베이스에 가해지는 한 묶음의 작업을 트랜잭션이라고 한다. 트랜잭션에 여러 쿼리가 포함될 경우, 트랜잭션 시작과 동시에 Autocommit 을 off 로 설정하여 쿼리가 실행 즉시 반영되지 않도록 한다. 트랜잭션이 종료되면 Autocommit 옵션을 트랜잭션 시작 시점으로 되돌려 놓는다.

 

트랜잭션의 4가지 속성 - ACID

트랜잭션의 핵심을 관통하는 4가지 키워드로, Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(영구성) 이 그것이다.

1. 원자성(Atomicity)
원자성은 트랜잭션 내의 명령이 모두 실행되거나, 모두 실행되지 않거나 둘 중 하나여야 하는 특성을 나타낸다. 즉 트랜잭션은 All or Nothing(성공 또는 실패)의 결과만 가진다. 트랜잭션 내의 작업이 일부 완료되었더라도 중간에 문제가 발생하면 Rollback 을 통해 트랜잭션 이전의 상태로 돌아가야 하며, 트랜잭션 내의 작업이 모두 완료되면 Commit 을 통해 트랜잭션 작업이 정상적으로 반영되었음을 확정지어야 한다.

 

2. 일관성(Consistency)
일관성은 트랜잭션의 처리 결과가 항상 일관성을 가져야 한다는 특성이다.
일관성은 데이터베이스가 가지는 제약조건, 규칙 등 무결성을 유지해야 함을 뜻한다. 데이터베이스의 규칙에 위배된 작업을 시도하면 트랜잭션은 실패하여 롤백되며 데이터베이스는 일관된 상태를 유지한다.

 

3. 고립성(Isolation = 독립성, 격리성)
고립성(독립성)은 각 트랜잭션이 독립적으로 실행되어야 하며, 하나의 트랜잭션의 실행 중에 다른 트랜잭션이 영향을 끼칠 수 없는 특성을 말한다. 
트랜잭션의 독립성은 여러 개의 트랜잭션이 동시에 발생할 때 생각해 볼 수 있는 문제이다. 여러 트랜잭션이 동시에 발생하면, 성능을 위해 DBMS 는 트랜잭션을 병행으로 처리한다. 하지만 DB 상에서 트랜잭션은 실제로 병렬적으로 처리되더라도 연속으로(단일 트랜잭션이 하나씩 하나씩) 처리되는 것과 동일한 결과를 가져와야 한다. 그러나 성능을 위해서는 독립성을 완벽하게 지킬 수 없고, 독립성을 완전하게 지키기 위해서는 성능하락이 불가피하다. 결국 트랜잭션의 격리 원칙과(데이터 정합성) 성능(동시성) 사이에서 타협을 해야 하며, 이를 위해(동시성을 제어하기 위해) DBMS 는 4가지 수준의 isolation level(격리 수준) 을 제공한다. 

1) READ uncommitted - Level 0
 데이터 정합성과 트랜잭션 일관성에 대해 아무 조치도 취하지 않은 상태로, 변경 내용의 commit 이나 rollback 여부에 상관 없이 값을 읽어올 수 있다. 

2) READ committed - Level 1
 트랜잭션이 완료되고 commit 된 데이터만 다른 트랜잭션에서 접근할 수 있도록 허용된 격리 수준이다. 일반적으로 DBMS 에서 기본적으로 설정된 격리 수준이다.

3) REPEATABLE READ - Level 2
자신보다 앞서 시작된 트랜잭션에 대해서는 commit된 데이터만 읽을 수 있으며, 자신보다 나중에 시작된 트랜잭션에 대해서는 Undo 영역에 백업된 데이터를 읽도록 허용된 격리 수준이다. Non Repeatable Read 문제가 발생하지 않지만, 백업 레코드가 많아질수록 성능이 떨어지는 단점이 있다.

4) SERIALIZABLE - Level 3
트랜잭션의 고립성을 철저하게 준수하는, 가장 엄격한 형태의 격리 수준으로 모든 트랜잭션이 직렬되게 동작한다. 일관성 문제가 전혀 발생하지 않지만 동시성이 떨어지므로 잘 사용되지 않는 격리 수준이다.

각 격리 수준에 따라 DBMS 가 어떻게 동시성을 제어하는지에 대한 자세한 내용은 아래 글에 정리해 두었다.
https://shnowball.tistory.com/entry

 

트랜잭션 격리 수준(Isolation Level)과 동시성 제어

동시성 제어동시성 제어란, 동일한 데이터에 두 개 이상의 트랜잭션이 동시에 일어날 때 데이터의 정합성을 위해 트랜잭션의 작업 순서를 조정하는 것을 말한다. 같은 데이터에 일어나는 트랜

shnowball.tistory.com

 

 

 4. 영구성(Durability)
영구성은 데이터베이스에 반영된 내용이 영구적으로 저장되어야 함을 뜻한다. DBMS 에 어떠한 문제가 발생하더라도, 데이터베이스에 반영된 변경은 영구적으로 지속되어야 한다.

 

 

 

Java(Spring) 에서의 적용

Java에서 트랜잭션을 적용하는 방법엔 여러 가지가 있다. 기본적으로Java 어플리케이션에서는 JDBC 인터페이스를 통해 트랜잭션을 처리한다. 그 방법은 다음과 같이 간단하다.

Connection connection = DataSource.getConnection();
 
try{
	connection.setAutoCommit(false);
    // your query
    connection.commit();
}
catch(SQLException e){
	connection.rollback();
}

앞서 말한것과 같이 트랜잭션이 시작함과 동시에 AutoCommit 옵션을 false 로 세팅하고, 작업을 실행한 뒤 commit 을 실행하여 트랜잭션이 완료됨을 확정짓는다. 처리 도중 예외가 발생하면 rollback 을 통해 트랜잭션 이전의 상태로 돌려놓는다.

스프링에서의 적용은 transactionTemplate 를 사용하는 프로그래밍적 방식이 있고 @Transactional 을 사용하는 선언적 방식이 있는데, 대부분 간편한 @Transactional 을 많이 사용하는 추세이다. 

@Transactional
public ResponseDto userInfoUpdate(UserInfoUpdateDto userInfoUpdateDto){
	// your service logic
}

이게 끝(..?)이다. 너무 간단하게 트랜잭션 처리가 끝난다. @Transaction 어노테이션을 사용하는 방법도 마찬가지로 내부적으로 TransactionManager 를 사용하고 결국 JDBC 인터페이스로 트랜잭션을 처리하기 때문에 결과적으로는 동일한 방식이다. 그렇지만 어떻게? 이렇게 간단하게 어노테이션 선언만으로 트랜잭션 처리가 되는걸까? 우리는 setAutoCommit() 이나 commit(), rollback() 메서드를 사용한 적도 없고 찾을 수도 없다. 이게 가능한 이유는 @Transactional 어노테이션이 프록시 방식으로 동작하기 때문인데, 자세한 내용은 스프링 @Transactional의 동작 원리와 스프링 AOP 에서 확인할 수 있다.

결론만 말하자면, 런타임에 @Transactoin 이 선언된 클래스를 상속받은 프록시 객체가 생성된다. 프록시 객체는 쉽게 생각하면 대리로 원래 객체의 역할을 수행하는 껍데기 정도로 말할 수 있다. 즉 원래 객체를 상속한 프록시 객체 내부에서 트랜잭션 전후처리가 실행되고, 호출하는 쪽은 프록시 객체의 존재를 모른 채 안전하게 트랜잭션 처리된 응답을 받을 수 있는 것이다.
스프링의 선언적 트랜잭션 처리 방식은 쓰기 편할 뿐 아니라 코드의 객체지향적 설계에도 도움이 되는 만큼, 트랜잭션 처리를 위해서라면 가장 먼저 고려해야 할 방식이다. 그러나 내부적으로 동작하는 원리를 모른다거나, 언제 어떻게 사용되어야 하는 지 모른 채로 남용하는 것은 가장 경계해야 할 일이라는 것 또한 명심하자.