Ver3.0 ๐ฑ ๋ชจ๋ฐ์ผ ๋คํธ์ํน์ ํต์ฌ! HTTP ํด๋ผ์ด์ธํธ Retrofit๊ณผ Alamofire ์๋ฒฝ ํ์ฉ ๊ฐ์ด๋

๐ฑ ๋ชจ๋ฐ์ผ ๋คํธ์ํน์ ํต์ฌ! HTTP ํด๋ผ์ด์ธํธ Retrofit๊ณผ Alamofire ์๋ฒฝ ํ์ฉ ๊ฐ์ด๋
์๋๋ก์ด๋์ iOS ๊ฐ๋ฐ์๋ฅผ ์ํ ์ค์ ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ง์คํฐํ๊ธฐ
๐ ์ ๋ชจ๋ฐ์ผ ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ํ์ํ ๊น?
์๋ ! ์ค๋์ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์์ ์ ๋ ๋นผ๋์ ์ ์๋ ๋คํธ์ํน์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ์์ฆ ์ฑ๋ค ๋ณด๋ฉด ๊ฑฐ์ ๋๋ถ๋ถ์ด ์๋ฒ์ ํต์ ํ์์? ๋ ์จ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋, ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋, ์์ ๋ฏธ๋์ด ํผ๋๋ฅผ ๋ถ๋ฌ์ค๋ ๋ฑ ๋ง์ด์ผ. ๐
๊ทผ๋ฐ ๋ง์ด์ง, ์ฒ์๋ถํฐ ๋คํธ์ํน ์ฝ๋๋ฅผ ์ง์ ์ง๋ ค๊ณ ํ๋ฉด... ์ ๋ง ๋จธ๋ฆฌ๊ฐ ์ํ์ง๋ค๊ณ ! HTTP ์์ฒญ ๋ง๋ค๊ณ , ์๋ต ํ์ฑํ๊ณ , ์๋ฌ ํธ๋ค๋งํ๊ณ , ์ค๋ ๋ ๊ด๋ฆฌํ๊ณ ... ์๊ฐ๋ง ํด๋ ๋ณต์กํ์ง? ๐ต
๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก **Retrofit**(์๋๋ก์ด๋)๊ณผ **Alamofire**(iOS)์ผ. ์ด ์น๊ตฌ๋ค์ ๋ณต์กํ ๋คํธ์ํน ์์ ์ ์์ฒญ๋๊ฒ ๊ฐ๋จํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ๋ง๋ฒ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด์ง. ๋ง์น ์ฌ๋ฅ๋ท์์ ํ์ํ ์ฌ๋ฅ์ ์ฝ๊ฒ ์ฐพ์ ์ ์๋ ๊ฒ์ฒ๋ผ, ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค๋ ๋คํธ์ํน์ ์ฝ๊ฒ ๋ง๋ค์ด์ค๋ค๊ณ ๋ณด๋ฉด ๋ผ!
โข Alamofire: iOS/macOS๋ฅผ ์ํ ์ฐ์ํ HTTP ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โข ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ชจ๋ ๋ณต์กํ ๋คํธ์ํน ์์ ์ ๋จ์ํํ๊ณ ์์ฐ์ฑ์ ๊ทน๋ํํจ
๐ค Retrofit: ์๋๋ก์ด๋์ ๋คํธ์ํน ๊ฐ์
๐ Retrofit์ด ๋ญ๊ธธ๋?
Retrofit์ Square์ฌ์์ ๋ง๋ ์๋๋ก์ด๋์ฉ HTTP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. 2013๋ ์ ์ฒ์ ๋์๋๋ฐ, ์ง๊ธ๊น์ง๋ ์๋๋ก์ด๋ ๊ฐ๋ฐ์๋ค ์ฌ์ด์์ ๊ฐ์ฅ ์ฌ๋๋ฐ๋ ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค ํ๋์ง. ๐
Retrofit์ ๊ฐ์ฅ ํฐ ํน์ง์ ํ์ ์ธ์ดํ(Type-safe)ํ๋ค๋ ๊ฑฐ์ผ. ์ฝ๊ฒ ๋งํ๋ฉด, ์ปดํ์ผ ํ์์ ์๋ฌ๋ฅผ ์ก์๋ผ ์ ์๋ค๋ ๋ป์ด์ง. ๋ฐํ์์ ํฐ์ง๋ ๊ฒ๋ณด๋ค ํจ์ฌ ์์ ํ์์?
2. ์๋ ์ง๋ ฌํ/์ญ์ง๋ ฌํ: JSON์ ๊ฐ์ฒด๋ก, ๊ฐ์ฒด๋ฅผ JSON์ผ๋ก ์๋ ๋ณํ
3. ๋ค์ํ ์ปจ๋ฒํฐ ์ง์: Gson, Moshi, Jackson ๋ฑ
4. RxJava, Coroutines ์ง์: ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ๋งค์ฐ ํธ๋ฆฌ
5. ์ธํฐ์ ํฐ ์ง์: ๋ก๊น , ์ธ์ฆ ํ ํฐ ์ถ๊ฐ ๋ฑ์ด ์ฌ์
๐ง Retrofit ์ค์ ํ๊ธฐ
์, ์ด์ ์ค์ ์ผ๋ก ๋ค์ด๊ฐ๋ณผ๊น? ๋จผ์ Retrofit์ ํ๋ก์ ํธ์ ์ถ๊ฐํด์ผ๊ฒ ์ง? Gradle ํ์ผ์ ๋ค์ ์์กด์ฑ์ ์ถ๊ฐํด์ค:
// build.gradle (Module)
dependencies {
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// Gson ์ปจ๋ฒํฐ (JSON ํ์ฑ์ฉ)
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// OkHttp (Retrofit์ด ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉ)
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
}
๊ทธ๋ฆฌ๊ณ ๋น์ฐํ ์ธํฐ๋ท ๊ถํ๋ ํ์ํ๊ฒ ์ง? AndroidManifest.xml์ ์ถ๊ฐํด์ค:
<uses-permission android:name="android.permission.INTERNET" />
๐ API ์ธํฐํ์ด์ค ์ ์ํ๊ธฐ
Retrofit์ ํต์ฌ์ API๋ฅผ ์ธํฐํ์ด์ค๋ก ์ ์ํ๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ API๊ฐ ์๋ค๊ณ ํด๋ณด์:
interface UserApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): Response<User>
@GET("users")
suspend fun getUserList(
@Query("page") page: Int,
@Query("per_page") perPage: Int
): Response<List<User>>
@POST("users")
suspend fun createUser(@Body user: User): Response<User>
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") userId: Int,
@Body user: User
): Response<User>
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") userId: Int): Response<Unit>
}
๋ณด์ด์ง? ์ด๋ ธํ ์ด์ ์ผ๋ก HTTP ๋ฉ์๋๋ฅผ ์ง์ ํ๊ณ , ํ๋ผ๋ฏธํฐ๋ก ๊ฒฝ๋ก ๋ณ์๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ ์ ์์ด. suspend ํค์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝํ๋ฆฐ ์ฝ๋ฃจํด๊ณผ ํจ๊ป ์ฌ์ฉํ ์ ์์ง! ๐
@Path: URL ๊ฒฝ๋ก์ ๋ณ์ ์ฝ์
@Query: ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
@Body: ์์ฒญ ๋ณธ๋ฌธ์ ๊ฐ์ฒด ์ ๋ฌ
@Header: HTTP ํค๋ ์ถ๊ฐ
@FormUrlEncoded: Form ๋ฐ์ดํฐ ์ ์ก
@Multipart: ํ์ผ ์ ๋ก๋
๐๏ธ Retrofit ์ธ์คํด์ค ์์ฑํ๊ธฐ
์ด์ Retrofit ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ผ ํด. ๋ณดํต ์ฑ๊ธํค ํจํด์ผ๋ก ๋ง๋ค์ด์ ์ฑ ์ ์ฒด์์ ์ฌ์ฌ์ฉํ์ง:
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
val userApiService: UserApiService = retrofit.create(UserApiService::class.java)
}
์ฌ๊ธฐ์ ์ค์ํ ํฌ์ธํธ๋ค์ ์ง์ด๋ณผ๊ฒ:
1. HttpLoggingInterceptor: ๋คํธ์ํฌ ์์ฒญ๊ณผ ์๋ต์ ๋ก๊ทธ๋ก ์ฐ์ด์ค. ๋๋ฒ๊น ํ ๋ ์์ฒญ ์ ์ฉํ์ง! ๐
2. OkHttpClient: ํ์์์ ์ค์ , ์ธํฐ์ ํฐ ์ถ๊ฐ ๋ฑ ์ธ๋ฐํ ์ค์ ์ด ๊ฐ๋ฅํด.
3. GsonConverterFactory: JSON์ ์๋์ผ๋ก ๊ฐ์ฒด๋ก ๋ณํํด์ค. Gson ๋ง๊ณ Moshi๋ Jackson๋ ์ฌ์ฉํ ์ ์์ด.
๐ป ์ค์ ์ฌ์ฉ ์์
์, ์ด์ ์ค์ ๋ก API๋ฅผ ํธ์ถํด๋ณด์! ViewModel์์ ์ฝ๋ฃจํด๊ณผ ํจ๊ป ์ฌ์ฉํ๋ ์์ ์ผ:
class UserViewModel : ViewModel() {
private val _userState = MutableLiveData<UiState<User>>()
val userState: LiveData<UiState<User>> = _userState
fun fetchUser(userId: Int) {
viewModelScope.launch {
_userState.value = UiState.Loading
try {
val response = RetrofitClient.userApiService.getUser(userId)
if (response.isSuccessful) {
response.body()?.let { user ->
_userState.value = UiState.Success(user)
} ?: run {
_userState.value = UiState.Error("์๋ต ๋ณธ๋ฌธ์ด ๋น์ด์์ต๋๋ค")
}
} else {
_userState.value = UiState.Error("์๋ฌ ์ฝ๋: ${response.code()}")
}
} catch (e: Exception) {
_userState.value = UiState.Error(e.message ?: "์ ์ ์๋ ์ค๋ฅ")
}
}
}
fun createUser(name: String, email: String) {
viewModelScope.launch {
try {
val newUser = User(name = name, email = email)
val response = RetrofitClient.userApiService.createUser(newUser)
if (response.isSuccessful) {
// ์ฑ๊ณต ์ฒ๋ฆฌ
Log.d("UserViewModel", "์ฌ์ฉ์ ์์ฑ ์ฑ๊ณต: ${response.body()}")
}
} catch (e: Exception) {
Log.e("UserViewModel", "์ฌ์ฉ์ ์์ฑ ์คํจ", e)
}
}
}
}
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
์ด ์ฝ๋์์ ์ฃผ๋ชฉํ ์ ์:
โข viewModelScope.launch: ์ฝ๋ฃจํด์ ViewModel์ ์๋ช
์ฃผ๊ธฐ์ ๋ง์ถฐ ์คํ
โข response.isSuccessful: HTTP ์ํ ์ฝ๋๊ฐ 200~299์ธ์ง ํ์ธ
โข try-catch: ๋คํธ์ํฌ ์ค๋ฅ๋ ์์ธ ์ฒ๋ฆฌ
โข Sealed Class: UI ์ํ๋ฅผ ํ์
์ธ์ดํํ๊ฒ ๊ด๋ฆฌ
๐ Alamofire: iOS์ ์ฐ์ํ ๋คํธ์ํน ์๋ฃจ์
๐ Alamofire ์๊ฐ
์ด์ iOS ์ง์์ผ๋ก ๋์ด๊ฐ๋ณผ๊น? Alamofire๋ Swift๋ก ์์ฑ๋ HTTP ๋คํธ์ํน ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ. ์ด๋ฆ๋ถํฐ ๋ฉ์ง์ง ์์? ๐ฅ
Alamofire๋ Retrofit์ฒ๋ผ ๋ณต์กํ ๋คํธ์ํน ์์ ์ ๊ฐ๋จํ๊ฒ ๋ง๋ค์ด์ฃผ๋๋ฐ, Swift์ ๊ฐ๋ ฅํ ํ์ ์์คํ ๊ณผ ํด๋ก์ ๋ฅผ ํ์ฉํด์ ๋์ฑ ์ฐ์ํ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ด.
2. URL/JSON ํ๋ผ๋ฏธํฐ ์ธ์ฝ๋ฉ
3. ํ์ผ ์ ๋ก๋/๋ค์ด๋ก๋
4. ์ธ์ฆ ์ฒ๋ฆฌ
5. ๋คํธ์ํฌ ๋๋ฌ์ฑ ์ฒดํฌ
6. Combine ํ๋ ์์ํฌ ์ง์
7. ์๋ ์ฌ์๋ ๋ฐ ์ ์ํ ์์ฒญ
๐ฆ Alamofire ์ค์นํ๊ธฐ
Alamofire๋ฅผ ์ค์นํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ด. ๊ฐ์ฅ ๋ง์ด ์ฐ๋ ๋ฐฉ๋ฒ๋ค์ ์๊ฐํ ๊ฒ:
1. CocoaPods ์ฌ์ฉ:
# Podfile
platform :ios, '13.0'
use_frameworks!
target 'YourApp' do
pod 'Alamofire', '~> 5.8'
end
2. Swift Package Manager ์ฌ์ฉ:
Xcode์์ File โ Add Packages... โ ๋ค์ URL ์
๋ ฅ:
https://github.com/Alamofire/Alamofire.git
3. Carthage ์ฌ์ฉ:
github "Alamofire/Alamofire" ~> 5.8
๊ฐ์ธ์ ์ผ๋ก๋ Swift Package Manager๋ฅผ ์ถ์ฒํด! Xcode์ ๋ด์ฅ๋์ด ์์ด์ ๋ณ๋ ์ค์น๊ฐ ํ์ ์๊ฑฐ๋ . ๐
๐ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
Alamofire์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ๋ฒ๋ถํฐ ๋ณผ๊น? GET ์์ฒญ์ ๋ณด๋ด๋ ์์ ์ผ:
import Alamofire
// ๊ฐ๋จํ GET ์์ฒญ
AF.request("https://api.example.com/users")
.responseJSON { response in
switch response.result {
case .success(let value):
print("์๋ต ์ฑ๊ณต: \(value)")
case .failure(let error):
print("์๋ฌ ๋ฐ์: \(error)")
}
}
// Decodable์ ์ฌ์ฉํ ์๋ ํ์ฑ
struct User: Decodable {
let id: Int
let name: String
let email: String
}
AF.request("https://api.example.com/users/1")
.responseDecodable(of: User.self) { response in
switch response.result {
case .success(let user):
print("์ฌ์ฉ์ ์ด๋ฆ: \(user.name)")
case .failure(let error):
print("ํ์ฑ ์๋ฌ: \(error)")
}
}
๋ณด์ด์ง? responseDecodable์ ์ฌ์ฉํ๋ฉด JSON์ ์๋์ผ๋ก Swift ๊ฐ์ฒด๋ก ๋ณํํด์ค! Codable ํ๋กํ ์ฝ์ ๋ง๋ฒ์ด์ง. โจ
๐ง ๋ค์ํ HTTP ๋ฉ์๋ ์ฌ์ฉํ๊ธฐ
GET๋ง ์๋ ๊ฒ ์๋์ง! POST, PUT, DELETE ๋ฑ ๋ค์ํ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์์ด:
// POST ์์ฒญ
let parameters: [String: Any] = [
"name": "ํ๊ธธ๋",
"email": "hong@example.com",
"age": 25
]
AF.request("https://api.example.com/users",
method: .post,
parameters: parameters,
encoding: JSONEncoding.default)
.responseDecodable(of: User.self) { response in
// ์๋ต ์ฒ๋ฆฌ
}
// PUT ์์ฒญ
AF.request("https://api.example.com/users/1",
method: .put,
parameters: parameters,
encoding: JSONEncoding.default)
.response { response in
// ์๋ต ์ฒ๋ฆฌ
}
// DELETE ์์ฒญ
AF.request("https://api.example.com/users/1",
method: .delete)
.response { response in
if response.response?.statusCode == 204 {
print("์ญ์ ์ฑ๊ณต!")
}
}
๐ฏ ํค๋์ ์ธ์ฆ ์ฒ๋ฆฌ
API ํธ์ถํ ๋ ์ธ์ฆ ํ ํฐ์ด๋ ์ปค์คํ ํค๋๋ฅผ ์ถ๊ฐํด์ผ ํ ๋๊ฐ ๋ง์ง? Alamofire์์๋ ์ด๋ ๊ฒ ํด:
// ํค๋ ์ถ๊ฐ
let headers: HTTPHeaders = [
"Authorization": "Bearer your_token_here",
"Content-Type": "application/json",
"Accept": "application/json"
]
AF.request("https://api.example.com/protected",
headers: headers)
.responseJSON { response in
// ์๋ต ์ฒ๋ฆฌ
}
// Interceptor๋ฅผ ์ฌ์ฉํ ์๋ ํ ํฐ ์ถ๊ฐ
class AuthenticationInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest,
for session: Session,
completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ (์: Keychain์์)
if let token = getAccessToken() {
urlRequest.setValue("Bearer \(token)",
forHTTPHeaderField: "Authorization")
}
completion(.success(urlRequest))
}
func retry(_ request: Request,
for session: Session,
dueTo error: Error,
completion: @escaping (RetryResult) -> Void) {
// 401 ์๋ฌ๋ฉด ํ ํฐ ๊ฐฑ์ ํ ์ฌ์๋
if let response = request.task?.response as? HTTPURLResponse,
response.statusCode == 401 {
refreshToken { success in
completion(success ? .retry : .doNotRetry)
}
} else {
completion(.doNotRetry)
}
}
private func getAccessToken() -> String? {
// ์ค์ ๋ก๋ Keychain์ด๋ UserDefaults์์ ๊ฐ์ ธ์ด
return "your_access_token"
}
private func refreshToken(completion: @escaping (Bool) -> Void) {
// ํ ํฐ ๊ฐฑ์ ๋ก์ง
completion(true)
}
}
// Session์ Interceptor ์ ์ฉ
let interceptor = AuthenticationInterceptor()
let session = Session(interceptor: interceptor)
session.request("https://api.example.com/protected")
.responseJSON { response in
// ์๋์ผ๋ก ํ ํฐ์ด ์ถ๊ฐ๋๊ณ , 401 ์๋ฌ ์ ์ฌ์๋๋จ
}
์ด ์ฝ๋๋ ์ข ๋ณต์กํด ๋ณด์ด์ง๋ง, ํ ๋ฒ ์ค์ ํด๋๋ฉด ๋ชจ๋ ์์ฒญ์ ์๋์ผ๋ก ํ ํฐ์ด ์ถ๊ฐ๋๊ณ , ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด ์๋์ผ๋ก ๊ฐฑ์ ํด์ค. ์์ ํธ๋ฆฌํ์ง? ๐
๐ ๋น๋๊ธฐ ์ฒ๋ฆฌ: Coroutines vs Async/Await
โก ์๋๋ก์ด๋: Kotlin Coroutines
๋คํธ์ํน์ ๋ณธ์ง์ ์ผ๋ก ๋น๋๊ธฐ ์์ ์ด์ผ. UI ์ค๋ ๋๋ฅผ ๋ธ๋กํนํ๋ฉด ์ฑ์ด ๋ฉ์ถฐ๋ฒ๋ฆฌ๋๊น ๋ง์ด์ง. ์๋๋ก์ด๋์์๋ Kotlin Coroutines๋ฅผ ์ฌ์ฉํด์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด!
class UserRepository(private val apiService: UserApiService) {
// Flow๋ฅผ ์ฌ์ฉํ ๋ฐ์ํ ๋ฐ์ดํฐ ์คํธ๋ฆผ
fun getUserFlow(userId: Int): Flow<Result<User>> = flow {
emit(Result.Loading)
try {
val response = apiService.getUser(userId)
if (response.isSuccessful) {
response.body()?.let {
emit(Result.Success(it))
} ?: emit(Result.Error("๋ฐ์ดํฐ ์์"))
} else {
emit(Result.Error("HTTP ${response.code()}"))
}
} catch (e: Exception) {
emit(Result.Error(e.message ?: "์ ์ ์๋ ์ค๋ฅ"))
}
}.flowOn(Dispatchers.IO)
// ์ฌ๋ฌ API๋ฅผ ๋ณ๋ ฌ๋ก ํธ์ถ
suspend fun getUserWithPosts(userId: Int): Pair<User?, List<Post>?> {
return coroutineScope {
val userDeferred = async { apiService.getUser(userId) }
val postsDeferred = async { apiService.getUserPosts(userId) }
val user = userDeferred.await().body()
val posts = postsDeferred.await().body()
Pair(user, posts)
}
}
// ์์ฐจ์ API ํธ์ถ
suspend fun createUserAndProfile(user: User, profile: Profile): Result<Boolean> {
return try {
// 1. ์ฌ์ฉ์ ์์ฑ
val userResponse = apiService.createUser(user)
if (!userResponse.isSuccessful) {
return Result.Error("์ฌ์ฉ์ ์์ฑ ์คํจ")
}
val createdUser = userResponse.body() ?: return Result.Error("์๋ต ์์")
// 2. ํ๋กํ ์์ฑ (์ฌ์ฉ์ ID ํ์)
val profileResponse = apiService.createProfile(createdUser.id, profile)
if (profileResponse.isSuccessful) {
Result.Success(true)
} else {
Result.Error("ํ๋กํ ์์ฑ ์คํจ")
}
} catch (e: Exception) {
Result.Error(e.message ?: "์ ์ ์๋ ์ค๋ฅ")
}
}
}
sealed class Result<out T> {
object Loading : Result<Nothing>()
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
์ฌ๊ธฐ์ ํต์ฌ ํฌ์ธํธ๋ค:
โข Flow: ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ํํ. LiveData๋ณด๋ค ๊ฐ๋ ฅํ๊ณ ์ ์ฐํด! ๐
โข async/await: ์ฌ๋ฌ API๋ฅผ ๋ณ๋ ฌ๋ก ํธ์ถํด์ ์ฑ๋ฅ ํฅ์
โข coroutineScope: ๊ตฌ์กฐํ๋ ๋์์ฑ์ผ๋ก ์์ ํ ๋น๋๊ธฐ ์ฒ๋ฆฌ
โข Dispatchers.IO: ๋คํธ์ํฌ ์์
์ ์ต์ ํ๋ ์ค๋ ๋ ํ
๐ iOS: Swift Concurrency (async/await)
iOS๋ Swift 5.5๋ถํฐ async/await๋ฅผ ์ง์ํด! Alamofire๋ ์ด๋ฅผ ์๋ฒฝํ๊ฒ ์ง์ํ์ง:
class UserService {
// async/await๋ฅผ ์ฌ์ฉํ ๊ฐ๋จํ ์์ฒญ
func fetchUser(id: Int) async throws -> User {
let response = await AF.request("https://api.example.com/users/\(id)")
.serializingDecodable(User.self)
.response
switch response.result {
case .success(let user):
return user
case .failure(let error):
throw error
}
}
// ์ฌ๋ฌ API๋ฅผ ๋ณ๋ ฌ๋ก ํธ์ถ
func fetchUserWithPosts(id: Int) async throws -> (User, [Post]) {
async let user = fetchUser(id: id)
async let posts = fetchPosts(userId: id)
return try await (user, posts)
}
// Task Group์ ์ฌ์ฉํ ๋ค์ค ์์ฒญ
func fetchMultipleUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await self.fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
private func fetchPosts(userId: Int) async throws -> [Post] {
let response = await AF.request("https://api.example.com/users/\(userId)/posts")
.serializingDecodable([Post].self)
.response
switch response.result {
case .success(let posts):
return posts
case .failure(let error):
throw error
}
}
}
// SwiftUI์์ ์ฌ์ฉ
struct UserView: View {
@State private var user: User?
@State private var isLoading = false
@State private var errorMessage: String?
let userService = UserService()
var body: some View {
VStack {
if isLoading {
ProgressView()
} else if let user = user {
Text(user.name)
Text(user.email)
} else if let error = errorMessage {
Text("์๋ฌ: \(error)")
}
}
.task {
await loadUser()
}
}
func loadUser() async {
isLoading = true
defer { isLoading = false }
do {
user = try await userService.fetchUser(id: 1)
} catch {
errorMessage = error.localizedDescription
}
}
}
Swift์ async/await๋ ์ ๋ง ๊น๋ํ์ง? ์ฝ๋๊ฐ ๋ง์น ๋๊ธฐ ์ฝ๋์ฒ๋ผ ์ฝํ์ ์ดํดํ๊ธฐ ์ฌ์! ๐
๐ก๏ธ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ ์ ๋ต
๋คํธ์ํน์์ ์๋ฌ๋ ํผํ ์ ์์ด. ๋คํธ์ํฌ๊ฐ ๋๊ธฐ๊ฑฐ๋, ์๋ฒ๊ฐ ๋ค์ด๋๊ฑฐ๋, ํ์์์์ด ๋ฐ์ํ๊ฑฐ๋... ์ด๋ฐ ์ํฉ๋ค์ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ๋ ๊ฒ ์ค์ํ์ง! ๐ฏ
๐ง Retrofit ์๋ฌ ์ฒ๋ฆฌ
sealed class NetworkResult<T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error<T>(val code: Int, val message: String) : NetworkResult<T>()
data class Exception<T>(val e: Throwable) : NetworkResult<T>()
}
suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResult<T> {
return try {
val response = apiCall()
if (response.isSuccessful) {
response.body()?.let {
NetworkResult.Success(it)
} ?: NetworkResult.Error(response.code(), "์๋ต ๋ณธ๋ฌธ์ด ๋น์ด์์ต๋๋ค")
} else {
val errorBody = response.errorBody()?.string()
NetworkResult.Error(response.code(), errorBody ?: "์ ์ ์๋ ์ค๋ฅ")
}
} catch (e: IOException) {
NetworkResult.Exception(e)
} catch (e: HttpException) {
NetworkResult.Exception(e)
} catch (e: Exception) {
NetworkResult.Exception(e)
}
}
// ์ฌ์๋ ๋ก์ง
suspend fun <T> retryApiCall(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> NetworkResult<T>
): NetworkResult<T> {
var currentDelay = initialDelay
repeat(times - 1) {
when (val result = block()) {
is NetworkResult.Success -> return result
is NetworkResult.Error -> {
// 4xx ์๋ฌ๋ ์ฌ์๋ํ์ง ์์
if (result.code in 400..499) {
return result
}
}
is NetworkResult.Exception -> {
// ๋คํธ์ํฌ ์๋ฌ๋ง ์ฌ์๋
}
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block() // ๋ง์ง๋ง ์๋
}
// ์ฌ์ฉ ์์
class UserRepository(private val apiService: UserApiService) {
suspend fun getUser(id: Int): NetworkResult<User> {
return retryApiCall {
safeApiCall { apiService.getUser(id) }
}
}
}
โข ๋ฌดํ ์ฌ์๋๋ ์ ๋ ๊ธ๋ฌผ! ์ต๋ ํ์๋ฅผ ์ ํด์ผ ํด
โข Exponential Backoff๋ฅผ ์ฌ์ฉํด์ ์๋ฒ ๋ถํ๋ฅผ ์ค์ฌ์ผ ํด
โข 4xx ํด๋ผ์ด์ธํธ ์๋ฌ๋ ์ฌ์๋ํด๋ ์์ฉ์์ด
๐ Alamofire ์๋ฌ ์ฒ๋ฆฌ
enum NetworkError: Error {
case invalidURL
case noData
case decodingError
case serverError(statusCode: Int)
case networkError(Error)
var localizedDescription: String {
switch self {
case .invalidURL:
return "์๋ชป๋ URL์
๋๋ค"
case .noData:
return "๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค"
case .decodingError:
return "๋ฐ์ดํฐ ํ์ฑ์ ์คํจํ์ต๋๋ค"
case .serverError(let code):
return "์๋ฒ ์๋ฌ (์ฝ๋: \(code))"
case .networkError(let error):
return "๋คํธ์ํฌ ์๋ฌ: \(error.localizedDescription)"
}
}
}
class NetworkService {
func request<T: Decodable>(
_ url: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil
) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
AF.request(url, method: method, parameters: parameters)
.validate(statusCode: 200..<300)
.responseDecodable(of: T.self) { response in
switch response.result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
if let statusCode = response.response?.statusCode {
continuation.resume(
throwing: NetworkError.serverError(statusCode: statusCode)
)
} else if let underlyingError = error.underlyingError {
continuation.resume(
throwing: NetworkError.networkError(underlyingError)
)
} else {
continuation.resume(throwing: NetworkError.decodingError)
}
}
}
}
}
// ์ฌ์๋ ๋ก์ง
func requestWithRetry<T: Decodable>(
_ url: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
maxRetries: Int = 3
) async throws -> T {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
let result: T = try await request(url, method: method, parameters: parameters)
return result
} catch let error as NetworkError {
lastError = error
// 4xx ์๋ฌ๋ ์ฌ์๋ํ์ง ์์
if case .serverError(let code) = error, (400...499).contains(code) {
throw error
}
// ๋ง์ง๋ง ์๋๊ฐ ์๋๋ฉด ๋๊ธฐ
if attempt < maxRetries - 1 {
let delay = pow(2.0, Double(attempt)) // Exponential backoff
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
} catch {
lastError = error
}
}
throw lastError ?? NetworkError.networkError(NSError(domain: "", code: -1))
}
}
// ์ฌ์ฉ ์์
struct ContentView: View {
@State private var users: [User] = []
@State private var errorMessage: String?
let networkService = NetworkService()
var body: some View {
List(users) { user in
Text(user.name)
}
.task {
do {
users = try await networkService.requestWithRetry(
"https://api.example.com/users"
)
} catch let error as NetworkError {
errorMessage = error.localizedDescription
} catch {
errorMessage = "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค"
}
}
.alert("์๋ฌ", isPresented: .constant(errorMessage != nil)) {
Button("ํ์ธ") {
errorMessage = nil
}
} message: {
Text(errorMessage ?? "")
}
}
}
๐ ์ฑ๋ฅ ์ต์ ํ ํ
๋คํธ์ํน ์ฑ๋ฅ์ ์ต์ ํํ๋ฉด ์ฑ์ด ํจ์ฌ ๋นจ๋ผ์ง๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ด ์ข์์ ธ! ๋ช ๊ฐ์ง ์ค์ฉ์ ์ธ ํ๋ค์ ๊ณต์ ํ ๊ฒ. ๐
1๏ธโฃ ์๋ต ์บ์ฑ
๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ณตํด์ ์์ฒญํ๋ ๊ฑด ๋ญ๋น์ผ. ์บ์ฑ์ ํ์ฉํ๋ฉด ๋คํธ์ํฌ ํธ๋ํฝ์ ์ค์ด๊ณ ์๋ต ์๋๋ฅผ ๋์ผ ์ ์์ด!
// Retrofit - OkHttp ์บ์ ์ค์
val cacheSize = 10 * 1024 * 1024 // 10 MB
val cache = Cache(context.cacheDir, cacheSize.toLong())
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
var request = chain.request()
// ๋คํธ์ํฌ๊ฐ ์์ผ๋ฉด ์บ์๋ฅผ 5๋ถ๊ฐ ์ ์ง
// ์์ผ๋ฉด 7์ผ๊ฐ ์ ์ง
request = if (hasNetwork(context)) {
request.newBuilder()
.header("Cache-Control", "public, max-age=" + 300)
.build()
} else {
request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7)
.build()
}
chain.proceed(request)
}
.build()
// Alamofire - URLCache ์ค์
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
configuration.urlCache = URLCache(
memoryCapacity: 10 * 1024 * 1024, // 10 MB
diskCapacity: 50 * 1024 * 1024, // 50 MB
diskPath: "alamofire_cache"
)
let session = Session(configuration: configuration)
2๏ธโฃ ์์ฒญ ์์ถ
๋ฐ์ดํฐ๋ฅผ ์์ถํ๋ฉด ์ ์ก ์๊ฐ์ด ์ค์ด๋ค์ด! ํนํ ์ด๋ฏธ์ง๋ ํฐ JSON ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋ ํจ๊ณผ์ ์ด์ผ.
// OkHttp๋ ์๋์ผ๋ก gzip ์์ถ์ ์ง์ํด
// Accept-Encoding: gzip ํค๋๋ฅผ ์๋์ผ๋ก ์ถ๊ฐํจ
// ์๋์ผ๋ก ์์ถ ์ธํฐ์
ํฐ ์ถ๊ฐ
class GzipRequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (originalRequest.body == null ||
originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest)
}
val compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method, gzip(originalRequest.body!!))
.build()
return chain.proceed(compressedRequest)
}
private fun gzip(body: RequestBody): RequestBody {
return object : RequestBody() {
override fun contentType() = body.contentType()
override fun contentLength() = -1L
override fun writeTo(sink: BufferedSink) {
val gzipSink = GzipSink(sink).buffer()
body.writeTo(gzipSink)
gzipSink.close()
}
}
}
}
3๏ธโฃ ๋ฐฐ์น ์์ฒญ
์ฌ๋ฌ ๊ฐ์ ์์ ์์ฒญ๋ณด๋ค ํ๋์ ํฐ ์์ฒญ์ด ๋ ํจ์จ์ ์ด์ผ. API ์ค๊ณ ์ ๋ฐฐ์น ์๋ํฌ์ธํธ๋ฅผ ๊ณ ๋ คํด๋ด!
// ๋์ ์: ์ฌ๋ฌ ๋ฒ ์์ฒญ
for (id in userIds) {
val user = apiService.getUser(id)
// ์ฒ๋ฆฌ...
}
// ์ข์ ์: ํ ๋ฒ์ ์์ฒญ
val users = apiService.getUsers(userIds)
// API ์ธํฐํ์ด์ค
interface UserApiService {
@GET("users/batch")
suspend fun getUsers(@Query("ids") ids: List<Int>): Response<List<User>>
}
4๏ธโฃ ํ์ด์ง๋ค์ด์
ํฐ ๋ฐ์ดํฐ์ ์ ํ ๋ฒ์ ๋ก๋ํ์ง ๋ง๊ณ , ํ์ด์ง ๋จ์๋ก ๋๋ ์ ๋ก๋ํด! ์ฌ์ฉ์๋ ์ฒ์ ๋ช ๊ฐ๋ง ๋ณด๋ ๊ฒฝ์ฐ๊ฐ ๋ง๊ฑฐ๋ .
// Retrofit ํ์ด์ง๋ค์ด์
interface PostApiService {
@GET("posts")
suspend fun getPosts(
@Query("page") page: Int,
@Query("limit") limit: Int = 20
): Response<PostResponse>
}
data class PostResponse(
val posts: List<Post>,
val totalPages: Int,
val currentPage: Int
)
// ViewModel์์ ์ฌ์ฉ
class PostViewModel : ViewModel() {
private var currentPage = 1
private val _posts = MutableLiveData<List<Post>>()
val posts: LiveData<List<Post>> = _posts
fun loadMorePosts() {
viewModelScope.launch {
val response = apiService.getPosts(currentPage)
if (response.isSuccessful) {
response.body()?.let { postResponse ->
val currentList = _posts.value.orEmpty()
_posts.value = currentList + postResponse.posts
currentPage++
}
}
}
}
}
- โ ์บ์ฑ ์ ๋ต ์๋ฆฝ
- โ ์ด๋ฏธ์ง ์ต์ ํ (WebP, ์ ์ ํ ํด์๋)
- โ ๋ถํ์ํ ๋ฐ์ดํฐ ํ๋ ์ ๊ฑฐ
- โ ์ฐ๊ฒฐ ํ๋ง ํ์ฉ
- โ HTTP/2 ์ฌ์ฉ (OkHttp๋ ์๋ ์ง์)
- โ ํ์์์ ์ ์ ํ ์ค์
- โ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ํ๋ฆฌํ์นญ
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋คํธ์ํน์์ ๋ณด์์ ์ ๋ง์ ๋ง ์ค์ํด! ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํ๋ ๊ฑด ๊ฐ๋ฐ์์ ์ฑ ์์ด๋๊น. ๐ก๏ธ
1๏ธโฃ HTTPS ์ฌ์ฉ ํ์
HTTP๋ ์ ๋ ์ ๋ผ! ๋ชจ๋ ํต์ ์ HTTPS๋ก ์ํธํํด์ผ ํด. ์๋๋ก์ด๋ 9(API 28)๋ถํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก HTTP ์ฐ๊ฒฐ์ด ์ฐจ๋จ๋๊ฑฐ๋ .
// network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
// AndroidManifest.xml
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
2๏ธโฃ SSL Pinning
์ค๊ฐ์ ๊ณต๊ฒฉ(MITM)์ ๋ฐฉ์งํ๋ ค๋ฉด SSL Pinning์ ๊ตฌํํด์ผ ํด. ์๋ฒ์ ์ธ์ฆ์๋ ๊ณต๊ฐ ํค๋ฅผ ์ฑ์ ๊ณ ์ ์ํค๋ ๊ฑฐ์ง.
// OkHttp SSL Pinning
val hostname = "api.example.com"
val certificatePinner = CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add(hostname, "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
// Alamofire SSL Pinning
let evaluators = [
"api.example.com": PinnedCertificatesTrustEvaluator()
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)
let session = Session(serverTrustManager: serverTrustManager)
3๏ธโฃ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ๋ณดํธ
API ํค๋ ํ ํฐ์ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ฉด ์ ๋ ์ ๋ผ! ์์ ํ๊ฒ ์ ์ฅํ๊ณ ๊ด๋ฆฌํด์ผ ํด.
// ์๋๋ก์ด๋ - Keystore ์ฌ์ฉ
class SecureStorage(private val context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
fun saveToken(token: String) {
val cipher = getCipher()
val encryptedToken = cipher.doFinal(token.toByteArray())
context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
.edit()
.putString("token", Base64.encodeToString(encryptedToken, Base64.DEFAULT))
.apply()
}
fun getToken(): String? {
val encryptedToken = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
.getString("token", null) ?: return null
val cipher = getCipher()
val decryptedToken = cipher.doFinal(Base64.decode(encryptedToken, Base64.DEFAULT))
return String(decryptedToken)
}
private fun getCipher(): Cipher {
// Cipher ์ด๊ธฐํ ๋ก์ง
return Cipher.getInstance("AES/GCM/NoPadding")
}
}
// iOS - Keychain ์ฌ์ฉ
import Security
class KeychainManager {
func save(token: String, for key: String) -> Bool {
guard let data = token.data(using: .utf8) else { return false }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
func get(for key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}
return token
}
}
โข ๋ก๊ทธ์ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ์ถ๋ ฅํ์ง ๋ง ๊ฒ
โข ๋ฃจํ /ํ์ฅ๋ ๊ธฐ๊ธฐ ๊ฐ์ง ๊ณ ๋ ค
โข ์ ๊ธฐ์ ์ผ๋ก ํ ํฐ ๊ฐฑ์
โข ์ต์ ๊ถํ ์์น ์ ์ฉ
๐งช ํ ์คํธ์ ๋๋ฒ๊น
๋คํธ์ํน ์ฝ๋๋ฅผ ํ ์คํธํ๋ ๊ฑด ์ข ๊น๋ค๋ก์. ์ค์ ์๋ฒ์ ์์กดํ๋ฉด ํ ์คํธ๊ฐ ๋ถ์์ ํด์ง๊ฑฐ๋ . ๊ทธ๋์ Mock์ ์ฌ์ฉํด์ผ ํด! ๐ญ
๐ค ์๋๋ก์ด๋ ํ ์คํธ
// MockWebServer๋ฅผ ์ฌ์ฉํ ํ
์คํธ
class UserRepositoryTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var apiService: UserApiService
private lateinit var repository: UserRepository
@Before
fun setup() {
mockWebServer = MockWebServer()
mockWebServer.start()
val retrofit = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.build()
apiService = retrofit.create(UserApiService::class.java)
repository = UserRepository(apiService)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun `์ฌ์ฉ์ ์กฐํ ์ฑ๊ณต ํ
์คํธ`() = runTest {
// Given
val mockResponse = """
{
"id": 1,
"name": "ํ๊ธธ๋",
"email": "hong@example.com"
}
""".trimIndent()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(mockResponse)
)
// When
val result = repository.getUser(1)
// Then
assertTrue(result is NetworkResult.Success)
val user = (result as NetworkResult.Success).data
assertEquals("ํ๊ธธ๋", user.name)
assertEquals("hong@example.com", user.email)
}
@Test
fun `๋คํธ์ํฌ ์๋ฌ ํ
์คํธ`() = runTest {
// Given
mockWebServer.enqueue(
MockResponse()
.setResponseCode(500)
.setBody("Internal Server Error")
)
// When
val result = repository.getUser(1)
// Then
assertTrue(result is NetworkResult.Error)
val error = result as NetworkResult.Error
assertEquals(500, error.code)
}
}
// ์์กด์ฑ
testImplementation 'com.squareup.okhttp3:mockwebserver:4.11.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
๐ iOS ํ ์คํธ
import XCTest
@testable import YourApp
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockSession: URLSession!
override func setUp() {
super.setUp()
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [MockURLProtocol.self]
mockSession = URLSession(configuration: configuration)
sut = UserService(session: mockSession)
}
override func tearDown() {
sut = nil
mockSession = nil
super.tearDown()
}
func testFetchUserSuccess() async throws {
// Given
let mockUser = User(id: 1, name: "ํ๊ธธ๋", email: "hong@example.com")
let mockData = try JSONEncoder().encode(mockUser)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
return (response, mockData)
}
// When
let user = try await sut.fetchUser(id: 1)
// Then
XCTAssertEqual(user.name, "ํ๊ธธ๋")
XCTAssertEqual(user.email, "hong@example.com")
}
func testFetchUserFailure() async {
// Given
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(
url: request.url!,
statusCode: 404,
httpVersion: nil,
headerFields: nil
)!
return (response, Data())
}
// When & Then
do {
_ = try await sut.fetchUser(id: 999)
XCTFail("์๋ฌ๊ฐ ๋ฐ์ํด์ผ ํจ")
} catch {
XCTAssertNotNil(error)
}
}
}
// Mock URLProtocol
class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
fatalError("Handler is unavailable")
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {
// ํ์์ ๊ตฌํ
}
}
๐ ๋๋ฒ๊น ๋๊ตฌ
๋คํธ์ํน ๋ฌธ์ ๋ฅผ ๋๋ฒ๊น ํ ๋ ์ ์ฉํ ๋๊ตฌ๋ค์ด ์์ด:
| ๋๊ตฌ | ํ๋ซํผ | ์ค๋ช |
|---|---|---|
| Charles Proxy | Both | HTTP/HTTPS ํธ๋ํฝ์ ๊ฐ๋ก์ฑ์ ๋ถ์ |
| Postman | Both | API ํ ์คํธ ๋ฐ ๋ฌธ์ํ |
| Android Profiler | Android | ๋คํธ์ํฌ ํ๋ ๋ชจ๋ํฐ๋ง |
| Network Link Conditioner | iOS | ๋ค์ํ ๋คํธ์ํฌ ํ๊ฒฝ ์๋ฎฌ๋ ์ด์ |
| Flipper | Both | Facebook์ ๋ชจ๋ฐ์ผ ๋๋ฒ๊น ํ๋ซํผ |
โข ๋๋ฆฐ ๋คํธ์ํฌ ํ๊ฒฝ์์ ํ ์คํธ (3G, Edge ๋ฑ)
โข ํ์์์ ์๋๋ฆฌ์ค ํ ์คํธ
โข ์๋ฒ ์๋ฌ ์๋ต ์๋ฎฌ๋ ์ด์
โข ๋ฉ๋ชจ๋ฆฌ ๋์ ์ฒดํฌ (ํนํ ์ฝ๋ฐฑ ์ฌ์ฉ ์)
๐จ ์ค์ ํ๋ก์ ํธ ์์
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์ง! ์ค์ ๋ก ๋์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณด์. ๊ฐ๋จํ ๋ ์จ ์ฑ์ ์์ ๋ก ๋ค๊ฒ. โ๏ธ๐ง๏ธ
๐ค๏ธ ๋ ์จ ์ฑ - ์๋๋ก์ด๋ ๋ฒ์
// ๋ฐ์ดํฐ ๋ชจ๋ธ
data class Weather(
val temperature: Double,
val description: String,
val humidity: Int,
val windSpeed: Double,
val cityName: String
)
data class WeatherResponse(
val main: Main,
val weather: List<WeatherInfo>,
val wind: Wind,
val name: String
) {
data class Main(
val temp: Double,
val humidity: Int
)
data class WeatherInfo(
val description: String
)
data class Wind(
val speed: Double
)
fun toWeather(): Weather {
return Weather(
temperature = main.temp,
description = weather.firstOrNull()?.description ?: "",
humidity = main.humidity,
windSpeed = wind.speed,
cityName = name
)
}
}
// API ์ธํฐํ์ด์ค
interface WeatherApiService {
@GET("weather")
suspend fun getWeather(
@Query("q") city: String,
@Query("appid") apiKey: String,
@Query("units") units: String = "metric",
@Query("lang") lang: String = "kr"
): Response<WeatherResponse>
}
// Repository
class WeatherRepository(private val apiService: WeatherApiService) {
suspend fun getWeather(city: String): NetworkResult<Weather> {
return try {
val response = apiService.getWeather(
city = city,
apiKey = BuildConfig.WEATHER_API_KEY
)
if (response.isSuccessful) {
response.body()?.let {
NetworkResult.Success(it.toWeather())
} ?: NetworkResult.Error(response.code(), "๋ฐ์ดํฐ ์์")
} else {
NetworkResult.Error(response.code(), "๋ ์จ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค")
}
} catch (e: Exception) {
NetworkResult.Exception(e)
}
}
}
// ViewModel
class WeatherViewModel(
private val repository: WeatherRepository
) : ViewModel() {
private val _weatherState = MutableStateFlow<UiState<Weather>>(UiState.Idle)
val weatherState: StateFlow<UiState<Weather>> = _weatherState.asStateFlow()
fun fetchWeather(city: String) {
viewModelScope.launch {
_weatherState.value = UiState.Loading
when (val result = repository.getWeather(city)) {
is NetworkResult.Success -> {
_weatherState.value = UiState.Success(result.data)
}
is NetworkResult.Error -> {
_weatherState.value = UiState.Error(result.message)
}
is NetworkResult.Exception -> {
_weatherState.value = UiState.Error(
result.e.message ?: "์ ์ ์๋ ์ค๋ฅ"
)
}
}
}
}
}
sealed class UiState<out T> {
object Idle : UiState<Nothing>()
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
// Compose UI
@Composable
fun WeatherScreen(viewModel: WeatherViewModel = viewModel()) {
val weatherState by viewModel.weatherState.collectAsState()
var cityInput by remember { mutableStateOf("Seoul") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = cityInput,
onValueChange = { cityInput = it },
label = { Text("๋์ ์ด๋ฆ") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.fetchWeather(cityInput) },
modifier = Modifier.fillMaxWidth()
) {
Text("๋ ์จ ์กฐํ")
}
Spacer(modifier = Modifier.height(32.dp))
when (val state = weatherState) {
is UiState.Idle -> {
Text("๋์๋ฅผ ์
๋ ฅํ๊ณ ์กฐํ ๋ฒํผ์ ๋๋ฌ์ฃผ์ธ์")
}
is UiState.Loading -> {
CircularProgressIndicator()
}
is UiState.Success -> {
WeatherCard(weather = state.data)
}
is UiState.Error -> {
Text(
text = "์๋ฌ: ${state.message}",
color = MaterialTheme.colorScheme.error
)
}
}
}
}
@Composable
fun WeatherCard(weather: Weather) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = weather.cityName,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "${weather.temperature}ยฐC",
style = MaterialTheme.typography.displayLarge
)
Text(
text = weather.description,
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("์ต๋", style = MaterialTheme.typography.labelMedium)
Text("${weather.humidity}%")
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("ํ์", style = MaterialTheme.typography.labelMedium)
Text("${weather.windSpeed} m/s")
}
}
}
}
}
์ด ์์ ๋ ์ค์ ๋ก ๋์ํ๋ ์์ ํ ๋ ์จ ์ฑ์ด์ผ! OpenWeatherMap API๋ฅผ ์ฌ์ฉํ๊ณ ์์ด. ๐
๐ ๊ณ ๊ธ ๊ธฐ๋ฅ๊ณผ ํจํด
1๏ธโฃ GraphQL ์ง์
REST API ๋ง๊ณ GraphQL์ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด? Retrofit๊ณผ Alamofire ๋ชจ๋ ์ง์ํด!
// Retrofit with GraphQL
interface GraphQLApiService {
@POST("graphql")
suspend fun query(@Body request: GraphQLRequest): Response<GraphQLResponse>
}
data class GraphQLRequest(
val query: String,
val variables: Map<String, Any>? = null
)
data class GraphQLResponse(
val data: JsonObject?,
val errors: List<GraphQLError>?
)
// ์ฌ์ฉ ์์
val query = """
query GetUser(${'$'}id: ID!) {
user(id: ${'$'}id) {
id
name
email
posts {
title
content
}
}
}
""".trimIndent()
val variables = mapOf("id" to "123")
val request = GraphQLRequest(query, variables)
val response = apiService.query(request)
2๏ธโฃ WebSocket ์ง์
์ค์๊ฐ ํต์ ์ด ํ์ํ๋ค๋ฉด WebSocket์ ์ฌ์ฉํด์ผ ํด. ์ฑํ ์ฑ์ด๋ ์ค์๊ฐ ์๋ฆผ์ ํ์์ง! ๐ฌ
// OkHttp WebSocket
class WebSocketManager {
private var webSocket: WebSocket? = null
private val client = OkHttpClient()
fun connect(url: String, listener: WebSocketListener) {
val request = Request.Builder()
.url(url)
.build()
webSocket = client.newWebSocket(request, listener)
}
fun send(message: String) {
webSocket?.send(message)
}
fun close() {
webSocket?.close(1000, "Goodbye")
}
}
// ์ฌ์ฉ ์์
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
println("์ฐ๊ฒฐ๋จ!")
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("๋ฉ์์ง ์์ : $text")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
println("์๋ฌ: ${t.message}")
}
}
val wsManager = WebSocketManager()
wsManager.connect("wss://echo.websocket.org", listener)
3๏ธโฃ ํ์ผ ์ ๋ก๋/๋ค์ด๋ก๋
์ด๋ฏธ์ง๋ ํ์ผ์ ์ ๋ก๋ํ๋ ๊ฑด ๋ชจ๋ฐ์ผ ์ฑ์์ ํํ ์์ ์ด์ง. ํ๋ก๊ทธ๋ ์ค ๋ฐ๋ ๋ณด์ฌ์ค์ผ ํ๊ณ !
// Retrofit ํ์ผ ์
๋ก๋
interface FileApiService {
@Multipart
@POST("upload")
suspend fun uploadFile(
@Part file: MultipartBody.Part,
@Part("description") description: RequestBody
): Response<UploadResponse>
}
// ์ฌ์ฉ ์์
fun uploadImage(uri: Uri) {
viewModelScope.launch {
val file = File(uri.path!!)
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
val description = "ํ๋กํ ์ฌ์ง".toRequestBody("text/plain".toMediaTypeOrNull())
val response = apiService.uploadFile(body, description)
// ์ฒ๋ฆฌ...
}
}
// Alamofire ํ์ผ ์
๋ก๋ with Progress
AF.upload(
multipartFormData: { multipartFormData in
if let imageData = image.jpegData(compressionQuality: 0.8) {
multipartFormData.append(
imageData,
withName: "file",
fileName: "image.jpg",
mimeType: "image/jpeg"
)
}
},
to: "https://api.example.com/upload"
)
.uploadProgress { progress in
print("์
๋ก๋ ์งํ๋ฅ : \(progress.fractionCompleted * 100)%")
}
.responseDecodable(of: UploadResponse.self) { response in
switch response.result {
case .success(let uploadResponse):
print("์
๋ก๋ ์ฑ๊ณต: \(uploadResponse)")
case .failure(let error):
print("์
๋ก๋ ์คํจ: \(error)")
}
}
๐ก ์ค๋ฌด ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค์ ํ๋ก์ ํธ์์ ์ฌ์ฉํ ๋ ์์๋๋ฉด ์ข์ ํ๋ค์ ์ ๋ฆฌํด๋ดค์ด! ์ด๊ฑด ์ ๋ง ๊ฒฝํ์์ ๋์จ ๋ ธํ์ฐ๋ค์ด์ผ. ๐
์์กด์ฑ ์ฃผ์ : Hilt(Android)๋ Swinject(iOS)๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ ๊ฐ๋ฅํ ์ฝ๋ ์์ฑ
๋จ์ผ ์ฑ ์ ์์น: ๋คํธ์ํน, ๋ฐ์ดํฐ ๋ณํ, ๋น์ฆ๋์ค ๋ก์ง์ ๋ถ๋ฆฌํด
์ด๋ฏธ์ง ์ต์ ํ: Glide(Android)๋ Kingfisher(iOS) ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
๋ฐฑ๊ทธ๋ผ์ด๋ ๋๊ธฐํ: WorkManager(Android)๋ Background Tasks(iOS) ํ์ฉ
Analytics: API ํธ์ถ ์ฑ๊ณต๋ฅ , ์๋ต ์๊ฐ ์ธก์
๋ก๊ทธ ๋ ๋ฒจ ๊ด๋ฆฌ: ํ๋ก๋์ ์์๋ ๋ฏผ๊ฐํ ์ ๋ณด ๋ก๊น ๋นํ์ฑํ
๊ทธ๋ฆฌ๊ณ ํน์ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐ์ด๋ ๋คํธ์ํน ๊ด๋ จํด์ ๋์์ด ํ์ํ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ๋ค์ ๋์์ ๋ฐ์ ์๋ ์์ด! ์ค์ ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ค์ด ๋ง๊ฑฐ๋ . ๐ค
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ์๋ฃ๋ค์ ์ถ์ฒํด:
๐ ๊ณต์ ๋ฌธ์
Retrofit: https://square.github.io/retrofit/
Alamofire: https://github.com/Alamofire/Alamofire
OkHttp: https://square.github.io/okhttp/
Kotlin Coroutines: https://kotlinlang.org/docs/coroutines-overview.html
๐ ์จ๋ผ์ธ ๊ฐ์
โข Udemy, Coursera์์ "Android Networking" ๋๋ "iOS Networking" ๊ฒ์
โข YouTube์์ ์ค์ ์์ ์์๋ค
โข Medium, Dev.to์์ ์ต์ ๊ธฐ์ ๋ธ๋ก๊ทธ ํฌ์คํธ
๐ฌ ์ปค๋ฎค๋ํฐ
โข Stack Overflow: ๊ตฌ์ฒด์ ์ธ ๋ฌธ์ ํด๊ฒฐ
โข Reddit (r/androiddev, r/iOSProgramming): ํธ๋ ๋์ ํ ๋ก
โข GitHub Issues: ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ด๋ จ ์ง๋ฌธ
โข ํ๊ตญ ์๋๋ก์ด๋/iOS ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐ Retrofit๊ณผ Alamofire์ ๋ํด ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ ๊ธฐ๋ฅ๊น์ง ๋ค๋ค๋ดค๋๋ฐ, ์ด๋ ์ด?
๋ชจ๋ฐ์ผ ๋คํธ์ํน์ ์ฑ ๊ฐ๋ฐ์ ํต์ฌ์ด์ผ. ์ฌ์ฉ์ ๊ฒฝํ์ ์ข์ฐํ๋ ์ค์ํ ๋ถ๋ถ์ด๊ฑฐ๋ . ๋น ๋ฅด๊ณ ์์ ์ ์ธ ๋คํธ์ํน ๊ตฌํ์ ๋จ์ํ ๊ธฐ์ ์ ์ธ ๋ฌธ์ ๊ฐ ์๋๋ผ, ์ฌ์ฉ์์๊ฒ ์ ๋ขฐ๋ฅผ ์ฃผ๋ ๊ฑฐ์ผ. ๐ช
๊ธฐ์ตํด์ผ ํ ํต์ฌ ํฌ์ธํธ๋ค:
1. ์ ์ ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ ํ: Retrofit(Android)๊ณผ Alamofire(iOS)๋ ๊ฐ ํ๋ซํผ์ ๋ฒ ์คํธ ์ด์ด์ค
2. ๋น๋๊ธฐ ์ฒ๋ฆฌ: Coroutines์ async/await๋ก ๊น๋ํ ์ฝ๋ ์์ฑ
3. ์๋ฌ ์ฒ๋ฆฌ: ๋ชจ๋ ๊ฐ๋ฅํ ์คํจ ์๋๋ฆฌ์ค ๊ณ ๋ ค
4. ๋ณด์: HTTPS, SSL Pinning, ์์ ํ ํ ํฐ ๊ด๋ฆฌ
5. ํ
์คํธ: Mock์ ํ์ฉํ ์์ ์ ์ธ ํ
์คํธ ์ฝ๋
6. ์ต์ ํ: ์บ์ฑ, ์์ถ, ๋ฐฐ์น ์์ฒญ์ผ๋ก ์ฑ๋ฅ ํฅ์
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ์ตํ๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋คํธ์ํน ๋ง์คํฐ๊ฐ ๋์ด ์์ ๊ฑฐ์ผ! ๐
์ค์ ์์ ๋ถ๋ชํ๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ๋ฐฉ๋ฒ์ด๋๊น, ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด๋ด. ๋ ์จ ์ฑ, To-Do ์ฑ, ๋ด์ค ๋ฆฌ๋ ๋ฑ ๊ฐ๋จํ ๊ฒ๋ถํฐ ๋ง๋ค์ด๋ณด๋ฉด์ ๊ฐ์ ์ก๋ ๊ฑฐ์ง!
๊ทธ๋ฆฌ๊ณ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ฃผ์ ํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋, ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด. ํผ์ ๊ณ ๋ฏผํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ฒ ํด๊ฒฐํ ์ ์์ด. ๐
๋ชจ๋ฐ์ผ ๊ฐ๋ฐ์ ์ธ๊ณ๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ์๋ก์ด ๊ธฐ์ ๊ณผ ํจํด๋ค์ด ๊ณ์ ๋์ค๋๊น, ๊พธ์คํ ํ์ตํ๋ ์์ธ๊ฐ ์ค์ํด. ํ์ง๋ง ๊ธฐ๋ณธ๊ธฐ๋ง ํํํ๋ฉด ์ด๋ค ๋ณํ๋ ๋๋ ต์ง ์์ ๊ฑฐ์ผ!
์, ์ด์ ๋น์ ์ ์ฐจ๋ก์ผ! ๋ฉ์ง ์ฑ์ ๋ง๋ค์ด๋ด. ํ์ดํ ! ๐โจ
๐ Happy Coding! ๐
๋ชจ๋ฐ์ผ ๋คํธ์ํน์ ์ธ๊ณ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค!
์ง๋ฌธ์ด๋ ํผ๋๋ฐฑ์ ์ธ์ ๋ ํ์์ด์์ ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.