[STEP2] 작업 목록
- URL Session을 활용한 서버와의 통신
- JSON 데이터 매핑할 모델 설계 및 사용
- Unit Test
STEP2 진행시 고민한 부분
이전 STEP에서는 프로젝트의 폴더링을 어떻게 해야될지를 고민했습니다.
API, DTO, Model과 이를 테스트하기 위한 unitTest를 작성했는데 API는 Main API인 kobis API와 이를 채택하는 detailMovie, DailyBoxOffice가 있고 더 많은 API가 생성되는 경우 공통 부분으로 묶여진 kobis API를 프로토콜로 만들어 채택하는 방법을 사용하게 되었습니다.
Manager는 각각의 API를 관리하고자 하는 클래스입니다.
저는 API를 사용하고자 할때 해당하는 Manager를 생성하여 통신하는 방법을 사용했습니다.
그런데 이에 대한 단점으로는 새로운 API를 생성하고자 할떄 마다 새로운 Manager를 만들어야 한다!! 라는 것이지요
처음에는 그렇게 만들고 메인 매니저인 BoxOficceManager에서 이를 관리한다면 별도의 문제는 없을거라고 생각했지만 기능 구현이 완료되고 나서는 이러한 부분은 결국 코드 재사용성과 확장성에 불리하다고 생각했습니다.
폴더링 구조
│ └── Sources
│ ├── Application
│ │ ├── AppDelegate.swift
│ │ └── SceneDelegate.swift
│ ├── DTO
│ │ ├── DailyBoxOfficeDTO.swift
│ │ └── DetailMovieDTO.swift
│ ├── Model
│ │ └── Network
│ │ ├── JsonParser.swift
│ │ ├── KobisInterface
│ │ │ └── KobisAPI.swift
│ │ ├── KobisService
│ │ │ ├── BoxOfficeManager.swift
│ │ │ ├── DailyBoxOfficeManager.swift
│ │ │ ├── DetailMovieManager.swift
│ │ │ └── KobisService.swift
│ │ ├── NetworkError.swift
│ │ └── Result.swift
│ └── Presentation
│ └── ViewController.swift
└── BoxOfficeTests
└── BoxOfficeTests.swift
리펙토링 후 개선한 내용
폴더링 및 각 모델의 기능 분리
각 모듈의 기능이 독립적으로 존재하게끔 만들어야겠다 라는 고민을 하였고 저는 크게 Network, API, DTO, Model으로 분리해야겠다 라는 생각을 했습니다.
기존의 Network의 기능을 DetailBoxOffice, DailyBoxOffice에서 각가 선언되어 사용하는 구조에서 Network에 필요한 request의 프로퍼티 및 session을 전부 생성자로 전달받아
- Network
request, session
request와 세션에서 필요한 인터페이스는 전부 생성자로 통해 제공받음 - API
kobis (DailyBoxOffice, DetailMovie, etc) API
kobis API 에서 공통 인자는 프로토콜로 빼버리고 나머지 변경사항을 각각의 API에서 인터페이스로 제공받음 - DTO
입력되는 데이터를 사용자의 틀에 맞는 규격으로 사용 - Manager
네트워크 통신에서 사욛되는 코드를 조합하여
NetWork의 인터페이스에 API 정보를 전달하고 반환되는 데이터를 DTO로 변환함
Network 재사용
protocol NetworkRequestBuilderProtocol {
var baseUrl: String { get }
var path: String { get }
var header: [String: String] { get }
var query: [String: Any] { get }
var body: [String: Any] { get }
var method: HTTPMethodType { get }
var bodyEncoder: Encoderable { get }
var urlScheme: URLScheme { get }
func setURLRequest() -> URLRequest?
}
URL Session의 Request 기능을 좀더 확장성있고 유연하게 대처하기 위해서 사용했습니다.
프로토콜을 채택하는 클래스에서는 위 프로토콜에서 정의된 프로퍼티를 반드시 선언해줘야 합니다.
아래의 http 통신을 하기 위해 URLRequest를 생성하는 방법 입니다
이전 코드와 달라진 점은 urlRequest를 생성하기 위한 값들이 모두 NetworkRequestBuilder의 생성자를 통해 저장되는 프로퍼티를 사용했다는 것입니다.
기존에 값을 직접 넣는 방식보다 훨씬 재사용성에 용이하다고 생각합니다.
func setURLRequest() -> URLRequest? {
guard let url = setURL() else {
return nil
}
var urlRequest = URLRequest(url: url, timeoutInterval: 30.0)
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = header.reduce(into: [:]) { partialResult, header in
partialResult[header.key] = header.value
}
if !body.isEmpty {
urlRequest.httpBody = bodyEncoder.encode(body)
}
return urlRequest
}
API
열심히 작성했는데 업로드 하고 보니 여기서 부터 날라가서 ㅠㅠ 다시 작성...
API 부분은 Network에 책임을 분리하다 보니 가지고 있는 기능이 더욱 적어졌다...
사실 아래 코드도 path, code를 따로 API로 분류할 필요도 없을 정도로 그 API에 대한 기능이 없다고 생각됩니다.
왜냐하면 현재 사용되는 코드에서는 kobisAPI에 먼저 값을 주입하고 NetworkBuilder의 생성자에 kobisAPI의 값을 넣는 방법으로 구현되어 굳이 kobisAPI가 필요 없을 정도로 잘못된 설계인가? 라는 생각을 하게 되었습니다.
이러한 문제가 있어 추후 해결할 예정... 왜냐하면 Network 자체는 구현상 잘못되지 않았다고 생각하고 API는 향후에 다른 API를 사용하는 날이 오게되면 그떄 API 구현을 변경하면 되니까?? ㅎㅎ
struct DetailMovie: KobisAPI {
var path: String
var code: String
init(
path: String,
code: String
) {
self.code = code
self.path = path
}
}
Manager
NetworkManager를 만들게 된 이유는 이전에 설계를 위해 보여줬던 그림처럼 현재 프로젝트에서 사용되는 기능 API, Network, DTO 등등 에 대해서 분리하고 Manager에서 이를 관리하는 코드를 구현해야겠다고 생각했습니다.
그래서 외부에서 생성된 API, Network를 주입 받아 네트워크 처리를 Manger를 통해 구현하고자 생각하게 되었습니다.
또한 제네릭을 사용해서 Manger의 객체를 생성할 떄 사용하고자 하는 API에 대한 DTO 타입을 전달 받아서 http 서버에서 전달 되는 데이터를 변환하는 작업을 수행하게 됩니다.
struct NetworkManager<T: Decodable>: StatusCodeProtocol {
private let networkSession: NetworkSessionProtocol
private let networkRequestBuilder: NetworkRequestBuilderProtocol
init(
networkSession: NetworkSessionProtocol,
networkRequestBuilder: NetworkRequestBuilderProtocol
) {
self.networkSession = networkSession
self.networkRequestBuilder = networkRequestBuilder
}
mutating func request(complection: @escaping (T?) -> Void) {
let builder = networkRequestBuilder
guard let request = builder.setURLRequest() else {
return
}
networkSession.dataTask(with: request, complection: { [self] result in
switch result {
case .success(let networkResponse):
let decoder = JsonDecoder.shared
guard let networkResponse = networkResponse as? NetworkResponse,
let data = networkResponse.data,
let decodeData: T = try? decoder.decode(T.self, data)
else {
complection(nil)
return
}
if !success.contains(networkResponse.response.statusCode) {
complection(nil)
return
}
complection(decodeData)
case .failure(let error):
print(error.errorDescription)
}
})
}
}
구현시 발생한 문제점 및 개선 방향
블로그 글을 작성할떄 백업을 만들지 않은 내가 문제다....
진짜 멍청했다...이놈
API
현재 코드에서 제일 기능이 없고 애매하다고 생각됩니다.
KobisAPI 프로토콜을 채택하여 서로 다른 API에서 추가로 프로퍼티를 설정하는 방법은 괜찮다고 생각하지만
사용되는 메서드가 없는 이상에는 이게 굳이 필요한가 라는 의문이 생겼습니다.
이에 대한 답을 다른 사람들의 코드를 통해서 얻게 되었는데
오오 제토 가라사데!
그분의 코드를 보니 따로 API에서 Network builder를 제공받고 이를 처리하는 코드를 만드셨어요.
제가 생각한 Manager의 기능을 API에서 처리하는 것을 보고 따로 Manager라는 것이 필요 없겠네 라는 생각을 했습니다 ㅎㅎ
Escaping Closure
이번 프로젝트에서 탈출 클로저를 사용하게된 이유는 제가 생각하기에 네트워크 통신의 결과를 가져오는 간편한 방법중 하나라는 생각이 들어서입니다.
그런데 탈출 클로저를 사용하게 되면 내가 원하는 곳에서 결과를 반환 받기 위해 클로저를 중첩되게 사용해야 합니다.
이러한 부분은 클린 코드로서도 혹은 유지보수로써도 좋은 코드가 아니라고 생각되기 때문에 수정해야 겠다고 생각을 하게 되었습니다
동시성 프로그래밍 async, await를 사용할 생각 입니다.
이유는 네트워크 비동기 통신에 대해서 탈출 클로저보다 더욱 좋은 방법이라고 생각하기 때문이에요
따로 옵저버 패턴과 같은 코드 동작이 완료되면 value를 전달받는 것도 생각하긴 했지만
동시성에 대해서 계속 회피하는거 같아서 제대로 해봐야겠다 생각하게 되었네요
'iOS' 카테고리의 다른 글
[iOS] Concurrency(async & await) (0) | 2024.03.31 |
---|---|
[iOS] 새싹 프로젝트 (BoxOffice STEP3) #2 CollectionView 구현 (0) | 2024.03.24 |
[iOS] 새싹 프로젝트 (BoxOffice STEP3) #1 코드 베이스 화면 작업 (2) | 2024.03.15 |
[iOS] Hashable (0) | 2024.03.03 |
[iOS] UserDefaults (0) | 2024.02.23 |