초기 코드는 아래와 같다
OpenAI에 API를 요청하는 메서드를 만들었으나 ...
부족한 점들이 많이 보여 refactoring을 결정했다.
class OpenAIService {
func sendRequestToOpenAI(_ messages: [RequestMessageModel], completion: @escaping (Result<[RequestMessageModel], Error>) -> Void) {
let endpoint = OpenAIEndPoint.chatCompletionsBaseURL
guard let url = endpoint.url else { return }
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue
request.addValue("Bearer \(APIKeyManager.openAIAPIKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let requestBody: [String: Any] = [
"model": "gpt-3.5-turbo",
"messages": messages.map { ["role": $0.role.rawValue, "content": $0.content] }
]
request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
return
}
do {
let responseDTO = try JSONDecoder().decode(OpenAICheatResponseDTO.self, from: data)
let messages = responseDTO.choices.compactMap { choice -> RequestMessageModel? in
guard let messageContent = choice.message?.content,
let messageRole = choice.message?.role,
let role = MessageRole(rawValue: messageRole) else { return nil }
return RequestMessageModel(role: role, content: messageContent)
}
completion(.success(messages))
} catch {
completion(.failure(error))
}
}
task.resume()
}
}
문제점
OpenAIService객체가 너무 많은 일들을 한다.
- URLRequest 생성
- 네트워크 요청 실행
- 응답처리
Request시 model의 선택권을 주지 않고 하드코딩 형식으로 되어있다.
- 추후 모델을 바꿀 시 불편함
- 휴먼 에러의 가능성이 높아진다.
Error타입에 대한 정의가 제대로 되어있지 않다.
- 디버깅시 불편함
- 앱이 강제종료되는 불상사가 생길 수 있음
해결방법 Point
1. SRP 적용해서 역할 분리
OpenAIService 객체가 하는 역할들을 분리해줘 결합도를 떨어뜨리고 가독성을 좋게 해준다.
2. ReqeustDTO 모델 구현
model을 선택 할 수 있게 ReqeustDTO모델을 구현해 관리해준다.
3. Error Handling
에러 타입을 정의하고 상황에 맞는 에러들을 적용해 보다 편하게 상황을 파악 할 수 있게한다.
코드
1. DTO 모델 정의
struct RequestDTO: Encodable {
var model: GPTModel
let requestMessage: [RequestMessageModel]
}
enum GPTModel: Encodable {
case gpt3Turbo0125
case gpt3Turbo
case gpt3Turbo1106
func aiModel() -> String {
switch self {
case GPTModel.gpt3Turbo0125:
return "gpt-3.5-turbo-0125"
case GPTModel.gpt3Turbo:
return "gpt-3.5-turbo"
case GPTModel.gpt3Turbo1106:
return "gpt-3.5-turbo-1106"
}
}
}
2. Error정의
에러 코드 정의는 NSError를 활용한 정리도 있지만 간단하게 Enum타입과 Error프로토콜을 적용해 구현했다.
enum NetworkError: Error {
case urlError
case connectionError
case timeout
case decodingError
case serverError
case dataNotFound
var errorDescription: String {
switch self {
case .urlError:
return "URL 형식이 잘못되었거나 존재하지 않습니다."
case .connectionError:
return "인터넷 연결에 문제가 있습니다."
case .timeout:
return "요청 시간이 초과되었습니다."
case .decodingError:
return "데이터 디코딩 중 에러가 발생했습니다."
case .serverError:
return "서버에서 에러가 발생했습니다."
case .dataNotFound:
return "요청한 데이터를 찾을 수 없습니다."
}
}
3. URLRequest를 생성해주는 Builder 구현
struct URLRequestBuilder {
func makeRequest(url: URL?,
for model: GPTModel,
APIKey apiKey: String,
withMessages messages: [RequestMessageModel]) throws -> URLRequest {
guard let url = url else {
throw NSError(domain: "RequestURLError", code: 404, userInfo: nil)
}
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue
request.addValue("Bearer $\(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDTO = RequestDTO(model: model, requestMessage: messages)
print(requestDTO)
do {
let jsonData = try JSONEncoder().encode(requestDTO)
request.httpBody = jsonData
} catch {
throw NSError(domain: "JSONParsingError", code: 1001)
}
return request
}
}
4. OpenAIService객체로 불러오기 및 응답 로직 분리
class OpenAIService {
func sendRequestToOpenAI(_ messages: [RequestMessageModel],
model: GPTModel,
APIkey: String,
completion: @escaping (Result<[RequestMessageModel],Error>) -> Void) {
let reqeustBuilder = URLRequestBuilder()
guard let url = OpenAIEndPoint.chatCompletionsBaseURL.url else {
completion(.failure(NetworkError.urlError))
return
}
do {
let request = try reqeustBuilder.makeRequest(url: url, for: model, APIKey: APIkey, withMessages: messages)
performReqeust(request, completion: completion)
} catch {
completion(.failure(NetworkError.connectionError))
}
}
private func performReqeust(_ request: URLRequest, completion: @escaping (Result<[RequestMessageModel], Error>) -> Void) {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.dataNotFound))
return
}
do {
let responseDTO = try JSONDecoder().decode(OpenAICheatResponseDTO.self, from: data)
let messages = responseDTO.choices.compactMap { choice -> RequestMessageModel? in
guard let messageContent = choice.message?.content,
let messageRole = choice.message?.role,
let role = MessageRole(rawValue: messageRole) else { return nil }
return RequestMessageModel(role: role, content: messageContent)
}
completion(.success(messages))
} catch {
completion(.failure(NetworkError.connectionError))
}
}
task.resume()
print(task)
}
}
끝인줄 알았는데 ..... 에러가 뜬다
이럴떄는 방법이 있는데
내가 보낸 요청들을 하나씩 체크를 해보는 방법이 가장 확실하다고 생각한다.
리팩토링 코드 디버깅
1. HTTP Status Code 확인
HTTP StatusCode를 확인해보기 위해
아래와 같은 코드를 추가해줬다.
결과는 401... 즉 요청형식이 잘못되었다는 응답을 받았고
//class OpenAIService
private func performReqeust(_ request: URLRequest, completion: @escaping (Result<[RequestMessageModel], Error>) -> Void) {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.dataNotFound))
return
}
//HTTP status 확인을 위한 로직 추가
if let httpResponse = response as? HTTPURLResponse {
print("statusCode: \(httpResponse.statusCode)")
}
do {
let responseDTO = try JSONDecoder().decode(OpenAICheatResponseDTO.self, from: data)
let messages = responseDTO.choices.compactMap { choice -> RequestMessageModel? in
guard let messageContent = choice.message?.content,
let messageRole = choice.message?.role,
let role = MessageRole(rawValue: messageRole) else { return nil }
return RequestMessageModel(role: role, content: messageContent)
}
completion(.success(messages))
} catch {
completion(.failure(NetworkError.connectionError))
}
}
task.resume()
print(task)
}
2. Reqeust 구조 확인하기
URL을 만들어주는 Builder쪽에다가 확인 메서드를 추가해준다.
func makeRequest(url: URL?,
for model: GPTModel,
APIKey apiKey: String,
withMessages messages: [RequestMessageModel]) throws -> URLRequest {
guard let url = url else {
throw NSError(domain: "RequestURLError", code: 404, userInfo: nil)
}
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDTO = RequestDTO(model: model, messages: messages)
do {
let jsonData = try JSONEncoder().encode(requestDTO)
request.httpBody = jsonData
printRequestDetails(request) //여기서 확인
} catch {
throw NSError(domain: "JSONParsingError", code: 1001)
}
return request
}
//확인 메서드
private func printRequestDetails(_ request: URLRequest) {
print("URL: \(request.url?.absoluteString ?? "Invalid URL")")
print("HTTP Method: \(request.httpMethod ?? "No HTTP Method")")
print("Headers: \(request.allHTTPHeaderFields ?? [:])")
if let httpBody = request.httpBody, let jsonString = String(data: httpBody, encoding: .utf8) {
print("HTTP Body: \(jsonString)")
}
}
결과는 .....
URL: https://api.openai.com/v1/chat/completions
HTTP Method: POST
Headers: ["Authorization": "apikey", "Content-Type": "application/json"]
HTTP Body: {"model":"gpt-3.5-turbo","messages":[]}
message에 빈배열이 들어가있지 ?????????
3. 진짜로 디버깅을 해보자
MVVM 패턴을 사용한 프로젝트로, message는 유저 인터렉션을 전달해주는 ViewModel쪽 문제라고 판단을 했고
ViewModel쪽부터 디버깅에 들어갔다.
func processUserMessage(_ content: String) {
let userMessage = RequestMessageModel(role: .user, content: content)
apiService.sendRequestToOpenAI(messageRepository.messagesStorage, model: GPTModel.gpt3Turbo, APIkey: APIKeyManager.openAIAPIKey) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let receivedMessages):
receivedMessages.forEach { responseMessage in
self?.messageRepository.addMessage(responseMessage)
}
case .failure(let error):
self?.onError?("Error 발생: 인터넷 연결을 확인해주세요. \(error.localizedDescription)")
}
}
}
}
위 메서드는 VIew에서 흘러들어온 String값을 받아 Reqeust를 만들어주는 객체로 보내주는 역할을 하는데...
repository객체에 추가해주는 로직을 만들지 않아 빈배열로 만들어진거란 것을 알았다...
func processUserMessage(_ content: String) {
let userMessage = RequestMessageModel(role: .user, content: content)
messageRepository.addMessage(userMessage) //이놈이 문제였음
apiService.sendRequestToOpenAI(messageRepository.messagesStorage, model: GPTModel.gpt3Turbo, APIkey: APIKeyManager.openAIAPIKey) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let receivedMessages):
receivedMessages.forEach { responseMessage in
self?.messageRepository.addMessage(responseMessage)
}
case .failure(let error):
self?.onError?("Error 발생: 인터넷 연결을 확인해주세요. \(error.localizedDescription)")
}
}
}
}
이렇게 추가를 하니 StatusCode도 200 ok가 잘 뜨고
배열에도 추가되고 응답도 받는 것을 확인 할 수 있었다
리팩토링 완료 !!
'🍎swift' 카테고리의 다른 글
swift indicator 구현 (0) | 2024.04.15 |
---|---|
Swift 배열, 동시성 문제 (0) | 2024.04.04 |
TLI - Concurrency (0) | 2024.01.17 |
[Swift] Stroyboard Navigation Controller를 활용한 화면이동 (1) | 2023.12.21 |
제어 흐름 (Control Flow) (0) | 2023.11.21 |