← 뒤로

프로젝트 요약

문제(AS-IS)

목표(TO-BE)

설계/선택(Key decisions)

결과(Impact)

아래에서 해당 코드 확인 가능 (Github) citron0137/exp-message


구현 상세

목차

  1. 로그인 실패에 따른 횟수 제한 기능 구현
  2. E2E 테스트를 통한 동시성 이슈 발견 및 해결
    • 2-1) E2E 테스트 구현
    • 2-2) 이슈 수정 1 - 실패 횟수 증가 누락 문제 해결
    • 2-3) 이슈 수정 2 - 로그인 실패 카운트 이전에 시작된 로그인 시작 문제 해결
  3. 성능 최적화
    • 3-1) 분산락 도입으로 1차 성능 개선
    • 3-2) 실패 카운트 사전 증가로 2차 성능 개선

1) 로그인 실패에 따른 횟수 제한 기능 구현

loginfailure 도메인에 아래와 같은 기능 구현

  1. (로그인 시작시) email과 ip를 입력받아 실패 횟수가 제한을 넘겼는지 확인하는 로직 추가
  2. (로그인 실패시) email과 ip에 대해 실패 횟수를 1씩 추가하는 로직 추가 (TTL 15분)
  3. (로그인 성공시) email과 ip에 대해 실패 횟수를 초기화해즈는 로직 추가

레포지터리는 아래와 같은 이유로 Redis 선택

2-1) E2E 테스트 구현

아래 E2E 테스트를 Kotlin으로 구현

  1. (Given) 동일 Email로 4개의 실패 요청 전송
  2. (When) 1번과 동일 Email로 20개의 실패 요청 전송
  3. (Then) 2번의 응답 코드와 Redis 실패 카운트 조회

테스트 결과

아래 2가지 이슈로 인해 정상적으로 작동하지 않는 것 확인

  1. 로그인 실패시 Read And Write로 인해 실패 횟수 증가가 누락되는 동시성 이슈 발생
  2. 로그인 실패 카운트 이전에 여러 쓰레드가 로그인을 시작한 경우 동시성 이슈 발생

2-2) 이슈 수정 1 - 실패 횟수 증가 누락 문제 해결

로그인 실패시 count 로직의 Read-Modify-Write를 Luascript를 사용하여 원자 연산으로 변경

테스트 결과

2-3) 이슈 수정 2 - 로그인 실패 카운트 이전에 시작된 로그인 시작 문제 해결

마지막에 한 번 더 실패 횟수가 제한을 넘겼는지 확인 로직 추가

테스트 결과

해당 로직이 비효율적이라는 것을 알았기 때문에 바로 성능 최적화 시작

3-1) 분산락 도입으로 1차 성능 개선

동일 Email로 20개의 실패 요청을 동시에 보내는 E2E 테스트를 K6로 구현 => 응답시간 P95 92.05ms

응답 시간이 느린 편은 아니나,
모든 요청이 Mysql을 거치는 부분을 개선하여 성능 개선이 가능할 것으로 예상됨

따라서 락을 통해 아래와 같은 성능 개선을 기대함

동시성 이슈 상황에서 차단 로직 분석

AS-IS:

  1. Redis loginFailCount 조회
  2. Mysql User 조회
  3. Redis loginFailCount++ 및 재조회
  4. LOCKED 반환

TO-BE(락 적용):

  1. Redis loginFailCount 조회
  2. Redis Set Lock or Throw “LOCKED” // 대부분이 이때 LOCKED 반환
  3. Redis loginFailCount 재조회 // 나머지가 이때 LOCKED 반환
  4. (참고) Mysql User 조회
  5. (참고) Redis loginFailCount++ 및 재조회
  6. (참고) Redis Unset Lock

동시성 이슈 발생 상황에서
기존의 경우 Redis 연산 2회와 Mysql연산 1회가 있는데 반해
새로운 경우 Redis 연산 2회 만 있을 것으로 예상됨 (Mysql 연산 1회 감소)

다만 2번의 Waiting에 대한 Redis연산이 더 있을 수 있기 때문에, Waiting은 과감히 없앰 이로 인해 동시성 이슈 발생 시 무조건 1개 요청은 LOCKED 응답받음

구현 후 위와 동일한 테스트 실행시 => 응답시간 P95 20.8ms (77% 감소)

3-2) 실패 카운트 사전 증가로 2차 성능 개선

아래와 같이 한 번 더 개선이 가능하다고 생각됨

AS-IS(락 적용):

  1. Redis loginFailCount 조회
  2. Redis Set Lock or Throw “LOCKED” // 대부분이 이때 LOCKED 반환
  3. Redis loginFailCount 재조회 // 나머지가 이때 LOCKED 반환
  4. (참고) Mysql User 조회
  5. (참고) Redis loginFailCount++ 및 재조회
  6. (참고) Redis Unset Lock

TO-BE(락 미 적용):

  1. Redis increaseLoginFailCountOrThrow // 5미만일 경우 증가 아니면 LOCKED 반환
  2. (참고) Mysql User 조회
  3. (참고) (성공시에만) Redis resetLoginFailureCount

이 경우 Redis 연산을 기존 2회에서 1회로 감소가 가능하다고 생각됨

구현 후 위와 동일한 테스트 실행시 => 응답시간 P95 17.95ms (10% 감소)

다만 LuaScript가 비교적 복잡해짐으로 인한 Trade-off는 있는 것 같음
=> 명확한 함수명과 주석을 사용해서 잘 캡슐화하는 것이 필요할 것 같음


어떠한 날카로운 피드백이더라도 환영합니다. 사소한 의견도 괜찮습니다.

citron0137@gmail.com 또는 LinkedIn 을 통해 피드백을 보내주세요.