"6장만 들어갈 수 있는 게시물 S3 스토리지에 그림이 왜 22장이나 들어있어요?"

실전 프로젝트 에서 여러사람이 그림을 이어그리고 끝에는 이 그림들을 하나의 gif 파일로 엮어서 움직이게 하는 서비스를 기획했다. 첫 게시글 작성자가 첫번째 순번의 그림을 그리면서 총 몇장을 그려야 gif 로 엮이는지 부터 제시어가 무엇인지를 설정해서 게시글을 올리면 작성자를 포함한 다른 유저들이 해당 게시글에 이어그리기를 참여하고 gif 로 만들어 가는 것이다.
그렇기 때문에 위의 말을 들었을 때 많이 당황할 수 밖에 없었다.


정말로 22장이 들어있었으며


gif 로 만들어진 파일도 한개만 있어야 하지만 4개나 만들어져 있었다.

어떻게 했냐고 물었더니 연속으로 눌렀다고 답변을 받았다.
분명 개발할 때에는 조건을 걸어서 지정된 갯수 이상일때 추가하기를 누르면 오류를 발생시키게 했고 트랜잭션도 걸어놔서 고려하지 않아도 괜찮을 거라 여겼었다. 이런 식으로 동시성에 관련된 문제가 터질지 몰랐다. 이어그린 그림을 추가하기 눌렀을때 백 서버에서 진행되던 매서드가 끝나기 전에 해당 매서드를 또 호출해서 트랜잭션 과정이 여러개 발생하면서 일어난 문제로 파악되었다.

"영화나 콘서트 예매의 동시성 제어를 생각해 보신 적 있으신가요?"

멘토님께 기획단계에서 받았던 멘토링이었다. 문제가 터지고 나서야 생각이 났다. 우리같이 가벼운 서비스에서는 문제가 크지 않으나, 금융권에서는 매우매우 심각한 문제로 번질 수 있으므로 해당 문제는 중요한 고려사항이다.
사실 우리 서비스에서 S3 저장소에 여러 사진이 들어간다는 문제만 있을 뿐이지, 결국 유저가 보는 그림은 마지막에 생성된 gif 파일만 보게 되므로 사용자 입장에서 차이는 없다. 다만 서비스가 지속되고 데이터 량이 많아진다면 사용되지 않는 자원들을 계속해서 축적시키는 꼴이 되는지라 성능 면에서 반드시 해결해야 했고 데이터베이스나 트랜잭션을 잠구는 방법을 생각했다.

Transaction

어떤 일련의 과정을 하나로 묶어서 전부 실행되거나, 전부 실행되지 않게 조정한다.
돈을 송금하는 과정에서 에러가 발생했다면 송금된 돈까지 전부 없었던 일이 되면서 되돌아가야 하기 때문에 트랜젝션으로 묶는다.

ACID

트렌젝션의 속성을 지칭하는 약자 모음이다.
A : atomicity 의 약자로 원자성을 나타낸다. 모든 작업이 반영되거나 모든 작업이 롤백 되는 특성을 말한다.
C : consistancy 의 약자로 일관성을 나타낸다. 데이터는 미리 정의된 규칙에 의해서만 수정되는 특성이다.
I : isolation 의 약자로 격리성을 나타낸다. 잠금의 핵심으로 A트랜젝션과 B트랜젝션 사이의 격리 특성이다.
D : durability 의 약자로 영속성을 나타낸다. 커밋된 내용이 이제 완전히 반영된다는 특성이다.

Isolatino Level

이번 DB 잠금에 대한 핵심이다. 격리 레벨이 있는데 얼마나 느슨하게/강하게 DB 잠금을 하는지에 대한 정도이다.

  • Read Uncommited
    A 트랜젝션이 데이터를 건드는 중에 B 트랜젝션이 해당 데이터를 조회할 수 있는 격리 레벨 이다.
    가장 격리 수준이 느슨하며 도중에 A가 롤백을 진행하게 되면 B가 볼 때 데이터 부정합이 발생할 수 있다. 이런 부정합을 Dirty Read 라고 한다.

  • Read comitted
    A 트랜젝션이 데이터를 건드는 중에 B 트랜젝션이 해당 데이터를 조회하게 되면 바로 보는 게 아니라 A가 확실히 commit 해서 영속화 시키지 않는 이상은 Undo 영역에 있는 데이터를 보게 된다. B의 경우에는 A가 커밋전에는 기존 데이터를 보는 것과 같으므로 dirty read 같은 부정합을 볼 일이 없다. 허나 B가 보는 와중에 A가 커밋을 완료하게 될 경우에 B가 보는 데이터가 처음과 끝이 다른 Non-repeatable read 가 발생할 수 있다.

  • Repeatable read
    건드는 트랜젝션 마다 고유의 순번을 부여하고 접근하는 트랜젝션의 번호와 비교해서 진행중인 트랜젝션의 번호보다 뒤에 있다면 Undo 영역의 데이터를 읽게 만드는 격리 레벨이다. 그렇기 때문에 트랜젝션이 오래 지속되면 Undo 영역이 커져서 성능 이슈가 발생할 수 있다. 또한 데이터가 갑자기 생겨나거나(update) 갑자기 사라지는(delete) Phantom read 가 발생 할 수 있다.

  • Serializable
    가장 강하게 잠금하는 격리 레벨이다. A 트랜젝션이 수행하는 도중에는 B는 해당 트랜젝션이 접근할 수 없다. 가장 안정적이지만 그 만큼 동시성이 확립되지 못한다. 동시성이 중시되는 서비스에는 적합하지 못하다.

Passimistic Lock VS Optimistic Lock

격리 레벨을 참고하여 대표적으로 두가지 잠금 방식이 존재한다.

  • Passimistic Lock
    비관적 잠금.
    두 트랜젝션 간에 업데이트 간격이 짧고 DB와 직접 연계 되는 2계층 시스템에 적용되며 그럼으로써 DB 충돌이 반드시 발생하게 될것이라 예상되는 곳에 사용한다.
    금융권에서 사용하는 잠금 방식이다.

  • Optimistic Lock
    낙관적 잠금.
    두 트랜젝션에 의한 동시 충돌이 일어날 가능성이 적은 곳에 사용된다. DB와 상시 연결되어 있는것이 아니라 필요할 때만 연결되는 경우와 그러므로 DB 충돌이 일어나지 않을것이라 예상되는 곳에 사용된다. 또한 충돌이 일어나서 dirty read 가 발생해도 큰 문제를 일으키지 않는 곳에서 사용된다.

현재 충돌이 일어났어도 사용자 입장에서 문제없이 조회가 가능하기 때문에 충돌 자체에 대한 문제는 없고 단지 DB에 들어가는 더미들이 늘어나기에 낙관적 잠금방법을 채택했다. 허나 그렇게 생각하면 안되었다. 현재 문제는 DB 에 들어가는 더미 이미지들을 막기 위함이므로 애초에 dirty read 조차 일어나서는 안되었다. 따라서 낙관적 잠금이 아닌 비관적 잠금으로 다시 생각해봤다.


트랜젝션과 함께 락 어노테이션을 적용하고 잠금 타입을 비관적 잠금으로 설정했는데
읽기,쓰기,버전관리에 따른 읽기 종류가 있었다. 쓰는 것을 잠가야 했기에 pessimistic_write 를 택했고 격리 시간을 1초로 잡아내어 해당 문제를 해결 할 수 있었다.

0개의 댓글