Skip to content

Networking

JEONG edited this page Jun 29, 2026 · 3 revisions

Networking

Moya 기반 네트워크 레이어. Router 설계 규칙과 Response DTO 디코딩 규칙을 다룹니다.


1. Network Router (Moya)

API Router는 엔드포인트 메타데이터(Path / Method / Encoding)만 책임집니다. 파라미터 키 이름과 직렬화 규칙은 Request/Query DTO가 캡슐화합니다.

원칙

  1. Router의 task 안에 인라인 딕셔너리 금지 — key 문자열이 Router에 흩어지면 스펙 변경 시 모든 case를 뒤져야 한다.
  2. Body → Encodable DTO + .requestJSONEncodable(body)
  3. Query → DTO + toParameters + .requestParameters(..., encoding: URLEncoding.queryString) 직렬화 책임을 var toParameters: [String: Any] computed property로 캡슐화. 네이밍은 toParameters로 통일.
  4. Path 변수({id})는 case associated value로 받아 path에서만 사용 — 쿼리/바디와 섞지 않는다.
  5. 단일 path 변수만 받는 case는 DTO 불필요 — 파라미터 컬렉션이 생기는 순간 DTO 분리.

❌ 안티패턴

// Router가 파라미터 키 이름까지 알고 있음
case .getNoticeReadStatusList(_, let cursorId, let filterType, let organizationIds, let status):
    return .requestParameters(
        parameters: [
            "cursorId": cursorId,
            "filterType": filterType,
            "organizationIds": organizationIds,
            "status": status
        ],
        encoding: URLEncoding.queryString
    )

✅ 권장 패턴

// 1. Query/Body DTO 정의
struct NoticeReadStatusListQuery: Encodable {
    let cursorId: Int
    let filterType: String
    let organizationIds: [Int]
    let status: String

    var toParameters: [String: Any] {
        ["cursorId": cursorId, "filterType": filterType,
         "organizationIds": organizationIds, "status": status]
    }
}

// 2. Router는 DTO만 받아 위임
case .getNoticeReadStatusList(_, let query):
    return .requestParameters(parameters: query.toParameters, encoding: URLEncoding.queryString)

case .addLink(_, let body):
    return .requestJSONEncodable(body)

DTO 파일 위치

  • Body: Features/{Feature}/Data/DTO/Request/{Name}RequestDTO.swift
  • Query: Features/{Feature}/Data/DTO/Request/{Name}Query.swift

2. Response DTO 디코딩

서버는 응답으로 내려주는 모든 정수 값을 String으로 직렬화합니다. Int로 선언된 필드를 그대로 디코딩하면 런타임 DecodingError.typeMismatch가 발생합니다.

원칙

  1. Response DTO의 모든 Int 필드는 String 폴백을 보장한다
  2. synthesized Codable 금지Int 필드가 있으면 반드시 custom init(from:) + encode(to:) 작성
  3. decode(Int.self) / decodeIfPresent(Int.self) 직접 호출 금지decodeIntFlexibleIfPresent 헬퍼 사용
  4. 폴백 순서: IntString("123")Double(123.0) → throw
  5. Request DTO(Encodable, 보내는 쪽)는 제외Int 그대로 OK

✅ 권장 패턴

struct UserDTO: Codable {
    let userId: Int
    let name: String

    private enum CodingKeys: String, CodingKey { case userId, name }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        userId = try container.decodeIntFlexibleIfPresent(forKey: .userId) ?? 0
        name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(userId, forKey: .userId)
        try container.encode(name, forKey: .name)
    }
}

private extension KeyedDecodingContainer {
    func decodeIntFlexible(forKey key: Key) throws -> Int {
        if let value = try? decode(Int.self, forKey: key) { return value }
        if let value = try? decode(String.self, forKey: key), let intValue = Int(value) { return intValue }
        if let value = try? decode(Double.self, forKey: key) { return Int(value) }
        throw DecodingError.typeMismatch(Int.self, DecodingError.Context(
            codingPath: codingPath + [key],
            debugDescription: "Expected Int/String-number/Double for key '\(key.stringValue)'"))
    }

    func decodeIntFlexibleIfPresent(forKey key: Key) throws -> Int? {
        if (try? decodeNil(forKey: key)) == true { return nil }
        return try? decodeIntFlexible(forKey: key)
    }
}

리뷰 체크리스트

  • Int / Int? / [Int] / Int64 등 정수 타입 필드가 있는가
  • 정수 필드가 있다면 custom init(from:) 정의 (synthesized Codable 금지)
  • init(from:) 안에서 decodeIntFlexibleIfPresent 사용 — decode(Int.self) 직접 호출 없음
  • 헬퍼가 파일 내 private extension KeyedDecodingContainer로 정의되어 있는가
  • Request/Encodable DTO는 적용 제외 확인

관련 문서: Coding Conventions · Error Handling

Clone this wiki locally