오늘의 삽질 일기 - 동시성 이벤트

·2022년 8월 3일
4

삽질

목록 보기
3/11

그냥 어떻게든 해보고 싶다! 라는 마음이 들었던 동시성 이벤트를 해결해보려고 했다.

근데 여기서 다른 분들이 이것저것 이야기를 해주셨다.

  • 정확하게 버튼을 누른 순서를 보장할 수 없다.
  • 서버에서 보장할 수 있는 것에 대해서 고민을 해보면 좋을 것 같다.
  • 사용자가 컴퓨터의 시간을 조작하는 경우가 있다면?
  • 서버가 한개가 아니라 두개 이상으로 분리가 되어있을 경우의 동기화는 어떻게 해야?

정말 고려해야할 것이 말도 안되게 많은 것이였다(.....)

몇 분은 그냥....추첨식으로 하는게 좋다고....^^.....
선착순하면 서버터지고 고객센터 폭주하고 별 일이 다 생긴다고 이야기를 해주셨다.

그래도 뭔가 해보고는 싶어서 그냥 한번 건드려봤다.

어, 나 이제 부하테스트 도구 쓸 수 있다?

지금까지는 그냥 마우스로 광클하는걸로 부하테스트(...)를 하고 있었는데, JMeter를 통해서 부하테스트를 할 수 있게 됐다.

GraphQL에서는 사용법을 정말 이해를 못하겠어가지고 답답해하고 있었는데
REST API로 바꾸니까 생각보다 쉽게 해결이 됐다.

1초에 1000번도 쏴보고 정말 별거 다해봤다(?)
이제는 뭔가 입력값을 서로 다르게 보내야하는 그런걸 어떻게 해야하는지 알아보면 좋을 것 같다.
직접 다 바꿔서 적는거야 할 수 있긴 한데..... 뭔가 어떻게든 방법이 있지 않을까 싶어서?

데이터베이스는 느리지 않다 + 목적에 따라 사용하는 것일 뿐

나는 데이터베이스가 느리다고 생각했다.

위의 조건을 그냥 실행시킬경우 더티리드가 발생해서 쿠폰을 등록한 사람은 100명이 나오는데
쿠폰의 재고는 90개 이상이 남는 일이 벌어졌기 때문인데(....)

이건 데이터베이스가 느린게 아니라 그냥 비동기여서 발생하는 문제같기도 하고
내가 생각하는 것처럼 레디스라는 메모리DB를 사용하는 것 보다는 DB로도 대부분 해결할 수 있다는 이야기도 함께 해주셨다.

그리고 엘라스틱서치에 관한 이야기가 나왔는데

내가 부트캠프에서 배울 때는, 수백만까지는 일반 디비를 써도 충분히 빠르기 때문에 그 이상의 데이터에서 사용한다. 라는 이야기를 들었다.

그런데 스페이스에서 이런 이야기를 해드렸더니 용도에 맞게 사용하는 것이지, 데이터의 양에 따라 결정되는 것이 아니다. 라고 알려주셨다.

예를 들어, 복잡한 내용을 검색하는 것이 필요하다면 데이터의 양이 100개정도 밖에 없더라도
분석기를 통한 검색이 가능한 엘라스틱서치를 사용하는게 맞다는 것.

많은 사람들이 기술(스택)을 정할 때 데이터의 양을 기준으로 사용하는 이유를 말한다고 하는데
데이터의 양이 아니라, 목적따라서 새로운 기술의 도입을 결정하는 것이라고 알려주셨다.

어, NestJS Scope가 이런거였네?

면접에서 질문도 받아봤지만 사용해보지 못했던 스코프를 직접 써보게 됐다.

왜? 발버둥이라도 쳐보려고, 뭐라도 되면 좋겠다 싶어서.

뭐를 써봤냐면 위에 있는 리퀘스트스코프를 써봤다.

들어오는 요청을 한개로 다 묶어버린다면, 값이 틀어지지 않을거야! 라는 마음 하나로.
는 개뿔 요청이 서로 각기 다른 곳에서 오는 것이라 그냥 다 달랐다(....)

왼쪽설정값이 없는 디폴트 스코프, 오른쪽리퀘스트 스코프를 적용해놓았을 때의 this.data의 결과값의이다.

저 this.data를 호출하는 비즈니스 로직에는 1회 호출당 this.data +=1이라는 연산이 들어가있다.

디폴트 스코프싱글톤을 지원하다보니, 요청이 서로 다르더라도 한개의 서버에서 작동하는 것이라
값이 증가하는 것을 확인할 수 있고

리퀘스트 스코프는 각 각의 호출이 새로운 인스턴스를 생성하는 것이라
1이라는 값만 출력되는 것을 볼 수 있었다.

써보면 안다고 하더니, 써봤더니 바로 이놈의 사용 목적을 어느정도 알았다고 해야할까?

한번 호출을 할 때, 여러가지 작업이 로직이 이루어지고 다른것에 의하여 값이 절대로 틀어지면 안되는 일이 생길 경우 사용을 하면 될 것 같다.
마치 결제같은 비즈니스 로직을 짤 때 좋은 것 같기도하고...

아무튼 저거는 내가 해결하려고 했던 이슈에서는 전혀 쓸모가 없었다
땡!

그래서 해결 했니? 아니 못했는데?

저 조건을 올리자마자 수많은 분들이 오셔가지고 이것저것 이야기를 많이 던져주셨는데
결국은 서버에 직접 접근하는 것이 아니라, 메세지큐를 앞에 붙이는 것이 좋다. 라는 결론으로 이어졌다.

왜냐하면 로직이라는 것은 한번 짜놓고 여러번 재사용을 할 수 있기 때문에, 최대한 안정성을 갖추는 것이 좋은 코드라고 하셨고

그렇기 때문에 나중을 위해서라도 한번 짤 때 제대로 짜놓는 것이 좋기에. 메세지큐가 없더라도 만들 수는 있지만 도입하는게 좋다고 하셨다.

그렇게 받은 여러가지 조언들

  • 버튼 누를때 컴퓨터 시간을 같이 보낸다-물론 이건 위변조 가능성이 있으니 백단에서 체크하는 로직 필요
    백단에서 서버가 직접 받지 말고 앞에 큐를 둘것 3. 백단에서 리퀘스트->db트랜잭션 이후 바로 쿠폰 당첨결과 리스폰스하는 응답을 넣지 말것. 노티는 다른방식으로 하는게 차라리 낫습니다
  • 이거 백 명이라 해도 결국은 람다 아키텍쳐 문제예요.
    딱 백 명만? 그러면, MQ에 100개의 리퀘스트 받고, 101번째부터 소진 에러 뿌리고, 한 방에 DB에 100개 레코드 저장하면 되겠지요.
    큐는 그러라고 쓰는 것이니. 그런데, 천만 이벤트 한다고 하면?
  • 물론 큐가 정석적인 해법이긴 하지만요~ 실제로는 redis나 DB에 카운터 물려놓고 트랜잭션 걸어서 해결하기도 해요 (구현 사이즈가 작고, 빠른 구현이 필요한데 큐가 기존에 없을 때)
    redis는 빠른 큐죠. ㅎㅎㅎ

고려를 해야하는 것이 정말 많았던 기능이였다.
예로부터 선착순 이벤트는 서버가 폭발해서 유저들이 쌍욕박는게 일상이였는데
그걸 이 조막만한 지식으로 건드려보려고 했으니 힌트를 주시는 것 마다 알아먹을 수 있는게 하나도 없었다(....)

람다 아키텍처, 람다, AWS 람다에 대해서도 한번 알아봐야할 것 같고....
요청하는 시간을 위변조 할 수 있다는 생각도 못해서 저런 검증도 생각을 해봐야할 것 같다.

그래도 내 지식 수준에서는 모래성을 쌓아보고 싶어서 고민을 해가지고 어느정도 실행까지 해봤다.

그렇게 고민을 해서 생각해낸 방법

아직 메세지큐를 사용해본 적이 없다보니, 그저 비슷하게 구현하는 것이 목표여서 나왔던 결론이 아래와 같았다.

  1. 호출이 들어올 때 마다 레디스에 쿠폰을 key값으로 유저의 아이디를 value로 저장한다.
  2. 전역변수를 0으로 선언하고, ++1씩 카운트한다
  3. 새로운 호출이 들어오면 같은 쿠폰의 key 값에 value에 유저의 아이디를 추가한다
  4. 전역변수가 100이 되면 호출이 또 다시 될 경우 에러메세지를 반환한다.
  5. 전역변수의 값이 100이 되는 기점에서 이벤트리스너라던가 트리거등을 이용하여 해당 쿠폰에 입력되어있는 유저의 모든 아이디값을 가져온다.
  6. 그거로 실제 유저 DB에 입력(동기화) 시킨다.

결국은 레디스를 큐처럼 사용하는 형태가 되어버리긴 했다ㅋㅋ

문제는 이벤트리스너를 어떻게 달아야하는지 모르겠어서 데이터 저장까진 못해봤다(...) 안 X 못 O

심지어 같은 키에 값을 추가하는 것도 몰라서 전역변수의 숫자가 key로 저장되고
값은 쿠폰과 이메일로 들어가있는 것을 볼 수 있다..^^ 숫자는 콘솔로 같이 찍어놓은 것이다.

근데 여기서도 수많은 문제가 있었으니.....

  • 선배님 : 오홍 저 data 변수가 자동으로 atomic 하게 설정되나봐요? += 연산자는 해당 값을 '읽어와서' 조작 후 '쓰는' 과정인데, 읽어올때랑 쓸때 다른 스레드간 경합이 없나봐요?
    : 비동기를 달아놓지 않고, 노드가 싱글스레드라서 그런가 영향을 받지 않더라구요.
    그냥 생각? 아토믹하게 설정된다는게 무슨 말인가 생각을 해봤는데, 결국은 싱글스레드라 영향을 주지 않는다 생각했다.
    정말 맞았음!

  • 선배님 : 전역변수라고 적으셨는데, 물리서버가 두 대 이상이라면요?
    : 예... 망합니다... 제 생각에는 물리서버가 두 대 이상이라면 서버 앞단에 무언가를 달아야하는게 맞는 것 같아요(이게 메세지큐가 될 것 같다.)

  • 선배님 : 1번과 2번 명령 사이의 간격은 어떻게 되나요? 문제가 없을까요?
    : 한개는 클래스 내부의 싱글톤이고, 한개는 비동기라 괜찮을 것 같긴 합니다.
    선배님 : 상상하지말고 문서를 읽어봐요. https://redis.io/commands/rpush/#return
    결론 Redis의 append 명령을 쓰는게 더 좋지 않을까요?
    : 제가 쓰는 라이브러리에는 아무리 찾아봐도 없더라구요. 아래처럼 보이는 주석이 달려있긴 한데, 이게 타입스크립트는 지정이 안되어있어서 좀 알아봐야할 것 같아요.
    선배님 : 이거 한번 알아봐요 https://github.com/redis/node-redis

결국 내가 가지고 있던 생각으로는 한계가 명확한 것이였다.

그렇지만 적어도 레디스의 명령어들만 자유롭게 사용할 수 있기만 한다면, 대부분 해결이 될 것 같기도 해서....
아니 왜 리스트가 없냐고요 진짜 미치겠네(....)
저것에 대해서 조금 더 알아보는 시간을 가진다면 얼추 비슷하게 구현을 할 수 있지 않을까?

그리고 이벤트리스너만 제대로 찾아가지고 적용시키면 아마도? 잘?? 비스무레하게 구현을 할 수 있을 것 같다.

사실 여기서 더 해보는 것도 도움이 될 것이라 생각하지만, 내 지식의 한계점으로는 해결하기 쉬운 문제가 아닌 것 같아서
이쯤에서 내려놓고 알고리즘 공부하다가 조금 더 나의 지식이 쌓이고나면 다시 한번 시도를 해봐야겠다.

끝!

profile
물류 서비스 Backend Software Developer

0개의 댓글