12월 21, 2025
37 분 읽기

주니어 Android 개발자 면접 질문과 답변

interview
career-advice
job-search
entry-level
주니어 Android 개발자 면접 질문과 답변
Milad Bonakdar

Milad Bonakdar

작성자

Kotlin, 생명주기, Jetpack Compose, Room, 네트워킹, 코루틴 질문으로 주니어 Android 면접을 준비하세요.


소개

주니어 Android 개발자 면접에서는 Kotlin 기본기, Android 생명주기, Compose 상태 관리, 로컬 저장소, 네트워크 호출, 코루틴을 실무적으로 설명할 수 있는지 확인합니다. 좋은 답변은 작은 앱을 안정적으로 만드는 방식과 연결됩니다. UI 상태를 적절한 위치에 두고, 메인 스레드를 막지 않으며, 데이터를 안전하게 저장하고, 오류를 명확히 처리해야 합니다.

이 가이드로 짧은 답변을 연습한 뒤 이력서나 포트폴리오의 작은 프로젝트와 연결해 보세요. 할 일 앱, 날씨 앱, API 기반 화면을 예로 들면 외운 답변보다 훨씬 구체적으로 들립니다.


Kotlin 기본 사항 (6 문제)

1. Kotlin에서 valvar의 차이점은 무엇입니까?

답변:

  • val: 불변(읽기 전용) 변수를 선언합니다. 한 번 할당되면 해당 값을 변경할 수 없습니다.
  • var: 가변 변수를 선언합니다. 초기화 후 값을 변경할 수 있습니다.
  • 모범 사례: 안전을 위해 기본적으로 val을 사용하십시오. 값을 재할당해야 할 때만 var을 사용하십시오.
val name = "John"  // 변경할 수 없음
var age = 25       // 변경할 수 있음
age = 26           // 유효함
// name = "Jane"   // 오류: Val은 재할당할 수 없음

희소성: 매우 흔함 난이도: 쉬움


2. Kotlin에서 Nullable 타입과 Safe Call 연산자를 설명하십시오.

답변: Kotlin의 타입 시스템은 null 포인터 예외를 방지하기 위해 nullable 타입과 non-nullable 타입을 구별합니다.

  • Nullable 타입: 타입 뒤에 ?를 추가합니다: String?
  • Safe Call (?.): 객체가 null이 아닌 경우에만 메서드를 호출합니다.
  • Elvis 연산자 (?:): null인 경우 기본값을 제공합니다.
  • Not-null 단정 (!!): null인 경우 예외를 발생시킵니다 (가급적 사용하지 마십시오).
var email: String? = "[email protected]"

// Safe call
val length = email?.length  // email이 null이면 null을 반환합니다.

// Elvis 연산자
val displayEmail = email ?: "No email"

// Let 함수
email?.let {
    println("Email: $it")
}

// Not-null 단정 (위험)
val len = email!!.length  // email이 null이면 예외를 발생시킵니다.

희소성: 매우 흔함 난이도: 쉬움


3. Kotlin에서 classdata class의 차이점은 무엇입니까?

답변:

  • 일반 클래스: 표준 클래스 정의
  • Data Class: 데이터를 보유하는 데 유용한 메서드를 자동으로 생성합니다.
    • equals()hashCode()
    • toString()
    • copy() 함수
    • 디스트럭처링을 위한 componentN() 함수
// 일반 클래스
class Person(val name: String, val age: Int)

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

val user1 = User("John", 25)
val user2 = User("John", 25)

println(user1 == user2)  // true (equals() 생성됨)
println(user1)           // User(name=John, age=25) (toString() 생성됨)

// 수정된 복사본
val user3 = user1.copy(age = 26)

// 디스트럭처링
val (name, age) = user1

희소성: 매우 흔함 난이도: 쉬움


4. Kotlin에서 람다 표현식과 고차 함수는 무엇입니까?

답변:

  • 람다: 값으로 전달될 수 있는 익명 함수
  • 고차 함수: 함수를 매개변수로 사용하거나 함수를 반환하는 함수
// 람다 표현식
val sum = { a: Int, b: Int -> a + b }
println(sum(5, 3))  // 8

// 고차 함수
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

val result = calculate(10, 5) { x, y -> x * y }
println(result)  // 50

// 컬렉션 연산
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }

println(doubled)  // [2, 4, 6, 8, 10]
println(evens)    // [2, 4]

희소성: 매우 흔함 난이도: 중간


5. Kotlin에서 확장 함수를 설명하십시오.

답변: 확장 함수를 사용하면 소스 코드를 수정하지 않고도 기존 클래스에 새 함수를 추가할 수 있습니다.

// String에 대한 확장 함수
fun String.isPalindrome(): Boolean {
    return this == this.reversed()
}

println("radar".isPalindrome())  // true
println("hello".isPalindrome())  // false

// 매개변수가 있는 확장 함수
fun Int.times(action: () -> Unit) {
    repeat(this) {
        action()
    }
}

3.times {
    println("Hello")
}
// "Hello"를 세 번 출력합니다.

희소성: 흔함 난이도: 쉬움


6. Kotlin에서 =====의 차이점은 무엇입니까?

답변:

  • ==: 구조적 동등성 (equals()를 사용하여 값을 비교합니다.)
  • ===: 참조적 동등성 (메모리 참조를 비교합니다.)
val str1 = "Hello"
val str2 = "Hello"
val str3 = str1

println(str1 == str2)   // true (값이 같음)
println(str1 === str2)  // true (문자열 풀 최적화)
println(str1 === str3)  // true (같은 참조)

val list1 = listOf(1, 2, 3)
val list2 = listOf(1, 2, 3)

println(list1 == list2)   // true (내용이 같음)
println(list1 === list2)  // false (다른 객체)

희소성: 흔함 난이도: 쉬움


Android 구성 요소 (5 문제)

7. Activity란 무엇이며 해당 라이프사이클을 설명하십시오.

답변: Activity는 사용자 인터페이스가 있는 단일 화면을 나타냅니다. 잘 정의된 라이프사이클이 있습니다.

Loading diagram...
  • onCreate(): Activity가 생성됩니다. UI를 초기화하고 콘텐츠 뷰를 설정합니다.
  • onStart(): Activity가 표시됩니다.
  • onResume(): Activity가 포그라운드에 있고 상호 작용이 가능합니다.
  • onPause(): Activity가 포커스를 잃습니다 (다른 Activity가 포그라운드로 이동).
  • onStop(): Activity가 더 이상 보이지 않습니다.
  • onDestroy(): Activity가 소멸됩니다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 뷰를 초기화하고 데이터를 설정합니다.
    }
    
    override fun onStart() {
        super.onStart()
        // Activity가 표시됩니다.
    }
    
    override fun onResume() {
        super.onResume()
        // Activity가 상호 작용이 가능합니다. 애니메이션을 시작합니다.
    }
    
    override fun onPause() {
        super.onPause()
        // Activity가 포커스를 잃습니다. 진행 중인 작업을 일시 중지합니다.
    }
}

희소성: 매우 흔함 난이도: 쉬움


8. Activity와 Fragment의 차이점은 무엇입니까?

답변:

  • Activity: 전체 화면을 나타냅니다. 사용자 상호 작용을 위한 진입점입니다. 자체 라이프사이클이 있습니다.
  • Fragment: Activity 내에서 UI의 재사용 가능한 부분입니다. 하나의 Activity에 여러 Fragment가 존재할 수 있습니다. 호스트 Activity에 연결된 자체 라이프사이클이 있습니다.
  • Fragment의 장점:
    • Activity 간 재사용성
    • 모듈식 UI 구성 요소
    • 태블릿 지원 (멀티 패널 레이아웃)
    • Navigation component 통합
class MyFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_my, container, false)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 뷰를 초기화합니다.
    }
}

// Activity에서
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, MyFragment())
            .commit()
    }
}

희소성: 매우 흔함 난이도: 쉬움


9. Intent란 무엇이며 그 유형은 무엇입니까?

답변: Intent는 다른 앱 구성 요소에서 작업을 요청하는 데 사용되는 메시징 객체입니다.

  • 명시적 Intent: 시작할 정확한 구성 요소를 지정합니다 (클래스 이름으로).
  • 암시적 Intent: 일반 작업을 선언하고 시스템이 적절한 구성 요소를 찾습니다.
// 명시적 Intent - 특정 Activity 시작
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("USER_ID", 123)
startActivity(intent)

// 암시적 Intent - URL 열기
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
startActivity(webIntent)

// 암시적 Intent - 텍스트 공유
val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "이것 좀 봐!")
}
startActivity(Intent.createChooser(shareIntent, "다음으로 공유"))

// 암시적 Intent - 전화 걸기
val callIntent = Intent(Intent.ACTION_DIAL).apply {
    data = Uri.parse("tel:1234567890")
}
startActivity(callIntent)

희소성: 매우 흔함 난이도: 쉬움


10. Android에서 Service란 무엇입니까?

답변: Service는 사용자 인터페이스 없이 백그라운드에서 실행되어 장기 실행 작업을 수행하는 구성 요소입니다.

  • 유형:
    • 포그라운드 Service: 눈에 띄는 작업을 수행합니다 (음악 플레이어). 알림을 표시합니다.
    • 백그라운드 Service: 사용자가 직접적으로 알 수 없는 작업을 수행합니다.
    • 바운드 Service: 구성 요소가 바인딩하여 상호 작용할 수 있습니다.
class MyService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 백그라운드 작업 수행
        Thread {
            // 장기 실행 작업
            Thread.sleep(5000)
            stopSelf()
        }.start()
        
        return START_STICKY
    }
    
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

// Service 시작
val serviceIntent = Intent(this, MyService::class.java)
startService(serviceIntent)

// Service 중지
stopService(serviceIntent)

희소성: 흔함 난이도: 중간


11. BroadcastReceiver란 무엇입니까?

답변: BroadcastReceiver는 시스템 전체 브로드캐스트 알림에 응답하는 구성 요소입니다.

  • 사용 사례: 배터리 부족, 네트워크 연결 변경, SMS 수신, 부팅 완료
  • 등록:
    • 정적: AndroidManifest.xml에서 (최신 Android 버전에서는 제한됨)
    • 동적: 코드에서 (선호됨)
class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        when (intent?.action) {
            Intent.ACTION_BATTERY_LOW -> {
                // 배터리 부족 처리
            }
            ConnectivityManager.CONNECTIVITY_ACTION -> {
                // 네트워크 변경 처리
            }
        }
    }
}

// 동적으로 등록
class MainActivity : AppCompatActivity() {
    private val receiver = MyBroadcastReceiver()
    
    override fun onStart() {
        super.onStart()
        val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
        registerReceiver(receiver, filter)
    }
    
    override fun onStop() {
        super.onStop()
        unregisterReceiver(receiver)
    }
}

희소성: 흔함 난이도: 쉬움


UI 개발 (4 문제)

12. LinearLayout, RelativeLayout 및 ConstraintLayout의 차이점은 무엇입니까?

답변:

  • LinearLayout: 자식을 단일 행 또는 열로 정렬합니다. 간단하지만 중첩된 레이아웃으로 이어질 수 있습니다.
  • RelativeLayout: 자식을 서로 또는 부모를 기준으로 배치합니다. 더 유연하지만 복잡합니다.
  • ConstraintLayout: 최신, 유연한 레이아웃입니다. 플랫 뷰 계층 구조입니다. 복잡한 UI에 권장됩니다.
<!-- ConstraintLayout 예제 -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="제목"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="클릭하세요"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

희소성: 매우 흔함 난이도: 쉬움


13. RecyclerView란 무엇이며 어떻게 작동합니까?

답변: RecyclerView는 뷰를 재활용하여 큰 목록을 효율적으로 표시하는 위젯입니다.

  • 구성 요소:
    • Adapter: 데이터를 뷰에 바인딩합니다.
    • ViewHolder: 뷰에 대한 참조를 보유합니다 (findViewById 호출을 피합니다.)
    • LayoutManager: 항목을 배치합니다 (Linear, Grid, Staggered).
// Data class
data class User(val name: String, val email: String)

// ViewHolder
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val nameText: TextView = itemView.findViewById(R.id.nameText)
    private val emailText: TextView = itemView.findViewById(R.id.emailText)
    
    fun bind(user: User) {
        nameText.text = user.name
        emailText.text = user.email
    }
}

// Adapter
class UserAdapter(private val users: List<User>) : 
    RecyclerView.Adapter<UserViewHolder>() {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_user, parent, false)
        return UserViewHolder(view)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(users[position])
    }
    
    override fun getItemCount() = users.size
}

// Activity에서
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = UserAdapter(userList)

희소성: 매우 흔함 난이도: 중간


14. Jetpack Compose란 무엇입니까?

답변: Jetpack Compose는 Android의 최신 선언적 UI 툴킷입니다.

  • 선언적: UI를 빌드하는 방법이 아닌 UI가 어떻게 보여야 하는지 설명합니다.
  • 장점: 더 적은 코드, 직관적, 강력, 개발 속도 향상
  • Composable 함수: Compose UI의 빌딩 블록
@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column(
        modifier = Modifier.padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

@Preview
@Composable
fun PreviewCounter() {
    Counter()
}

희소성: 매우 흔함 난이도: 쉬움


15. match_parentwrap_content의 차이점은 무엇입니까?

답변: 이들은 뷰 크기를 정의하는 레이아웃 매개변수입니다.

  • match_parent: 뷰가 부모의 크기를 채우도록 확장됩니다.
  • wrap_content: 뷰가 콘텐츠에 맞게 크기를 조정합니다.
  • 고정 크기: 특정 dp 값 (예: 100dp)
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <!-- 부모 너비를 채우고 콘텐츠 높이를 래핑합니다. -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="제목" />
    
    <!-- 두 치수를 모두 래핑합니다. -->
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="클릭하세요" />
    
    <!-- 고정 크기 -->
    <ImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/icon" />
</LinearLayout>

희소성: 매우 흔함 난이도: 쉬움


데이터 및 네트워킹 (5 문제)

16. Android에서 네트워크 요청을 어떻게 만듭니까?

답변: 네트워킹에는 Retrofit 또는 OkHttp와 같은 라이브러리를 사용하십시오. HttpURLConnection을 직접 사용하지 마십시오.

// Retrofit 설정
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: Int): User
    
    @POST("users")
    suspend fun createUser(@Body user: User): User
}

object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/"
    
    val apiService: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

// ViewModel에서 사용
class UserViewModel : ViewModel() {
    fun fetchUser(userId: Int) {
        viewModelScope.launch {
            try {
                val user = RetrofitClient.apiService.getUser(userId)
                // UI 업데이트
            } catch (e: Exception) {
                // 오류 처리
            }
        }
    }
}

희소성: 매우 흔함 난이도: 중간


17. Room이란 무엇이며 어떻게 사용합니까?

답변: Room은 더 쉬운 데이터베이스 액세스를 위한 SQLite에 대한 추상화 레이어입니다.

  • 구성 요소:
    • Entity: 테이블을 나타냅니다.
    • DAO (Data Access Object): 데이터베이스 작업을 정의합니다.
    • Database: 데이터베이스 홀더
// Entity
@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "name") val name: String,
    @ColumnInfo(name = "email") val email: String
)

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): List<User>
    
    @Query("SELECT * FROM users WHERE id = :userId")
    fun getUserById(userId: Int): User?
    
    @Insert
    suspend fun insertUser(user: User)
    
    @Delete
    suspend fun deleteUser(user: User)
    
    @Update
    suspend fun updateUser(user: User)
}

// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

희소성: 매우 흔함 난이도: 중간


18. SharedPreferences란 무엇입니까?

답변: SharedPreferences는 적은 양의 기본 데이터를 키-값 쌍으로 저장합니다.

  • 사용 사례: 사용자 설정, 환경 설정, 간단한 플래그
  • 적합하지 않은 경우: 대용량 데이터, 복잡한 객체 (대신 Room 사용)
// 데이터 저장
val sharedPref = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)
with(sharedPref.edit()) {
    putString("username", "John")
    putInt("age", 25)
    putBoolean("isLoggedIn", true)
    apply()  // 또는 commit()
}

// 데이터 읽기
val username = sharedPref.getString("username", "Guest")
val age = sharedPref.getInt("age", 0)
val isLoggedIn = sharedPref.getBoolean("isLoggedIn", false)

// 데이터 제거
with(sharedPref.edit()) {
    remove("username")
    apply()
}

// 모두 지우기
with(sharedPref.edit()) {
    clear()
    apply()
}

희소성: 매우 흔함 난이도: 쉬움


19. SharedPreferences에서 apply()commit()의 차이점은 무엇입니까?

답변: 둘 다 SharedPreferences에 변경 사항을 저장하지만 동작이 다릅니다.

  • apply(): 비동기식입니다. 즉시 반환됩니다. 변경 사항은 백그라운드에서 디스크에 기록됩니다. 반환 값이 없습니다.
  • commit(): 동기식입니다. 변경 사항이 기록될 때까지 차단됩니다. 부울 (성공/실패)을 반환합니다.
  • 모범 사례: 반환 값이 필요하지 않으면 apply()를 사용하십시오.
val editor = sharedPref.edit()

// apply() - 비동기식, 선호됨
editor.putString("key", "value")
editor.apply()  // 즉시 반환

// commit() - 동기식
editor.putString("key", "value")
val success = editor.commit()  // 쓰기를 기다리고 부울을 반환합니다.
if (success) {
    // 성공적으로 저장됨
}

희소성: 흔함 난이도: 쉬움


20. Kotlin에서 코루틴이란 무엇입니까?

답변: 코루틴은 비동기 코드를 순차적으로 작성하는 방법을 제공하여 읽고 유지 관리하기 쉽게 만듭니다.

  • 장점: 경량, 구조화된 동시성, 예외 처리
  • 주요 개념:
    • suspend: 일시 중지 및 재개될 수 있는 함수
    • launch: 코루틴을 시작합니다 (발사 후 잊어버림).
    • async: 코루틴을 시작하고 결과를 반환합니다.
    • Dispatchers: 코루틴이 실행되는 스레드를 제어합니다.
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            try {
                // 메인 스레드
                showLoading()
                
                // 네트워크 호출을 위해 IO 스레드로 전환
                val data = withContext(Dispatchers.IO) {
                    fetchDataFromNetwork()
                }
                
                // 메인 스레드로 돌아가기
                updateUI(data)
            } catch (e: Exception) {
                handleError(e)
            }
        }
    }
    
    suspend fun fetchDataFromNetwork(): String {
        delay(2000)  // 네트워크 지연 시뮬레이션
        return "서버에서 가져온 데이터"
    }
}

// 병렬 실행
viewModelScope.launch {
    val deferred1 = async { fetchUser(1) }
    val deferred2 = async { fetchUser(2) }
    
    val user1 = deferred1.await()
    val user2 = deferred2.await()
}

희소성: 매우 흔함 난이도: 중간


Newsletter subscription

실제로 효과가 있는 주간 커리어 팁

최신 인사이트를 받은 편지함으로 직접 받아보세요

채용 담당자에게 눈에 띄고 꿈의 직장을 얻으세요

ATS를 통과하고 채용 담당자에게 깊은 인상을 주는 AI 기반 이력서로 커리어를 변화시킨 수천 명의 사람들과 함께하세요.

지금 만들기 시작

이 게시물 공유

6초를 최대한 활용하세요

채용 담당자는 평균적으로 6~7초만 이력서를 훑어봅니다. 우리의 검증된 템플릿은 즉시 주목을 끌고 계속 읽게 하도록 설계되었습니다.