Study

안드로이드 면접 북 스터디 Q60 ~ Q66

hegunhee 2025. 10. 23. 17:00

안드로이드 기술 면접 책 북 스터디를 하면서
보다 간결하게 실제 면접하는 것처럼 글을 정리해보려 한다.

 

중요하다고 생각된 질문만 정리했다.

 

Q60) Json 형식을 어떻게 객체로 직렬화하나요?

최신 안드로이드 앱은 원격 서버와 자주 상호작용하기 때문에 Json을 객체로 직렬화합니다.

앱은 Kotlin 객체를 Json으로 직렬화하여

데이터를 백엔드로 보내 필요한 정보를 요청하고, 서버의 Json 응답을 받으면 다시 객치로 역직렬화 합니다.

 

수동 직렬화 및 역직렬화

외부의 솔루션을 사용하지 않고도 직접 수동적으로 문자열 조작 및 파싱 기술을 통해

객체를 Json 문자열로 변환하고, 그 반대로 변환하여 직, 역직렬를 할 수 있습니다.

data class User(val name: String, val age: Int)

fun serializeUser(user: User): String {
    // 각 속성을 JSON 형식에 맞게 문자열로 변환
    return """{
        "name": "${user.name}",
        "age": ${user.age}
    }""".trimIndent() // 들여쓰기 제거
}

// 사용 예제
val user = User("John", 30)
val jsonString = serializeUser(user)
// 출력: {"name":"John","age":30}

 

반면에 수동 역직렬화는 Json 문자열을 파싱하고 값을 추출하여 객체를 재구성하는 것을 동반합니다.

fun deserializeUser(json: String): User {
    // 정규 표현식을 사용하여 값 추출 (간단한 예시)
    val nameRegex = """"name"\s*:\s*"([^"]*)"""".toRegex()
    val ageRegex = """"age"\s*:\s*(\d+)""".toRegex()

    val name = nameRegex.find(json)?.groups?.get(1)?.value ?: ""
    val age = ageRegex.find(json)?.groups?.get(1)?.value?.toIntOrNull() ?: 0

    return User(name, age)
}

// 사용 예제
val jsonString = """{"name":"John","age":30}"""
val user = deserializeUser(jsonString)
// 출력: User(name="John", age=30)

 

이 방법은 권장되지 않습니다.

kotlinx.serialization, Moshi, Gson과 같은 라이브러리를 통해 해당 프로세스를 훨씬 더 효과적으로 구현하여

Json 문자열을 Kotlin 또는 Java 객체로 또는 그 반대로 역변환할 수 있습니다.

Kotlinx.Serialization

Kotlinx.Serialization은 Kotlin과 직접적으로 통합되어

Kotlin의 언어적 기능을 활용하도록 설계되었습니다.

어노테이션을 사용하여 직렬화 동작을 정의gkqslek.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable // 직렬화 가능 클래스임을 명시
data class User(val name: String, val age: Int)

val json = """{"name": "John", "age": 30}"""
// JSON 문자열을 User 객체로 역직렬화
val user: User = Json.decodeFromString<User>(json)
// User 객체를 JSON 문자열로 직렬화
val serializedJson: String = Json.encodeToString(user)

kotlinx.serialzation은 Kotlin 컴파일러 플러그인을 사용하여

Kotlin 객체를 Json 문자열로 변환하고 Json 문자열을 다시 Kotlin 객체로 파싱하는 타입 안정성을 보장하여

내부적으로 리플렉션을 사용하지 않는 메커니즘을 제공하므로

최신 안드로이드 및 Kotlin 개발에서 가장 선호되는 방법 중 하나입니다.

 

내부적으로 Kotlin은 Moshi의 리플렉션 모드 및 Gson과 달리 무거운 런타임 리플렉션 없이

효율적인 직렬화를 수앻아기 위해 컴파일러가 생성하는 코드에 의존합니다.

 

Moshi

Moshi는 타입 안정성과 Kotlin 지원을 강조하는 Json 라이브러리

Gson과 달리 Moshi는 Kotlin의 Null 가능성 및 기본 매개변수를 기본적으로 지원합니다.

data class User(val name: String, val age: Int)

// Moshi 인스턴스 생성 (Kotlin 지원 추가 필요)
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory()) // Kotlin 지원 추가
    .build()

// User 클래스에 대한 어댑터 가져오기
val adapter: JsonAdapter<User> = moshi.adapter(User::class.java)

val json = """{"name": "John", "age": 30}"""

// JSON을 객체로 역직렬화
val user: User? = adapter.fromJson(json)

// 객체를 JSON으로 직렬화
val serializedJson: String? = user?.let { adapter.toJson(it) }

Moshi에서는 Json 직렬화 및 역직렬화를 처리하는 두 가지 주요 접근 방식이 있는데

리플렉션에 기반한 동작 방식과 컴파일 타임에 코드를 생성하는 두 가지입니다.

  • 리플렉션 기반 Moshi
    - 리플렉션 기반 Moshi는 Java 리플렉션을 사용하여 런타임에 동적으로 Json 어댑터를 생성
    - 추가 설정 필요 없지만 런타임 오버헤드가 발생
  • 코드 생성 기반
    - 어노테이션 프로세스 기법을 통해 컴파일 타임에 Json 어댑터를 생성하며
    더 빠른 런타임 성능과 컴파일 타임 오류 검사를 제공

Gson

Java 객체를 Json으로 직렬화하고 Json을 다시 Java 객체로 역직렬화할 수 있습니다.
간단한 API와 통합 용이성 덕분에 인기 있는 라이브러리입니다.

 

data class User(val name: String, val age: Int)

val gson = Gson()
val json = """{"name": "John", "age": 30}"""
val user = gson.fromJson(json, User::class.java) // JSON을 객체로 역직렬화
val serializedJson = gson.toJson(user) // 객체를 JSON으로 직렬화

 

요약

kotlinx.serialziation은 Kotlin 우선 및 KMP 프로젝트에 가장 적합한 선택입니다.
Moshi는 빠른 성능과 더 안전한 Json 처리가 필요한 안드로이드 중심 앱에 이상적입니다.

Gson은 JVM 기반의 프로젝트에 통합하기 쉽지만, Kotlin 지원 및 현대 앱 개발의 모범 사례에서는 뒤쳐집니다.

 

Q61) 원격 데이터를 가져오기 위해 네트워크 요청을 어떻게 처리하나요?

일반적으로 Retrofit과 OkHttp를 사용합니다.

 

Retrofit은 선언적 인터페이스를 제공하여 API 상호 작용을 단순화하는 반면

OkHttp는 기본적인 HTTP 클라이언트 역할을 하여 연결 풀링, 캐싱 및 효율적인 통신을 제공합니다.

 

Retrofit을 사용한 네트워크 요청

Retrofit은 HTTP 요청을 깔끔하고 타입 세이프한 API 인터페이스로 추상화합니다.

Json 응답을 Kotlin 또는 Java 객체로 변환하기 위해 Gson 또는 Moshi와 같은 직렬화 라이브러리와 원활하게 작동합니다.

 

  • API 인터페이스 정의
    어노테이션을 사용하여 API 엔드포인트와 HTTP 메서드를 선언합니다.
  • Retrofit 인스턴스 생성
    베이스 URL과 Json 직렬화를 위해 ConverterFactory를 선언하여 Retrofit을 구성합니다.
  • 네트워크 호출
    코루틴을 사용하여 API를 비동기적으로 호출합니다.

OkHttp와 Retrofit 통합

Retrofit은 내부적으로 OkHttp를 HTTP 클라이언트로 사용합니다.
인터셉터를 추가하며 로깅, 인증 또는 캐싱과 같은 OkHttp의 동작을 커스텀할 수 있습니다.

val okHttpClient = OkHttpClient.Builder()
    // 로깅 인터셉터 추가 (예시)
    .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
    // 인증 헤더 추가 인터셉터
    .addInterceptor { chain ->
        val originalRequest = chain.request()
        val newRequest = originalRequest.newBuilder()
            .header("Authorization", "Bearer your_token") // 실제 토큰 사용
            .build()
        chain.proceed(newRequest)
    }
    .connectTimeout(30, TimeUnit.SECONDS) // 타임아웃 설정 (선택 사항)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient) // 커스텀 OkHttpClient 설정
    .addConverterFactory(GsonConverterFactory.create()) // 또는 다른 Converter Factory
    .build()

요약

Retrofit은 내부적으로 복잡한 처리를 하고 있어도 API 표면은 개발자들이 사용하기 쉽게 설계되었습니다.

OkHttp는 네트워크 동작을 사용자가 쉽게 커스텀할 수 있는 유연함을 제공합니다.

 

Q66) 초기 데이터 로딩을 위한 작업을 Compose의 LaunchedEffect와 ViewModel.init() 중 어디에서 하는 것이 더 이상적인가요?

공식 안드로이드 문서 및 architecture-samples Github의 예제는 일반적으로 구성 변경 시 더 나은 생명주기 관리

및 데이터 지속성을 위해 ViewModel.init() 내에서 데이터를 로드할 것을 예시로 보여주고 있습니다.

 

설문 조사 결과 62.6 : 37.4로 VIewModel.init()이 우세합니다.

 

ViewModel.init()이 더 낫다

어떤 안드로이드 개발자는 이렇게 설명했습니다.

Jetpack Compose UI를 애플리케이션의 상태 또는 데이터의 시각적 표현으로 간주한다면

앱에 무엇을 해야 할지 지시하기 위해 UI에 의존한다는 것을 사실 설계 결합으로 볼 수 있습니다.

그래서 LaunchedEffect를 이용해서 UI에서 데이터를 가져오는 것 보다

ViewModel.init()을 사용하는 것이 더 나은 관심사 분리를 제공하여 비즈니스 로직과 UI 상태 관리가 분리되도록 하는게 낫습니다.

유연하게 사용하자

또 다른 안드로이드 개발자는 다음과 같은 관점을 공유했습니다.

ViewModel.init에만 의존하면 특정 동작이 트리거 되는 시점에 대한

제어가 어려워지고 유닛 테스트가 복잡해질 수 있다는 것을 명심해야 합니다.

대신 ViewModel 내에서 이벤트 기반 흐름을 관찰하여 트리거되고 지연될 수 있는

독빌적인 함수를 정의하는 것을 선호합니다

 

해당 접근 방식은 더 큰 유연성과 제어권을 제공하여 LaunchedEffect 또는 이벤트 트리거 작업과 같은

메서드가 데이터 로딩을 더 효과적으로 관리할 수 있다고 합니다.

ViewModel.init 블록에 전적으로 의존하는 대신

사용자 상호 작용이나 특정 이벤트를 기반으로 데이터를 다시 가져올 때 효율성이 향상된다는 것입니다.

 

요약

안드로이드에서 초기 데이터를 로드하는 방법에는 LaunchedEffect, ViewModel.init(), 콜드 플로우를 위한 지연 관찰 등 여러 가지가 있습니다.

이 내용의 디스커션은 효율성을 높이고 부작용을 줄이기 위해 콜드 플로우를 제안하는 것으로 끝났습니다.

그러나 늘 그렇듯 모든 상황에 완벽한 해결책이라는 것은 없습니다.