트랜잭션 이야기 - 개발자 마다 다른 DB를 대하는 자세

Posted by 엘키의 주절 주절 on February 7, 2020

내 입장에서 웹 개발자 분들과 같이 협업하고 대화하면서 아이러니 했던 두가지가 있었다.

10년도 더 넘게 지난 얘기지만, 한가지는 비동기 응답이 어렵다는 이야기였고, 그렇다보니 여러가지 고민들 중의 해결책인 Ajax라는 이름으로 화두가 되고 있다는 얘기였다.

Native Socket를 다루는게 너무나도 당연하던 나에겐 어째서 비동기 응답을 받을 수 없는걸까에 대해 당황했던 기억이 있다.

또 한가지는 바로 DB를 대하는 자세였다.


기본적으로 웹 서버는 DB에 모든 것을 기록하려 한다. (혹은 아예 정적 데이터를 사용하던지)

로컬 파일 IO, 로컬 메모리 사용을 할 경우, 로드 밸런싱시의 유실등의 우려로 임시 파일 혹은 임시 데이터로만 다뤘다.

이전 요청과 현재 요청이 같은 서버로 간다는 가정을 하는 것 자체가, 로드밸런싱을 가로 막는 제약이 되어버리기 쉽상이기 때문이다.

이로 인한 DB성능에 대한 제약을 웹은 지연된 응답, 즉 캐싱으로 대처한다.

심지어 브라우저마저 캐싱을 기본적으로 지원하며 이는 웹 서버의 부담을 줄이기 위해 클라이언트인 브라우저 마저 캐싱을 지원하다니!

이는 지난번에 언급한대로, 웹의 발전에는 다수가 같은 컨텐츠를 봐도 되는 상황, 소수의 편집자가 데이터를 변경하는 상황, 일시적인 지연을 용인하는 상황 등이 근거다.


반면 훨씬 더 짧은 시간 내에 수많은 변경을 다뤄야 하는 게임에서는 DB를 그렇게 사용할 경우, 쓰루풋을 따라잡지 못해 병목 포인트가 된다.

그래서 게임 서버에서의 흔한 선택은 메모리 캐싱 -> 캐시 서버 -> DBMS 다.

백섭, 돈복사, 아이템 복사 등의 이슈는 바로 캐싱을 위한 제약 (캐싱이 동시에 여러곳에서 이루어 지지 않게 한다거나, 캐싱된 데이터가 저장되기 전에 다른 곳에서의 변경이 이루어지지 않게 한다거나, 일정 시간 단위 저장을 한다거나, 일정 이벤트에 맞춰 저장 한다거나)을 위배하게 되었을 때, 혹은 제약 자체가 서버 장애 혹은 네트웍 오류, 타이밍 버그 등으로 인해 붉어졌을 때 발생하는 일종의 취약점이자, 리스크라고 할 수 있다.

이런 선택과 한계, 장단점을 듣고 의아해하고 신기해하는 웹 개발자 분들이 많았다. 오히려, 나로썬 당연했던 이런 이야기가 신기하다는 것이 놀라웠다.

게임 서버의 응답성이 중요한 데이터 처리량은 일반적으로 디비 서버의 쓰루풋으론 따라가기 힘들다. 심지어 여기에 트랜잭션이 묶으면 쓰루풋 지옥이 펼쳐지기 쉽상이다.

트랜잭션은 실질적으로 동시에 접근하는데, 특정 데이터 묶음의 일관성을 유지하기 위해 큰 비용을 소모해서 거는 잠금이다. 그렇다보니 의도치 않은 쓰루풋의 급감을 끌어내기도 하지만, 호출 구조가 받쳐주지 않는다면 어쩔 수 없는 필연적 선택이기도 하다.

웹에서도 중요 데이터에는 트랜잭션을 묶고, 데이터 무결성을 지키게 되는데, 이는 애초에 같은 DB를 바라보는 웹 서버를 여럿 띄워서 로드 밸런싱하는 것이 아주 일반화된 상황이기 때문이라고 할 수 있다.

시대가 바뀌어가며 잊게되곤 하지만, 데이터 베이스 개론과 같은 책에서 언급하듯 DB의 시작은 파일 시스템이며, DBMS에 모든 것을 위임하면 쓰루풋이 급감하므로, 어플리케이션이 테이블 구조, 연관관계, 접근 방식을 DBMS에 맞출 필요가 있었다. 여전히 DBMS 운용 비용은 싸진 않으므로, 모든 코스트를 DBMS에 위임하는 것은 아직까진 낭비의 영역에 있다.

반면 게임에서는 트랜잭션이 필요하지 않게끔 어플리케이션 레이어의 DB 접근 규약을 정리하거나, 캐시 서버 단계에서 순서 관리, 동시 접근 제어 등을 통해 트랜잭션을 우회하는 방법이 존재한다.

이는 웹보다 게임이 다루는 엔티티가 많고, 데이터 변화량이 잦고, 빠른 응답성을 위해 여러가지 해결책이 고안되었고, 발전해왔다고도 볼 수 있다.

만약 게임서버건, 웹서버건 어쩔 수 없이 트랜잭션을 사용해야 하는 아키텍쳐를 구성해야 하거나, 이미 구성된 서비스를 운용해야 한다면, DB단위를 격리하고 (수평 파티셔딩 등), 결합이 있는 상황만 별개의 방식으로 처리하는 것이 좋지 않나 싶다.


DB의 가용량이 서비스의 가용량이라는 점은 게임 서버나, 웹 서버나 동일한 제약이다.

그래서 여러가지 우회방법이 고민되고, 서비스의 특성에 맞게 트레이드 오프를 하게 되는데, 이 과정에서의 접근이 발상이 게임이 조금 더 극단적이라는 것은, 아마도 모든 자원이 한참 부족했던 시기에 어떻게든 반응성, 쓰루풋, 안정성을 확보해야 했던 게임이라는 특수한 환경에서 벌어진 이슈였다고 생각하고, 지금은 조금 더 안정적인 아키텍쳐가 필요한게 아닐까 싶은 생각이 드는 것도 사실이다.


시기적인 영향도 없다고 볼 순 없는데, 서버 머신 스펙이 부족하던 시기, 네트웍 트래픽이 부족하던 시기, 네트웍 장비 오류, 네트웍 장비 성능도 부족하던 시기에 할 수 있는 최적화는 어플리케이션 내부에 대한 최적화였던 것도 한몫 했다.

나 역시 도커가 활성화 된지 한참 후에야 사용하고 적용하게 되었고, 도커가 상태 기반 서버에 적합하지 않은 측면이 많다는 점도 함께 알게 됐다. (몇가지 제약을 통해 우회 가능하나, 근본적으로 상태 기반 서버가 되면 될수록 제약과 단점이 많이 생기는게 사실이다.)

애초에 상태 기반 서버에서의 스케일 아웃은 꽤나 많은 실수의 소지, 운용 비용, 관리 비용, 기술적 난이도가 상승한다. Akka와 같은 Actor 기반 모델을 사용해서 해소할 수 있는 부분도 있긴하지만, 상태 기반 서버에서의 많은 습관과 상식을 바꿔야한다.

지금처럼 DBMS를 IOPS만큼 비용 지불하고 관리해주는 서비스도 없었다. 어떻게 접근해야 DBMS의 성능을 극대화 시키면서, 무결성을 지킬 수 있는지 잘 알지 못했다.


결과론적으로 트랜잭션은 어플리케이션에서 동시 접근 할 가능성이 높은 상황에서의 무결성을 위한 고비용의 잠금 객체라고 볼 수 있다

이를 위해 크리티컬 섹션과 보다 더 큰 코스트를 소모한다. 결과적으로 DB의 성능 효율과 트레이드 오프하게 되는 것인데, 성능이 덜 중요하되 무결성과 관리 비용이 중요하다면 선택함직 하지만, DB 성능의 중요성이 있다면 어플리케이션 단계의 아키텍쳐로 트랜잭션을 제거하는 접근을 하는 것이 현재의 대용량 처리의 트렌드이기도 하다.

그런 선택지 중에 NoSQL이나, Cosmos DB, Spanner같은 선택지도 고민해볼 수 있다. 즉 RDB와 트랜잭션은 절대적 선택지가 아니라, 상황별, 요구사항별 선택지라고 생각하는 유연함이 안정성과, 쓰루풋 모두 잡을 수 있는 접근이라고 생각한다.

RDB와 트랜잭션이 필수 불가결한 요소라고 생각된다면, 자신이 주로 사용하고 있는 프레임워크나 엔진의 아키텍쳐 떄문은 아닌지 검토해볼 필요가 있다. 혹은 자신이 익숙한 것에 집착하고 있는 것일 수도 있겠다.

꽤나 많은 기술 도입 과정에서의 논쟁에서, RDB와 트랜잭션에 대한 집착적인 생각을 가진 분들을 많이 만났고, 성능이 중요해질 수록 트랜잭션에 고통 받으면서도 그 선택이 피할 수 없는 굴레라고 느끼는 분들이, 편견을 내려 놓는 계기가 되었으면 하는 바램이다.