트랜잭션?
트랜잭션이란 왜 사용하는걸까?
평소 공부할때는 트랜잭션은 “DB의 데이터를 변화시키는 동작들을 하나의 작업으로 관리하기 위해서 사용한다.” 라고 개념만 알고 있는 것 같다.
여기서 “DB의 데이터를 변화시키는 동작” 들은 INSERT / DELETE / UPDATE 와 같은 DML이 있을 것이다.
감으로는 이해가 어느정도 되는 것 같은데, 아래에 한번 예시를 들어보겠다.
간단하게 예시를 들면, 분식집에서 손님이 짜파게티를 주문하여 주방에서 라면을 끓인다고 해보자.
식당마다 레시피가 있겠지만, 농심의 공식 사이트에 있는 조리법으로 끓이는 가게라고 가정해보자.
그렇다면 작업은 아래와 같이 나눌 수 있다.
1.
물 600ml를 끓인다.
2.
면을 넣는다.
3.
후레이크를 넣는다.
4.
5분을 더 끓인다.
5.
물 8스푼을 남기고 따라 버린다.
6.
과립 스프를 넣는다.
7.
올리브조미유를 넣는다.
짜파게티를 자주 끓여 먹어본 사람이라면, 아래와 같이 “올리브조미유”를 빼먹는 실수를 해보았을 것이다.
1.
물 600ml를 끓인다.
2.
면을 넣는다.
3.
후레이크를 넣는다.
4.
5분을 더 끓인다.
5.
물 8스푼을 남기고 따라 버린다.
6.
과립 스프를 넣는다.
7.
올리브조미유를 넣는다.
만약 손님에게 위와 같이 일부 재료가 누락된 짜파게티를 준다면, 몇몇 손님은 짜파게티에 부족함을 느낄 것이다.
하지만, 이 빼먹은 작업이 올리브조미유가 아닌, “과립 스프”라면? 상상도 하기 힘들다…
이렇듯, 음식을 제공하는 식당에서 직원의 실수 등으로 조리의 일부가 누락된 결과의 음식이 나온다면 식당은 보통 해당 음식을 폐기 처리하고 다시 조리하여 손님께 제공한다.
이 예시에서 알 수 있듯이, 하나의 음식이 나오기까지의 조리 과정을 하나의 트랜잭션으로 묶을 수 있다.
DB도 마찬가지다. 짜파게티에 재료를 넣는게 INSERT / 물을 따라 버리는게 DELETE 라고 간단하게 비유해도 어느정도 맞다.
동일하게, DB에서도 트랜잭션 내부 작업 중 문제가 발생한다면, 해당 작업은 모두 롤백된다.
이게 트랜잭션의 4가지 특징인 “원자성” / “일관성” / “독립성” / “영구성” 중 “원자성” 을 보장한다고 할 수 있다.
만약, 이렇게 UPDATE / INSERT / DELETE 동작이 존재하는 작업을 트랜잭션으로 관리하지 않는다면…?
그 식당은 그냥 배짱장사를 하는거다. 그런 식당은 보통 손님들이 잘 찾지 않게 되며, 이는 식당의 음식에 대한 손님들의 신뢰를 잃어버린 것을 의미한다.
Django에서 트랜잭션을 쓰는 2가지 방법
Django는 트랜잭션을 쓰는 방법이 여러가지가 있다.
1.
트랜잭션을 HTTP Request 단위로 설정하는 방법
2.
Atomic 객체를 사용하여 명시적으로 설정하는 방법
HTTP Request 단위로 설정
이 방법은 각 Request를 하나의 트랜잭션으로 래핑하는 방식이다.
from django.db import transaction
# my_view 내부 전체 코드가 하나의 트랜잭션으로 처리
def my_view(request):
do_stuff()
# my_other_view 내부 전체 코드가 하나의 트랜잭션으로 처리
def my_other_view(request):
do_stuff_on_the_other_database()
# 트랜잭션이 필요없는 view에는 non_atomic_requests 데코레이터를 사용하여 트랜잭션 래핑을 비활성화 할 수 있다.
@transaction.non_atomic_requests
def not_use_transaction(request):
read_all_users()
Python
복사
이는 ATOMIC_REQUESTS 라는 옵션을 True 값으로 설정해 활성화하여 사용할 수 있다.
기본적으로는 False 이다.
옵션을 활성화하면 별도의 추가 설정 없이, 모든 view 요청이 각 트랜잭션으로 래핑된다.
→ 즉, 특정 Request로 인해 view 작업 중 문제가 생긴다면 해당 view 작업은 모두 Rollback 된다.
개인적으로는 그리 추천하는 방식은 아니다.
모든 view가 데이터의 변화를 주는 방식을 사용하는 것도 아닐 것이고, 각 Request마다 모두 트랜잭션이 걸린다면 DB의 부하가 상당할 것이다.
그렇다고 사용하지 않는 모든 view마다 데코레이션을 설정하는건 그리 좋은 것 같지 않다.
트랜잭션을 짧게 가져가야하는 이유?
이는 클라이언트 ~ DB 서버간의 커넥션을 하는 방식을 보면 이해할 수 있다.
DB와의 통신은 TCP로 진행되기 때문에 통신 전후로 여러 과정이 진행된다.
Django + PostgreSQL이라고 하면, 아래와 같을 것이다.
•
settings.py 정보와 데이터베이스 드라이버(보통 psycopg2)를 사용하여 PostgreSQL 커넥션 Open
•
데이터베이스 드라이버(보통 psycopg2)와 PostgreSQL에서 데이터 읽기/쓰기를 위한 TCP 소켓 Open
•
소켓을 통한 데이터 읽기/쓰기
•
커넥션 Close, 소켓 Close
위 과정을 보면, DB와의 한번의 커넥션하는 과정은 단순히 “딸깍” 하는 단순한 과정이 아니다.
만약 아래처럼 각 요청마다 매번 커넥션 과정을 새롭게 진행하고 작업 후 Close한다고 생각하면, DB에는 많은 부하를 불러올 것이다.
[ Data Create 작업 1 ]
“서버-DB 커넥션 Open” → “소켓 Open” → “Data Create” 요청 → “Data Write” → “커넥션 Close, 소켓 Close”
[ Data Update 작업 2 ]
“서버-DB 커넥션 Open” → “소켓 Open” → “Data Update” 요청 → “Data Write” → “커넥션 Close, 소켓 Close”
…
Plain Text
복사
이를 위해, PostgreSQL에서는 PGPool과 같은 미들웨어 도구로 각 커넥션을 관리(그리고 추가적인 여러 기능)해준다.
정확하게는, 이미 생성된 커넥션을 제공-반납하는 방식을 사용하여 불필요한 커넥션 생성/Close 작업을 줄여준다.
보통은 DB의 부하를 막기 위해 동시에 진행 가능한 커넥션을 PGPool과 같은 도구가 제어한다.
물론 MySQL, PostgreSQL DBMS 모두 자체적으로 커넥션 풀이 존재한다.
하지만, 이런 도구를 쓴다면 커넥션 관리 뿐 아니라 부하 분산 / FailOver 등 여러 부가 기능을 사용할 수 있다.
PGPool <---> PostgreSQL 연결 상태
[ Data Create 작업(트랜잭션) 1 ]
"PGPool이 서버에 커넥션 제공“ -> “Data Create” 요청 → “Data Write” → “커넥션 반납"
[ Data Update 작업(트랜잭션) 2 ]
"PGPool이 서버에 커넥션 제공“ -> “Data Update” 요청 → “Data Write” -> "커넥션 반납"
…
Plain Text
복사
이때 중요한점은, 현재 커넥션을 제공받고 데이터 관련 작업(트랜잭션)이 길어지면 제공받은 커넥션의 점유 시간이 길어진다.
결국, 앞선 작업(트랜잭션)이 오래걸린다면 다른 작업(트랜잭션)들은 대기 상태에 빠질 수 있는 것이다.
그래서 트랜잭션 내부에 데이터 변화를 주지 않는 SELECT 작업이나, 네트워크 요청 작업 제외 등 방식으로 가급적 트랜잭션은 짧게 진행하는게 좋다.
물론 트랜잭션의 작업 시간이 무조건 짧은게 정답은 아니다.
작업에 따라, 다른 트랜잭션의 시간 지연 손해를 조금 보더라도 우선적으로, 그리고 매우 안정적으로 동작해야하는 작업(금융 작업 등)이 있을 것이다.
이는 상황에 맞춰서 정하면 된다.
Atomic 객체를 사용하여 명시적으로 설정
이 방식은 HTTP Request 단위로 설정했던 위 방식과 반대이다.
기본적으로 흔하게 사용되는 방식으로, 필요한 영역에 Atomic 객체를 사용하여 명시적으로 설정하는 것이다.
방법은 데코레이터를 사용하여, view 단위로 설정하거나 Context Manager 방식을 사용하는 방법도 있다.
from django.db import transaction
# 데코레이터를 사용하는 방식
@transaction.atomic
def viewfunc(request):
create_user()
# Context Manager를 사용하는 방식
def viewfunc2(request):
read_user()
with transaction.atomic():
update_user()
update_info()
Python
복사
with transaction.atomic() 사용 시, 주의 사항
보통 파이썬에서 Raise를 처리하기 위해 try-except 를 많이 사용한다.
그래서 간혹 with transaction.atomic() 사용 시, 아래와 같이 사용하는 경우가 있다.
간단하게 유저 생성 후 통계 갱신 작업이 하나의 트랜잭션으로 진행되는 예문이다.
이때 우리가 원하는 것은 update_stat() 와 같은 작업에 문제가 생기는 경우 create_user() 로 생성된 사용자 정보도 롤백되는 것이었다.
얼핏보면 정상 작동할 것 같은데, 이렇게 작성하면, 롤백은 진행되지 않는다.
그 이유는 Atomic 객체는 __exit__ 메서드가 동작할 때(with문 내부 코드가 끝나거나, Raise가 발생할 때) 상황에 따라 롤백이 진행되기 때문이다.
def test_func(request):
with transaction.atomic():
try:
create_user()
update_stat()
except:
logging("Raise when create_user()!")
Python
복사
Atomic 객체는 __exit__ 메서드는 아래와 같은 작업을 진행한다.
아래 정리 내용은 현재 이해가 어려울 수 있다.
Atomic 객체를 사용해 트랜잭션 처리 시, 어떤 동작이 진행되는지는 다른 글에서 상세히 다룰 것이기 때문에 큰 작업만 보고 넘어가도 무방하다.
전체 코드는 Github에서도 사용 가능하다. → transaction.py#L224
1.
트랜잭션 상태 업데이트
a.
현 커넥션에 설정된 트랜잭션이 중첩이라면 현재 depth 스택의 트랜잭션 블럭 제거 (이는 Django에서 nested Transaction을 스택으로 depth를 관리하기 때문)
b.
현 커넥션에 설정된 Savepoint가 존재한다면, 해당 Savepoint를 가져오거나 없다면 이후 로직에서 현재는 트랜잭션 블럭 내 존재하지 않도록 변수 값 변경
2.
트랜잭션 종료 처리
a.
현재 DB와 트랜잭션 연결이 닫힌 경우, DB 자체적으로 롤백을 진행하기 때문에 추가적인 작업을 하지 않음
b.
트랜잭션 내 작업 중 예외가 발생하지 않은 경우, 그리고 롤백이 필요 여부를 나타내는 플래그가 False 인 경우
i.
가져온 Savepoint를 커밋한다.
ii.
Savepoint가 없다면 트랜잭션 전체를 커밋한다.
3.
마무리 작업
a.
커넥션 관련 변수를 현 상태에 맞게 갱신한다.
b.
자동 커밋 모드를 다시 활성화한다.
즉, 중요한점은 try-except 내부에 있는 create_user() / update_stat() 작업 시 Raise가 발생한다면 이 Raise가 try-except를 감싸는 Atomic의 __exit__ 에서 처리할 수 있도록 Raise가 전파되지 않는다는 것이다.
결국 Raise가 전파되지 않고 except 영역만 동작하면서 Atomic 객체는 정상적으로 DB 작업이 동작되었다고 생각할 수 있다.
그렇다면 Atomic 객체의 __exit__ 작업 시 Raise 발생 시 동작하는 Rollback 작업이 아닌 일반 Commit 작업이 동작한다.
이는 꼭 Atomic 객체 뿐 아니라, 컨텍스트 매니저 방식(__enter__ + __exit__) 으로 만들어진 객체를 사용할 때 Raise 처리는 꼭 주의해야한다!
Autocommit
Autocommit은 데이터베이스에서 각 SQL 명령이 자동으로 개별 트랜잭션으로 처리되는 기능을 의미한다.
즉 내가 입력한 명령어가 원래는 별도의 처리 없이도 명시적으로 커밋 / 롤백을 지원하는 것이다.
다만, 각 SQL 명령마다 트랜잭션을 나누어 우리가 못 느끼는 것 뿐이다.
Python에서 DB와 연결할 때 사용되는 라이브러리(드라이버)를 제작할 때 지켜야하는 가이드라인인 PEP 249에는 기본적으로 Autocommit를 False 로 설정하여 연결하도록 가이드 되어있다.
만약, Mysql나 PostgreSQL처럼 Autocommit이 On이어도 False 로 연결하여 사용되게 개발을 권장하는 것이다.
하지만, 모든 드라이버가 이를 지키지는 않는다.
mysql-connector-python 와 같이 이 가이드를 지키는 경우가 있고, psycopg2 처럼 이를 지키지 않고 Autocommit을 On으로 설정하는 경우도 있다.
Django에서도 다양한 DBMS와 연결되어 ORM을 제공하는 만큼, 다양한 드라이버를 사용하고 있다.
하지만 Django에서도 Autocommit은 True로 사용하고 있다.
모든 쿼리를 명시적으로 커밋 / 롤백을 하는 것보다, Autocommit을 제공하는 것이 더 좋을 것 같다는 생각이었던 것 같다.
이는 settings.py 에서 AUTOCOMMIT 를 False 로 설정하여 비활성화할 수 있다.
하지만 Django 공식문서에서는 크게 권장하지 않는다.