Skip to content

Feat/#3 네트워크 모듈화#5

Merged
KimNahun merged 14 commits into
developfrom
feat/#3-Network/Module
Jan 21, 2026
Merged

Feat/#3 네트워크 모듈화#5
KimNahun merged 14 commits into
developfrom
feat/#3-Network/Module

Conversation

@KimNahun
Copy link
Copy Markdown
Contributor

@KimNahun KimNahun commented Jan 19, 2026

🔗 연결된 이슈

📄 작업 내용

  • 서버 API 스펙을 분석하여 확장 가능한 네트워크 모듈 설계
  • API 에러 응답을 enum으로 타입 안전하게 처리할 수 있는 구조 구현
  • Moya + Swift Concurrency(async/await) 기반 네트워크 레이어 구축
  • 실제 API 호출 테스트 코드 작성

💻 주요 코드 설명

0. 설계 시 신경 쓴 점

에러 코드와 필드를 String이 아닌 Enum으로 접근하도록 설계했습니다.

// String으로 처리하면 오타, 자동완성 불가, 컴파일 타임 검증 불가                                         
if error.code == "COMM-01-005" { ... }                                                                       
if error.field == "fcmToken" { ... }                                                                         
                                                                                                             
// Enum으로 처리하면 타입 안전, 자동완성 지원, 컴파일 타임 검증                                           
switch error.type {                                                                                          
case .validationError: ...    // COMM-01-005                                                                 
case .internalServerError: ...// COMM-08-001                                                                 
case .undefined(let code): ...// 정의되지 않은 코드도 대응 가능                                              
}                                                                                                            
                                                                                                             
switch error.field {                                                                                         
case .fcmToken: ...                                                                                          
case .unknown(let raw): ...   // 새로운 필드가 추가되어도 대응 가능                                          
}                                                                                                            

이렇게 하면:

  • 자동완성 지원.
  • 오타 방지 - 컴파일러가 잡아줌
  • 확장성 - undefined(code:), unknown(String)으로 정의되지 않은 값도 안전하게 처리

1. 서버 응답 구조 대응

서버의 공통 응답 형식을 분석하여 BaseResponseErrorResponse로 대응했습니다.

// 성공 응답                                                                                                 
{                                                                                                            
    "code": "2000",                                                                                          
    "message": "요청에 성공하였습니다.",                                                                     
    "data": { ... }                                                                                          
}                                                                                                            
                                                                                                             
// 에러 응답                                                                                                 
{                                                                                                            
    "code": "COMM-01-005",                                                                                   
    "message": "유효성 검증에 실패했습니다.",                                                                
    "errors": [                                                                                              
        { "field": "fcmToken", "message": "FCM 토큰은 필수입니다." }                                         
    ]                                                                                                        
}                                                                                                            

2. API별 에러를 Enum으로 타입 안전하게 처리

각 API마다 발생할 수 있는 에러를 enum으로 정의하여 컴파일 타임에 안전하게 처리할 수 있습니다.

public struct SignupError: APIErrorProtocol {                                                                
    public let type: ErrorType                                                                               
    public let message: String                                                                               
    public let field: SignupField?                                                                           
    public let fieldMessage: String?                                                                         
                                                                                                             
    public enum ErrorType {                                                                                  
        case validationError      // COMM-01-005                                                             
        case internalServerError  // COMM-08-001                                                             
        case undefined(code: String)                                                                         
    }                                                                                                        
}                                                                                                            
                                                                                                             
public enum SignupField {                                                                                    
    case fcmToken                                                                                            
    case unknown(String)                                                                                     
}                                                                                                            

3. 에러 발생 시 대응 방법

서버에서 errors 배열로 여러 필드 에러가 올 수 있지만, 첫 번째 에러를 기준으로 매칭하여 처리합니다.

  // 서버 응답 예시                                                                                            
  {                                                                                                            
      "code": "COMM-01-005",                                                                                   
      "message": "유효성 검증에 실패했습니다.",                                                                
      "errors": [                                                                                              
          { "field": "interestRegions", "message": "관심 지역은 최소 1개 이상 선택해야 합니다." },             
          { "field": "scheduleType", "message": "일정 스타일은 필수입니다." },                                 
          { "field": "travelCompanions", "message": "동행은 최소 1개 이상 선택해야 합니다." }                  
      ]                                                                                                        
  }                                                                                                            
                                                                                                              
  // 사용 예시                                                                                                 
  let result = await authService.signup(request: request)                                                      
                                                                                                               
  switch result {                                                                                              
  case .success(let response):                                                                                 
      print("가입 성공: \(response.nickname)")                                                                 
                                                                                                               
  case .failure(let error):                                                                                    
      // 에러 타입별 분기 처리                                                                                 
      switch error.type {                                                                                      
      case .validationError:                                                                                   
          // 필드별 에러 메시지 표시                                                                           
          if let field = error.field, let fieldMessage = error.fieldMessage {                                  
              switch field {                                                                                   
              case .fcmToken:                                                                                  
                  showToast("FCM 토큰 오류: \(fieldMessage)")                                                  
              case .unknown(let fieldName):                                                                    
                  showToast("\(fieldName): \(fieldMessage)")                                                   
              }                                                                                                
          }                                                                                                    
      case .internalServerError:                                                                               
          showToast("서버 오류가 발생했습니다")                                                                
      case .undefined(let code):                                                                               
          showToast("알 수 없는 오류: \(code)")                                                                
      }                                                                                                        
                                                                                                               
  case .networkFailure(let error):                                                                             
      showToast(error.message) // "네트워크 연결을 확인해주세요"                                               
  }                                                                                                            

4. 테스트 코드

실제 API를 호출하는 테스트 코드로 네트워크 모듈의 동작을 검증할 수 있습니다.

  func test_signup_실제API호출() async throws {                                                                
      // Given                                                                                                 
      let sut = AuthService()                                                                                  
      let request = SignupRequest(                                                                             
          fcmToken: "test-fcm-token",                                                                          
          deviceModel: "iPhone",                                                                               
          deviceOs: "iOS",                                                                                     
          deviceOsVersion: "17.0",                                                                             
          appVersion: "1.0.0"                                                                                  
      )                                                                                                        
                                                                                                               
      // When                                                                                                  
      let result = await sut.signup(request: request)                                                          
                                                                                                               
      // Then                                                                                                  
      switch result {                                                                                          
      case .success(let response):                                                                             
          XCTAssertFalse(response.uuid.isEmpty)                                                                
      case .failure(let error):                                                                                
          XCTAssertFalse(error.message.isEmpty)                                                                
      case .networkFailure(let error):                                                                         
          XCTFail("네트워크 에러: \(error.message)")                                                           
      }                                                                                                        
  }                                                                                                            

5. 확장성

  새로운 API 추가 시 아래 파일만 추가하면 됩니다:                                                              
  - DTO/{Domain}/{API}DTO.swift - Request/Response 모델                                                        
  - Error/{Domain}/{API}Error.swift - 에러 타입 정의                                                           
  - TargetType/{Domain}API.swift - Moya TargetType                                                             
  - Service/{Domain}Service.swift - 서비스 레이어                                                              

👀 기타 더 이야기해볼 점

  • errors 배열의 첫 번째 에러만 처리하는 방식으로 결정했습니다. 추후 모든 에러를 표시해야 한다면 fieldErrors:
    [SignupField: String] 형태로 확장 가능합니다.
  • BASE_URL은 xcconfig로 관리하여 Debug/Release 환경 분리가 가능합니다.
  • 테스트 코드는 실제 서버에 API를 호출합니다. GET 요청은 괜찮지만, POST/PUT/DELETE 같은 요청은
    서버 데이터에 영향을 줄 수 있으므로 필요할 때만 실행하는 것이 좋을 것 같습니다. 아니면 지금 구조 외에 다르게 하면 좋을지에 대해서도 의견 주시면 좋을 것 같습니다.

📄 작업 내용

  • 서버 API 스펙을 분석하여 확장 가능한 네트워크 모듈 설계
  • API 에러 응답을 enum으로 타입 안전하게 처리할 수 있는 구조 구현
  • Moya + Swift Concurrency(async/await) 기반 네트워크 레이어 구축
  • 실제 API 호출 테스트 코드 작성

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 회원가입(서버 연동) 기능 추가
    • async/await 기반 비동기 네트워크 호출 지원
    • 표준화된 API 응답/오류 모델 및 에러 매핑 제공
    • 네트워크 로거와 구성(베이스 URL) 처리 추가
  • Tests

    • 인증 서비스 관련 단위 테스트 추가
  • Chores

    • 불필요 파일 정리 및 프레임워크 빌드 설정 조정
    • 앱 모듈에 네트워크 모듈 의존성 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 19, 2026

Walkthrough

네트워크 모듈을 새로 추가하고, 인증(signup) API 호출용 DTO·서비스·타겟·에러 매핑·로거·비동기 Moya 확장과 도메인/데이터/DI 연결을 구현했습니다. 빈 플레이스홀더 파일들을 삭제하고 Info.plist에 BASE_URL을 추가했습니다.

Changes

Cohort / File(s) 변경 요약
플러그인 설정
Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift, Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift
Info.plist에 BASE_URL 키 추가 ("$(BASE_URL)"); BUILD_LIBRARY_FOR_DISTRIBUTION 값을 "YES"에서 "NO"로 변경
모듈 메타/의존성
Projects/Modules/Networks/Project.swift, Projects/App/Project.swift
Networks 모듈에 .domain 의존성 추가, NetworksTests 테스트 타깃 추가 및 EnvPlugin import; App 타깃에 .Modules.networks 의존성 추가
기본 응답/에러 모델
Projects/Modules/Networks/Sources/Base/BaseResponse.swift, .../EmptyResponse.swift, .../ErrorResponse.swift, .../NetworkError.swift, .../NetworkResult.swift
제네릭 BaseResponse, EmptyResponse, ErrorResponse(및 ErrorDetail), NetworkError enum, 제네릭 NetworkResult enum 추가
DTO / 엔티티 파일 추가/삭제
Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift, 삭제된 파일들: APIEmpty.swift, NBaseEmpty.swift, EntityEmpty.swift, ServiceEmpty.swift
SignupRequest/SignupResponse DTO 추가; 공허한 플레이스홀더 파일 삭제
Moya 비동기 확장 및 로거
Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift, Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift
request<T,E> / requestPlain<E> async 메서드 추가(continuation 기반), 에러 매핑 로직과 mapMoyaError 구현; 디버그용 네트워크 로거 추가
네트워크 설정 및 타겟
Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift, Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift
NetworkConfiguration.baseURL (테스트 번들 우선) 추가; AuthAPI TargetType 구현(/api/v1/auth/users, POST, JSON 인코딩)
서비스 / 리포지토리 / DI / 변환
Projects/Modules/Networks/Sources/Service/AuthService.swift, Projects/Data/Sources/Repository/Auth/AuthRepository.swift, Projects/Data/Sources/DI/AuthRepositoryFactory.swift, Projects/Data/Sources/Transform/Auth/AuthTransform.swift
AuthServiceProtocol/AuthService 추가; AuthRepository 구현 및 DI 팩토리 함수 추가; DTO↔도메인 변환 이니셜라이저 추가
도메인 모델 및 인터페이스
Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift, Projects/Domain/Sources/Model/Auth/SignupInfo.swift, .../SignupResult.swift, .../SignupError.swift, Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift
AuthRepositoryProtocol 추가; SignupInfo, SignupResult, SignupError/SignupField 타입 추가; ErrorResponse → SignupError 매핑 초기자 구현
테스트
Projects/Modules/Networks/Tests/AuthServiceTests.swift
실제 AuthService.signup를 호출하는 비동기 단위 테스트 추가 (결과 분기별 assert/출력)

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant Repository as AuthRepository
    participant Service as AuthService
    participant Provider as MoyaProvider
    participant Network as NetworkLayer
    participant API as Auth API

    App->>Repository: signup(info: SignupInfo)
    Repository->>Service: signup(request: SignupRequest)
    Service->>Provider: request<SignupResponse, SignupError>(AuthAPI.signup)
    Provider->>Network: 네트워크 요청 (async)
    Network->>API: POST /api/v1/auth/users

    alt 응답 코드 == "2000"
        API-->>Network: BaseResponse<SignupResponse>
        Network-->>Provider: 디코딩 성공
        Provider-->>Service: NetworkResult.success(SignupResponse)
        Service-->>Repository: .success(SignupResponse)
        Repository-->>App: Result.success(SignupResult)
    else 응답 코드 != "2000"
        API-->>Network: BaseResponse + ErrorResponse
        Network-->>Provider: ErrorResponse 디코딩
        Provider-->>Service: NetworkResult.failure(SignupError) via errorMapper
        Service-->>Repository: .failure(SignupError)
        Repository-->>App: Result.failure(SignupError)
    else 네트워크/디코딩 오류
        Network-->>Provider: MoyaError / decoding error
        Provider-->>Service: NetworkResult.networkFailure(NetworkError)
        Service-->>Repository: .failure(.networkError(...))
        Repository-->>App: Result.failure(SignupError.networkError)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

개요

네트워크 모듈의 완전한 구현으로, 인증 가입 API 호출을 위한 기반 구조, 에러 처리, 데이터 전송 객체, 저장소 계층 및 의존성 주입 패턴을 추가합니다. 불필요한 빈 파일들을 제거합니다.

변경 사항

그룹 / 파일 설명
플러그인 설정
Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift, Plugins/EnvPlugin/ProjectDescriptionHelpers/Settings+Extension.swift
Info.plist에 BASE_URL 엔트리 추가; BUILD_LIBRARY_FOR_DISTRIBUTION을 YES에서 NO로 변경
네트워크 모듈 기본 구조
Projects/Modules/Networks/Project.swift
테스트 타겟 NetworksTests 추가; .domain 의존성 추가; EnvPlugin 임포트
기본 응답 타입
Projects/Modules/Networks/Sources/Base/BaseResponse.swift, Projects/Modules/Networks/Sources/Base/EmptyResponse.swift, Projects/Modules/Networks/Sources/Base/ErrorResponse.swift
제네릭 BaseResponse, EmptyResponse, ErrorResponse 구조 정의; ErrorDetail 중첩 구조 포함
네트워크 에러 처리
Projects/Modules/Networks/Sources/Base/NetworkError.swift, Projects/Modules/Networks/Sources/Base/NetworkResult.swift
NetworkError 열거형 정의 (connectionFailed, decodingFailed, unknown); NetworkResult 제네릭 열거형 (success, failure, networkFailure 케이스)
데이터 전송 객체
Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift
SignupRequest (fcmToken, deviceModel 등), SignupResponse (uuid, accessToken, nickname) 정의
Moya 확장
Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift
request<T, E>, requestPlain 비동기 메서드 추가; errorMapper를 통한 에러 변환; BaseResponse 디코딩 및 성공 코드(2000) 검증 로직
네트워크 설정
Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift
Bundle에서 BASE_URL 읽기; URL 검증; 테스트 번들 우선 처리
인증 서비스
Projects/Modules/Networks/Sources/Service/AuthService.swift
AuthServiceProtocol 정의; AuthService 구현 (signup 메서드)
API 대상
Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift
AuthAPI 열거형 정의; TargetType 구현 (/api/v1/auth/users POST)
네트워크 테스트
Projects/Modules/Networks/Tests/AuthServiceTests.swift
AuthService 유닛 테스트; signup 메서드 검증
도메인 모델
Projects/Domain/Sources/Model/Auth/SignupInfo.swift, Projects/Domain/Sources/Model/Auth/SignupResult.swift, Projects/Domain/Sources/Model/Auth/SignupError.swift
SignupInfo (fcmToken), SignupResult (uuid, accessToken, nickname), SignupError (validationFailed, serverError, networkError, unknown) 및 SignupField 정의
도메인 인터페이스
Projects/Domain/Sources/Interface/Auth/AuthRepositoryProtocol.swift
signup(info:) 비동기 메서드 정의
데이터 저장소
Projects/Data/Sources/Repository/Auth/AuthRepository.swift
AuthRepositoryProtocol 구현; AuthService 의존성; SignupRequest/Result 변환 로직
의존성 주입
Projects/Data/Sources/DI/AuthRepositoryFactory.swift
makeAuthService(), makeAuthRepository(authService:) 팩토리 함수 정의
데이터 변환
Projects/Data/Sources/Transform/Auth/AuthTransform.swift
SignupResult.init(from: SignupResponse), SignupRequest.init(from: SignupInfo) 확장 메서드
프로젝트 의존성
Projects/App/Project.swift
.Modules.networks 의존성 추가
삭제된 파일
Projects/Modules/Networks/Sources/API/APIEmpty.swift, Projects/Modules/Networks/Sources/Base/NBaseEmpty.swift, Projects/Modules/Networks/Sources/Entity/EntityEmpty.swift, Projects/Modules/Networks/Sources/Service/ServiceEmpty.swift
빈 플레이스홀더 파일 제거

시퀀스 다이어그램

sequenceDiagram
    participant App
    participant AuthService
    participant MoyaProvider
    participant NetworkLayer as Moya Network
    participant API as Auth API

    App->>AuthService: signup(request: SignupRequest)
    AuthService->>MoyaProvider: request<SignupResponse, SignupError>(...)
    MoyaProvider->>NetworkLayer: 비동기 요청
    NetworkLayer->>API: POST /api/v1/auth/users

    alt API 성공 (코드: 2000)
        API-->>NetworkLayer: BaseResponse<SignupResponse>
        NetworkLayer-->>MoyaProvider: 데이터 디코딩
        MoyaProvider-->>AuthService: NetworkResult.success(SignupResponse)
        AuthService-->>App: Result<SignupResponse, SignupError>
    else API 실패 (코드 != 2000)
        API-->>NetworkLayer: BaseResponse + ErrorResponse
        NetworkLayer-->>MoyaProvider: errorMapper 호출
        MoyaProvider-->>AuthService: NetworkResult.failure(SignupError)
        AuthService-->>App: Result with SignupError
    else 네트워크 오류
        API-->>NetworkLayer: MoyaError 발생
        NetworkLayer-->>MoyaProvider: NetworkError.connectionFailed
        MoyaProvider-->>AuthService: NetworkResult.networkFailure(NetworkError)
        AuthService-->>App: Result with NetworkError
    end
Loading

코드 검토 예상 소요 시간

🎯 4 (복잡함) | ⏱️ ~60분

🐰 네트워크 흐름, 우아하게 설계되고
응답과 에러, 모두 처리하고
비동기 가입, 매끄럽게 흐르고
도메인부터 데이터까지, 계층 정확하고
API의 길, 이제 열렸다네!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Feat/#3 네트워크 모듈화'는 PR의 주요 변경사항인 네트워크 모듈 설계 및 구현을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으며, 연결된 이슈(#3), 작업 내용, 주요 코드 설명이 충실하게 작성되어 있습니다.
Linked Issues check ✅ Passed PR은 이슈 #3의 모든 주요 요구사항을 충족합니다: 확장 가능한 네트워크 모듈 설계(Moya + async/await 기반), 타입 안전한 에러 처리 구조(enum 기반 SignupError), BaseResponse/ErrorResponse로 서버 응답 대응, 실제 API 호출 테스트 코드 작성이 모두 포함되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #3의 네트워크 모듈화 범위 내에 있으며, 추가된 파일들(DTO, Service, TargetType, Repository, Error Mapping 등)은 모두 네트워크 계층 구축과 관련이 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 2

🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift`:
- Around line 40-42: The code currently constructs SignupField(rawValue:
firstError.field ?? "") which conflates a missing field (nil) with an
empty/unknown raw value; change the logic to explicitly check firstError.field:
if firstError.field is nil then assign a distinct sentinel (e.g., set self.field
to nil if field is optional or to a dedicated enum case like .missingField),
otherwise attempt SignupField(rawValue: fieldRaw) and fall back to an explicit
unknown case for unparsable values; keep assigning self.fieldMessage =
firstError.message. Ensure you update code paths referencing field to handle the
new sentinel/optional case.

In `@Projects/Modules/Networks/Tests/AuthServiceTests.swift`:
- Around line 27-76: The test test_signup_실제API호출 in AuthServiceTests makes a
real network call via sut.signup which should be replaced with a unit-test using
a mocked MoyaProvider; update AuthServiceTests to inject a stubbed MoyaProvider
(e.g., MoyaProvider<AuthAPI> with stubClosure: .immediatelyStub) into the
AuthService instance (sut) and change test_signup_실제API호출 to assert
parsing/handling of the stubbed response instead of calling the real API, moving
the live-call test to a separate integration test suite.
🧹 Nitpick comments (10)
Projects/Modules/Networks/Sources/Base/NetworkError.swift (1)

11-26: LocalizedError 프로토콜 채택 고려

message 프로퍼티 대신 LocalizedError 프로토콜을 채택하면 Swift의 에러 처리 시스템과 더 자연스럽게 통합됩니다.

♻️ 제안된 리팩토링
-public enum NetworkError: Error {
+public enum NetworkError: LocalizedError {
     case connectionFailed
     case decodingFailed
     case unknown(Error)
 
-    public var message: String {
+    public var errorDescription: String? {
         switch self {
         case .connectionFailed:
             return "네트워크 연결을 확인해주세요"
         case .decodingFailed:
             return "데이터 처리 중 오류가 발생했습니다"
         case .unknown:
             return "알 수 없는 오류가 발생했습니다"
         }
     }
 }
Projects/Modules/Networks/Tests/AuthServiceTests.swift (2)

9-10: Import 정렬 필요

SwiftLint 경고에 따라 import 문을 알파벳 순으로 정렬하세요.

♻️ 제안된 수정
-import XCTest
 `@testable` import Networks
+import XCTest

29-42: 디버깅용 print 문 제거 필요

테스트 코드에 디버깅용 print 문이 다수 포함되어 있습니다. 프로덕션 테스트에서는 제거하거나 적절한 로깅 프레임워크를 사용하세요.

Also applies to: 47-51, 56-65, 69-71

Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift (1)

12-21: fatalError 대신 빌드 타임 검증 또는 안전한 처리 고려

BASE_URL이 누락된 경우 앱이 즉시 크래시됩니다. 프로덕션에서는 다음 방안을 고려하세요:

  1. 빌드 스크립트에서 검증: xcconfig에 BASE_URL이 없으면 빌드 실패하도록 설정
  2. 캐싱: baseURL이 호출될 때마다 번들을 탐색하는 대신 한 번만 계산
♻️ 캐싱 적용 제안
 public enum NetworkConfiguration {
-    public static var baseURL: URL {
+    public static let baseURL: URL = {
         // 테스트 환경에서는 Bundle.main이 Xcode 도구를 가리키므로 테스트 번들에서 찾음
         let bundle = Bundle.allBundles.first { $0.bundlePath.hasSuffix(".xctest") } ?? Bundle.main
 
         guard let urlString = bundle.infoDictionary?["BASE_URL"] as? String,
               let url = URL(string: urlString) else {
             fatalError("BASE_URL not found in Info.plist")
         }
         return url
-    }
+    }()
 }
Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift (1)

18-20: 암시적 반환으로 SwiftLint 경고를 정리해주세요.
스타일 경고 제거용으로만 추천합니다.

♻️ 제안 수정
public var baseURL: URL {
-    return NetworkConfiguration.baseURL
+    NetworkConfiguration.baseURL
}

public var headers: [String: String]? {
-    return ["Content-Type": "application/json"]
+    ["Content-Type": "application/json"]
}

Also applies to: 43-45

Projects/Modules/Networks/Sources/DTO/Auth/SignupDTO.swift (1)

2-2: 파일 헤더명이 실제 파일명과 다릅니다.
문서 정합성을 위해 맞춰두는 편이 좋습니다.

📝 제안 수정
-//  Signin.swift
+//  SignupDTO.swift
Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (3)

15-57: requestrequestPlain 메서드 간 코드 중복 고려

두 메서드의 응답 처리 로직(성공 코드 확인, 에러 응답 디코딩, continuation resume)이 거의 동일합니다. 공통 로직을 private 헬퍼로 추출하면 유지보수성이 향상될 수 있습니다.

♻️ 중복 제거를 위한 리팩토링 제안
private func handleResponse<T: Decodable, E: APIErrorProtocol>(
    _ result: Result<Response, MoyaError>,
    transform: (BaseResponse<T>) -> NetworkResult<T, E>?
) -> NetworkResult<T, E> {
    switch result {
    case .success(let response):
        do {
            let baseResponse = try JSONDecoder().decode(
                BaseResponse<T>.self,
                from: response.data
            )
            
            if baseResponse.code == Self.successCode {
                if let transformed = transform(baseResponse) {
                    return transformed
                }
                return .networkFailure(.decodingFailed)
            } else {
                let errorResponse = try JSONDecoder().decode(
                    ErrorResponse.self,
                    from: response.data
                )
                let apiError = E(
                    code: errorResponse.code ?? "",
                    message: errorResponse.message ?? "",
                    errors: errorResponse.errors ?? []
                )
                return .failure(apiError)
            }
        } catch {
            return .networkFailure(.decodingFailed)
        }
        
    case .failure(let moyaError):
        return .networkFailure(Self.mapMoyaError(moyaError))
    }
}

Also applies to: 59-97


13-13: 성공 코드에 대한 문서화 고려

"2000" 성공 코드가 하드코딩되어 있습니다. 이 값의 의미와 출처(API 스펙)에 대한 주석을 추가하면 향후 유지보수에 도움이 됩니다.


99-107: 추가 네트워크 에러 케이스 매핑 고려

현재 NSURLErrorDomain.connectionFailed로 매핑됩니다. 타임아웃(NSURLErrorTimedOut), 취소(NSURLErrorCancelled) 등 세분화된 에러 처리가 필요하다면 추가 매핑을 고려해 보세요.

♻️ 세분화된 에러 매핑 예시
 private static func mapMoyaError(_ error: MoyaError) -> NetworkError {
     switch error {
     case .underlying(let nsError as NSError, _)
         where nsError.domain == NSURLErrorDomain:
-        return .connectionFailed
+        switch nsError.code {
+        case NSURLErrorTimedOut:
+            return .timeout
+        case NSURLErrorCancelled:
+            return .cancelled
+        default:
+            return .connectionFailed
+        }
     default:
         return .unknown(error)
     }
 }

Note: NetworkError에 해당 케이스 추가가 필요합니다.

Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift (1)

48-52: 에러 코드에 대한 문서화 권장

"COMM-01-005", "COMM-08-001" 등 에러 코드의 의미와 출처를 주석으로 남기면 팀원들이 코드를 이해하는 데 도움이 됩니다.

// API 에러 코드 매핑 (참고: API 스펙 문서 링크)
// COMM-01-005: 요청 검증 실패
// COMM-08-001: 내부 서버 오류
switch code {
case "COMM-01-005": self.type = .validationError
case "COMM-08-001": self.type = .internalServerError
default:            self.type = .undefined(code: code)
}

Comment on lines +40 to +42
if let firstError = errors.first {
self.field = SignupField(rawValue: firstError.field ?? "")
self.fieldMessage = firstError.message
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

field가 nil일 때의 처리 확인 필요

firstError.fieldnil인 경우 SignupField(rawValue: "")가 호출되어 .unknown("")이 됩니다. 이는 "필드 정보가 없음"과 "알 수 없는 빈 필드"가 구분되지 않는 문제가 있습니다.

🔧 수정 제안
         if let firstError = errors.first {
-            self.field = SignupField(rawValue: firstError.field ?? "")
+            self.field = firstError.field.flatMap { $0.isEmpty ? nil : SignupField(rawValue: $0) }
             self.fieldMessage = firstError.message
         } else {
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Error/Auth/SignupError.swift` around lines
40 - 42, The code currently constructs SignupField(rawValue: firstError.field ??
"") which conflates a missing field (nil) with an empty/unknown raw value;
change the logic to explicitly check firstError.field: if firstError.field is
nil then assign a distinct sentinel (e.g., set self.field to nil if field is
optional or to a dedicated enum case like .missingField), otherwise attempt
SignupField(rawValue: fieldRaw) and fall back to an explicit unknown case for
unparsable values; keep assigning self.fieldMessage = firstError.message. Ensure
you update code paths referencing field to handle the new sentinel/optional
case.

Comment on lines +27 to +76
func test_signup_실제API호출() async throws {
// Given
print("========== 테스트 시작 ==========")
let request = SignupRequest(
fcmToken: "test-fcm-token-12345",
deviceModel: "iPhone",
deviceOs: "iOS",
deviceOsVersion: "17.0",
appVersion: "1.0.0"
)
print("Request 생성 완료")

// When
print("API 호출 시작...")
let result = await sut.signup(request: request)
print("API 호출 완료")

// Then
switch result {
case .success(let response):
print("========== 성공 ==========")
print("UUID: \(response.uuid)")
print("AccessToken: \(response.accessToken)")
print("Nickname: \(response.nickname)")
print("==============================")
XCTAssertFalse(response.uuid.isEmpty, "UUID가 비어있음")
XCTAssertFalse(response.accessToken.isEmpty, "AccessToken이 비어있음")

case .failure(let error):
print("========== API 에러 ==========")
print("Type: \(error.type)")
print("Message: \(error.message)")
if let field = error.field {
print("Field: \(field)")
}
if let fieldMessage = error.fieldMessage {
print("FieldMessage: \(fieldMessage)")
}
print("=================================")
XCTAssertFalse(error.message.isEmpty, "에러 메시지가 비어있음")

case .networkFailure(let error):
print("==========네트워크 에러 ==========")
print("Error: \(error.message)")
print("=====================================")
XCTFail("네트워크 연결 실패: \(error.message)")
}

print("========== 테스트 종료 ==========")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

실제 API 호출 테스트는 통합 테스트로 분리 권장

현재 테스트는 실제 서버 API를 호출하고 있어 다음과 같은 문제가 발생할 수 있습니다:

  1. 불안정성: 네트워크 상태나 서버 상태에 따라 테스트 결과가 달라짐
  2. 속도 저하: CI/CD 파이프라인에서 테스트 시간 증가
  3. 부작용: 실제 사용자 데이터가 생성될 수 있음

단위 테스트에서는 MoyaProvider를 모킹하고, 실제 API 호출은 별도의 통합 테스트 스위트로 분리하는 것을 권장합니다.

💡 모킹을 활용한 단위 테스트 예시
import Moya

final class AuthServiceTests: XCTestCase {
    var sut: AuthService!
    
    override func setUp() {
        super.setUp()
        // 스텁 데이터를 반환하는 MoyaProvider 사용
        let stubbingProvider = MoyaProvider<AuthAPI>(
            stubClosure: MoyaProvider.immediatelyStub
        )
        sut = AuthService(provider: stubbingProvider)
    }
    
    func test_signup_성공시_응답_파싱() async throws {
        // Given - stubbed response
        // When
        let result = await sut.signup(request: request)
        // Then - assert parsed response
    }
}
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Tests/AuthServiceTests.swift` around lines 27 - 76,
The test test_signup_실제API호출 in AuthServiceTests makes a real network call via
sut.signup which should be replaced with a unit-test using a mocked
MoyaProvider; update AuthServiceTests to inject a stubbed MoyaProvider (e.g.,
MoyaProvider<AuthAPI> with stubClosure: .immediatelyStub) into the AuthService
instance (sut) and change test_signup_실제API호출 to assert parsing/handling of the
stubbed response instead of calling the real API, moving the live-call test to a
separate integration test suite.

@KimNahun KimNahun changed the title Feat/#3 network/module Feat/#3 네트워크 모듈화 Jan 19, 2026
@KimNahun KimNahun requested a review from ChoiAnYong January 19, 2026 11:00
@KimNahun KimNahun self-assigned this Jan 19, 2026
@KimNahun KimNahun linked an issue Jan 19, 2026 that may be closed by this pull request
2 tasks
Copy link
Copy Markdown
Contributor

@ChoiAnYong ChoiAnYong 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 +15 to +18
func request<T: Decodable, E: APIErrorProtocol>(
_ target: Target,
errorType: E.Type
) async -> NetworkResult<T, E> {
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.

async throws를 통해서 error를 넘겨줄 수 있을 거 같은데
NetworkResult를 사용해서 하는 이유가 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

async throws를 사용하면 나중에 사용하는쪽에서

func signup(request: SignupRequest) async throws -> SignupResponse    
// 사용시
 do {                                                                                                           
     let response = try await authService.signup(request: request)                                              
 } catch let error as SignupError {                                                                             
     // 타입 캐스팅 필요                                                                                        
 } catch {                                                                                                      
     // 알 수 없는 에러                                                                                         
 }                                                                                                              

이렇게 캐스팅이 필요하고,
NetworkResult를 사용하면,

 func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError>                        
                                                                                                                 
  // 사용 시                                                                                                     
  switch result {                                                                                                
  case .success(let response): ...                                                                               
  case .failure(let error): ...      // 이미 SignupError 타입                                                    
  case .networkFailure(let error): ...                                         
  }      

에러 타입이 명시적이라 Generic E로 컴파일 타임에 에러 타입 보장이 되어서 캐스팅도 필요없고, 코드가 짧아지는것이 장점이라고 생각했습니다.

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.

하나의 경우에는 이점이 있는 거 같은데 async let이나 병렬처리를 사용해서 여러 usecase를 불러오는 경우
ansync throws의 경우 하나의 do catch문으로 해결하면 되지만 NetworkResult를 사용하게 되면
코드가 더 복잡해지지 않을까요??

Copy link
Copy Markdown
Contributor Author

@KimNahun KimNahun Jan 20, 2026

Choose a reason for hiding this comment

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

그런 고려를 많이는 못해봤었네요. 좋은 의견 감사합니다.
병렬처리가 필요한 부분이 있다면 말씀하신 것도 방법이 될 수 있을 것 같습니다.
그런데 병렬처리가 된다고 했을때, 예를들어 2개의 api를 병렬처리했다고 했을때, Partial Success부분이 조금 어려울 수 있을 것 같습니다. 예를들어 홈 화면에서 유저 정보 + 추천 피드 + 알림 개수를 동시에 불러오는 경우에는, 알림 API 하나 실패했다고 홈 화면 전체를 에러 처리를 해야하는 부분이 될 수 있습니다.
실제 앱에서는 실패한 부분만 대체하고 나머지는 보여줘야 하는 부분이 있어야한다고하면, 지금 구조처럼 각각을 분리해서 사용하는 걸로 생각했습니다. 어떻게 생각하시나용?

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.

넵 좋습니다. 해당 방법으로 진행해보다가 추후에 복잡하다고 판단되면 다른 방법 다시 생각해보면 좋을 거 같습니다!

Comment on lines +11 to +31
public struct SignupRequest: Encodable {
public let fcmToken: String
public let deviceModel: String?
public let deviceOs: String?
public let deviceOsVersion: String?
public let appVersion: String?

public init(
fcmToken: String,
deviceModel: String? = nil,
deviceOs: String? = nil,
deviceOsVersion: String? = nil,
appVersion: String? = nil
) {
self.fcmToken = fcmToken
self.deviceModel = deviceModel
self.deviceOs = deviceOs
self.deviceOsVersion = deviceOsVersion
self.appVersion = appVersion
}
}
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.

Service 객체를 통해 Domain 모듈에서 직접 Request 객체를 생성해서 주입하는 형태로 진행되기 때문에
Request를 init까지 public으로 열고 있는걸까요?

Copy link
Copy Markdown
Contributor Author

@KimNahun KimNahun Jan 20, 2026

Choose a reason for hiding this comment

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

맞습니다! 제가 이해하기로는 Domain/Feature 모듈에서 Request 객체를 직접 생성해야 하기 때문에 Public으로 열어두었는데 다른 방법도 추천해주시는게 있을까요? public이 많으니 개인적으로는 어색하더라구요..

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.

Domain 레이어에서 Network 레이어의 Request 객체를 알고 있는 게 클린아키텍처 관점에서 맞는 방식인가요??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

말씀해주신 방향이 조금 더 좋을 것 같습니다. 아무래도 Entity를 직접 만들어서 사용하는게 모델이 너무 많아지다보니 직접 사용하는 것을 고려했었는데, 조금 모듈화를 한 의미가 없어지는 것 같기도 하네요..

개선한다면 Domain이 Network DTO를 모르게 Protocol로 추상화를 하고, Repository Pattern을 도입하여 Domain이 Network DTO를 모르도록 분리해보면 어떨까요 ?

 Feature → Domain ← Data → Network                                                                           
            ↑                                                                                                
       Protocol만 정의   

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.

Protocol로 추상화하는 방법도 있고, 매개변수로 값들만 넘겨주는 방식으로 진행해도 좋을 것 같습니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

그런데 지금 network쪽 모듈을 잘못 설계했다는 생각이들어서 구조 리팩토링을 진행했습니다.. network쪽 모듈에 코드를 다 넣었었는데 data레이어와 분리하고, domain레이어는 순수하게 유지하였습니다.

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.

넵 해당 사항 반영되고 로거 추가되면 머지해도 될 거 같아요

Copy link
Copy Markdown
Contributor Author

@KimNahun KimNahun Jan 21, 2026

Choose a reason for hiding this comment

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

로거 추가했는데 네트워크 모듈쪽에 추가하는걸로 이해했는데 맞을까요? 아니면 Core에 생각하셨나요 ? 🥲 우선 맞으시다면 저도 머지하겠습니다~!

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.

네트워크 모듈 쪽에 생각했습니다!


import Foundation


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.

개행 한 줄 지워주세요!

@ChoiAnYong
Copy link
Copy Markdown
Contributor

그리고 CI 실패했는데 로그 보니까 Sendable 채택 안해서 그런 거 같습니다!

@KimNahun
Copy link
Copy Markdown
Contributor Author

프로젝트 Swift Version도 6인데, 왜 제가 컴파일할때는 관련 경고가 없었을까요..? 좀 찾아보니 Release 설정에
"SWIFT_COMPILATION_MODE": "wholemodule" 가 켜져서 그렇다고 하긴하는데, 혹시 이유를 아시나용?

@ChoiAnYong
Copy link
Copy Markdown
Contributor

해당 부분은 한 번 알아봐야할 것 같아요 근데 아마 빌드할 때는 debug를 사용해서 Release 설정이랑은 무관할 거 같아요!

@KimNahun
Copy link
Copy Markdown
Contributor Author

KimNahun commented Jan 20, 2026

CI 말씀하시는게 맞나요 ? CI가 Debug로 돌아가고 있나요 ?

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: 2

🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Project.swift`:
- Around line 32-42: Add the missing xcconfig files referenced by settings:
create "xcconfigs/Debug.xcconfig" and "xcconfigs/Release.xcconfig" and define
the BASE_URL build setting inside them (e.g. BASE_URL = https://api.example.com)
so the infoPlist .extendingDefault with "BASE_URL": .string("$(BASE_URL)")
resolves; also ensure NetworkConfiguration.swift’s fatalError path no longer
triggers by providing a valid BASE_URL in the xcconfigs or adding a fallback in
NetworkConfiguration (e.g., guard for BASE_URL before using fatalError) to
prevent runtime crashes during tests.

In `@Projects/Modules/Networks/Sources/Service/AuthService.swift`:
- Around line 17-22: The class AuthService currently declares `@unchecked`
Sendable while holding a non-Sendable MoyaProvider<AuthAPI>, causing potential
data races; update the implementation to provide proper concurrency isolation:
either make AuthService an actor (change "public final class AuthService" to
"public actor AuthService" and keep the provider as a non-Sendable stored
property) and keep AuthServiceProtocol as Sendable, or remove the Sendable
requirement from AuthServiceProtocol and ensure provider access is confined to a
single serial executor (e.g., an internal actor or DispatchQueue) inside methods
that call provider.request; locate usages of AuthService, AuthServiceProtocol
and MoyaProvider<AuthAPI> to update signatures and call sites accordingly so the
concurrency model is consistent.
🧹 Nitpick comments (6)
Projects/Domain/Sources/Model/Auth/SignupError.swift (1)

21-26: LGTM!

타입 안전한 에러 처리를 위한 잘 설계된 enum입니다. unknown 케이스를 통해 확장성도 고려되어 있습니다.

테스트 편의성을 위해 Equatable 준수를 고려해볼 수 있습니다:

♻️ 선택적 개선 제안
-public enum SignupError: Error, Sendable {
+public enum SignupError: Error, Sendable, Equatable {
Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift (1)

16-25: 중복 코드 제거 가능

SignupField(rawValue:) 이니셜라이저가 이미 동일한 매핑 로직을 제공하고 있어, 이 클로저를 간소화할 수 있습니다.

♻️ DRY 원칙 적용 제안
         let field: SignupField = {
             guard let fieldString = errors.first?.field else {
                 return .unknown("unknown")
             }
-            switch fieldString {
-            case "fcmToken": return .fcmToken
-            case "email":    return .email
-            default:         return .unknown(fieldString)
-            }
+            return SignupField(rawValue: fieldString)
         }()
Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (2)

59-97: requestrequestPlain 간 코드 중복

두 메서드가 거의 동일한 로직을 공유하고 있어, 내부 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.

♻️ 공통 로직 추출 제안
private func performRequest<T: Decodable & Sendable, E: Error & Sendable>(
    _ target: Target,
    decoder: (Data) throws -> T,
    errorMapper: `@escaping` `@Sendable` (String, String, [ErrorResponse.ErrorDetail]) -> E
) async -> NetworkResult<T, E> {
    await withCheckedContinuation { continuation in
        self.request(target) { result in
            switch result {
            case .success(let response):
                do {
                    let baseResponse = try JSONDecoder().decode(
                        BaseResponse<T>.self,
                        from: response.data
                    )
                    if baseResponse.code == Self.successCode {
                        guard let data = baseResponse.data else {
                            continuation.resume(returning: .networkFailure(.decodingFailed))
                            return
                        }
                        continuation.resume(returning: .success(data))
                    } else {
                        let errorResponse = try JSONDecoder().decode(
                            ErrorResponse.self,
                            from: response.data
                        )
                        let apiError = errorMapper(
                            errorResponse.code ?? "",
                            errorResponse.message ?? "",
                            errorResponse.errors ?? []
                        )
                        continuation.resume(returning: .failure(apiError))
                    }
                } catch {
                    continuation.resume(returning: .networkFailure(.decodingFailed))
                }
            case .failure(let moyaError):
                let networkError = Self.mapMoyaError(moyaError)
                continuation.resume(returning: .networkFailure(networkError))
            }
        }
    }
}

99-107: MoyaError 매핑 확장 고려

현재 NSURLErrorDomainconnectionFailed로 매핑되고 나머지는 모두 unknown으로 처리됩니다. 타임아웃, 인증서 오류 등 추가 케이스를 명시적으로 처리하면 디버깅에 유용할 수 있습니다.

♻️ 추가 에러 케이스 매핑 예시
 private static func mapMoyaError(_ error: MoyaError) -> NetworkError {
     switch error {
     case .underlying(let nsError as NSError, _)
         where nsError.domain == NSURLErrorDomain:
+        switch nsError.code {
+        case NSURLErrorTimedOut:
+            return .timeout
+        case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
+            return .connectionFailed
+        default:
+            return .connectionFailed
+        }
-        return .connectionFailed
     default:
         return .unknown(error.localizedDescription)
     }
 }

NetworkError.timeout 케이스 추가가 필요합니다.

Projects/Data/Sources/Repository/Auth/AuthRepository.swift (2)

20-31: 변환 로직을 AuthTransform 확장으로 일원화하면 중복을 줄일 수 있습니다.
이미 SignupRequest(from:)SignupResult(from:)가 추가되어 있으니 Line 20-31에서 활용하면 매핑 중복을 피할 수 있습니다.

♻️ 리팩터 예시
-        let request = SignupRequest(fcmToken: info.fcmToken)
+        let request = SignupRequest(from: info)
...
-            let signupResult = SignupResult(
-                uuid: response.uuid,
-                accessToken: response.accessToken,
-                nickname: response.nickname
-            )
+            let signupResult = SignupResult(from: response)

13-14: @unchecked Sendable 사용 재평가 필요

AuthServiceProtocol은 이미 Sendable로 정의되어 있으므로, 원래 제안("AuthServiceProtocol을 Sendable로 만들어라")은 불필요합니다. 다만 AuthRepository@unchecked Sendable을 사용하는 이유는 의존성인 AuthService가 외부 라이브러리 MoyaProvider로 인해 @unchecked를 사용하기 때문입니다.

AuthRepository 자체는 불변 상태만 가지고 있고 순수하게 authService에 위임하므로, @unchecked를 제거하고 순수 Sendable로 변경할 수 있는지 검토해 주세요. 다만 현재 구조에서 데이터 경합 위험은 없어 보입니다.

Comment on lines +32 to +42
infoPlist: .extendingDefault(with: [
"BASE_URL": .string("$(BASE_URL)")
]),
sources: ["Tests/**"],
dependencies: [
.target(name: "Networks")
],
settings: .settings(configurations: [
.debug(name: "Debug", xcconfig: .relativeToRoot("xcconfigs/Debug.xcconfig")),
.release(name: "Release", xcconfig: .relativeToRoot("xcconfigs/Release.xcconfig"))
])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for BASE_URL definition in xcconfig files
echo "=== Searching for BASE_URL in xcconfig files ==="
rg -n "BASE_URL" xcconfigs -g "*.xcconfig"

echo -e "\n=== Listing all xcconfig files ==="
fd -t f -e xcconfig xcconfigs

echo -e "\n=== Content of xcconfig files ==="
fd -t f -e xcconfig xcconfigs -x cat -n {}

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 194


🏁 Script executed:

#!/bin/bash
# Check if xcconfigs directory exists
echo "=== Checking xcconfigs directory ==="
ls -la xcconfigs 2>&1 || echo "Directory does not exist"

echo -e "\n=== Searching for any xcconfig files in entire repo ==="
find . -name "*.xcconfig" -type f 2>/dev/null

echo -e "\n=== Checking project structure around Projects/Modules/Networks ==="
ls -la Projects/Modules/Networks/ 2>&1 | head -30

echo -e "\n=== Searching for BASE_URL references in entire project ==="
rg -n "BASE_URL" . -g "!.git" | head -20

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1364


xcconfig 파일 누락으로 인한 빌드 실패 위험 - 즉시 수정 필요

39-42줄에서 참조하는 xcconfigs/Debug.xcconfigxcconfigs/Release.xcconfig 파일이 존재하지 않습니다. 33줄의 $(BASE_URL) 빌드 변수는 해석되지 않아 테스트 빌드가 실패하거나, NetworkConfiguration.swift의 fatalError(18줄)로 인해 런타임에 충돌합니다. 누락된 xcconfig 파일들을 추가하고 BASE_URL을 정의해야 합니다.

🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Project.swift` around lines 32 - 42, Add the
missing xcconfig files referenced by settings: create "xcconfigs/Debug.xcconfig"
and "xcconfigs/Release.xcconfig" and define the BASE_URL build setting inside
them (e.g. BASE_URL = https://api.example.com) so the infoPlist
.extendingDefault with "BASE_URL": .string("$(BASE_URL)") resolves; also ensure
NetworkConfiguration.swift’s fatalError path no longer triggers by providing a
valid BASE_URL in the xcconfigs or adding a fallback in NetworkConfiguration
(e.g., guard for BASE_URL before using fatalError) to prevent runtime crashes
during tests.

Comment on lines +17 to +22
public final class AuthService: AuthServiceProtocol, @unchecked Sendable {
private let provider: MoyaProvider<AuthAPI>

public init(provider: MoyaProvider<AuthAPI> = MoyaProvider<AuthAPI>()) {
self.provider = provider
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Moya MoyaProvider Sendable Swift 6 concurrency support

💡 Result:

Short answer: Moya’s MoyaProvider is not annotated as Sendable by the library and therefore will trigger Sendable checks under Swift 6’s stricter concurrency model. The Moya codebase still exposes MoyaProvider as a reference-type provider/AnyObject-based API (not @Sendable), so you should treat it as non‑Sendable or use an explicit workaround (actor/flow-confinement or @unchecked Sendable) until the library authors add Sendable compatibility. [1][2]

Practical options

  • Confine MoyaProvider usage to a single actor or main/serial queue (recommended). This avoids Sendable violations without changing library code. [0]
  • If you must share a provider across tasks/actors and you are sure it’s safe, declare a small wrapper or extension: extension MoyaProvider: @unchecked Sendable {} — but understand this silences the compiler check (unsafe unless you audited thread-safety). [3][0]

Sources

  1. Moya repository / docs (shows MoyaProvider is a class/AnyObject-based provider; no Sendable annotations). [1]
  2. Swift Concurrency / Swift 6 Sendable guidance (explains why libraries must mark types Sendable for Swift 6). [0]
  3. Swift community discussion on marking library types unchecked Sendable (tradeoffs of @unchecked Sendable). [3]

Would you like a short code example for (a) an actor-wrapped provider or (b) an @unchecked Sendable wrapper?


🏁 Script executed:

# Search for CI configuration and any CI failure context
git ls-files | grep -E '\.(yml|yaml|swift)' | head -20

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1059


🏁 Script executed:

# Look for MoyaProvider usage and Sendable related comments in the codebase
rg -i "moya|sendable" --type swift -A 3 -B 3 | head -100

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 8042


🏁 Script executed:

# Check the actual AuthService file to understand the full context
cat -n Projects/Modules/Networks/Sources/Service/AuthService.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1023


@unchecked Sendable 사용 - Moya 라이브러리 Sendable 미지원으로 인한 동시성 안전 우려

Moya 15.0.0의 MoyaProvider는 Sendable을 지원하지 않으며, 내부적으로 가변 상태를 포함합니다. AuthServiceProtocolSendable을 선언하므로 구현 클래스도 이를 만족해야 하는데, @unchecked Sendable은 컴파일러 검사를 우회하는 임시 해결책입니다. 이로 인해 async/await 경계에서 데이터 경쟁 상태(data race)가 발생할 수 있습니다.

권장 해결 방안:

  1. 가장 권장: AuthService를 단일 actor로 격리하거나 전담 스레드/큐에 한정하여 provider 접근을 제어합니다.
  2. 프로토콜 재설계: AuthServiceProtocolSendable 요구사항 제거를 검토하고, actor 기반 구현으로 전환합니다.
♻️ Actor 기반 구현 예시
public actor AuthService: AuthServiceProtocol {
    private let provider: MoyaProvider<AuthAPI>
    
    public init(provider: MoyaProvider<AuthAPI> = MoyaProvider<AuthAPI>()) {
        self.provider = provider
    }
    
    public func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError> {
        await provider.request(.signup(request: request), errorMapper: SignupError.init)
    }
}

public protocol AuthServiceProtocol: Sendable {
    func signup(request: SignupRequest) async -> NetworkResult<SignupResponse, SignupError>
}
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Service/AuthService.swift` around lines 17
- 22, The class AuthService currently declares `@unchecked` Sendable while holding
a non-Sendable MoyaProvider<AuthAPI>, causing potential data races; update the
implementation to provide proper concurrency isolation: either make AuthService
an actor (change "public final class AuthService" to "public actor AuthService"
and keep the provider as a non-Sendable stored property) and keep
AuthServiceProtocol as Sendable, or remove the Sendable requirement from
AuthServiceProtocol and ensure provider access is confined to a single serial
executor (e.g., an internal actor or DispatchQueue) inside methods that call
provider.request; locate usages of AuthService, AuthServiceProtocol and
MoyaProvider<AuthAPI> to update signatures and call sites accordingly so the
concurrency model is consistent.

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

🤖 Fix all issues with AI agents
In `@Projects/Modules/Networks/Sources/Extensions/MoyaProvider`+Async.swift:
- Around line 15-62: The request(_:errorMapper:) async extension captures self
(MoyaProvider) inside a `@Sendable` withCheckedContinuation but MoyaProvider in
Moya 15 isn't Sendable under Swift 6, causing strict-concurrency compile errors;
fix by adding `@preconcurrency` import Moya at the top of this file (before any
use of Moya types) to opt-out of Sendable checking for the Moya module, or
alternatively ensure callers use an `@unchecked` Sendable wrapper when passing
MoyaProvider into async contexts; target the request<T: Decodable & Sendable, E:
Error & Sendable> function and its use of withCheckedContinuation as the
locations to address.
🧹 Nitpick comments (2)
Projects/Modules/Networks/Sources/Logger/NetworkLogger.swift (1)

49-62: parameters.description은 JSON 형식이 아닌 Swift Dictionary 기본 출력 형식을 반환합니다.

requestParameters 케이스에서 parameters.description을 사용하면 ["key": value] 형태의 Swift Dictionary 문자열이 출력됩니다. requestJSONEncodable 케이스와 일관성을 맞추려면 JSONSerialization을 사용하여 JSON 문자열로 변환하는 것이 좋습니다.

♻️ 개선 제안
 private static func requestBody(from task: Moya.Task) -> String? {
     switch task {
     case .requestParameters(let parameters, _):
-        return parameters.description
+        if let data = try? JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted),
+           let string = String(data: data, encoding: .utf8) {
+            return string
+        }
+        return parameters.description
     case .requestJSONEncodable(let encodable):
         if let data = try? JSONEncoder().encode(AnyEncodable(encodable)),
            let string = String(data: data, encoding: .utf8) {
             return string
         }
         return nil
     default:
         return nil
     }
 }
Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift (1)

64-107: requestrequestPlain의 디코딩 로직 중복

성공/에러 디코딩과 NetworkResult 매핑이 두 함수에 거의 동일하게 복제돼 유지보수 비용이 커질 수 있습니다. 공통 helper(예: decodeResponse<T> 또는 handleResponse)로 추출해 DRY하게 유지하는 것을 추천합니다.

Comment on lines +15 to +62
func request<T: Decodable & Sendable, E: Error & Sendable>(
_ target: Target,
errorMapper: @escaping @Sendable (String, String, [ErrorResponse.ErrorDetail]) -> E
) async -> NetworkResult<T, E> {
NetworkLogger.logRequest(target)

return await withCheckedContinuation { continuation in
self.request(target) { result in
switch result {
case .success(let response):
NetworkLogger.logResponse(response)

do {
let baseResponse = try JSONDecoder().decode(
BaseResponse<T>.self,
from: response.data
)

if baseResponse.code == Self.successCode {
guard let data = baseResponse.data else {
continuation.resume(returning: .networkFailure(.decodingFailed))
return
}
continuation.resume(returning: .success(data))
} else {
let errorResponse = try JSONDecoder().decode(
ErrorResponse.self,
from: response.data
)
let apiError = errorMapper(
errorResponse.code ?? "",
errorResponse.message ?? "",
errorResponse.errors ?? []
)
continuation.resume(returning: .failure(apiError))
}
} catch {
continuation.resume(returning: .networkFailure(.decodingFailed))
}

case .failure(let moyaError):
NetworkLogger.logError(moyaError)
let networkError = Self.mapMoyaError(moyaError)
continuation.resume(returning: .networkFailure(networkError))
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file structure and related files
find . -type f -name "*.swift" | grep -i moya | head -20

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 146


🏁 Script executed:

# Look for the MoyaProvider definition and its Sendable conformance
rg -l "MoyaProvider" --type swift | head -10

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 204


🏁 Script executed:

# Check the specific file structure
fd -t f "MoyaProvider+Async.swift" 2>/dev/null

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 57


🏁 Script executed:

# Look for Sendable conformance in the codebase
rg "extension MoyaProvider|class MoyaProvider|actor MoyaProvider" --type swift -A 5

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 756


🏁 Script executed:

# Read the full MoyaProvider+Async.swift file
cat -n Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 5542


🏁 Script executed:

# Check if there are any `@unchecked` Sendable or Sendable conformance declarations
rg "MoyaProvider.*Sendable|@unchecked.*Sendable" --type swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 349


🏁 Script executed:

# Look for import statements and package configuration
cat -n Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift | head -20

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 726


🏁 Script executed:

# Find project configuration files to check Swift version and strict concurrency settings
fd -t f "Package.swift|project.yml|.swift-version" | xargs cat 2>/dev/null | head -100

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 983


🏁 Script executed:

# Check for Moya import and version pinning
rg "import Moya|preconcurrency" --type swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 372


🏁 Script executed:

# Check if there are any Xcode build settings or strict concurrency configurations
fd -t f "*.xcconfig|project.yml|Tuist" | xargs grep -l "SWIFT_STRICT_CONCURRENCY\|strict" 2>/dev/null || echo "No strict concurrency configs found"

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 435


🏁 Script executed:

# Look for MoyaProvider in Moya library or check if there's any additional setup
rg "extension MoyaProvider" --type swift -B 2 -A 10

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1446


🏁 Script executed:

# Check if the Moya library files are present or check for any `@preconcurrency` imports
rg "@preconcurrency" --type swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 57


🏁 Script executed:

# Check Moya version in dependencies and look for any swift-concurrency related files
rg "Moya" Projects/Modules/Networks/Project.swift 2>/dev/null || \
rg "Moya" Projects/Modules/Networks/ --type swift | head -20

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1660


🏁 Script executed:

# Search for any NetworkError, NetworkResult definitions to understand error handling
fd -t f "NetworkError.swift|NetworkResult.swift"

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 191


🏁 Script executed:

# Check NetworkError and NetworkResult definitions
cat -n Projects/Modules/Networks/Sources/Base/NetworkError.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 831


🏁 Script executed:

# Check NetworkResult definition
cat -n Projects/Modules/Networks/Sources/Base/NetworkResult.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 500


🏁 Script executed:

# Check Moya version compatibility with Swift 6 concurrency
web_search
Moya 15.0.0 Swift 6 Sendable support compatibility

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 168


🏁 Script executed:

# Check if MoyaProvider is thread-safe or has Sendable conformance in documentation
web_search
MoyaProvider Sendable Swift 6 concurrency

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 176


🏁 Script executed:

# Look at the AuthService to see how MoyaProvider is used with `@unchecked` Sendable
cat -n Projects/Modules/Networks/Sources/Service/AuthService.swift | head -30

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1023


🌐 Web query:

Moya 15.0.0 Swift 6 Sendable concurrency support

💡 Result:

Short answer: Moya 15.0.0 does not ship with explicit Swift 6 "Sendable"-first annotations/guarantees. Moya 15 was released for Swift 5.x compatibility (README maps Moya 15 → Swift ≥5.2) and the project does not advertise a Swift‑6 / full Sendable migration in the 15.0.0 docs/releases—so you should expect to need local fixes (e.g. adding @unchecked Sendable extensions, adding explicit Sendable conformances, or small wrapper/adapters) when compiling with Swift 6 strict concurrency. [1][2][3]

What to do:

  • If you compile in Swift 6 mode, add targeted Sendable/@unchecked Sendable annotations in your app or submit small PRs to Moya for missing conformances. [2]
  • Alternatively compile in Swift 5 language mode / opt out of strict Swift 6 concurrency until you migrate. [2]
  • Check the Moya repo for any community PRs or later releases if you need upstream changes. [1][3]

Sources:

  • Moya repository / README (Moya 15.0.0 guidance). [1]
  • Swift Concurrency adoption / Sendable guidance (why libraries must add annotations for Swift 6). [2]
  • Moya releases page (15.0.0/alpha notes). [3]

Would you like me to scan the Moya repo now for specific files/types you need marked Sendable and suggest exact patches?


🌐 Web query:

withCheckedContinuation Sendable closure self capture Swift 6

💡 Result:

Short answer: In Swift 6 Task/async closures are @Sendable by default, so using withCheckedContinuation (or Task) inside a method can trigger "capture of 'self' with non‑sendable type" when the closure implicitly captures self. Fixes: capture only Sendable things (capture the actor property instead of self), copy needed values into local Sendable variables, make the captured type Sendable (or use an actor/immutable types), or explicitly avoid sending non‑Sendable state by using weak captures or the new sending/move semantics. Examples and guidance: capture the actor/property directly (Task { [worker] in await worker.doWork() }), or do let value = self.foo; Task { [value] in ... } rather than capturing self. References: Dev Solve (fixes and capture-list advice), Swift6 Sendable closure discussion, and notes about withCheckedContinuation/actor interaction. [1][2][3]

References

  • Fixing 'Capture of self with non-sendable type' in Swift 6 — Dev Solve. [1]
  • “Taming 'self' in @Sendable Closures” (Swift 6 capture-list and sending closure patterns). [2]
  • Sendable closures & withCheckedContinuation / actor notes. [3]

Swift 6 strict concurrency 환경에서 Sendable 규칙 위반 가능성

withCheckedContinuation 클로저는 @Sendable이며, 여기서 self(MoyaProvider)가 capture되는데, Moya 15.0.0은 Swift 6 Sendable 마이그레이션을 완료하지 않아 MoyaProvider가 Sendable을 준수하지 않습니다. 따라서 Swift 6 strict concurrency 모드에서 컴파일 오류/경고가 발생할 수 있습니다.

권장 해결책: import Moya 앞에 @preconcurrency import Moya를 추가하거나, 현재 AuthService 패턴처럼 호출자 레벨에서 @unchecked Sendable을 명시적으로 적용합니다.

🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Extensions/MoyaProvider`+Async.swift around
lines 15 - 62, The request(_:errorMapper:) async extension captures self
(MoyaProvider) inside a `@Sendable` withCheckedContinuation but MoyaProvider in
Moya 15 isn't Sendable under Swift 6, causing strict-concurrency compile errors;
fix by adding `@preconcurrency` import Moya at the top of this file (before any
use of Moya types) to opt-out of Sendable checking for the Moya module, or
alternatively ensure callers use an `@unchecked` Sendable wrapper when passing
MoyaProvider into async contexts; target the request<T: Decodable & Sendable, E:
Error & Sendable> function and its use of withCheckedContinuation as the
locations to address.

@ChoiAnYong ChoiAnYong self-requested a review January 21, 2026 08:02
@KimNahun KimNahun merged commit 75204c5 into develop Jan 21, 2026
2 checks passed
@KimNahun KimNahun deleted the feat/#3-Network/Module branch January 29, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

네트워크 모듈 설계

2 participants