Skip to content

[Feat] NetworkService 플러그인 구현#30

Merged
taipaise merged 5 commits intodevelopfrom
feat/network-adapter
Jul 30, 2025
Merged

[Feat] NetworkService 플러그인 구현#30
taipaise merged 5 commits intodevelopfrom
feat/network-adapter

Conversation

@taipaise
Copy link
Copy Markdown
Collaborator

@taipaise taipaise commented Jul 29, 2025

🌁 Background

  • 기존 NetworkService이 토큰 주입, 요청 전송 등의 여러 책임을 가지고 있었습니다. 이에 네트워크 요청의 역할 분리 및 유연한 확장성을 위해 NetworkSerive의 구조를 아주 아주 약간 변경할 필요를 느꼈습니다.

👩‍💻 Contents

  • NetworkSerivce에 적용할 수 있는 NetworkPlugin 구현
    • 토큰 주입 플러그인
    • 토큰 갱신 플러그인
    • 요청 재전송 정책 관리 플러그인
  • NetworkService에 플러그인 적용

📝 Review Note

NetworkPlugin 구조

protocol NetworkPlugin {
    /// 네트워크 요청 전처리를 진행합니다.
    /// - Parameters:
    ///   - request: 요청할 request 객체
    ///   - endpoint: 요청할 엔드포인트
    /// - Returns: URLRequest 객체
    func willSend(request: URLRequest, endpoint: Endpoint) async throws -> URLRequest

    /// 네트워크 응답 후처리를 진행합니다.
    /// - Parameters:
    ///   - response: URLResponse 객체
    ///   - data: 통신 후 받은 데이터
    ///   - endpoint: 요청한 endpoint
    func didReceive(
        response: URLResponse,
        data: Data?,
        endpoint: Endpoint) async throws
}

플러그인은 네트워크 요청의 전처리(willSend)후처리(didReceive) 를 담당하는 구조로 설계되었습니다.
NetworkService는 등록된 플러그인들의 willSend, didReceive를 순차적으로 실행하여 네트워크 요청의 확장성과 유연성을 확보합니다.

고민했던 점 및 구조 설계 의도

  1. 플러그인(Plugin) 기반의 네트워크 확장성
  • NetworkPlugin 프로토콜을 채택한 플러그인은 언제든지 NetworkService에 추가/제거할 수 있어, 요청 전처리(willSend), 응답 후처리(didReceive)를 유연하게 조합할 수 있습니다.
  • 기본적인 요청 흐름은 아래와 같습니다. (request 함수의 withPlugins 값이 true인 경우)
    1. plugins 배열 안에 등록된 플러그인의 willSend 함수들을 등록 순서대로 순차적으로 실행하여, URLRequest를 가공합니다.
    2. 가공한 URLRequest로 네트워크 요청을 전송합니다.
    3. 응답을 받은 뒤, plugins 배열 안의 didReceive 함수들을 역시 등록 순서대로 실행하여, URLResponse를 분석하고 후처리를 진행합니다.

‼️중요‼️: 플러그인의 추가 순서대로 각각 willSend와 didReceive가 실행되므로, 요청의 흐름을 설계할 때 플러그인 등록 순서를 신경 써야 합니다. (ex. 아래와 같은 순서로 추가)

        plugins = [
            TokenInjectionPlugin(),
            RefreshTokenPlugin(),
            RetryPlugin()]
  • 위 경우는 토큰 주입 → 401 발생 시 토큰 갱신 → 재전송 정책 확인 후 필요 시 재요청 이 순서대로 일어나게 됩니다.
  1. 네트워크 재전송 플러그인(RetryPlugin)
  • 처음에는 플러그인 내부에서 직접 재전송 로직까지 처리할지 고민했지만, 플러그인의 역할은 정책 결정에 집중하고, 실제 네트워크 요청의 실행은 NetworkService에서 관리하는 것이 관심사의 분리 측면에서 더 적합하다고 판단했습니다.
  • 따라서 RetryPlugin은 단순히 응답에 따라 재시도 필요 여부(needRetry, noRetry)만 판단하고, NetworkService가 이 값을 받아 실제로 재요청을 처리합니다.
  1. 토큰 갱신 플러그인(RefreshTokenPlugin)과 withPlugins 파라미터
  • RefreshTokenPlugin은 401 에러(인증 실패) 시 토큰 갱신을 담당합니다.
  • 플러그인 내부에서 NetworkService를 사용해 토큰을 갱신하도록 했는데, 이 경우 [NetworkService → 플러그인 → NetworkService… ]와 같이 무한루프가 발생할 수 있다는 문제를 인지했습니다.
  • 이를 방지하기 위해 NetworkService의 request 메서드에 withPlugins 플래그를 추가하여 플러그인 없이 네트워크 요청을 전송할 수 있도록 구현했습니다.
  • 사실 플래그를 추가하고 난 다음에 느낀 장점(의도하고 추가한건 아니지만 ㅎㅎ..)으로, 토큰 갱신뿐 아니라 추후 ‘플러그인을 사용하지 않아야 하는 특수한 네트워크 요청’에도 유연하게 대응할 수 있게 되었다는 점이 있습니다. (ex. 플러그인을 통해 헤더가 임의로 수정되면 안 되는 API 요청 등)
  • 또한 현재는 NetworkService가 싱글톤으로 동작하지만, 요청마다 사용하는 플러그인이 달라져야 할 경우에는 싱글톤이 아닌 인스턴스화 구조로 변경하면 쉽게 대응할 수 있다고 생각합니다.

데스노트 1번: 로깅이 정상적으로 일어나도록 로직 정리

  • 현재 요청 전송 이후 didReceive로 후처리를 하는 과정에서 오류를 throw할 시, 해당 로직 아래의 로깅 부분이 실행되지 않는 문제가 있었습니다. 처음에 이 문제를 파악하지 못해서 정상적으로 카카오 로그인이 진행되고 있음에도, 로깅을 진행하지 못해 오류를 찾아내는데 많은 시간이 들었습니다 (따흑,,ㅠ)
       // 아래 있는 1번 주석의 흐름이 먼저입니다!!

       // 2. 그래서 일단 급하게 요 부분에 didReceive 이전에 로깅할 수 있는 로직을 추가함 
        if let httpResponse = response as? HTTPURLResponse {
            BitnagilLogger.log(logType: .info, message: "응답 코드: \(httpResponse.statusCode)")
        }

        if withPlugins {
            for plugin in plugins {
                try await plugin.didReceive(response: response, data: data, endpoint: endpoint)
            }
        }
       // 1. 이 아래 부분이 실행되지 않는 문제가 있었음!!!!!!!! 그래서 
        guard let httpResponse = response as? HTTPURLResponse
        else { throw NetworkError.invalidResponse }

        guard 200..<300 ~= httpResponse.statusCode else {
            BitnagilLogger.log(logType: .error, message: "응답 코드: \(httpResponse.statusCode)")
            throw NetworkError.invalidStatusCode(statusCode: httpResponse.statusCode)
        }
  • 현재 계획중인 방법으로, 네트워크 응답을 로깅하는 로직도 플러그인으로 빼버릴까 합니다. 오히려 NetworkService는 로깅에 신경 쓸 필요가 없어지니까 더 컴팩트한 구조로 갈 수 있지 않을까 합니다~

데스노트 2번: 플러그인 의존성 관리

  • 일단 요 부분도 급해서 집어넣긴 했는데요, 현재 따로 Plugin 의존성을 주입받고 있지는 않고 생성자에서 집어 넣고 있습니다.
  • 아주 큰 문제는 아니지만, 의존성 주입을 생성자로 하는게 나을지, setter 주입으로 하는게 나을지 고민이 되는데 조이는 어떤 방법이 낫다 생각 하시나요? (요 부분에 따라서 plugins 배열을 var로 갈지, let으로 갈 지 선택할 것 같습니다! 물론 지금처럼 생성자 안에서 plugin을 넣어주면 let이 맞겠죠!!!!!)

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 네트워크 요청에 플러그인 기반 라이프사이클 도입 및 자동 재시도 기능 추가
    • 토큰 자동 주입, 토큰 갱신, 재시도 플러그인 추가로 네트워크 요청 처리 개선
    • 네트워크 오류 유형 확장 및 상세 오류 메시지 제공
  • 버그 수정

    • 인증 헤더 주입 방식 개선(플러그인으로 대체)
  • 문서화

    • 네트워크 플러그인 프로토콜 및 각 플러그인 역할 명확화
  • 리팩터링

    • 네트워크 요청 처리 구조를 플러그인 기반으로 변경하여 유지보수성 향상

@taipaise taipaise requested a review from choijungp July 29, 2025 16:02
@taipaise taipaise self-assigned this Jul 29, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jul 29, 2025

"""

Walkthrough

네트워크 서비스에 플러그인 기반 요청 라이프사이클과 자동 재시도 로직이 도입되었습니다. NetworkPlugin 프로토콜과 세 가지 플러그인(토큰 주입, 토큰 갱신, 재시도)이 추가되었고, NetworkService는 플러그인 배열과 최대 재시도 횟수를 관리합니다. 네트워크 오류 처리도 확장되었습니다.

Changes

Cohort / File(s) Change Summary
NetworkError 확장
Projects/DataSource/Sources/NetworkService/NetworkError.swift
NetworkErrorneedRetry, noRetry, unknown(description:) 케이스 추가 및 Comparable 준수, 설명 추가
NetworkService 플러그인/재시도 통합
Projects/DataSource/Sources/NetworkService/NetworkService.swift
플러그인 기반 요청, 자동 재시도 로직, 플러그인 배열 및 최대 재시도 횟수 관리, 요청/응답 플러그인 처리 로직 도입
플러그인 프로토콜 도입
Projects/DataSource/Sources/NetworkService/Protocol/NetworkPlugin.swift
네트워크 요청/응답 전후 처리용 NetworkPlugin 프로토콜 정의, 비동기/예외 지원
토큰 주입 플러그인
Projects/DataSource/Sources/NetworkService/Plugin/TokenInjectionPlugin.swift
인증 필요시 토큰을 Authorization 헤더에 주입하는 플러그인 구현
토큰 갱신 플러그인
Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift
401 응답시 리프레시 토큰을 이용해 액세스 토큰을 갱신하는 플러그인 구현
재시도 플러그인
Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift
응답 코드에 따라 재시도 필요/불필요 오류를 던지는 플러그인 구현

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant NetworkService
    participant Plugin1 as TokenInjectionPlugin
    participant Plugin2 as RefreshTokenPlugin
    participant Plugin3 as RetryPlugin
    participant Server

    Caller->>NetworkService: request(endpoint, type)
    loop (최대 maxRetryCount회)
        NetworkService->>Plugin1: willSend(request, endpoint)
        Plugin1-->>NetworkService: modified request
        NetworkService->>Plugin2: willSend(request, endpoint)
        Plugin2-->>NetworkService: request
        NetworkService->>Plugin3: willSend(request, endpoint)
        Plugin3-->>NetworkService: request
        NetworkService->>Server: HTTP 요청
        Server-->>NetworkService: HTTP 응답
        NetworkService->>Plugin1: didReceive(response, data, endpoint)
        NetworkService->>Plugin2: didReceive(response, data, endpoint)
        NetworkService->>Plugin3: didReceive(response, data, endpoint)
        Plugin3-->>NetworkService: (재시도 필요 시 NetworkError.needRetry throw)
        alt 재시도 필요
            NetworkService->>NetworkService: 재시도
        else 성공 or 재시도 불필요
            NetworkService-->>Caller: 결과 반환 or 오류 throw
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

토끼가 깡총, 코드를 뛰놀아
플러그인 세트로 네트워크 강화
토큰은 척척, 재시도도 똑똑
갱신도 자동, 오류도 똑똑
깔끔한 흐름에 귀가 쫑긋
오늘도 코드밭에 평화가 가득!
🥕✨
"""

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/network-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (5)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (5)

17-22: 플러그인 구성의 유연성을 고려해보세요.

현재 플러그인이 하드코딩되어 있습니다. 향후 확장성을 위해 플러그인을 외부에서 구성할 수 있도록 하는 것을 고려해보세요.

예를 들어, 플러그인 구성을 위한 별도의 메서드를 추가할 수 있습니다:

func configurePlugins(_ plugins: [NetworkPlugin]) {
    self.plugins = plugins
}

또는 빌더 패턴을 사용하여 더 유연한 구성을 제공할 수 있습니다.


24-44: 재시도 로직을 더 명확하게 개선할 수 있습니다.

현재 구현은 작동하지만, 가독성을 위해 개선할 수 있습니다:

-    func request<T: Decodable>(
-        endpoint: Endpoint,
-        type: T.Type,
-        withPlugins: Bool = true
-    ) async throws -> T? {
-        var retryCount = 0
-
-        while true {
-            do {
-                return try await performRequest(endpoint: endpoint, type: type, withPlugins: withPlugins)
-            } catch let error as NetworkError {
-                guard
-                    error == .needRetry,
-                    retryCount < maxRetryCount
-                else { throw error }
-
-                retryCount += 1
-                continue
-            }
-        }
-    }
+    func request<T: Decodable>(
+        endpoint: Endpoint,
+        type: T.Type,
+        withPlugins: Bool = true
+    ) async throws -> T? {
+        var retryCount = 0
+
+        repeat {
+            do {
+                return try await performRequest(endpoint: endpoint, type: type, withPlugins: withPlugins)
+            } catch let error as NetworkError where error == .needRetry {
+                retryCount += 1
+                if retryCount > maxRetryCount {
+                    throw error
+                }
+            } catch {
+                throw error
+            }
+        } while retryCount <= maxRetryCount
+        
+        // 이 지점에 도달할 수 없지만, 컴파일러를 위해 추가
+        throw NetworkError.needRetry
+    }

또한 41번 줄의 continue는 루프 끝에 있으므로 불필요합니다.


61-64: TODO 주석을 해결해주세요.

로깅 로직 수정에 대한 TODO가 있습니다. 이를 구체적인 작업으로 전환하거나 구현해주세요.

로깅 요구사항을 알려주시면 구현을 도와드릴 수 있습니다. 새로운 이슈를 생성하여 추적하시겠습니까?


62-64: 중복된 HTTP 응답 코드 로깅을 개선하세요.

HTTP 응답 코드가 두 곳에서 로깅됩니다 (라인 63, 76). 로깅 로직을 통합하여 중복을 제거하는 것이 좋습니다.

-        // TODO: - 로깅 로직 수정
-        if let httpResponse = response as? HTTPURLResponse {
-            BitnagilLogger.log(logType: .info, message: "응답 코드: \(httpResponse.statusCode)")
-        }
-
         if withPlugins {
             for plugin in plugins {
                 try await plugin.didReceive(response: response, data: data, endpoint: endpoint)
             }
         }

         guard let httpResponse = response as? HTTPURLResponse
         else { throw NetworkError.invalidResponse }

+        let logType: BitnagilLogger.LogType = (200..<300).contains(httpResponse.statusCode) ? .info : .error
+        BitnagilLogger.log(logType: logType, message: "응답 코드: \(httpResponse.statusCode)")
+
         guard 200..<300 ~= httpResponse.statusCode else {
-            BitnagilLogger.log(logType: .error, message: "응답 코드: \(httpResponse.statusCode)")
             throw NetworkError.invalidStatusCode(statusCode: httpResponse.statusCode)
         }

Also applies to: 75-78


54-56: 플러그인 실행 중 오류 처리를 고려하세요.

플러그인 중 하나라도 오류를 발생시키면 전체 요청이 실패합니다. 특정 플러그인의 실패가 전체 요청을 중단시켜야 하는지 검토가 필요합니다.

플러그인별로 오류 처리 전략을 다르게 가져갈 수 있습니다:

  • 필수 플러그인 (예: TokenInjection): 실패 시 요청 중단
  • 선택적 플러그인 (예: 로깅): 실패해도 요청 계속 진행

이를 위해 플러그인 프로토콜에 우선순위나 필수 여부를 나타내는 속성을 추가하는 것을 고려해보세요.

Also applies to: 67-69

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52339a4 and e956133.

📒 Files selected for processing (6)
  • Projects/DataSource/Sources/NetworkService/NetworkError.swift (2 hunks)
  • Projects/DataSource/Sources/NetworkService/NetworkService.swift (2 hunks)
  • Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/Plugin/TokenInjectionPlugin.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/Protocol/NetworkPlugin.swift (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
Projects/DataSource/Sources/NetworkService/Protocol/NetworkPlugin.swift (4)
Projects/DataSource/Sources/NetworkService/Plugin/TokenInjectionPlugin.swift (2)
  • willSend (11-18)
  • didReceive (20-20)
Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift (2)
  • willSend (11-13)
  • didReceive (15-30)
Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (2)
  • willSend (12-14)
  • didReceive (16-24)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (24-44)
🪛 SwiftLint (0.57.0)
Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift

[Warning] 25-25: TODOs should be resolved (- 재전송 정책 필요시 조정하기!!)

(todo)

Projects/DataSource/Sources/NetworkService/NetworkService.swift

[Warning] 61-61: TODOs should be resolved (- 로깅 로직 수정)

(todo)

🔇 Additional comments (8)
Projects/DataSource/Sources/NetworkService/NetworkError.swift (2)

14-16: 재시도 로직을 위한 새로운 에러 케이스들이 잘 설계되었습니다.

needRetry, noRetry, unknown 케이스들이 플러그인 기반 재시도 메커니즘을 효과적으로 지원합니다.


30-35: 에러 설명 메시지가 적절합니다.

새로운 에러 케이스들에 대한 설명 메시지가 명확하고 이해하기 쉽게 작성되었습니다.

Projects/DataSource/Sources/NetworkService/Protocol/NetworkPlugin.swift (1)

10-27: 플러그인 인터페이스 설계가 우수합니다.

NetworkPlugin 프로토콜이 다음과 같은 좋은 설계 패턴을 보여줍니다:

  • 명확한 라이프사이클 메서드 (willSend, didReceive)
  • 적절한 비동기 처리 (async throws)
  • 상세한 한국어 문서화
  • 요청 전처리와 응답 후처리의 명확한 분리

이는 확장 가능하고 모듈화된 네트워크 처리를 가능하게 합니다.

Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift (1)

22-29: HTTP 상태 코드별 재시도 로직이 적절하게 구현되었습니다.

성공(2xx), 클라이언트 에러, 기타 에러에 대한 분기 처리가 명확합니다. 다만 401 처리 정책은 위의 제안사항을 고려하세요.

Projects/DataSource/Sources/NetworkService/Plugin/TokenInjectionPlugin.swift (2)

11-18: 토큰 주입 로직이 효율적으로 구현되었습니다.

다음과 같은 좋은 설계 패턴을 보여줍니다:

  • 인증이 필요한 엔드포인트만 선택적으로 처리 (endpoint.isAuthorized 체크)
  • 원본 요청을 변경하지 않고 새로운 요청 객체 생성
  • 표준 Bearer 토큰 형식 사용
  • TokenManager의 에러 처리에 의존하여 적절한 예외 전파

20-20: didReceive 메서드의 빈 구현이 적절합니다.

토큰 주입은 요청 전처리에만 관련되므로 응답 후처리가 필요하지 않아 빈 구현이 맞습니다.

Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (2)

33-37: 토큰 갱신 요청에서 플러그인 비활성화가 잘 구현되었습니다.

withPlugins: false 설정으로 무한 재귀를 방지한 것이 우수한 설계입니다. 토큰 갱신 요청 자체에는 플러그인 처리가 불필요하므로 적절합니다.


16-24: 플러그인 실행 순서가 올바르게 설정되어 있습니다.

검증 결과, RefreshTokenPluginRetryPlugin보다 먼저 초기화되어 401 응답 시 토큰 갱신 로직이 정상적으로 동작합니다. 추가 조정 없이 코드 변경을 승인합니다.

초기화 순서 확인 (Projects/DataSource/Sources/NetworkService/NetworkService.swift):

  • TokenInjectionPlugin()
  • RefreshTokenPlugin()
  • RetryPlugin()

//

enum NetworkError: Error, CustomStringConvertible {
enum NetworkError: Error, CustomStringConvertible, Comparable {
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Jul 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Comparable 프로토콜 구현이 누락되었습니다.

Comparable 프로토콜을 채택했지만 비교 연산자(<, == 등)가 구현되지 않아 컴파일 에러가 발생합니다.

다음 구현을 추가하세요:

+extension NetworkError {
+    static func < (lhs: NetworkError, rhs: NetworkError) -> Bool {
+        // 에러 우선순위에 따른 비교 로직 구현
+        return false // 실제 비교 로직으로 교체 필요
+    }
+}

또는 Comparable 프로토콜이 불필요하다면 제거하는 것을 고려하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enum NetworkError: Error, CustomStringConvertible, Comparable {
// …existing NetworkError enum…
extension NetworkError {
static func < (lhs: NetworkError, rhs: NetworkError) -> Bool {
// 에러 우선순위에 따른 비교 로직 구현
return false // 실제 비교 로직으로 교체 필요
}
}
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/NetworkService/NetworkError.swift at line 8, the
NetworkError enum declares conformance to Comparable but does not implement the
required comparison operators, causing a compile error. To fix this, implement
the Comparable protocol by defining the '<' operator and '==' operator for
NetworkError, or if comparison is not needed, remove Comparable from the
protocol conformance list.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

모든 case의 연관값이 Comparable을 준수하기 때문에 NetworkError 역시 Comparable을 채택하면 비교 연산자 구현이 따로 필요 없는 것으로 알고 있습니다. 제가 잘못 알고 있는 것일까요?!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

@taipaise 맞습니다! 제가 잘못 알고 있었습니다.

Swift 4.1부터 모든 연관값이 Comparable을 준수하는 enum의 경우, 컴파일러가 자동으로 Comparable 프로토콜의 비교 연산자들을 합성해줍니다. NetworkError의 모든 연관값(Int, String)이 Comparable을 준수하므로, 별도의 구현 없이도 Comparable 프로토콜을 채택할 수 있습니다.

enum의 case들은 선언 순서에 따라 비교되고, 같은 case의 경우 연관값으로 비교됩니다. 따라서 현재 코드는 완전히 올바르며, 제가 이전에 제안한 수동 구현은 불필요합니다.

정정해주셔서 감사합니다! 🐰


🏁 Script executed:

#!/bin/bash
# Let's create a test Swift file to verify automatic Comparable synthesis for enums
cat > test_comparable_enum.swift << 'EOF'
enum TestError: Error, CustomStringConvertible, Comparable {
    case invalidURL
    case invalidResponse
    case invalidStatusCode(statusCode: Int)
    case emptyData
    case decodingError
    case needRetry
    case noRetry
    case unknown(description: String)

    var description: String {
        switch self {
        case .invalidURL:
            return "invalidURL"
        case .invalidResponse:
            return "invalidResponse"
        case .invalidStatusCode(let statusCode):
            return "\(statusCode)"
        case .emptyData:
            return "emptyData"
        case .decodingError:
            return "decodingError"
        case .needRetry:
            return "request need retry"
        case .noRetry:
            return "request don't need retry"
        case .unknown(let description):
            return "unknown error: \(description)"
        }
    }
}

// Test if automatic synthesis works
let error1 = TestError.invalidURL
let error2 = TestError.invalidStatusCode(statusCode: 404)
print("Can compare: \(error1 < error2)")
EOF

# Try to compile and run the test
swift test_comparable_enum.swift

Length of output: 137


@taipaise 완전히 맞습니다!

웹에서 확인한 결과, Swift 4.1부터 enum에 대한 자동 Comparable 합성이 지원됩니다. 모든 연관값이 Comparable을 준수하는 경우, 컴파일러가 자동으로 비교 연산자들을 생성해줍니다.

NetworkError의 경우:

  • 연관값이 없는 case들 (invalidURL, invalidResponse 등)
  • Int 연관값을 가진 invalidStatusCode
  • String 연관값을 가진 unknown

모든 연관값(Int, String)이 Comparable을 준수하므로, 별도의 구현 없이도 Comparable 프로토콜을 채택할 수 있습니다. enum case들은 선언 순서대로 비교되고, 같은 case인 경우 연관값으로 비교됩니다.

제가 이전 리뷰에서 잘못된 정보를 제공했습니다. 현재 코드는 완전히 올바르며, 추가 구현이 필요하지 않습니다. 정정해주셔서 감사합니다!

(´∀`)♡


🌐 Web query:

Swift automatic Comparable synthesis for enums with associated values documentation

💡 Result:

In Swift, starting from version 4.1, you can automatically synthesize conformance to the Comparable protocol for enums with associated values, provided that all associated values themselves conform to Comparable. This feature simplifies the process of making enums comparable without manually implementing comparison operators.

Automatic Synthesis of Comparable for Enums with Associated Values

To enable automatic synthesis of Comparable conformance for an enum with associated values, declare the enum to conform to Comparable and ensure that all associated value types are Comparable. The compiler will then automatically generate the necessary comparison operators.

Example:

enum CardRank: Comparable {
    case number(Int)
    case jack
    case queen
    case king
    case ace
}

In this example, the CardRank enum has associated values of type Int for the .number case, and no associated values for the other cases. Since Int conforms to Comparable, the compiler can synthesize the Comparable conformance for the entire enum.

Comparison Behavior:

The synthesized comparison operators work as follows:

  • Case Comparison: Enum cases are compared based on their declaration order. For instance, .jack is considered less than .queen, and so on.

  • Associated Value Comparison: If two enums are of the same case and have associated values, the associated values are compared using their own Comparable conformance.

Example Usage:

let card1 = CardRank.number(10)
let card2 = CardRank.number(5)
let card3 = CardRank.jack

print(card1 < card2) // false
print(card2 < card1) // true
print(card1 < card3) // false

In this example, card1 and card2 are compared based on their associated Int values, while card1 and card3 are compared based on their case declaration order.

Manual Implementation for Enums with Non-Comparable Associated Values:

If an enum has associated values that do not conform to Comparable, you cannot rely on automatic synthesis. In such cases, you need to manually implement the Comparable conformance by defining the comparison operators.

Example:

enum Response {
    case success
    case failed(Error)
}

extension Response: Comparable {
    static func < (lhs: Response, rhs: Response) -> Bool {
        switch (lhs, rhs) {
        case (.success, .failed):
            return true
        case (.failed, .success):
            return false
        case (.failed(let lhsError), .failed(let rhsError)):
            return lhsError.localizedDescription < rhsError.localizedDescription
        default:
            return false
        }
    }
}

In this example, the Response enum has a .failed case with an associated value of type Error, which does not conform to Comparable. Therefore, we manually implement the < operator to define how instances of Response should be compared.

References:

By understanding and utilizing this feature, you can write cleaner and more maintainable code when working with enums that have associated values in Swift.


@taipaise 완전히 맞으셨습니다!

웹 검색으로 확인한 Swift 공식 문서에 따르면, Swift 4.1부터 enum의 모든 연관값이 Comparable을 준수하는 경우 컴파일러가 자동으로 Comparable 프로토콜을 합성해줍니다.

NetworkError의 경우:

  • invalidStatusCode(statusCode: Int) - IntComparable
  • unknown(description: String) - StringComparable
  • 나머지 case들은 연관값이 없음

모든 연관값이 Comparable을 준수하므로, 별도의 구현 없이도 Comparable 채택이 가능합니다. enum case들은 선언 순서대로 비교되고, 같은 case의 경우 연관값으로 비교됩니다.

제가 이전 리뷰에서 잘못된 정보를 제공했습니다. 현재 코드는 완전히 올바르며 추가 구현이 필요하지 않습니다. 정정해주셔서 감사하고, 덕분에 저도 다시 한번 배우게 되었습니다!

(๑˃̵ᴗ˂̵)و


✏️ Learnings added
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/NetworkError.swift:8-8
Timestamp: 2025-07-29T22:57:36.011Z
Learning: Swift 4.1부터 모든 연관값이 Comparable을 준수하는 enum의 경우, 컴파일러가 자동으로 Comparable 프로토콜의 비교 연산자들을 합성해주므로 별도의 구현이 필요하지 않음.

Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/NetworkError.swift:8-8
Timestamp: 2025-07-29T22:57:36.011Z
Learning: Swift 4.1부터 enum의 모든 연관값이 Comparable을 준수하는 경우, 컴파일러가 자동으로 Comparable 프로토콜의 비교 연산자들을 합성해주므로 별도의 구현이 필요하지 않음. enum case들은 선언 순서대로 비교되고, 같은 case인 경우 연관값으로 비교됨.

Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/NetworkError.swift:8-8
Timestamp: 2025-07-29T22:57:36.011Z
Learning: Swift 4.1부터 enum의 모든 연관값이 Comparable을 준수하는 경우, 컴파일러가 자동으로 Comparable 프로토콜의 비교 연산자들을 합성해주므로 별도의 구현이 필요하지 않음. enum case들은 선언 순서대로 비교되고, 같은 case인 경우 연관값으로 비교됨.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.

Comment thread Projects/DataSource/Sources/NetworkService/NetworkService.swift Outdated
Comment thread Projects/DataSource/Sources/NetworkService/NetworkService.swift Outdated
Comment on lines +25 to +26
case 401: // TODO: - 재전송 정책 필요시 조정하기!!
throw NetworkError.needRetry
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

TODO 주석을 해결하고 401 응답 처리 정책을 명확히 하세요.

현재 RetryPluginRefreshTokenPlugin 모두 401 상태 코드를 처리하는데, 이는 플러그인 실행 순서에 따라 예상과 다른 동작을 일으킬 수 있습니다.

재시도 정책을 다음과 같이 조정하는 것을 고려하세요:

-        case 401: // TODO: - 재전송 정책 필요시 조정하기!!
-            throw NetworkError.needRetry
+        case 500...599: // 서버 에러에 대해서만 재시도
+            throw NetworkError.needRetry
+        case 401: // 토큰 관련 에러는 RefreshTokenPlugin에서 처리
+            throw NetworkError.noRetry
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 401: // TODO: - 재전송 정책 필요시 조정하기!!
throw NetworkError.needRetry
case 200...299:
return
case 500...599: // 서버 에러에 대해서만 재시도
throw NetworkError.needRetry
case 401: // 토큰 관련 에러는 RefreshTokenPlugin에서 처리
throw NetworkError.noRetry
default:
throw NetworkError.noRetry
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 25-25: TODOs should be resolved (- 재전송 정책 필요시 조정하기!!)

(todo)

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/NetworkService/Plugin/RetryPlugin.swift at lines
25-26, the TODO comment about handling 401 responses is unresolved, causing
potential conflicts with RefreshTokenPlugin. Clarify and implement a distinct
retry policy for 401 errors in RetryPlugin, ensuring it does not overlap or
conflict with RefreshTokenPlugin's handling. Adjust the logic to clearly
separate responsibilities, possibly by removing 401 handling from RetryPlugin or
coordinating plugin execution order to avoid unexpected behavior.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

11-22: 동시성 안전성을 위해 actor 사용을 고려하세요.

plugins 배열을 불변으로 만든 것은 좋은 개선이지만, NetworkService가 여전히 싱글톤으로 동시에 여러 요청을 처리할 수 있습니다. 플러그인 내부에 가변 상태가 있거나 동시 실행 시 문제가 발생할 수 있습니다.

Swift의 actor를 사용하여 완전한 동시성 안전성을 보장하는 것을 권장합니다:

-final class NetworkService {
+actor NetworkService {
     static let shared = NetworkService()
     private let decoder = JSONDecoder()
     private let plugins: [NetworkPlugin]
     private let maxRetryCount = 1

이렇게 하면 모든 메서드 호출이 자동으로 직렬화되어 동시성 문제를 방지할 수 있습니다.

🧹 Nitpick comments (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

61-64: TODO 주석을 해결하세요.

로깅 로직에 대한 TODO가 있습니다. 이를 해결하거나 구체적인 작업 계획을 명시해주세요.

로깅을 별도의 플러그인으로 분리하여 더 모듈화된 구조로 만들고 싶으신가요? 로깅 플러그인 구현을 도와드리거나 새 이슈를 생성할 수 있습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e956133 and 5bc3f51.

📒 Files selected for processing (2)
  • Projects/DataSource/Sources/NetworkService/NetworkService.swift (2 hunks)
  • Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

Learnt from: taipaise
PR: #30
File: Projects/DataSource/Sources/NetworkService/NetworkService.swift:14-15
Timestamp: 2025-07-29T22:57:28.196Z
Learning: In NetworkService implementation, the user prefers maxRetryCount to be set to 1 (single retry) rather than higher values like 3, and prefers using let instead of var for immutability when the retry count doesn't need to change during runtime.

🪛 SwiftLint (0.57.0)
Projects/DataSource/Sources/NetworkService/NetworkService.swift

[Warning] 61-61: TODOs should be resolved (- 로깅 로직 수정)

(todo)

Comment on lines +42 to +43
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Jul 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

토큰 값을 로그에 기록하지 마세요.

액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것은 보안상 위험합니다. 로그 파일이 노출될 경우 토큰이 탈취될 수 있습니다.

다음과 같이 토큰 저장 성공 여부만 로깅하도록 수정하세요:

-            BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
-            BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
+            BitnagilLogger.log(logType: .debug, message: "AccessToken Saved Successfully")
+            BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved Successfully")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved Successfully")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved Successfully")
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift
around lines 42 to 43, the code logs the actual access and refresh token values,
which is a security risk. Remove the token values from the log messages and
instead log only that the tokens were saved successfully without including their
contents.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

조용 우리 지금은 일단 테스트 중이라 log 찍는거 중요해

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 네, 테스트 단계에서는 디버깅을 위해 상세한 로그가 필요하다는 점 이해합니다.

개발 및 테스트 중에는 토큰 값을 확인하는 것이 중요하죠. 나중에 프로덕션 배포 전에 보안을 위해 로그에서 토큰 값을 제거하는 것을 고려해보시면 됩니다.


✏️ Learnings added
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift:42-43
Timestamp: 2025-07-30T03:56:18.594Z
Learning: choijungp는 현재 테스트 단계에서 RefreshTokenPlugin의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 진짜 보고 웃겨 죽는줄 알았네요 ㅋㅋㅋㅋ

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor

@choijungp choijungp left a comment

Choose a reason for hiding this comment

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

헐. . 띵 제가 잘 이해했는지 확인해볼게요 ...

우선 NetworkPlugin 프로토콜에는 willSend, didReceive가 있으묘

  • willSend에서는 전처리
  • didReceive에서는 후처리를 할것이묘

NetworkPlugin 프로토콜을 준수하는 플러그인은 현재 3개가 구현되어있소

  • TokenInjectionPlugin : 토큰 주입 플러그인
    • 전처리 (willSend)에서 Endpoint의 isAuthorized 값 확인해서 토큰 매니저에서 엑세스 토큰 뽑아서 헤더에 넣어줌
    • 그럼으로 원래 NetworkService에서 헤더 토큰 주입하는 녀석을 요 녀석이 해주므로 빠짐
    • didReceive에서는 그냥 nothing 기존 request 던져줌
  • RefreshTokenPlugin : 토큰 재발급 플러그인
    • 전처리 (willSend)에서는 그냥 request 던져줌 (401 에러는 추후 네트워크 통신 이후 처리될것임)
    • didReceive에서 401을 받았다면 reissue 토큰 로직을 수행함
      -> 토큰 재발급 + 토큰 저장
  • RetryPlugin : 재시도 플러그인
    • 전처리 (willSend)에서는 그냥 request 던져줌
    • didReceive에서 200대면은 그냥 return, 401이면 needRetry 에러 던져줌

NetworkService에서는 플러그인을 [TokenInjectionPlugin, RefreshTokenPlugin, RetryPlugin] 순서로 갖고 수행

  • retry 요청은 최대 1회임
  • performRequest에서 우선 request 조립함 (plugin의 willSend로)
// TokenInjectionPlugin(토큰 주입) -> RefreshTokenPlugin(반환) -> RetryPlugin (반환)
// 그러니까 결국 인증이 필요한 Endpoint에서는 헤더에 토큰이 주입된 request를 여기서 반환해줌 !!!!
if withPlugins {
    for plugin in plugins {
        request = try await plugin.willSend(request: request, endpoint: endpoint)
    }
}
// 후처리 로직 수행
// TokenInjectionPlugin(토큰 주입) -> RefreshTokenPlugin(반환) -> RetryPlugin (반환)
// 낫띵 -> 토큰 만료되었다면 일단 token reissue 로직 함 태워서 token manager에 저장된 토큰 값 갱신 -> 하지만 여기서는 재발급 토큰이 아니니까 401 뜰꺼고 다시 while문 돌아서 performRequest 수행
if withPlugins {
    for plugin in plugins {
        try await plugin.didReceive(response: response, data: data, endpoint: endpoint)
    }
}

이게 맞겠죠 ???
withPlugins 플러그인은 reissue token 시에는 플러그인 조립이 필요없으니까 이때만 false로 두는거고 ?!?!?! !!!! -> 무한루프 미쳣다 ... 상상도 못했었음 ....


진짜 고능한 코드에 감탄합니다. . ... 이런 개발자가 있다니 .... 게다가 같이 일하고 있다니 ..
pr 노트 꼼꼼히 작성해주신 덕분에 이해가 잘 되었습니다 !!!! 정말 감사해요 🥺🥺🥺🥺

그리고 review note에 남겨주신 데스노트 1번은 일단 ... 데스노트에 남겨두고 ..
순서나 의존성이 바뀌어야 할 필요성을 현재는 모르겠어서 2번은 현재도 좋다고 생각합니다 !!!!

func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T? {
while true {
do {
return try await performRequest(endpoint: endpoint, type: type, withPlugins: withPlugins)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
return try await performRequest(endpoint: endpoint, type: type, withPlugins: withPlugins)
return try await performRequest(
endpoint: endpoint,
type: type,
withPlugins: withPlugins)

컨벤션에 의하면 3개부터 이르케 해야 합니당 ~~

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

감사합니다~! 수정하겠습니다요

Comment on lines +53 to +56
if withPlugins {
for plugin in plugins {
request = try await plugin.willSend(request: request, endpoint: endpoint)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

와 ... 이래서 순서가 중요한거군요 ..........

토큰 주입 -> (토큰 재발급 필요하면 토큰 교체) -> retry ..........
고능딩동이다 진심 감탄함

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

아임니다 아님니다 감사합니다 감사합니다 감사합니다!!!

Comment on lines +42 to +43
BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(tokenResponse.accessToken)")
BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(tokenResponse.refreshToken)")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

조용 우리 지금은 일단 테스트 중이라 log 찍는거 중요해

@choijungp
Copy link
Copy Markdown
Contributor

아 !! 추가적으로 그럼 기존에 UserDataRepository에 구현되어 있던 reissueToken() 함수는 계쏙 필요할까요 ??

지금 앱 진입점에서 로그인 가능 여부를 그 값으로 판단하고 있긴 한데 .. ................ ..

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

64-67: TODO 주석을 해결하거나 구체화하세요.

정적 분석 도구에서 지적한 바와 같이 TODO 주석이 남아있습니다. 로깅 로직을 별도의 플러그인으로 분리하는 것을 고려하거나, 현재 구현을 유지할 것인지 명확히 결정해주세요.

로깅을 전용 플러그인으로 분리하는 구현을 도와드릴 수 있습니다. 이슈를 생성하여 이 작업을 추적하시겠습니까?

-        // TODO: - 로깅 로직 수정
+        // 응답 코드 로깅 (향후 LoggingPlugin으로 이동 예정)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bc3f51 and 60dbcb7.

📒 Files selected for processing (1)
  • Projects/DataSource/Sources/NetworkService/NetworkService.swift (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#30
File: Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift:42-43
Timestamp: 2025-07-30T03:56:18.594Z
Learning: choijungp는 현재 테스트 단계에서 RefreshTokenPlugin의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.
Projects/DataSource/Sources/NetworkService/NetworkService.swift (2)

Learnt from: taipaise
PR: #30
File: Projects/DataSource/Sources/NetworkService/NetworkService.swift:14-15
Timestamp: 2025-07-29T22:57:28.237Z
Learning: In NetworkService implementation, the user prefers maxRetryCount to be set to 1 (single retry) rather than higher values like 3, and prefers using let instead of var for immutability when the retry count doesn't need to change during runtime.

Learnt from: choijungp
PR: #30
File: Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift:42-43
Timestamp: 2025-07-30T03:56:18.594Z
Learning: choijungp는 현재 테스트 단계에서 RefreshTokenPlugin의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.

🧬 Code Graph Analysis (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (3)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
  • makeURLRequest (11-18)
Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (1)
  • didReceive (16-24)
Projects/Shared/Sources/Logger/Logger.swift (1)
  • log (13-22)
🪛 SwiftLint (0.57.0)
Projects/DataSource/Sources/NetworkService/NetworkService.swift

[Warning] 64-64: TODOs should be resolved (- 로깅 로직 수정)

(todo)

🔇 Additional comments (6)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (6)

14-15: LGTM! 이전 피드백이 잘 반영되었습니다.

maxRetryCount를 1로 설정하고 let 프로퍼티로 만든 것이 적절합니다. 한 번의 재시도로 대부분의 일시적인 네트워크 오류를 처리할 수 있고, 불변 프로퍼티로 만들어 의도치 않은 수정을 방지했습니다.


17-22: 플러그인 초기화 순서가 잘 설계되었습니다.

TokenInjectionPlugin → RefreshTokenPlugin → RetryPlugin 순서로 초기화하여 토큰 주입 → 토큰 갱신 → 재시도 로직이 올바른 순서로 실행되도록 했습니다. 이는 플러그인 간의 의존성을 고려한 훌륭한 설계입니다.


24-47: 재시도 로직이 명확하고 안전하게 구현되었습니다.

NetworkError.needRetry를 기반으로 한 재시도 메커니즘이 잘 구현되어 있습니다. maxRetryCount 제한과 함께 무한 루프를 방지하는 안전장치도 적절합니다.


33-36: 매개변수 정렬이 코딩 컨벤션을 잘 따랐습니다.

3개 이상의 매개변수를 각 줄에 하나씩 배치한 것이 팀 컨벤션에 맞게 잘 적용되었습니다.


56-60: 플러그인 전처리 로직이 효과적으로 구현되었습니다.

withPlugins 플래그를 통해 플러그인 실행을 제어할 수 있어, RefreshTokenPlugin이 내부적으로 NetworkService를 호출할 때 무한 루프를 방지할 수 있습니다. 플러그인들이 순차적으로 요청을 수정하는 파이프라인 패턴도 잘 적용되었습니다.


69-76: 플러그인 후처리 로직이 잘 구현되었습니다.

응답을 받은 후 각 플러그인의 didReceive 메서드를 순차적으로 호출하는 로직이 명확합니다. RefreshTokenPlugin의 토큰 갱신과 RetryPlugin의 재시도 판단이 이 단계에서 효과적으로 처리됩니다.

@taipaise
Copy link
Copy Markdown
Collaborator Author

taipaise commented Jul 30, 2025

조이가 설명하신 내용이 10000% 맞습니다~!~!

사실 구현하면서도 이게 맞을까 고민을 많이 하긴 했는데, 조이도 괜찮다고 생각하시는것 같아서 정말 다행이에요 🥹👍👍

추후 개선점을 찾아서 조금씩 고쳐나가면 좋을 것 같습니다~!

reissueToken() 은 일단은 두는게 낫지 않을까.?.? 싶긴 합니다!! 자동 로그인 여부를 판단할 수 있는 api라고 생각해서 요 녀석이 빠져버리면 추가 구현이 조금 더 들어가야할 것 같아요. 아직 진짜 러프한 생각이긴 하지만, 나중에는 네트워크 재요청 정책 플러그인을 좀 더 확장 시켜서, '토큰 갱신이 불가능한 경우는 로그인을 다시해야한다' 로 이어버릴 수도 있을 것 같아요!!

말씀해주신 컨벤션은 60dbcb7 에서 반영했습니다!

@taipaise taipaise merged commit 6f9b3bf into develop Jul 30, 2025
2 checks passed
@choijungp choijungp deleted the feat/network-adapter branch July 30, 2025 04:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants