시스템아 미안해

Classes 본문

책/Kotlin Essentials

Classes

if else 2026. 1. 12. 00:23

주변의 세계를 한 번 둘러보면, 수많은 객체들을 쉽게 발견할 수 있을 것이다.
책일 수도 있고, 전자책 리더기일 수도 있으며, 모니터나 커피 머그잔일 수도 있다. 우리는 객체들에 둘러싸여 살아간다.

 

이러한 관점은 우리가 객체로 이루어진 세계에 살고 있다는 결론으로 이어지며, 따라서 프로그램 역시 같은 방식으로 구성되어야 한다는 생각으로 확장된다. 이것이 바로 객체 지향 프로그래밍(Object-Oriented Programming) 접근 방식의 개념적 기반이다.

 

모든 사람이 이 세계관에 동의하는 것은 아니다.
일부는 세계를 가능한 행동들의 집합으로 바라보는 것을 선호하는데, 이것이 함수형 프로그래밍(Functional Programming) 접근 방식의 개념적 기반이다.
하지만 어떤 접근 방식을 선호하든 간에, 클래스와 객체는 코틀린 프로그래밍에서 중요한 구조임에는 변함이 없다.

 

클래스는 구체적인 특성을 가진 객체를 생성하기 위한 템플릿이다.
코틀린에서 클래스를 만들기 위해서는 class 키워드 뒤에 클래스 이름을 작성하면 된다.
가장 단순한 클래스의 경우, 이것이 전부다. 클래스 본문은 선택 사항이기 때문이다.

 

클래스의 인스턴스인 객체를 생성하기 위해서는 기본 생성자 함수를 사용한다. 이는 클래스 이름 뒤에 괄호를 붙이는 방식이다.
C++이나 Java와 같은 다른 언어들과 달리, 코틀린에서는 new 키워드를 사용하지 않는다.

 
 
kotlin
// 가장 간단한 클래스 정의
class A

fun main() {
    // 클래스로부터 객체 생성
    val a: A = A()
}

Member Functions (멤버 함수)

클래스 내부에 함수를 정의할 수 있습니다. 그렇게 하려면 먼저 클래스 정의에서 중괄호를 열어 클래스 본문을 지정해야 합니다.

 
 
kotlin
class A {
    // 클래스 본문
}

여기서 함수를 지정할 수 있습니다. 이런 방식으로 정의된 함수는 두 가지 중요한 특성을 가집니다:

  • 함수는 이 클래스의 인스턴스에서 호출되어야 합니다. 즉, 메서드를 호출하려면 먼저 객체를 생성해야 합니다
  • 메서드 내부에서 this를 사용할 수 있는데, 이것은 이 메서드를 호출한 클래스의 인스턴스에 대한 참조입니다
 
 
kotlin
class A {
    fun printMe() {
        println(this)
    }
}

fun main() {
    val a = A()
    println(a) // A@ADDRESS
    a.printMe() // A@ADDRESS (동일한 주소)
}

클래스 본문 내부에 정의된 모든 요소를 **멤버(members)**라고 합니다.

따라서 클래스 본문 내부에 정의된 함수를 **멤버 함수(member function)**라고 합니다.

클래스와 연관된 함수는 **메서드(methods)**라고 불리며, 멤버 함수와 확장 함수를 포함합니다.

개념적으로 메서드는 객체가 할 수 있는 것을 나타냅니다.

예를 들어, 커피머신 클래스의 경우 makeCoffee 메서드로 표현될 수 있습니다.

이것이 메서드가 있는 클래스가 실제 세계를 모델링하는 데 어떻게 도움이 되는지입니다.

Properties (프로퍼티)

클래스 본문 내부에 변수도 정의할 수 있습니다. 클래스 본문 내부에 정의된 변수를 fields라고 합니다. "encapsulation"이라고 알려진 중요한 아이디어가 있는데, 이는 필드가 클래스 외부에서 직접 사용되거나 수정되어서는 안 된다는 것을 의미합니다. 그런 일이 발생하면 상태에 대한 통제력을 잃게 됩니다. 대신 필드는 접근자(accessor)를 통해 사용되고 설정되어야 합니다:

  • getter: 이 필드의 현재 값을 가져오는 데 사용되는 함수
  • setter: 이 필드에 새 값을 설정하는 데 사용되는 함수

이 패턴은 매우 영향력이 있습니다. Java 프로젝트에서는 데이터를 보관하는 클래스에 사용되는 수많은 getter와 setter 함수를 볼 수 있습니다. 이들은 캡슐화를 달성하는 데 주로 사용되지만, 불필요한 보일러플레이트 코드를 만들기도 합니다. 따라서 언어 제작자들은 더 강력한 개념인 **"properties"**를 발명했습니다. **프로퍼티(property)**는 클래스 내부에서 자동으로 캡슐화되는 변수로, getter와 setter가 자동으로 생성됩니다. Kotlin에서 클래스 내부에 정의된 모든 변수는 프로퍼티이지 필드가 아닙니다.

일부 언어(예: JavaScript)는 프로퍼티에 대한 내장 지원을 가지고 있지만 Java는 그렇지 않습니다. 따라서 Kotlin/JVM에서는 각 프로퍼티에 대해 접근자 함수가 생성됩니다: var에 대해서는 getter와 setter가, val에 대해서는 getter만 생성됩니다.

 
 
kotlin
// Kotlin 코드
class User {
    var name: String = ""
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "Alex" // setter 호출
    println(user.name) // getter 호출
}
 
 
javascript
// 동등한 JavaScript 코드
function User() {
    this.name = '';
}

function main(args) {
    var user = new User();
    user.name = 'Alex';
    println(user.name);
}
 
 
java
// 동등한 Java 코드
public final class User {
    @NotNull
    private String name = "";
    
    // getter
    @NotNull
    public final String getName() {
        return this.name;
    }
    
    // setter
    public final void setName(@NotNull String name) {
        this.name = name;
    }
}

public final class PlaygroundKt {
    public static void main(String[] var0) {
        User user = new User();
        user.setName("Alex"); // setter 호출
        System.out.println(user.getName()); // getter 호출
    }
}

Kotlin의 각 프로퍼티는 접근자를 가지므로, 명시적 함수를 사용하여 getter나 setter를 정의해서는 안 됩니다. 기본 접근자를 변경하고 싶다면 특별한 구문이 있습니다.

 
 
kotlin
class User {
    private var name: String = ""
    
    // 프로퍼티 GETTER를 정의하지 마세요!
    fun getName() = name
    
    // 프로퍼티 SETTER를 정의하지 마세요!
    fun setName(name: String) {
        this.name = name
    }
}

커스텀 getter를 지정하려면 프로퍼티 정의 후에 get 키워드를 사용합니다. 나머지는 파라미터가 없는 함수를 정의하는 것과 동일합니다. 이 함수 내부에서 field 키워드를 사용하여 백킹 필드에 대한 참조를 얻을 수 있습니다. 기본 getter는 필드 값을 반환하지만, 이 동작을 변경하여 이 값을 수정하거나 다른 방식으로 반환되도록 할 수 있습니다. getter를 정의할 때는 단일 표현식 구문이나 일반 본문과 return 키워드를 사용할 수 있습니다.

 
 
kotlin
class User {
    var name: String = ""
        get() = field.uppercase()
    // 또는
    // var name: String = ""
    //     get() {
    //         return field.uppercase()
    //     }
}

fun main() {
    val user = User()
    user.name = "norbert"
    println(user.name) // NORBERT
}

getter는 항상 프로퍼티와 동일한 가시성과 결과 타입을 가져야 합니다. Getter는 예외를 던져서는 안 되며 집약적인 계산을 수행해서는 안 됩니다.

모든 프로퍼티 사용이 접근자 사용임을 주의하세요. 접근자 내부에서는 프로퍼티 이름 대신 field를 사용해야 합니다. 그렇지 않으면 무한 재귀에 빠지게 됩니다.

 
 
kotlin
class User {
    // 이렇게 하지 마세요!
    var name: String = ""
        // getter 내부에서 프로퍼티 이름을 사용하면
        // 무한 재귀로 이어집니다
        get() = name.uppercase()
}

fun main() {
    val user = User()
    user.name = "norbert"
    println(user.name) // Error: java.lang.StackOverflowError
}

//내부 동작
val user = User()
user.name = "Amy"  // setter 호출
    ↓
set(value) {
    name = value  // 이게 다시 setter를 호출!
        ↓
    set(value) {
        name = value  // 또 setter 호출!
            ↓
        set(value) {
            name = value  // 계속 반복...
            // StackOverflowError!

Setter도 유사하게 지정할 수 있지만, set 키워드를 사용해야 하며, 설정되는 값을 나타내는 단일 파라미터가 필요합니다. 기본 setter는 필드에 새 값을 할당하는 데 사용되지만, 이 동작을 수정할 수 있습니다. 어떤 조건을 만족하는 경우에만 새 값을 설정하는 인스턴스 setter를 만들 수 있습니다.

 
 
kotlin
class User {
    var name: String = ""
        get() = field.uppercase()
        set(value) {
            if (value.isNotBlank()) {
                field = value
            }
        }
}

fun main() {
    val user = User()
    user.name = "norbert"
    user.name = ""
    user.name = "  "
    println(user.name) // NORBERT
}

이것이 대부분의 Kotlin 클래스가 정의되는 방식입니다: 프로퍼티가 있는 primary constructor를 사용합니다.

 

Kotlin과 Java의 클래스 비교

Kotlin과 Java에서 정의된 클래스를 비교할 때, Kotlin이 얼마나 많은 보일러플레이트 코드를 제거했는지 알 수 있습니다. 이름, 성, 나이를 가진 간단한 사용자를 나타내기 위해 일반적인 구현은 다음과 같습니다:

 
 
java
public final class User {
    @NotNull
    private final String name;
    @NotNull
    private final String surname;
    private final int age;
    
    public User(
        @NotNull String name,
        @NotNull String surname,
        int age
    ) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }
    
    @NotNull
    public String getName() {
        return name;
    }
    
    @NotNull
    public String getSurname() {
        return surname;
    }
    
    public int getAge() {
        return age;
    }
}

Kotlin에서는 동일한 클래스를 다음과 같이 표현합니다:

 
 
kotlin
class User(
    val name: String,
    val surname: String,
    val age: Int,
)

컴파일 결과는 실질적으로 동일합니다. getter와 생성자가 모두 있습니다. 믿기지 않는다면 직접 확인해보세요Kotlin은 간결하지만 강력한 언어입니다.

Inner Classes (내부 클래스)

Kotlin에서는 클래스 내부에 클래스를 정의할 수 있습니다.

이들은 기본적으로 static이며, 이는 바깥 클래스에 접근할 수 없다는 것을 의미합니다. (Java의 static nested class)

따라서 바깥 클래스의 인스턴스를 생성하지 않아도 생성할 수 있습니다.

 
 
kotlin
class Puppy(val name: String) {
    
    class InnerPuppy {
        fun think() {
            // 여기서는 name에 접근할 수 없습니다
            println("Inner puppy is thinking")
        }
    }
}

fun main() {
    val innerPuppy = Puppy.InnerPuppy()
    // 클래스에서 InnerPuppy를 생성했지, 객체가 아닙니다
    innerPuppy.think() // Inner puppy is thinking
}

 

내부 클래스가 외부 클래스에 대한 참조를 갖도록 하려면 inner 수정자를 사용하여 inner로 만들어야 합니다. (Java의 inner class)

하지만 이러한 클래스에서 객체를 생성하려면 외부 클래스의 인스턴스가 필요합니다.

 
 
kotlin
class Puppy(val name: String) {
    
    inner class InnerPuppy {
        fun think() {
            println("Inner $name is thinking")
        }
    }
}

fun main() {
    val puppy = Puppy("Cookie")
    val innerPuppy = puppy.InnerPuppy() // puppy가 필요합니다
    innerPuppy.think() // Inner Cookie is thinking
}

표준 라이브러리에서 내부 클래스의 예시는 다음과 같습니다:

  • iterator의 private 구현
  • 바깥 클래스와 안쪽 클래스 사이에 밀접한 연관성이 있는 경우의 클래스들,
    그리고 라이브러리 네임스페이스에 또 다른 이름을 정의하지 않기 위해 사용되는 내부 클래스.
 
 
kotlin
// Kotlin stdlib의 클래스
class FileTreeWalk(
    ...
) : Sequence<File> {
    
    /** 파일을 순회하는 iterator를 반환합니다. */
    override fun iterator(): Iterator<File> =
        FileTreeWalkIterator()
    
    private inner class FileTreeWalkIterator
        : AbstractIterator<File>() {
        ...
    }
    
    ...
}

요약

보시다시피, 코틀린에서는 매우 간결한 문법을 사용하여 클래스를 정의할 수 있으며, 그 결과 가독성 또한 매우 뛰어납니다.
주 생성자(primary constructor)는 매우 훌륭한 개념이며, 코틀린이 필드 대신 프로퍼티를 사용한다는 점 역시 인상적입니다. 또한 내부 클래스(inner class)에 대해서도 학습하셨습니다.

이 모든 내용은 매우 유용하지만, 객체 지향 스타일을 선호하는 개발자들에게 매우 중요한 개념인 상속(inheritance) 에 대해서는 아직 다루지 않았습니다.
다음 장에서는 인터페이스(interface) 와 추상 클래스(abstract class) 와 함께 이 주제를 자세히 살펴보겠습니다.

'책 > Kotlin Essentials' 카테고리의 다른 글

Data Classes  (1) 2026.01.18
Inheritance  (1) 2026.01.16
Nullability  (0) 2026.01.11
The power of the for loop  (0) 2026.01.11
Functions  (0) 2026.01.11