객체지향 프로그래밍에 한번 짚고 넘어가 볼까요?

객체지향 프로그래밍 (OOP)은 프로그래밍 패러다임 중 하나로, 프로그램을 단순히 함수나 명령어의 집합으로 보는 것이 아니라, 데이터를 객체라는 단위로 묶어 처리하는 방법입니다. 객체는 상태와 행동을 갖는 독립적인 단위로, 현실 세계의 사물이나 개념을 프로그래밍으로 표현할 때 유용합니다.

객체지향 프로그래밍은 코드의 재사용성과 유지보수성을 높이는 데 매우 유용한 패러다임입니다. 코틀린은 이러한 객체지향 프로그래밍을 쉽게 구현할 수 있는 언어로, 클래스, 상속, 캡슐화, 다형성, 추상화, 인터페이스 등의 개념을 통해 효율적인 프로그램을 작성할 수 있습니다.

1. 객체와 클래스

객체는 상태와 행동을 가진 독립적인 단위입니다. 예를 들어, 고양이라는 객체는 "이름", "나이", "색깔" 등의 상태(속성)를 가지며, "울기", "걷기" 등의 행동(메서드)을 가질 수 있습니다. 객체는 특정 클래스의 인스턴스입니다.

클래스는 객체를 만들기 위한 청사진입니다. 클래스는 객체의 속성과 행동을 정의합니다. 클래스는 속성(멤버 변수)과 메서드(멤버 함수)로 구성됩니다.

// 고양이 클래스를 정의합니다.
class Cat(val name: String, var age: Int, val color: String) {
    // 고양이의 행동을 정의합니다.
    fun meow() {
        println("$name: Meow!")
    }

    fun getOlder() {
        age++
    }
}

// 고양이 객체를 생성합니다.
val myCat = Cat("Whiskers", 2, "Gray")

// 고양이 객체의 메서드를 호출합니다.
myCat.meow()  // 출력: Whiskers: Meow!
myCat.getOlder()
println(myCat.age)  // 출력: 3

위의 코드에서 Cat 클래스는 고양이의 이름, 나이, 색깔을 속성으로 가지고, meowgetOlder라는 두 개의 메서드를 가집니다. myCat 객체는 Cat 클래스의 인스턴스로 생성되어, 해당 객체의 속성과 메서드를 사용할 수 있습니다.

2. 상속

상속은 기존 클래스의 속성과 메서드를 다른 클래스가 물려받아 사용하는 기능입니다. 상속을 통해 코드 재사용성을 높일 수 있습니다. 상속을 사용하면, 새로운 클래스는 기존 클래스의 기능을 확장하거나 변경할 수 있습니다.

// 동물 클래스를 정의합니다.
open class Animal(val name: String, var age: Int) {
    fun eat() {
        println("$name is eating.")
    }
}

// 동물 클래스를 상속받아 고양이 클래스를 정의합니다.
class Cat(name: String, age: Int, val color: String) : Animal(name, age) {
    fun meow() {
        println("$name: Meow!")
    }
}

val myCat = Cat("Whiskers", 2, "Gray")
myCat.eat()  // 출력: Whiskers is eating.
myCat.meow()  // 출력: Whiskers: Meow!

위의 예제에서 Animal 클래스는 nameage 속성을 가지고, eat 메서드를 정의합니다. Cat 클래스는 Animal 클래스를 상속받아 nameage 속성을 물려받고, meow라는 추가적인 메서드를 정의합니다. Cat 클래스는 Animal 클래스의 모든 속성과 메서드를 사용할 수 있습니다.

3. 캡슐화

캡슐화는 객체의 속성을 외부에서 직접 접근하지 못하도록 하고, 메서드를 통해서만 접근할 수 있도록 하는 것입니다. 이를 통해 객체의 상태를 보호하고, 잘못된 사용으로부터 객체를 안전하게 지킬 수 있습니다. 코틀린에서는 접근 제한자를 사용하여 캡슐화를 구현합니다.

class Person(private var name: String, private var age: Int) {
    // Getter 메서드
    fun getName() = name
    fun getAge() = age

    // Setter 메서드
    fun setName(newName: String) {
        if (newName.isNotBlank()) {
            name = newName
        }
    }

    fun setAge(newAge: Int) {
        if (newAge > 0) {
            age = newAge
        }
    }
}

val person = Person("John", 30)
println(person.getName())  // 출력: John
person.setAge(31)
println(person.getAge())  // 출력: 31

위의 예제에서 Person 클래스는 nameage 속성을 private로 정의하여 외부에서 직접 접근할 수 없도록 했습니다. 대신, getName, getAge, setName, setAge 메서드를 통해서만 nameage 속성에 접근하고 변경할 수 있습니다. 이렇게 하면 속성에 대한 잘못된 접근을 방지할 수 있습니다.

4. 다형성

다형성은 동일한 인터페이스나 상위 클래스를 통해 서로 다른 형태의 객체를 다룰 수 있게 하는 기능입니다. 이를 통해 유연하고 확장성 있는 코드를 작성할 수 있습니다. 다형성은 주로 메서드 오버라이딩과 인터페이스를 통해 구현됩니다.

open class Animal {
    open fun sound() {
        println("Some sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        println("Bark")
    }
}

class Cat : Animal() {
    override fun sound() {
        println("Meow")
    }
}

fun makeSound(animal: Animal) {
    animal.sound()
}

val dog = Dog()
val cat = Cat()

makeSound(dog)  // 출력: Bark
makeSound(cat)  // 출력: Meow

위의 예제에서 Animal 클래스는 sound 메서드를 정의하고, DogCat 클래스는 이를 오버라이딩하여 각각의 소리를 정의합니다. makeSound 함수는 Animal 타입의 객체를 받아서 sound 메서드를 호출합니다. 이 함수는 전달받은 객체의 실제 타입에 따라 적절한 sound 메서드를 호출합니다. 이렇게 하면 코드의 유연성이 증가합니다.