본문 바로가기
Android (Kotlin & Compose)/Part 1. 개발 시작과 코틀린

[오늘의 코드 조각] [1-3] 코틀린 객체지향 프로그래밍

by 생각하는 개발자님 2025. 8. 26.
반응형

안녕하세요, '생각하는 개발자'입니다.

지난 시간에는 변수, 함수 등 코드를 구성하는 가장 작은 단위들을 배웠습니다. 하지만 앱이 복잡해지면 수많은 변수와 함수들이 뒤죽박죽 섞여 관리하기 어려워지겠죠.

그래서 우리는 **객체지향 프로그래밍(Object-Oriented Programming, OOP)**이라는 강력한 설계 방법을 사용합니다. 말이 조금 어렵게 들리지만, 사실은 **'세상의 사물을 흉내 내어 코드를 정리하는 방법'**이라고 생각하면 쉽습니다. 이 개념을 이해하면 훨씬 더 체계적이고 재사용하기 좋은 코드를 작성할 수 있습니다.

 

3.1. 클래스와 객체: 붕어빵 틀과 붕어빵

OOP의 가장 핵심적인 두 가지 개념은 바로 **클래스(Class)**와 **객체(Object)**입니다.

  • 클래스(Class): 객체를 만들기 위한 설계도 또는 틀입니다. (예: 붕어빵 틀)
  • 객체(Object): 그 설계도를 바탕으로 실제로 만들어진 실체입니다. (예: 붕어빵)

붕어빵 틀(클래스) 하나만 있으면, 수많은 붕어빵(객체)을 똑같은 모양으로 찍어낼 수 있죠. 코틀린 코드로 직접 확인해 봅시다.

 

// '몬스터'라는 설계도를 만든다.
class Monster {
    var name: String = "슬라임"
    var level: Int = 1

    fun attack() {
        println("${name}(Lv.${level})이(가) 공격했다!")
    }
}

fun main() {
    // 'Monster' 설계도로 실제 몬스터 2마리를 만든다.
    val monster1 = Monster() // 첫 번째 몬스터 객체 생성
    val monster2 = Monster() // 두 번째 몬스터 객체 생성

    monster1.attack() // 출력: 슬라임(Lv.1)이(가) 공격했다!

    // 두 번째 몬스터의 능력치를 바꿔보자.
    monster2.name = "고블린"
    monster2.level = 3
    monster2.attack() // 출력: 고블린(Lv.3)이(가) 공격했다!
}

 

Monster라는 클래스(설계도)를 한 번 정의해두니, monster1과 monster2라는 속성(이름, 레벨)과 행동(공격)을 가진 실제 객체들을 쉽게 만들어낼 수 있습니다.

 

3.2. 생성자: 객체가 태어날 때 정해지는 것

위 예제에서는 모든 몬스터가 일단 '슬라임'으로 태어난 뒤에 이름을 바꿔줘야 했습니다. 불편하죠? **생성자(Constructor)**를 사용하면 객체가 만들어지는 그 순간에 속성 값을 지정해 줄 수 있습니다.

 

// 클래스 이름 옆에 ( )를 열고 생성자를 정의한다.
class Monster(val name: String, var level: Int) {
    fun attack() {
        println("${name}(Lv.${level})이(가) 공격했다!")
    }
}

fun main() {
    // 객체를 만들 때 바로 이름과 레벨을 지정해준다.
    val monster1 = Monster("오크", 5)
    val monster2 = Monster("드래곤", 99)

    monster1.attack() // 출력: 오크(Lv.5)이(가) 공격했다!
    monster2.attack() // 출력: 드래곤(Lv.99)이(가) 공격했다!
}

이제 몬스터를 만들 때마다 각기 다른 초기 능력치를 부여할 수 있게 되어 훨씬 더 유용해졌습니다.

3.3. 상속: 설계도를 물려받아 확장하기

만약 '보스 몬스터'처럼 일반 몬스터의 특징을 모두 가지면서, 추가로 특별한 스킬까지 가진 존재를 만들고 싶다면 어떡할까요? Monster 클래스의 코드를 전부 복사해서 붙여넣어야 할까요?

이럴 때 사용하는 것이 바로 **상속(Inheritance)**입니다. 부모 클래스(설계도)의 모든 속성과 행동을 물려받아, 자식 클래스(설계도)에서 기능을 추가하거나 변경할 수 있습니다.

// 'open' 키워드를 붙여 다른 클래스가 상속할 수 있도록 허용한다.
open class Monster(val name: String, var level: Int) {
    fun attack() {
        println("${name}(Lv.${level})이(가) 공격했다!")
    }
}

// Monster 클래스를 상속받는 BossMonster 클래스를 만든다.
class BossMonster(name: String, level: Int, val specialSkill: String) : Monster(name, level) {
    fun useSpecialSkill() {
        println("${name}이(가) 필살기 '${specialSkill}'을(를) 사용했다!")
    }
}

fun main() {
    val boss = BossMonster("발록", 150, "파이어 브레스")

    boss.attack()          // 부모(Monster)로부터 물려받은 기능
    boss.useSpecialSkill() // 자식(BossMonster)만의 새로운 기능

    // 출력:
    // 발록(Lv.150)이(가) 공격했다!
    // 발록이(가) 필살기 '파이어 브레스'을(를) 사용했다!
}

BossMonster는 Monster의 attack() 기능을 그대로 물려받았기 때문에 코드를 중복해서 작성할 필요가 없었습니다. 이것이 바로 상속의 강력함입니다.

지금까지 객체지향 프로그래밍의 가장 기본적인 세 가지 기둥인 클래스/객체, 생성자, 상속에 대해 알아보았습니다. 이 개념들은 앞으로 우리가 만들 모든 안드로이드 앱의 뼈대를 이루게 될 것입니다.

반응형