
이번 주엔
1. TDD를 활용 하기 (RED-GREEN-REFATOR)
2. 포인트에서의 동시성 락을 어떻게 해결할까?
(non-blocking, single-thread 기반의 nest.js)
사실 첫만남은 매우 비호감입니다; 중간고사와 겹치는 불상사!어쩔 수 없습니다.
해내는게 저이고 말고요.
일단 TDD의 기본 베이스는 이렇습니다.
Red : 실패하는 테스트 코드를 먼저 작성
Green : 테스트 코드를 성공시키기 위한 실제 코드를 작성
Refactor : 테스트 결과에 맞추어 (커버리지나 중복 코드 및 읽기 쉬운 코드 등) 리팩토링을 수행하는 것

개인적으로 느낀 장점은 이렇습니다.
이렇게 개발하는 방법은 처음 겪어 봤기에 테스트 코드를 작성을 먼저 해봤습니다.
// 동시에 충전과 사용 요청
const promises = [
service.chargePoint(userId, chargeAmount), // +3000
service.usePoint(userId, useAmount), // -2000
service.chargePoint(userId, chargeAmount) // +3000
];
const results = await Promise.all(promises);
// 최종 잔고는 15000 + 3000 - 2000 + 3000 = 19000이어야 함
const expectedFinalBalance =
initialBalance + chargeAmount - useAmount + chargeAmount;
// 모든 연산이 순차적으로 적용되었는지 확인
// 현재는 동시성 제어가 없어서 예상과 다를 것
const balances = results.map(result => result.point);
const maxBalance = Math.max(...balances);
expect(maxBalance).toBe(expectedFinalBalance);
}, 10000);
이렇게 spec.ts를 기준으로 가설을 세우고, 동시에 충전과 사용 요청을 만들어 봤습니다.

초기값과 최종값에만 의존하는 상태함수 개념 (간단한 아이디어)
그래도 아직 테스트 코드 자체의 문법이 익숙하지 않아 학습할 필요가 있어
숙달해야할 듯 합니다.
사실 제대로 했을까?

마치 문제를 정의하고 틀리게 해결하면 무슨 소용일까요.
위의 테스트 코드의 전략과 구현의 글을 보고 좋은 테스트 코드는 무엇인지 잘 숙지를 해야한다는 점입니다.
Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 함
Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안 됨
Repeatable: 어느 환경에서도 반복 가능해야 함
Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 함
Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 함

위 그림을 본다면 덮어쓰기가 2번 동작하네??? 어라,,, 그러면 115달러가 아니고 105달러가 최종 답이네...? John과 Alice는 계좌 금액에 대해 동시 접근을 해...
경쟁 상태(race condition)란?
여러 개의 프로세스가 공유 자원에 동시 접근할 때 실행 순서에 따라 결과값이 달라질 수 있는 현상
그렇다면 어떻게 해결하는 것이 좋을까?
// 전통적인 Mutex 대신 Promise 기반 접근
private readonly locks = new Map<number, Promise<any>>();
Node.js는 싱글 스레드 이벤트 루프 기반
메모리 내 Map을 통한 간단하고 효율적인 구현
별도 라이브러리 의존성 없음
async withLock<T>(userId: number, operation: () => Promise<T>): Promise<T> {
const existingLock = this.locks.get(userId);
// 사용자별 개별 Lock 처리
}
다음과 같은 플로우 차트를 기반으로 기획을 했었다.

사실 이건 거짓 락에 불과한 건데, 제출하고 나서 스스로 분석도 하고 피드백을 참고해보니까...
if (existingLock) await Promise.race([existingLock, timeout]);
const newLock = operation();
this.locks.set(userId, newLock);
여러 호출이 같은 기존 락이 끝나길 기다린 뒤, 동시에 operation()을 시작할 수 있습니다. 마치 오픈런...
operation().finally(() => this.locks.delete(userId));
```
나보다 나중에 들어온 작업이 this.locks.set(userId, next)로 새 작업(tail)을 세팅해도, 먼저 끝난 작업의 finally가 무조건 delete를 호출하면 현재 진행 중인 끝 부분도 같이 지워진다.
existingLock이 reject 되면 다음 호출도 await에서 같이 터짐
-> 연쇄 실패. (보통 직렬화 큐는 “이전 실패와 무관하게 다음
작업은 진행”이 자연스러움)
FIFO 보장 없음. “먼저 도착한 요청이 먼저 실행”이 깨지기 쉽습니다.

가차없는 피드백을 받았습니다,,, 사실 map말고 queue를 썼다면 좋을텐데~
플로우 차트를 그리면서 고민 또 고민을 했어야했고, 왜 맵을 썼는지 이유를 근거해서 구현을 하면 좋을 것 같아.
구현을 하면서 테스트코드 자체가 맞다고 생각하면서 구현을 하니 아무것도 성공할 수 없었습니다. 잘못된 테스트는 잘못된 리팩토링을 불러옵니다