Skip to content

9. 프로젝트 회고

hyeonsunny edited this page Feb 7, 2025 · 7 revisions

프로젝트를 구상할 때 고민했으면 좋았을텐데싶은 만들고 난 뒤에 후회와
그 당시 고민의 결론이 났었던 고민들에 대한 회고


프로젝트 시작하기

익숙한 기술, 이미 정의되어 있는 요구사항과 설계가 있는 상황에서 개발을 해오다 프로젝트 주제부터 주체적으로 정하려니 막막함이 앞섰다.
막막함 때문에 무엇도 선택하지 못하고 있을 때 작년 KBO 포스트시즌 티켓팅 실패라는 경험 덕분에 고민이 단순해졌다.
“인터파크 예매 시스템을 이해하면 티켓팅 성공에 가까워질 수 있지 않을까?” 라는 지극히 개인적인 욕심과 개발자만 떠올릴 수 있는 궁금증으로 이 프로젝트를 시작할 수 있게 되었다.

선택이유

  • 티켓팅에 실패한 개발자 동지들의 공감대를 형성할 수 있다
  • 참고할 서비스와 프로젝트들이 있다
  • 가설과 검증을 통해 대기열 시스템을 구현하고 경험해볼 수 있다

자매품) 정말 마음대로 설계해도 될까?

회사에서 업무 진행시 팀에서 배정된 역할만 담당했기에 기술적 고민과 선택은 개인이 아닌 팀원들과의 대화 또는 코더였다.
하지만 이 프로젝트는 온전히 스스로 고민해야 하고 의사 결정도 타인이 수긍할 수 있는 근거가 있어야 했기에 낯선 상황뿐이였다.
더군다나 고민 없이 사용하던 스프링 버전, 데이터베이스, 라이브러리들이 매번 선택으로 다가오니 고민을 해결해줄 기준이 필요했다.

선택기준

  • 공식 지원 종료된 것은 사용하지 않는다
  • Spring에서 지원하는 라이브러리와 동일한 기능을 지원하는 라이브러리는 추가하지 않는다
  • 메이저 버전을 변경하고 싶다면 가용성이 큰 버전을 사용한다
  • 이전 회사에서 익숙한, 오래된 것에서 벗어나 새로운 지식을 습득할 가치가 있는 것이 있다면 선택한다

자매품) 개발자의 궁금증을 요구사항으로

서비스가 궁금하다고 하여 처음부터 만들 수는 없었다.
멘토링이 끝나기 전에 대기열 시스템을 다 만들어야 했기 때문에 절대적인 시간과 지식을 습득할 시간, 구현할 시간, 삽질할 시간이 부족했다.
빠르고 간결하게 궁금증을 작성하는 것부터 시작하여 이슈를 만들어갔다.
그러나 막상 작성된 요구사항을 기준으로 개발을 하다보니 기능간 결합도가 강한 부분과 필요하지 않는 요구사항들이 생겼다.

“어디서 부터 잘못된 걸까?”

팔짱을 낀채 모니터를 뚫어져라 쳐다보아도 뭐가 문제인지 알 수 없었다.
나의 지식의 한계로 발생한 문제였고, 상황을 해결할 방법으로 빠른길도 없었다.
요구사항이란 무엇인가부터 시작했고 기존에 작성한 문서의 문제점을 찾게되었다.

본문보기


PR의 책임과 분리

자매품 시리즈 상황을 먼저 접하고 PR을 올렸다면 PR에 올라간 Files Changed 개수가 몇십개가 되는 사태는 벌어지지는 않았을텐데..
덕분에 브랜치마다 rebase, merge의 늪에 빠졌었다. 각설은 접어두고, PR을 올릴때마다 PR의 책임이 커졌다 이유는 아래와 같다.

Files Changed 개수가 몇십개가 된 이유

  1. ‘A 기능을 수행하기 위해서는 B 기능도 함께 수행되어야 하는데 함께 PR을 올려야 리뷰어가 코드 맥락 파악할 수 있지 않을까’ 라는 생각
  2. 구체적이지 않고 추상적이였던 요구사항
  3. 구체적이지 않은 요구사항을 범주로한 그룹화 및 이슈 생성
  4. 2번 요구사항 + 1번의 생각 = 기능간 결합도가 높아지고 PR의 책임이 커짐

PR 책임이 큼에 따라 발생할 수 있는 문제점

  • PR 리뷰 어려움
  • 롤백의 어려움
  • 이력 관리의 어려움
  • 병합시 충돌의 가능성

PR 분리 기준

  • 기능의 독립성

    독립적으로 동작 가능한 기능이라면 PR 분리 가능

  • 리뷰어 또는 협업 개발자의 이해도

    기능의 독립성때문에 Create, Read, Update, Delete 각각 PR을 하는건 논리적인 흐름을 이해하기 어렵다.
    때문에 PR의 목적과 변경 내용을 명확히 이해할 수 있는 범위를 갖는 PR을 올려야 한다.

  • 추적, 롤백 가능성

    하나의 PR에 모든 CRUD 및 부가적인 작업까지 모두 포함된다면 문제가 발생하였을 때 수정 및 롤백이 어렵다.
    독립적인 기능 단위로 PR을 나눈다면 이슈가 발생한 기능의 추적과 롤백이 가능해진다.

    예) ‘티켓 예매 사이트의 소셜 연동 실패로 회원가입 실패’ 버그 발생

    • PR1 : 회원가입 로직(사용자 입력을 받아 가입하는 방식)
    • PR2 : 소셜연동 로직

    회원가입 관련 PR이 위의 2가지 있다면 이슈를 추적 가능한 PR은 ‘PR2’이다.

PR 사태 이후

PR 사태 이후에도 나아지진 않았다. 분리해야 함을 알고 있었지만 그때에도 어떻게 PR을 분리해야 할지 감이 오지 않았다. 그저 이 기능은 API와도 연관되어 있는데 수정 하게되면 PR단위가 너무 작아지는 것 아닌가? 하는 유사고민에 빠졌었기 때문이다. 결국 요구사항이 추상적이였던게 원인이였다는 걸 안건 회고때였다는게 가장 큰 아쉬움이다.


MySQL에서 왜 sequence를 찾아?

모든 의사결정에는 타당한 이유가 있어야하지만 이 프로젝트의 데이터베이스는 여태 회사에서 써온 MariaDB가 아닌 (단순히)오픈소스 데이터베이스 1위인 MySQL를 써보고 싶었다.
하지만 왜 1위인지, MariaDB와 무엇이 다른점이 있는지, 언제 구분하여 사용해야하는지 알지 못한 선택이였다.
고민이 짧았던 선택은 MySQL에서 sequence를 찾는 부끄러운 결과를 남길 수 밖에.. 문제될 것 없을거라 생각했던 데이터베이스 선택하기 위한 과정을 기록해본다.

목차

본문보기


Redis 단일 인스턴스 성능과 역할 분리

Redis의 ‘R’도 몰랐을 적에 “고민만 하고 공부만 하다가 시간에 쫓기지 말고 일단 만들어보자!”로 만들었다가
한대의 Redis는 캐싱 데이터, 대기열 데이터, 메세지 브로커 역할을 맡았다.
그러다 보니 정말 이게 최선인가? 싶었고 기억하고자 회고 주제중 하나로 삼게되었다.

단일 인스턴스를 선택한 이유

  • 물리적인 Redis 서버를 확장 비용 부담
  • 분산환경을 구축하려고 딥다이브 및 삽질하느라 대기열을 만들지 못할 것이라 판단
  • 빠른 개발 속도

단일 인스턴스로 발생할 수 있는 문제

  • CPU 사용량, 네트워크 I/O 증가로 인한 성능 저하
  • 단일 장애점(Single Point of Failure, SPOF)의 가능성
  • 여러 역할에 따른 다른 역할의 병목 구간 발생 가능성

결론

대기열 시스템이란 결국 고객의 대기 시간을 효율적으로 관리하기 위함인데 한대만으로 대기열 데이터를 관리하고 요청을 처리한다? scale-up이 아니라면 애초에 대용량을 처리할 수 없는 시스템이라고 밖에 말할 수 없을 것 같다.

결국 ‘한개의 Redis 인스턴스에서 caching 및 메세지 브로커 역할을 맡아도 성능상 문제가 없을까?’ 라는 문제가 주어졌었다면 아래의 과정으로 고민 후 코드로 작성해야 했다.

  • 단일 Redis 인스턴스로 구성할 경우 발생할 수 있는 문제
  • 문제 내부 동작원리 파악
  • 문제에 대한 원인과 가설을 세우고 검증 진행
  • 가설 기각 후 새로운 가설을 세우거나 검증
  • 검증 완료된 가설을 문제를 해결하기 위한 코드로 작성

리팩토링과 많은 공부가 필요해 보이는 부분이다.


대기열 생성 전략

프론트가 필요하지 않는데도 대기열에 필요하지도 않는 CRUD API를 만드는 작업을 끝내고나니 대기열을 만들어야 했다.
처음엔 티켓 예매니까 티켓 예매에 대한 대기열만 생각했지 현실 세계에서 겪었던 ‘대기’의 경험은 떠올리지 못했었다.
또한 블로그에서 설명하는 은행 창구, 놀이동산 메커니즘을 이해는 가능하나 이를 단계적으로 구체화 하는 논리력, 문제 해결 능력이 부족했다.
또다시 시간에 쫓기는 개발을 하겠지만 ‘지식을 습득할 시간’에서 부터 시작해야했다.

대기열이란?

여러 프로세스에서 공유 자원을 동시에 요청할 때 발생할 동시성, 병목을 위해 고안된 방법으로 고객의 대기 시간을 관리하고 요청을 효율적으로 처리하기 위한 시스템이다.

대기열을 현실의 문제에서 찾아보자

  • 은행 창구 방식
    • 정의 : 고객이 실제로 줄을 서서 순서대로 서비스를 받는 전통적인 대기열 방식
    • 특징 : 선입선출, 물리적 또는 논리적 대기열
    • 단계별 구체화
      1. 대기열 입장
      2. 대기
      3. 서비스 제공
      4. 종료 및 다음 고객 호출
      5. 예외 처리
    • 장점 : 단순하고 직관적인 순서 관리
    • 단점 : 대기 시간 동안 고객은 이탈 불가
  • 놀이동산 방식
    • 정의 : 예약을 통해 대기를 관리하는 방식
    • 특징 : 예약을 하기 때문에 고객은 다른 활동이 가능해짐, 고객은 시스템으로부터 남은 대기 시간, 순번 데이터를 지속적으로 제공받음
    • 단계별 구체화
      1. 가상 대기열 등록
      2. 자유 활동 및 대기 상태 유지
      3. 사전 통보 및 호출
      4. 최종 대기 및 서비스 제공
      5. 종료 및 피드백
    • 장점 : 고객 대기 시간 최소화
    • 단점 : 구현의 복잡도, 실시간 알림과 예약 관리

결론

단계별 구체화 방식을 그 당시 알았다면 PR의 책임이 커지거나 Redis의 역할이 커지는 일이 없었을텐데 싶은 아쉬움이 남는다.
개발 당시엔 현실적으로 좀 더 구현 가능성이 있어보이는 은행 창구 방식을 선택했다. 하지만 고객이 현재 대기 순서를 알고 싶어하지 않을까? 라는 욕심을 내었고
결국 대기열 시스템은 ‘은행 창구 방식’, 시스템이 제공해 줄 수 있는 데이터는 ‘놀이동산 방식’을 택했었다.
고객에게 push 해줄 수 있는 시스템을 만들지 못한채 말이다(용두사미)


Lock 전략 선택

좌석 예매를 할 때 동시성을 제어하려면 Lock을 걸어야 한다는 새로운 키워드를 얻었다. 그럼 DB와 Redis 중 어느 곳에 Lock을 걸어야 시스템이 안정적이고 예상한 방향으로 동작할지 궁금했다. 하지만 선택을 하려면 Lock에 대해서 제대로 알고 적재적소에 배치해야 했다.

일단 Lock이 뭘까?

Lock은 여러 실행 단위가 공유된 자원을 동시에 접근할 때, 경쟁 상태에 놓이게 되는데 것을 방지하고 데이터의 일관성과 무결성을 보장하기 위한 제어 수단이다.

Redis와 DB 각 Lock 종류가 다를까?

  • DB의 Lock

    • 데이터베이스 ACID 중 일관성, 격리성을 보장하기 위한 Lock을 제공한다.
    • 비관적 락
      • 데이터를 접근하는 시점에 락을 획득하여 다른 트랜잭션의 접근과 수정을 막는다.
      • 동시성 충돌을 사전에 방지할 수 있다
      • 락을 오래 유지할 수록 다른 트랜잭션의 대기시간이 길어질 수 있고, 성능 저하 및 데드락 가능성 있음
    • 낙관적 락
      • 별도의 락 걸지 않음
      • 버전 번호나 타임 스탬프를 확인하여 충돌 감지
      • 충돌이 발생하면 작업을 재시도하거나 실패 처리
      • 락 오버헤드가 적어 충돌이 적은 환경에서 성능 우수
      • 실패시 재시도하기 때문에 충돌이 많은 환경에서는 성능 저하로 이어짐
  • Redis의 Lock

    • 인메모리 저장소로 빠른 응답 속도를 바탕으로 분산 락 용도로 많이 사용된다
    • SET NX 명령어를 이용한 락
      • SET key value NX EX seconds 명령어를 사용하여 key가 존재하지 않을 때만 설정
      • key 설정 시 만료 시간도 함께 지정하기 때문에 lock이 무한정 유지할 수 없다
      • 구현이 간단하며 단일 인스턴스에 효과적이다
      • 단일 Redis 인스턴스 장애 시 락 관리에 문제가 발생 할 수 있다
    • Redlock 알고리즘을 이용한 락(분산 락)
      • 락의 안정성을 보장하기 위한 Redlock 알고리즘 사용한 락
      • 여러 Redis 인스턴스(노드)에 걸쳐 락을 획득하여 단일 장애점을 제거
      • 분산 환경에서 신뢰할 수 있는 락을 제공
      • 단일 장애점(SPOF) 문제 해결 가능
      • 알고리즘 구현 복잡도

Redis에 Lock을 걸 경우

  • 장점
    • 메모리 기반 시스템이므로 Lock 처리, 좌석 관리에 빠른 읽기/쓰기가 가능하다
    • 빠르게 처리할 수 있는 비영구 데이터는 Redis에서 관리함으로써 데이터베이스에 집중될 수 있는 부하를 줄일 수 있다
  • 한계
    • 영구적이지 않은 데이터이므로 장애 상황에서 데이터 유실이 있을 수 있다
    • 장애 복구 설계, 분산 락 구현의 복잡성 → DB는 Lock걸때 안복잡한가? 코드도 구현해야 하잖아?
  • Redis는 어떤 시나리오에서 적합할까
    • 좌석 예매를 시도할 때? 좌석 예매 후 좌석 상태를 변경할 때?
    • 비영구적인 데이터의 빠른 선점이 필요할 때 적합
    • 분산 환경이 공유 자원에 접근할 경우 적합
    • 빠른 좌석 선택!

DB에 Lock을 걸 경우

  • 장점
    • 데이터베이스의 ACID 특징을 활용할 수 있다
    • 영구적인 데이터에 대한 정확성, 무결성을 보장받을 수 있다
  • 한계
    • 데이터베이스에 Lock을 걸면 대용량 트래픽에서 부하가 집중된다
    • 빠른 처리보다 트랜잭션의 정확성이 중요하다
  • 어떤 시나리오, 자원이 경쟁에 적합할까?
    • 좌석 예매를 시도할 때? 좌석 예매 후 좌석 상태를 변경할 때?
    • 영구적인 데이터의 정확성이 필요할 때 적합
    • 데이터의 무결성이 DB 내에서만 관리되는 작업일 시 적합
    • 예매 확정이 되었을 때!

둘다 Lock을 걸 경우

  • 장점
    • 빠른 좌석 할당
    • 여러 사용자에게 중복된 자리가 할당되지 않도록 보장
    • 정확한 예매 확정
  • 한계
    • Redis의 장애 복구와 락 구현 복잡성
  • 어떤 시나리오, 자원이 경쟁에 적합할까?
    • 영구적, 비영구적 데이터를 분리하여 처리하고 할 때
    • 특정 요구사항을 반영해야 할 때

결론 : Redis에만 Lock 걸기

  • Redis의 구현 복잡성은 시간을 들이면 되는 것이기에 문제가 되지 않을 것으로 판단
  • DB에 Lock을 걸음으로써 부하가 걸릴 수 있는 문제는 Redis의 비영구적인 데이터의 유실보다 크다고 판단
  • DB에 부하가 걸리면 Hang 현상 발생 가능성이 높고 웹애플리케이션 단에서 복구할 수 있는 부분이 아니기 때문이라고 판단

Redis Key 보안

사용자를 특정할 수 있는 정보는 pk일거라 생각하고 민감정보를 key로 넣어도 되는지 고민했었다.

하지만

  • 민감정보가 db의 pk가 될 수 있지만 시퀀스도 pk가 될 수 있다
  • redis의 key로 사용자의 민감정보를 사용하는 것은 보안측면으로도 좋은 선택이 아님
  • 네임스페이스와 암호화를 통해 key를 설정할 수 있지만 암호화된 문자열에 대한 추가적인 관리가 필요함
  • 암호화 문자열이 들어간 key는 사용성이 떨어짐

결론

  • redis key에 민감정보 대신 db 사용자 테이블 설계를 변경
  • 사용자의 시퀀스와 네임스페이스를 혼합하여 key로 사용

프로젝트 종료하기

체크항목 실행 여부 문제점
개발 전 요구사항 작성하였는가? X 1. 개발 단계에서 필요한데로 요구사항이 수정되어짐
2. 계속되는 수정으로 무엇을 위한 시스템인지 목적을 잃어감
개발 전 API Spec 작성하였는가? O 1. API의 책임이 커져서 API 목적이 명확하지 않음
2. 대기열 시스템 구현이 목적인 어플리케이션에서 상품 등록, 수정, 삭제와 같은 불필요한 API 발생
개발 전 시퀀스 다이어그램 작성하였는가? X 객체 간 메시지를 시간의 흐름으로 나눌 수 없을만큼 역할이 비대해진 API
개발 전 서버 구성도 그렸는가? X 서버 간 논리적, 물리적 배치에 따른 어플리케이션 비용을 측정하기 어려워짐
개발 전 ERD 작성하였는가? O 컬럼 정의서를 작성하지 않아 리뷰 받을 시 설명을 반복해야 하는 어려움 발생
개발 전 WBS 작성 및 실행하였는가? X 현재 개발 진척률를 체크하지 않아 누적된 딜레이
개발 중 기능별 테스트 코드를 작성하였는가? O 1. 추상적인 요구사항 때문에 성공/실패 조건이 명확하지 않음
2. 테스트 라이브러리에 대한 이해도 없이 작성된 코드
3. 대기열 기능 테스트만 이뤄지고 성능 테스트가 이뤄지지 않음
기술적 고민과 결정 과정을 기록하였는가? X 한번쯤 문제를 되돌아 보았으면 연쇄적으로 발생하지 않았을 문제들인데 동일 또는 유사한 문제 발생
(예: PR과 API의 책임이 비대해졌던 것)
트러블 슈팅을 작성하였는가? X 차후 리팩토링 시 동일한 문제 발생 가능해짐
이 프로젝트를 완성했다고 말할 수 있는가? X 대기열 시스템 구축이라는 프로젝트 목적을 반영하고 있지 않으므로 미완성

가장 아쉬운 점

서비스 범위

이 프로젝트의 목표가 대기열 시스템이라 하여도 (뜬금없이)예매를 할 수 있는 부가적인 기능이 만들어져야 한다는 생각때문에
주객이 전도 된 유저 시나리오를 따라가느라 급급했다. 자연스럽게 대기열 시스템의 Actor가 필요하지 않는 기능들이 생겨났고
이에 코드와 문서를 정리하는 비용도 증가한 점이 아쉬움으로 남는다.

이렇게 구현할 범위를 혼동하지 않기 위해 제대로된 요구사항을 작성해보자.

자매품) 개발자의 궁금증을 요구사항으로

투두 리스트 격파식 개발

개인 프로젝트를 진행하는 것, 구상하는 것 등.. 모든 것들이 처음이다보니 그저 빨리 투두 리스트를 해결하고 대기열을 만들어야겠다는 생각뿐이였다.
하지만 막상 구현된 작업물을 보고 있자니 온전한 의미에서의 제대로 된 기능이 없었고, 일단 만들긴했다는 기쁨에 취해 있었다.

뒤돌아보면 SI에서 일했던 습관처럼 그저 납기일에 맞춰 어떻게든 돌아가는 코드를 짰을 뿐인데 말이다.

이부분은 회고를 통해서 더욱 더 느꼈다. 그래서 리팩토링을 하고 난 뒤에 회고록이든 뭐든 써야 설득력이 있지 않을까 싶었지만..
프로젝트 마무리 제대로 지어야 다음을 기약할 수 있을거라 생각했다.
이렇게 다른사람이 보는 글을 써야 이전 프로젝트에서의 문제점을 조금이라도 객관적인 시각에서 볼 수 있지 않을까?

분산환경 미구축

트래픽 발생시 부하를 줄이기 위한 시스템인 대기열을 만든 만큼 제대로 된 환경에서 테스트를 해봐야 하는데 동작만 구현하고 환경은 구축하지 않은 것이 아쉽다.
대기열 시스템에서 반드시 발생할 수 있는 문제를 해결하기 위한 분산환경을 구축하지 않아서 더더욱 완성이라고 말하기 어렵다는 생각이 든다.

단일 인스턴스로 이뤄진 것들은 모두 분산해보자.

  • 로드 밸런서
  • 데이터베이스
  • 애플리케이션
  • 대기열 시스템(Redis)

부하 테스트 미진행

API마다 책임이 크더라도 기능적인 단위 테스트는 진행했다. 그뿐이다.
고객의 대기 경험을 개선하고 공유자원에 대한 대기를 관리하기 위한 시스템을 만든 만큼 대기 경험이 어떨지 부하를 만들고, 성능을 테스트하고,
실제 발생되는 병목지점은 어딘지 확인하는 작업이 이뤄져야 단일 장애 지점을 알 수 있을텐데
그에 따른 트러블 슈팅 기록도 얻을 수 있었을테고 가장 아쉬운 것중 하나로 뽑힌다.

어떤 시스템을 만들든 부하 테스트를 하지 않을 순 없지 않은가? 부하 테스트를 할 수 있는 도구는 어떤 것들이 있을까?

코드 컨벤션, 브랜치 전략

코드 컨벤션이 없던 회사에서 쓰던 습관을 고치려면 컨벤션을 고려했었어야 했는데 전혀 반영되지 않았다.
또한 SVN을 사용하다보니 브랜치도 전략적으로 사용하지 못한 것 같다. 그저 feature, main 뿐.

앞으로 혼자서 개발할건 아니니까 최소한 신뢰할 수 있는 회사의 컨벤션이라도 공부하며 가랑이라도 찢어보자.


총평

개인적으로 제대로 잘 만든 프로젝트가 아니다보니 어디다가 대기열 시스템을 만들었다고 명함을 내밀지 못할 것 같다.
(뒤죽박죽 엉망진창 짜잔 만든 수준이랄까)

그래도 이 애증의 프로젝트를 통해 알게된 부족함들을 뿌셔버렸을 때 발전한 모습을 그려볼 수 있는 발판이 되어줄 거라 생각한다.

무엇보다 가장 발전시키고 싶은 역량은,
문제를 명확하고 간단하게 정의하고 전체 흐름을 그릴 줄 아는 능력을 발전시키고 싶다.
코드가 깔끔한 것도 좋아하지만, 다양한 생각들이 하나로 귀결되기 위해 정리되는 과정과 결과물을 보는게 재밌다 : )
그걸 잘 못해내서 많은 자괴감과 갑갑함을 느꼈지만 말이다.

기대해볼 수 있는 미래가 있다는건 오늘을 잘 살아가게 하는 활력소인듯 리팩토링이나 새로운 플젝 가자💪