어플리케이션, 앱 (Application)/안드로이드 (Android)

안드로이드 jetpack compose 공부 정리 4강 (리스트(List)와 객체(Object))

sobal 2025. 6. 27. 20:10

 

4일차는 리스트와 객체에 대한 내용이다.

 

1. 주석 (Comments)

다른 언어들과 마찬가지로 주석은 코드에 대한 설명을 작성하는 부분으로, 컴파일러가 이를 무시하기 때문에 프로그램 실행에 영향을 주지 않는다.

주석의 종류

 
// 한 줄 주석

/*
여러 줄 주석
이 안에 여러 줄의 설명을 작성 가능
*/

/**
 * 문서화 주석 (Documentation Comment)
 * 클래스나 함수에 대한 설명을 작성할 때 사용
 * @param name 매개변수 설명
 * @return 반환값 설명
 */

2. 리스트 (List)

리스트는 데이터를 순서대로 저장하는 컬렉션이다. 코틀린에서는 불변 리스트(Immutable List)와 가변 리스트(Mutable List) 두 종류를 제공한다

2.1. 불변 리스트 (Immutable List)

listOf() 함수를 사용하여 생성하며, 생성 후 요소를 추가, 삭제, 변경할 수 없다.

 
val immutableList = listOf("A", "B", "C")
println(immutableList[0]) // "A" 출력
// immutableList.add("D") // 컴파일 오류: 불변 리스트는 변경 불가능

2.2. 가변 리스트 (Mutable List)

mutableListOf() 함수를 사용하여 생성하며, 요소를 자유롭게 추가, 삭제, 변경할 수 있다.

 
val mutableList = mutableListOf("A", "B", "C")
mutableList.add("D")        // 요소 추가
mutableList.remove("B")     // 요소 삭제
mutableList[0] = "E"        // 요소 변경
println(mutableList)        // [E, C, D] 출력

2.3. 리스트 주요 메서드와 속성

가변 리스트 전용 메서드

  • add(element): 요소를 리스트 끝에 추가
  • add(index, element): 지정된 인덱스에 요소 추가
  • remove(element): 해당 요소를 삭제 (첫 번째 발견된 요소만)
  • removeAt(index): 지정된 인덱스의 요소를 삭제
  • set(index, element) 또는 [index] = element: 지정된 인덱스의 요소를 변경
  • clear(): 모든 요소 삭제

공통 메서드와 속성

  • get(index) 또는 [index]: 지정된 인덱스의 요소를 반환
  • contains(element): 리스트에 해당 요소가 포함되어 있는지 확인
  • size: 리스트의 크기(요소 개수)를 반환
  • isEmpty(): 리스트가 비어있는지 확인
  • isNotEmpty(): 리스트가 비어있지 않은지 확인
  • indexOf(element): 해당 요소의 첫 번째 인덱스 반환 (없으면 -1)
  • lastIndexOf(element): 해당 요소의 마지막 인덱스 반환 (없으면 -1)
  • first(): 첫 번째 요소 반환
  • last(): 마지막 요소 반환
  • subList(fromIndex, toIndex): 지정된 범위의 새로운 리스트 반환

함수형 메서드 (새로운 리스트 반환)

  • reversed(): 역순으로 정렬된 새로운 리스트 반환
  • shuffled(): 무작위로 섞인 새로운 리스트 반환
  • sorted(): 오름차순으로 정렬된 새로운 리스트 반환
  • sortedDescending(): 내림차순으로 정렬된 새로운 리스트 반환
  • filter { condition }: 조건을 만족하는 요소들로 새로운 리스트 생성
  • map { transform }: 각 요소를 변환하여 새로운 리스트 생성
 
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }  // [2, 4]
val doubled = numbers.map { it * 2 }              // [2, 4, 6, 8, 10]

2.4. 리스트 순회 방법

val shoppingList = mutableListOf("Processor", "RAM", "Graphics Card", "SSD")

// 1. for 루프를 사용한 순회
for (item in shoppingList) {
    println(item)
}

// 2. 인덱스와 함께 순회
for (i in shoppingList.indices) {
    println("Index: $i, Item: ${shoppingList[i]}")
}

// 3. withIndex() 사용
for ((index, item) in shoppingList.withIndex()) {
    println("Index: $index, Item: $item")
}

// 4. forEach 함수 사용
shoppingList.forEach { item ->
    println(item)
}

// 5. forEachIndexed 사용
shoppingList.forEachIndexed { index, item ->
    println("Index: $index, Item: $item")
}

2.5. 리스트 실습 예제

 
fun main() {
    val shoppingList = mutableListOf("Processor", "RAM", "Graphics Card", "SSD")

    // 요소 추가
    shoppingList.add("Cooling System")
    println("Added Cooling System: $shoppingList")

    // 요소 삭제 (값으로)
    shoppingList.remove("Graphics Card")
    println("Removed Graphics Card: $shoppingList")

    // 요소 삭제 (인덱스로)
    shoppingList.removeAt(2)
    println("Removed at index 2: $shoppingList")

    // 특정 위치에 요소 추가
    shoppingList.add(2, "Graphics Card RTX 4090")
    println("Added Graphics Card at index 2: $shoppingList")

    // 요소 접근
    println("Element at index 3: ${shoppingList[3]}")

    // 요소 변경
    shoppingList[1] = "DDR5 RAM"
    println("Modified element at index 1: $shoppingList")

    // 요소 포함 여부 확인
    val hasRam = shoppingList.contains("DDR5 RAM")
    println("Has DDR5 RAM in the list: $hasRam")

    // 안전한 요소 삭제 방법
    val iterator = shoppingList.iterator()
    while (iterator.hasNext()) {
        val item = iterator.next()
        if (item.contains("RAM")) {
            iterator.remove()
            println("Removed RAM item safely")
            break
        }
    }
    println("Final shopping list: $shoppingList")
}

3. 클래스 (Class) 심화

코틀린에서 클래스는 객체지향 프로그래밍의 핵심 요소라 할 수 있다.

3.1. 실습 예제 - BankAccount 클래스

 
class BankAccount(var accountHolder: String, var balance: Double) {
    private val transactionHistory = mutableListOf<String>()

    fun deposit(amount: Double) {
        if (amount > 0) {
            balance += amount
            transactionHistory.add("$accountHolder deposited $$amount")
        } else {
            println("Deposit amount must be positive.")
        }
    }

    fun withdraw(amount: Double) {
        if (amount > 0) {
            if (amount <= balance) {
                balance -= amount
                transactionHistory.add("$accountHolder withdrew $$amount")
            } else {
                println("Insufficient funds. Current balance: $$balance")
            }
        } else {
            println("Withdrawal amount must be positive.")
        }
    }

    fun displayTransactionHistory() {
        println("Transaction History for $accountHolder:")
        if (transactionHistory.isEmpty()) {
            println("No transactions yet.")
        } else {
            transactionHistory.forEach { println("- $it") }
        }
    }

    fun getBalance(): Double = balance
}

fun main() {
    val account = BankAccount("Denis Panjuta", 1000.0)
    
    println("Account Holder: ${account.accountHolder}")
    println("Initial Balance: $${account.getBalance()}")

    account.deposit(500.0)
    println("Balance after deposit: $${account.getBalance()}")

    account.withdraw(200.0)
    println("Balance after withdrawal: $${account.getBalance()}")

    account.withdraw(2000.0) // 잔액 부족 메시지 출력

    account.displayTransactionHistory()
}

3.2. 클래스 멤버 (Class Members)

클래스 멤버는 클래스 내부에 정의된 속성(properties)과 메서드(methods)를 의미한다. 속성은 객체의 상태를 저장하고, 메서드는 객체의 행동을 정의한. 클래스 멤버를 적절히 설계하면 코드의 재사용성과 유지보수성을 크게 향상시킬 수 있다.

  • 속성(Properties): 클래스의 상태를 나타내는 변수로, val(읽기 전용) 또는 var(읽기/쓰기)로 선언
  • 메서드(Methods): 클래스의 동작을 정의하는 함수로, 속성을 조작하거나 특정 작업을 수행

3.3. 접근 제한자 (Visibility Modifiers)

접근 제한자는 클래스의 멤버에 대한 접근 범위를 제어하는 키워드이다. 이를 통해 캡슐화(Encapsulation)를 구현하여 객체의 내부 구현을 숨기고, 외부에서의 잘못된 접근을 방지할 수 있다.

  • public: 어디서나 접근 가능 (기본값이므로 생략 가능)
  • private: 해당 클래스 내부에서만 접근 가능
  • protected: 해당 클래스와 하위 클래스에서만 접근 가능
  • internal: 같은 모듈 내에서만 접근 가능
class ExampleClass {
    public val publicProperty = "Everyone can access"     // 기본값
    private val privateProperty = "Only within this class"
    protected val protectedProperty = "This class and subclasses"
    internal val internalProperty = "Same module only"
    
    private fun privateMethod() {
        println("Only accessible within this class")
    }
    
    fun publicMethod() {
        println("Accessible from anywhere")
        privateMethod() // 클래스 내부에서는 private 멤버 접근 가능
    }
}

3.4. 생성자 (Constructor)

생성자는 클래스의 인스턴스를 생성할 때 호출되는 특별한 함수이다. 코틀린에서는 주 생성자(Primary Constructor)와 부 생성자(Secondary Constructor)를 지원하여 다양한 방식으로 객체를 초기화할 수 있다.

  • 주 생성자: 클래스 헤더에 정의되며, 클래스와 함께 매개변수를 선언
  • 부 생성자: 클래스 본문에 constructor 키워드로 정의되며, 여러 개 선언 가능
// 주 생성자 (Primary Constructor)
class Person(val name: String, var age: Int) {
    
    // 부 생성자 (Secondary Constructor)
    constructor(name: String) : this(name, 0) {
        println("Secondary constructor called")
    }
    
    constructor() : this("Unknown", 0) {
        println("Default constructor called")
    }
}

fun main() {
    val person1 = Person("Alice", 25)    // 주 생성자
    val person2 = Person("Bob")          // 부 생성자
    val person3 = Person()               // 기본 생성자
}

3.5. 초기화 블록 (Initializer Block)

초기화 블록은 객체가 생성될 때 주 생성자와 함께 실행되는 코드 블록이다. init 키워드로 정의하며, 복잡한 초기화 로직을 수행하거나 생성자 매개변수의 유효성을 검사할 때 사용한다. 여러 개의 초기화 블록을 선언할 수 있고 클래스 본문에서 나타나는 순서대로 실행다.

 
class Person(val name: String, var age: Int) {
    val isAdult: Boolean
    val greeting: String

    init {
        require(age >= 0) { "Age cannot be negative" }
        isAdult = age >= 18
        greeting = "Hello, I'm $name"
        println("Person 객체 생성: $name (나이: $age)")
    }

    // 여러 개의 init 블록 가능
    init {
        println("두 번째 초기화 블록 실행")
    }
}

fun main() {
    val person = Person("Alice", 25)
    println("${person.name}는 성인인가요? ${person.isAdult}")
    println(person.greeting)
}

3.6. 상속 (Inheritance)

상속은 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받는 객체지향 프로그래밍의 핵심 개념이다. 코틀린에서는 모든 클래스가 기본적으로 final이므로, 상속을 허용하려면 open 키워드를 사용해야 한다. 상속을 통해 코드 재사용성을 높이고 계층적인 클래스 구조를 만들 수 있다.

  • open: 상속이 가능한 클래스임을 나타내는 키워드
  • override: 부모 클래스의 메서드를 재정의할 때 사용하는 키워드
open class Animal(val name: String, val species: String) {
    open fun makeSound() {
        println("$name makes a generic animal sound")
    }
    
    open fun move() {
        println("$name is moving")
    }
}

class Dog(name: String) : Animal(name, "Canine") {
    override fun makeSound() {
        println("$name barks: Woof! Woof!")
    }
    
    fun fetch() {
        println("$name is fetching the ball")
    }
}

class Cat(name: String) : Animal(name, "Feline") {
    override fun makeSound() {
        println("$name meows: Meow!")
    }
    
    override fun move() {
        println("$name is sneaking around silently")
    }
}

fun main() {
    val dog = Dog("Buddy")
    val cat = Cat("Whiskers")
    
    dog.makeSound()  // Buddy barks: Woof! Woof!
    cat.makeSound()  // Whiskers meows: Meow!
    cat.move()       // Whiskers is sneaking around silently
    dog.fetch()      // Buddy is fetching the ball
}

3.7. 추상 클래스 (Abstract Class)

추상 클래스는 하나 이상의 추상 메서드를 포함하는 클래스로, 직접 인스턴스를 생성할 수 없다. 추상 클래스는 공통된 기능을 제공하면서도 구체적인 구현은 하위 클래스에 위임하고 싶을 때 사용한다. 추상 메서드는 선언만 있고 구현이 없으며, 하위 클래스에서 반드시 구현해야 한다.

  • abstract: 추상 클래스나 추상 메서드를 정의하는 키워드
  • 추상 클래스는 일반 메서드와 속성도 가질 수 있음
  • 하위 클래스는 모든 추상 메서드를 구현해야 함
abstract class Shape(val name: String) {
    abstract fun calculateArea(): Double
    abstract fun calculatePerimeter(): Double
    
    // 추상 클래스도 일반 메서드를 가질 수 있음
    fun displayInfo() {
        println("Shape: $name")
        println("Area: ${calculateArea()}")
        println("Perimeter: ${calculatePerimeter()}")
    }
}

class Circle(private val radius: Double) : Shape("Circle") {
    override fun calculateArea(): Double {
        return Math.PI * radius * radius
    }
    
    override fun calculatePerimeter(): Double {
        return 2 * Math.PI * radius
    }
}

class Rectangle(private val width: Double, private val height: Double) : Shape("Rectangle") {
    override fun calculateArea(): Double {
        return width * height
    }
    
    override fun calculatePerimeter(): Double {
        return 2 * (width + height)
    }
}

fun main() {
    val circle = Circle(5.0)
    val rectangle = Rectangle(4.0, 6.0)
    
    circle.displayInfo()
    println()
    rectangle.displayInfo()
}

3.8. 인터페이스 (Interface)

인터페이스는 클래스가 구현해야 하는 메서드들의 계약(contract)을 정의한다. 추상 클래스와 달리 상태를 저장할 수 없지만, 기본 구현을 제공할 수 있고 클래스는 여러 인터페이스를 동시에 구현할 수 있다. 이는 다중 상속의 제약을 해결하고 유연한 설계를 가능하게 한다.

  • 인터페이스는 interface 키워드로 정의
  • 메서드에 기본 구현을 제공할 수 있음
  • 클래스는 여러 인터페이스를 구현 가능 (다중 구현)
  • 속성을 선언할 수 있지만 backing field는 가질 수 없음
interface Flyable {
    fun fly()
    fun getMaxAltitude(): Int
}

interface Swimmable {
    fun swim()
    fun getMaxDepth(): Int
}

// 인터페이스는 속성을 가질 수 있지만 backing field는 없음
interface Drawable {
    val color: String
    fun draw() {
        println("Drawing with $color color")
    }
}

class Duck : Flyable, Swimmable, Drawable {
    override val color: String = "brown"
    
    override fun fly() {
        println("Duck is flying with wings")
    }
    
    override fun getMaxAltitude(): Int = 1000
    
    override fun swim() {
        println("Duck is swimming on water")
    }
    
    override fun getMaxDepth(): Int = 2
}

class Airplane : Flyable {
    override fun fly() {
        println("Airplane is flying with engines")
    }
    
    override fun getMaxAltitude(): Int = 12000
}

fun main() {
    val duck = Duck()
    duck.fly()
    duck.swim()
    duck.draw()
    println("Duck max altitude: ${duck.getMaxAltitude()}m")
    
    val airplane = Airplane()
    airplane.fly()
    println("Airplane max altitude: ${airplane.getMaxAltitude()}m")
}

3.9. 데이터 클래스 (Data Class)

데이터 클래스는 주로 데이터를 저장하기 위한 용도로 사용되는 클래스이다. data 키워드를 사용하여 정의하면, 컴파일러가 자동으로 equals(), hashCode(), toString(), copy() 등의 유용한 메서드들을 생성해준다. 이는 보일러플레이트 코드를 줄이고 개발자의 실수를 방지하는 데 도움이 다.

  • 자동 생성되는 메서드: equals(), hashCode(), toString(), copy(), componentN()
  • 구조 분해 선언(Destructuring Declaration) 지원
  • 주 생성자에 최소 하나의 매개변수가 필요
  • 주 생성자의 모든 매개변수는 val 또는 var로 선언되어야 함
data class Product(val name: String, val price: Double, val category: String)

fun main() {
    val product1 = Product("Laptop", 1200.0, "Electronics")
    val product2 = Product("Laptop", 1200.0, "Electronics")
    val product3 = Product("Mouse", 25.0, "Electronics")

    // equals() 메서드 자동 생성
    println("product1 == product2: ${product1 == product2}")  // true
    println("product1 == product3: ${product1 == product3}")  // false

    // toString() 메서드 자동 생성
    println(product1)  // Product(name=Laptop, price=1200.0, category=Electronics)

    // copy() 메서드 사용
    val discountedProduct = product1.copy(price = 1000.0)
    println("Original: $product1")
    println("Discounted: $discountedProduct")

    // 구조 분해 (Destructuring)
    val (name, price, category) = product1
    println("Name: $name, Price: $price, Category: $category")
}

3.10. sealed 클래스 (Sealed Class)

sealed 클래스는 제한된 클래스 계층 구조를 만들 때 사용하는 특별한 클래스이다. 모든 하위 클래스가 컴파일 타임에 알려져 있어야 하므로, when 식에서 모든 경우를 처리했는지 컴파일러가 검증할 수 있다. 이는 안전한 상태 관리나 결과 처리에 매우 유용하다.

  • sealed 키워드로 정의
  • 하위 클래스는 같은 파일 내에서만 정의 가능
  • when 식에서 else 분기가 필요 없음 (모든 경우를 다룬 경우)
  • 주로 상태 관리, API 응답 처리, 결과 타입 등에 사용
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handleResult(result: Result<String>) {
    when (result) {
        is Result.Success -> {
            println("작업 성공: ${result.data}")
        }
        is Result.Error -> {
            println("오류 발생: ${result.exception.message}")
        }
        Result.Loading -> {
            println("로딩 중...")
        }
        // sealed class이므로 else 문이 필요하지 않음
    }
}

// 실제 사용 예시
fun fetchData(): Result<String> {
    return try {
        // 데이터 가져오기 시뮬레이션
        Result.Success("데이터를 성공적으로 가져왔습니다!")
    } catch (e: Exception) {
        Result.Error(e)
    }
}

fun main() {
    val loadingResult = Result.Loading
    val successResult = Result.Success("Hello World")
    val errorResult = Result.Error(Exception("네트워크 오류"))

    handleResult(loadingResult)
    handleResult(successResult)
    handleResult(errorResult)
    
    // 실제 함수 호출
    val result = fetchData()
    handleResult(result)
}

3.11. 객체 선언 (Object Declaration)

객체 선언은 싱글톤 패턴을 구현하는 코틀린의 간편한 방법이다. object 키워드를 사용하여 클래스를 정의하면 해당 클래스의 인스턴스는 애플리케이션 전체에서 오직 하나만 존재한다. 데이터베이스 연결, 로그 관리, 설정 관리 등 전역적으로 하나의 인스턴스만 필요한 경우에 사용한다.

  • object 키워드로 정의
  • 자동으로 싱글톤 패턴 구현
  • 클래스 선언과 동시에 인스턴스 생성
  • 생성자를 가질 수 없음 (매개변수를 받을 수 없음)
object DatabaseManager {
    private var isConnected = false
    
    fun connect() {
        if (!isConnected) {
            println("데이터베이스에 연결되었습니다.")
            isConnected = true
        }
    }
    
    fun disconnect() {
        if (isConnected) {
            println("데이터베이스 연결이 해제되었습니다.")
            isConnected = false
        }
    }
    
    fun executeQuery(query: String) {
        if (isConnected) {
            println("쿼리 실행: $query")
        } else {
            println("데이터베이스에 연결되지 않았습니다.")
        }
    }
}

fun main() {
    DatabaseManager.connect()
    DatabaseManager.executeQuery("SELECT * FROM users")
    DatabaseManager.disconnect()
}