본문 바로가기
Database

[DB] 트랜잭션 이해하기 (동시성 이슈와 서비스의 경계)

by 마진 2025. 3. 24.

데이터베이스 트랜잭션이란?

데이터베이스 트랜잭션은 하나의 논리적 작업단위를 나타내는 작업들의 집합이다.

'논리적 작업단위'는 사용자가 DBMS에 처리를 요구하는 기능으로 1개 이상의 작업으로 구성된다.

간단한 예시로 이체 기능이 있다.

 

'사용자 A가 사용자 B에게 1만원을 이체한다.' 라는 요구사항을 처리하는 과정은 다음과 같다.

 

1) 사용자 A의 잔고를 1만원을 차감한다.

2) 사용자 B의 잔고에 1만원을 추가한다.

 

이 두 작업은 반드시 함께 성공하거나 함께 실패해야 하는데, 만약 A의 계좌에서 금액이 차감된 후 시스템 오류로 인해 B의 계좌에 금액이 추가되지 않는다면, 데이터의 일관성이 깨지게 되어 사용자들에게 혼란을 가져올 수 있다.

트랜잭션은 이러한 상황을 방지하여 데이터베이스의 무결성을 보장한다.

 

 

 

트랜잭션의 특성

데이터베이스의 무결성을 보장하여 데이터의 신뢰도를 높이는 트랜잭션은 일반적으로 ACID라는 4가지 특성을 갖는다.

 

1. 원자성

- 트랜잭션의 모든 연산은 전부 실행되거나 전혀 실행되지 않아야 한다.(All or Nothing).
- 트랜잭션 중간에 오류가 발생하면 모든 연산이 취소되어 트랜잭션 실행 전 상태로 되돌아간다.(Rollback).

 

2. 일관성

- 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지된다.

- 트랜잭션 실행 전과 후의 데이터베이스는 각각 일관된 상태여야 한다.

(트랜잭션이 실행되기 전과 후 모두 정해져 있는 제약조건을 만족한다.)

 

3. 격리성

- 여러 트랜잭션이 동시에 실행될 때, 각 트랜잭션은 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장한다.

- 트랜잭션 격리 수준(Isolation Level)을 통해 동시성과 일관성 간의 균형을 조절할 수 있다.

 

4. 지속성

- 성공적으로 완료된 트랜잭션의 결과는 시스템 장애가 발생하더라도 영구적으로 반영되어야 한다.

 

 

 

트랜잭션 사용방법

트랜잭션은 어떻게 사용할 수 있을까?

대표적인 DBMS인 Mariadb와 Application 프레임워크인 Spring을 통해 살펴본다.

 

1. MariaDB

MariaDB 클라이언트는 기본적으로 AUTO COMMIT 옵션이 켜져있기 때문에 쿼리를 수행할 때마다 자동으로 커밋이 이루어진다. 따라서 실제 트랜잭션이 이루어지는 과정을 확인하기 위해서는 start transaction 명령어를 활용해서 직접 트랜잭션(커밋 또는 롤백)을 관리해야한다.

 

(테이블 fruit에 'apple'과 'banana'를 넣는 논리적 작업을 수행하지만, 중간에 오류가 발생하여 연산이 취소되는 상황을 가정한다.)

 

 

2. Spring framework

일반적으로 개발자가 논리적 작업(비즈니스 요구사항)을 위해 클라이언트에 직접 접근하는 일은 없다.

개발한 Application을 통해 요구사항을 처리하는 데, 내부의 API를 활용하여 DB 데이터를 제어할 수 있다.

대표적인 Application 프레임워크인 Spring 에서는 어떻게 사용되는가?

 

매우 쉽다. 하나의 논리적 작업으로 처리하고싶은 메서드에 @Transactional 어노테이션을 사용하면 된다.

 

@Transactional
public void transfer(Long userA, Long userB, Long amount) {
	// userA's 계좌 조회
    // userB's 계좌 조회
    
    // userA's 계좌 잔고 차감 - amount
    // userB's 계좌 잔고 추가 - amount
}

 

 

 

사용 시 주의사항

트랜잭션은 데이터의 신뢰도를 높이는 매우 좋은 기능이지만, Application 측면에서 여러 사용자가 동시에 데이터 처리를 요청하거나 기능이 제공하는 논리적 범위의 경계에 주의를 기울여야한다.

 

1. 동시성 제어 

다수의 작업 프로세스가 공통된 리소스에 접근하여 상태를 변경하고자 할 때 '동시성 이슈'가 발생할 수 있다.

 

트랜잭션 격리 수준에 따라 데이터 일관성과 동시성 간의 균형이 결정되는데, 높은 격리 수준을 설정할 경우 더 나은 데이터 일관성을 제공하지만 동시성은 감소한다. 동시성이 감소하기 때문에 여러 작업을 동시에 처리하지 못하고 하나의 작업이 처리될 때 다른 작업들이 대기해야한다.

 

따라서 격리 수준을 높게 설정하면 Application의 성능을 저하시키기 때문에 일반적으로 대부분의 DBMS는 격리 수준으로 'READ COMMITTED'이나 'REPEATABLE READ'를 설정한다. 이 두 격리수준일 때 발생가능한 동시성 문제는

'데이터베이스락'이나 '분산락'을 활용하여 처리할 수 있다.

 

 

데이터베이스 락 (DB Locking)

동시성 문제를 해결하기 위해 애플리케이션에서 DB의 데이터(레코드) 변경을 제한하거나 잠그는 방식으로 낙관적 락과 비관적 락이 있다.

낙관적 락(Optimistic Lock)은 트랜잭션의 동시 업데이트가 빈번하지 않을 것이라고 가정하고 버전 관리를 통해 충돌을 감지하는 방식이다. 실제 DB 락을 사용하지 않아 성능상 이점이 있지만, 충돌 시 롤백이 필요하다는 단점이 존재한다.
일반적으로 나중에 요청된 업데이트를 실패(롤백)처리한다.

비관적 락(Pessimistic Lock)은 동시성 충돌이 빈번할 것으로 예상되는 경우 사용되며, DB 락을 통해 트랜잭션의 충돌을 방지한다. DB 락은 여러 트랜잭션이 동시에 동일한 데이터를 변경하지 못하도록 막아 데이터의 무결성을 보호하는 시스템이지만 성능 비용을 초래할 수 있다. 공유 락(S-Lock)과 베타 락(X-Lock)이 DB락의 주요 유형이다.

 

분산락 (Distributed Lock)
분산락은 여러 서버나 프로세스가 공유 자원에 접근할 때, 동시성을 제어하기 위해 사용하는 동기화 메커니즘이다.
예를 들어, 여러 서버에서 동시에 같은 데이터의 정보를 업데이트 할 때 분산락을 통해 한 번에 하나의 서버만 데이터를 수정할 수 있도록 보장한다.


분산락은 Redis 같은 공통 저장소를 활용하여 자원의 사용 여부를 체크하는 방식으로 구현된다. (Redis는 인메모리 기반의 고성능 키-값 저장소로, 분산락을 구현하는 데 널리 사용되는 솔루션이다.)


Redis에서 락을 획득한다는 의미는 '락의 존재여부 확인'과 '존재하지 않을 경우 락 획득'라는 두가지 행위가 원자적(Atomic)으로 이루어진다는 것을 나타내는데, 락을 획득할 때 사용하는 SETNX(SET if Not eXists) 명령어가 이를 잘 표현한다.

 

 

2. 트랜잭션의 경계 (논리적 범위의 경계)

트랜잭션 경계는 비즈니스 로직에 맞게 적절히 설정해야 하는데, 너무 넓게 설정하면 데이터베이스 락 경합이 증가할 수 있고, 너무 좁게 설정하면 데이터 일관성을 보장하지 못할 수 있다.

 

즉, 트랜잭션 범위는 서비스가 제공하는 기능(UseCase)에 대한 논리적 범위와 동일하다. 따라서 비즈니스 도메인 영역과 밀접한 관련이 있으며, Application이 도메인간의 협력으로 구성되더다도 각 도메인은 독립적일 수 있다.

 

단일 도메인 영역과 대응되는 트랜잭션과 복합적인 도메인 영역에 연결되는 트랜잭션 처리의 복잡도는 다르다. 서비스 규모가 작을 때는 트랜잭션 범위가 길고 처리 복잡도가 높더라도 문제가 되지 않으나, 서비스 사용자 규모가 달라지고 트래픽 및 사용 데이터가 증가하면 서버에 부하를 발생시켜 예상하지 못한 이슈가 발생할 수 있다.

 

 

 

마무리

트랜잭션의 개념, 특성, 사용 방법 및 주의사항에 대해 살펴보며, 데이터베이스 트랜잭션은 단순히 기술적인 도구가 아닌, 비즈니스 로직의 무결성을 보장하는 핵심 요소임을 알 수 있었다.

 

별다른 생각 없이 @Transactional 어노테이션을 사용하여 비즈니스 로직을 구현해왔지만, 이제는 보다 깊은 이해를 바탕으로 트랜잭션을 활용할 순간이다.

 

항해플러스를 진행할 때 비즈니스 요구사항과 시스템 특성에 맞는 락 전략을 선택해야 한다는 코멘트를 받았었다. 

 

"비관적 락은 동시성 충돌이 빈번하게 예상되거나 데이터 정합성이 매우 중요한 경우에 효과적인 반면, 낙관적 락은 동시 요청이 드물고 성능이 중요한 상황에 적합하다. 특히, 같은 도메인 내에서도 시나리오에 따라 다른 전략을 적용할 수 있음을 기억해야 한다." 

 

트랜잭션 관리는 기술적 제어인 동시에 비즈니스 설계이기도 하다. 시스템의 규모, 사용자 패턴, 데이터 중요도를 종합적으로 고려하여 트랜잭션 범위와 격리 수준, 락 전략을 설계해야한다는 점을 배웠었고 이번 글을 쓰며 다시 생각해 볼 수 있었다. 

'Database' 카테고리의 다른 글

데이터베이스 트랜잭션 (Database Transaction)  (0) 2022.04.18