트랜잭션 이야기 02 - 만능이 아닌 트랜잭션

Posted by 엘키의 주절 주절 on January 23, 2022

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

→ 전편에서 이어진다.


개요

당연한 말이지만 DB는 데이터 저장소다.

데이터 저장소로써의 효율은 무엇인가? 바로 쓰루풋이다.

쓰루풋을 저하 시키는 원인은 다양하게 존재하는데, 이 중 주요 원인중 하나인 트랜잭션이 무엇인지 살펴보자.

트랜잭션은 종종 언급한대로, 동시 접근 할지도 모르니 무결성을 지켜줘라고 할 수 있다.

DB 엔진 등의 설정으로 row단위 lock으로 잠금 영역을 최소화 했다고 해도, 많은 테이블을 동시 접근해야 될 경우 서로간에 영향을 주는 트랜잭션 단위가 되기 쉽다.

쿼리 매핑 혹은, Stored Procedure 호출은 물론이고, ORM 사용시 스프링의 JPA @Transaction 키워드를 이용한다거나, 명시적 트랜잭션 명령을 통한 방법 등이 있을 수 있다.

무엇을 사용하던 트랜잭션은 잠금의 코스트를 사용하고, 이는 전반적 데이터 베이스 처리 속도 저하를 의미한다.


물론 코드 레벨에서도 잠금 객체의 무분별한 사용, 데이터의 동시 접근을 막기 위한 고민없는 lock 사용이 없는건 아니다.

다만 현재 트렌드에서 어플리케이션, 특히 웹 서버 모델의 경우 lock을 사용할 일 자체가 거의 없게 프레임워크 구조와 코딩 규칙이 잡힌 편이라 이런 이슈를 만들어낼 여지가 적은 편이며, 그런 규칙을 잘 지켰다면 보통 웹 서버 스케일 아웃으로 확장으로 처리량을 늘리는 것도 수월한 편이다.


DB 관점

반면 저장소인 DB는 상황이 조금 다르다.

대부분, DB와 웹 서버는 1:1 관계를 계획하지 않는다.

웹 서버는 확장 가능하고, DB 고정 시키는 1:N 관계가 되는 경우가 일반적이다.

그래서 결국 DB에서 쓰루풋 최대치가 해당 서비스가 처리 가능한 최대치가 되기 쉽다.

이렇게 설계 되는 이유는 웹 서버는 데이터를 들고 있지 않고, DB에 보관된 데이터를 REST API를 통해 인스턴트하게 접근해서 처리하는 구조이기 떄문이다. 즉 웹 서버가 늘어나도, 혹은 웹서버가 줄어들어도 API 처리 단위로 저장된 데이터에 문제가 생기지 않게끔 의도하고 규칙이 성립된 것이다.

이런 상황에서 DB를 확장하기 위해서는 샤딩을 해야 하는데, 이 샤딩의 코스트를 동적으로 처리하기란 쉬운 문제가 아니다.

또한 이 확장과 별개로 1:N 관계다보니 여러개의 웹 서버가 같은 DB를 바라보고 있고, 심지어는 (설정에 따라, 게이트웨이 구성 등에 따라 막아 놨을 수도 있지만) API 호출이 겹칠 경우 같은 서버 내에서도 충분히 같은 데이터에 대한 접근이 이루어질 수 있다.

이를 해결 하기 위한 방법으로 트랜잭션을 사용하는 것이 가장 일반적인 해결책인데, 성능의 저하를 담보로 하는 만큼 고성능이 필요할 경우 다른 대안이 필요하다.

서버가 스케일 아웃되서 수많은 서버가 단일 DB를 바라볼 경우 더 심각한 상황이 오기 쉽다. 서버가 늘어났다는 것은 사용자가 많아졌다는 의미이며, 보통 사용자가 많아 지는 과정에서 비지니스 로직도 복잡해지고, 결합도도 생기기 쉽다. 이는 API 호출 횟수, 복잡한 비지니스 로직 단위로 트랜잭션이 묶일 수 있으므로 더더욱 DB의 한계치를 담보로한 고 코스트 동시 접근 배제라고 볼 수 있게 되는 것이다.


트랜잭션 배제를 위한 고찰

그렇다면 트랜잭션을 사용하지 않기 위한 접근에는 무엇이 있는가?

  1. 게이트웨이에서, 동시 접근되지 않게끔 처리
    • 고유 식별자만으로 체크 가능하면 아주 쉬운 방법이 됨.
    • 만약 여러 사용자 데이터 처리가 필요 할 경우, 예외처리나 추가 규칙이 마련되어야 함.
  2. MQ를 통한 분산 처리
    • 단일 MQ 사용시 MQ가 병목이 될 수 있으므로, 이 역시 큐를 분산해야되며, 큐 분산 규칙이 쟁점이 될 수 있음.
  3. 커스텀 캐시 서버
    • 게임 서버가 종종 선택하는 방법.
    • 트랜잭션없이 분배, 처리하고, DB를 직접 처리하지 않고 최대한 커스텀 캐시 서버 단에서 수행함으로써, DB의 트랜잭션 코스트 및 처리 부하를 경감 시켜주는 데에 목적이 있다.
  4. 동적 샤딩
    • 스케일 아웃 이벤트에 발맞춰, DB 데이터도 샤딩을 준비한다.
    • 해당 샤딩에 규칙에 맞게 게이트웨이는 라우팅 해주어야하며, DB가 아직 준비 중일 경우 기존 DB에서 처리되어야하므로, 타이밍 이슈가 존재하기 쉽다.
      • 사용하는 DB에 따라서, 동적 리밸런싱, 동적 샤딩 정책을 잘 살펴보고 적용 가능한 DB도 존재하는데, 테스트는 많이 필요하고, 예외처리도 해주는게 좋다.
      • 데이터가 없다면, 기존 샤딩 베이스 DB에서 가져오는 방법 등을 예외처리 해두어야, 타이밍 이슈를 해소할 수 있다.
    • 정적 샤딩을 배제하는 이유는, 서비스 점검 시간이 필요하기 떄문이지만, 미리 대비해둔 정적 샤딩 서버에 동적 리밸런싱을 하는 접근도 가능하다.
  5. NoSQL을 사용
    • NoSQL은 잠금 비용이 아주 작은 편이며, 대부분의 NoSQL은 동적 샤딩을 지원하며, 쓰루풋도 RDB보다 끌어올리기 쉽다.
      • 다만 NoSQL을 메인으로 사용하기에는 멀티 Document 트랜잭션을 지원하지 않는 케이스도 있으며, 지원한다 해도 트랜잭션의 원리가 그렇듯 코스트를 많이 사용할 수 밖에 없다.
    • 또한 애초에 규칙이 그렇듯, 무결성을 덜 지키며 성능을 끌어올렸다보니, 이를 감안한 서버간 구조 (아키텍쳐)나, 어플리케이션 내부 처리 구조가 뒷받침 되어야 할 때도 필요한 선택이 될 수 있는 점은 염두에 두어야 한다.

마치며

여러가지 이야기를 하게 됐는데, 업무나 토론, 교육을 진행하던 중에 꽤나 많은 개발자들에게 트랜잭션에 대한 맹목적인 믿음이 느껴졌다.

이 경우 시간을 들여 매번 설명하곤 했는데, 트랜잭션이 가진 장점은 당연히 뚜렷하지만, 그에 반사적인 단점에 대해서 대부분의 일반적 웹서버를 쓰는 경우 (특히 고성능이 필요하다면) 반드시 고민해야 한다.

이 점을 생각해줬으면 하는 바램에서 이렇게 글을 써보았다.