Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] RefreshToken 시간복잡도 O(n) -> O(1) 으로 감소 #411

Open
wants to merge 1 commit into
base: suyeon
Choose a base branch
from

Conversation

hooni0918
Copy link
Member

@hooni0918 hooni0918 commented Feb 26, 2025

🔗 연결된 이슈

📄 작업 내용

  • 콜백 대기열 도입
  • 디바운싱 매커니즘 추가
  • 지수 백오프 알고리즘 사용
스크린샷 2025-02-26 오후 10 41 09

기존코드의 코드트리

스크린샷 2025-02-26 오후 10 40 17

변경코드 코드트리

💻 주요 코드 설명

주석은 머지시 모두 삭제할 예정이니 리뷰시에만 참고해주세요

기존 코드의 문제

기존 코드는 재귀적 호출 방식을 사용하여 토큰 갱신 요청을 처리하고 있었습니다.

if self.isRefreshing {
    DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
        self.refreshToken(completion: completion)
    }
    return
}

이 코드는 토큰 갱신이 진행 중일 때 새로운 요청이 들어오면, 0.1초 지연 후 자기 자신(refreshToken)을 다시 호출하는 재귀적 패턴을 사용중이였습니다. 이렇게 했을때

  • O(n) 시간 복잡도: n개의 동시 요청이 들어올 경우, 최악의 시나리오에서 refreshToken 함수는 n번 호출됩니다.(직렬 큐 이기에 무한반복)
  • 불필요한 대기 시간: 각 재귀 호출마다 0.1초의 지연이 발생하여, 여러 요청이 있을 경우 마지막 요청은 (n-1) * 0.1초 동안 대기해야 합니다.
  • 중복된 네트워크 요청: 특정 시나리오(위에서 언급한 경우)에서는 여러 개의 불필요한 네트워크 요청이 발생할 가능성이 있습니다.
  • 깊은 호출 스택: 재귀 호출이 많이 발생하면 호출 스택이 깊어져 메모리 사용량이 증가합니다.

와 같은 문제가 발생했습니다.

매서운 알고리즘+ 자료구조의 도입!!

1. 콜백 대기열 도입

기존 코드는 각 요청마다 재귀적으로 함수를 호출했지만, 지금은 대기열 패턴을 도입했습니다:

// 대기 중인 콜백을 저장하는 배열 추가
private var pendingCompletions: [(Result<String, Error>) -> Void] = []

// 함수 내에서 콜백 저장
self.pendingCompletions.append(completion)

이렇게 변경해서

  • 모든 요청이 단일 배열에 저장됩니다.
  • 토큰 갱신이 한 번만 수행되어도 모든 요청에 응답할 수 있습니다.
  • 호출 스택의 깊이가 일정하게 유지됩니다.

2. 디바운싱 메커니즘 추가

기존 코드는 항상 0.1초 지연 후 다시 시도했지만, 개선된 코드는 더 똑똑한 디바운싱 메커니즘을 도입했습니다:

// 디바운싱 매커니즘을 위한 work item
private var refreshWorkItem: DispatchWorkItem?

// 디바운싱 구현
self.refreshWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
    guard let self = self else { return }
    self.performTokenRefresh(with: currentRefreshToken)
}
self.refreshWorkItem = workItem
DispatchQueue.global().asyncAfter(deadline: .now() + 0.02, execute: workItem)

이렇게 변경해서

  • 짧은 시간(0.02초) 내에 들어오는 모든 요청이 하나의 작업으로 통합됩니다.
  • 이전 대기 중인 작업이 취소되고 새 작업으로 대체됩니다.
  • 0.1초보다 짧은 0.02초 지연으로 반응성이 향상됩니다.

3. 지수 백오프 알고리즘 도입

네트워크 실패 시 재시도 메커니즘으로 지수 백오프 알고리즘을 도입했습니다!

// 지수 백오프: 2^n * 100ms 대기
let delay = pow(2.0, Double(self.requestCount)) * 0.1
DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in
    // 재시도 로직
}

이렇게 변경해서

  • 일시적인 네트워크 오류에 대한 복원력(?)을 향상시켯습니다
  • 재시도 횟수를 제한하여 무한 루프를 방지합니다.

알고리즘 복잡도 개선

알고리즘 복잡도를 O(n)에서 O(1)로 감소시켰습니다.

기존 코드 (O(n)):

  1. n개의 동시 요청이 들어오면
  2. 첫 번째 요청이 처리되는 동안 다른 (n-1)개의 요청은 대기합니다.
  3. 각 대기 요청은 재귀적으로 refreshToken을 호출합니다.
  4. 최악의 경우, 호출 스택 깊이는 n에 비례하여 증가합니다.
  5. 총 대기 시간은 0.1 * (n-1)초가 됩니다.

현재 코드 (O(1)):

  1. n개의 동시 요청이 들어오면
  2. 모든 콜백이 pendingCompletions 배열에 추가됩니다.
  3. 디바운싱을 통해 단 한 번의 네트워크 요청만 수행됩니다.
  4. 결과가 도착하면 모든 콜백이 한 번에 처리됩니다.
  5. 호출 스택 깊이는 상수로 유지됩니다.
  6. 총 대기 시간은 0.02초로 고정됩니다.

👀 기타 더 이야기해볼 점

알고리즘을 직접 프로젝트에 넣고 instruement로 직접 확인해보니 너무 재밋네요 여러분들도 해보세요!

@hooni0918 hooni0918 self-assigned this Feb 26, 2025
@hooni0918 hooni0918 added ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🧡 JiHoon 쌈뽕한 플러팅 꿀팁을 듣고 싶다면 labels Feb 26, 2025
Copy link
Contributor

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지훈님 덕분에 '지수 백오프 알고리즘'을 알게 되었습니다.
서버에 가해지는 부하를 줄이기 위한 이런 방법이 있다니 저도 다음에 필요하게 되면 적용해 보겠습니다.
고생하셨습니다!

Comment on lines +134 to +136
DispatchQueue.main.async {
callbacks.forEach { $0(.failure(error)) }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

콜백에 성공 신호를 메인 큐에서 전달하는 이유가 궁금합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드트리를 최대한 평평하게 하기 위함이였습니다만,, 글로벌 큐에서 작업을 했어도 되었을 작업이였습니다.
성능개선에만 집중하다보니 이상한 코드가 나와버렷네요

Comment on lines +25 to +27
// 네트워크 요청 횟수를 추적하는 카운터
private var requestCount = 0
private let maxRequestRetries = 3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxRequestRetries가 3인 기준이 궁금합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재시도 지연 시간이 0.2초, 0.4초, 0.8초로 증가하면서 총 1.4초까지만 대기하고자 하는 목적입니다 대부분 저 안에 해결이 될것이라 생각했고 대게 3-5회 정도 반복하는게 일시적인 문제를 해결할 수 있다고 생각했습니다

Comment on lines +139 to +142
// 재시도 여부 결정 로직
private func shouldRetry() -> Bool {
return requestCount < maxRequestRetries
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

계산 속성으로 구현해도 좋을 듯 하네요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은생각이에요!

Comment on lines +117 to +124
let callbacks = self.pendingCompletions
self.pendingCompletions = []
self.isRefreshing = false
self.requestCount = 0

DispatchQueue.main.async {
callbacks.forEach { $0(.success(token)) }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어차피 배열의 아이템마다 어떠한 동작을 수행하고, 배열을 비워야 한다면 아래와 같은 코드도 가능할 것 같습니다.

while let callback = self.pendingCompletions.popLast() {
    DispatchQueue.main.async {
        callback(.success(token))
    }
}
self.isRefreshing = false
self.requestCount = 0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 의견입니다 하지만 해당 코드로 진행했을때 극단적으로 생각해서 중간에 값이 변경될 가능성이 있지 않을까요? (원자성X) 그리고 현재 코드는 단일 비동기 블록으로 처리하는 반면 제안주신 코드는 각 콜백마다 비동기 작업을 예약하는 형식이여서 스케쥴링 오버헤드가 좀더 발생할 여지가 있지 않을까요?
(물론 아주 극단적이고 아주아주 미미할것이라는건 알고있습니다,,)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 확실히 그러한 부분도 있을 수 있겠군요.
동시성 프로그래밍 속에서 race condition을 고려하면서 짜는 건 너무 어려운 것 같습니다.

지훈님의 TokenRefreshManager 또한 여러 DispatchQueue의 콜백이 존재하다 보니, 어떤 큐에서 어떻게 동작하는지 파악해야 하는 부분이 있네요.
이렇기 때문에 Actor 클래스가 등장한게 아닐까 싶습니다.
TokenRefreshManager를 Actor로 구현해 보시는 것도 쿨럭,,

Comment on lines +102 to +103
guard let self = self,
let currentRefreshToken = self.authService.getRefreshToken() else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else 문은 개행 부탁드려요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

갱!

Comment on lines +74 to +77
provider.request(.refreshToken(refreshToken: refreshToken)) { [weak self] result in
guard let self = self else { return }

self.queue.async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 작업은 글로벌 큐(Concurrent)에서 실행되고, 네트워크 작업이기 때문에 백그라운드에서 실행되는데 이 때 커스텀 직렬 큐로 하위 작업을 실행시켜야 하는지 잘 몰라서 여쭤보고 싶습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공유 상태(isRefreshing, pendingCompletions 등)를 업데이트하기위해 하위작업으로 동기화 시켜주는 작업입니다

Copy link
Member

@youz2me youz2me left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 코드 이해하기만 해도 많은 부분을 얻어갈 수 있었던 것 같네요.
리뷰는 진웅오빠가 매섭게 진행한 것 같으니... 저는 어프로브만 놓고 가겠습니다 LGTM!

@youz2me youz2me linked an issue Mar 1, 2025 that may be closed by this pull request
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🧡 JiHoon 쌈뽕한 플러팅 꿀팁을 듣고 싶다면 ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[refactor] 자료구조 + 알고리즘 도입
3 participants