공부가 필요하다고 느낀 문법을 정리한 글입니다.
OOP(Object-Oriented Programming), 즉 객체지향 프로그래밍은 프로그램을 객체라는 단위로 나누어 바라보고 구성하는 방식입니다.
프로그래밍을 하다 보면 한 번쯤은 반드시 접하게 되는 개념이기도 합니다.
객체지향 언어를 사용한다는 것은 결국 객체(Object) 를 중심으로 코드를 구성한다는 뜻입니다.
그리고 이 객체는 보통 클래스(Class) 를 바탕으로 생성됩니다.
쉽게 말하면 클래스는 객체를 만들기 위한 설계도이고,
객체는 그 설계도를 바탕으로 실제로 만들어진 결과물입니다.
이번 글에서는 객체를 정의하는 핵심 문법인 클래스에 대해 정리해보려고 합니다.
클래스(Class)는 객체지향 프로그래밍에서 객체를 생성하기 위한 설계도입니다.
클래스 안에는 객체가 가져야 할 상태(프로퍼티) 와 동작(메서드) 를 정의할 수 있습니다.
즉, 같은 구조를 가지되 서로 다른 값을 가진 여러 객체를 만들 수 있게 해주는 틀이라고 볼 수 있습니다.
예를 들어 User라는 클래스를 만들면,
이 클래스를 바탕으로 이름과 나이가 다른 여러 사용자를 만들 수 있습니다.
클래스를 정의할 때 가장 기본이 되는 생성자가 주 생성자(primary constructor) 입니다.
주 생성자는 클래스 이름 옆에 바로 선언하며, Kotlin에서는 이를 통해 객체 생성에 필요한 값을 간결하게 받을 수 있습니다.
class User(val name: String, var age: Int)
fun main() {
val user = User("DH", 24)
println(user.name) // DH
}
위 코드에서 name, age는 User 객체가 생성될 때 함께 전달되는 값입니다.
이처럼 Kotlin은 주 생성자를 이용해 클래스를 짧고 명확하게 표현할 수 있다는 장점이 있습니다.
initinit 블록은 객체가 생성되는 시점에 실행되는 코드 블록입니다.
주 생성자로 받은 값을 바탕으로 추가 작업을 하거나, 유효성 검사를 할 때 자주 사용합니다.
class User(val name: String, val age: Int) {
init {
println("User가 생성되었습니다! 이름 : $name, 나이 : $age")
}
}
fun main() {
val newUser = User("DH", 24)
}
위 예제에서는 User 객체가 생성될 때마다 init 블록이 실행됩니다.
즉, 단순히 값을 저장하는 것뿐 아니라 객체 생성 시 필요한 초기 작업을 함께 넣을 수 있습니다.
init으로 유효성 검사하기init 블록은 값을 검증하는 데도 유용합니다.
예를 들어 이름 길이에 제한을 두고 싶다면 아래처럼 작성할 수 있습니다.
class User(val name: String, val age: Int) {
init {
require(name.length <= 3) { "이름은 3글자를 넘어갈 수 없습니다." }
println("User가 생성되었습니다! 이름 : $name, 나이 : $age")
}
}
fun main() {
val newUser = User("HDH1234", 24)
}
이 경우 조건을 만족하지 않으면 객체 생성 과정에서 예외가 발생합니다.
throw를 직접 써도 되지만,
이처럼 조건 검증에는 require()를 사용하는 방식도 자주 사용됩니다.
클래스에 생성자를 추가로 정의하고 싶을 때는 부 생성자(secondary constructor) 를 사용할 수 있습니다.
부 생성자는 constructor 키워드로 선언하며, 보통 주 생성자를 보완하는 용도로 사용합니다.
class Person(val name: String, val age: Int) {
var email: String = "unknown@example.com"
constructor(name: String, age: Int, email: String) : this(name, age) {
this.email = email
}
}
fun main() {
val person1 = Person("DH", 24)
println("${person1.name}, ${person1.email}") // DH, unknown@example.com
val person2 = Person("DH", 24, "dh@example.com")
println("${person2.name}, ${person2.email}") // DH, dh@example.com
}
위 코드에서는 기본적으로 이름과 나이만 받아 객체를 만들 수 있고,
필요하면 이메일까지 함께 받는 방식으로 생성자를 확장하고 있습니다.
즉, 부 생성자는 객체를 만드는 여러 방법이 필요할 때 사용할 수 있습니다.
thisthis는 현재 객체 자기 자신을 가리키는 키워드입니다.
같은 이름의 지역 변수와 프로퍼티를 구분하거나, 현재 객체의 멤버를 명확하게 나타내고 싶을 때 사용합니다.
class Box(val width: Int) {
fun printWidth() {
println("너비 : ${this.width}")
}
}
fun main() {
Box(25).printWidth() // 너비 : 25
}
위 예제에서 this.width는 현재 Box 객체의 width 프로퍼티를 의미합니다.
다만 Kotlin에서는 생략 가능한 경우가 많아서,
실제로는 아래처럼 쓰는 경우도 많습니다.
println("너비 : $width")
즉, this는 항상 필요한 것은 아니지만,
의도를 분명하게 보여주고 싶을 때 유용하게 사용할 수 있습니다.
override객체지향 프로그래밍에서는 기존 클래스를 바탕으로 새로운 클래스를 만드는 상속 개념도 자주 사용합니다.
Kotlin에서는 기본적으로 클래스가 상속 불가능하게 되어 있기 때문에,
상속을 허용하려면 open 키워드를 붙여야 합니다.
그리고 부모 클래스의 메서드를 자식 클래스에서 다시 정의할 때는 override를 사용합니다.
open class Animal {
open fun speak() {
println("동물 소리")
}
}
class Cat : Animal() {
override fun speak() {
println("야옹!")
}
}
fun main() {
val cat = Cat()
cat.speak() // 야옹!
}
위 코드에서 Animal은 부모 클래스이고, Cat은 이를 상속받은 자식 클래스입니다.
Cat은 speak()를 자신에 맞게 다시 정의하고 있습니다.
즉, override는 부모 클래스의 기능을 그대로 쓰는 것이 아니라,
자식 클래스에 맞게 다시 정의해서 사용하는 것이라고 이해하시면 됩니다.
추상 클래스는 직접 객체를 만들기 위한 클래스라기보다, 자식 클래스가 따라야 할 틀을 제공하는 클래스입니다.
즉, 부모 클래스는 공통 구조만 제시하고, 구체적인 구현은 자식 클래스가 맡게 됩니다.
abstract class Animal {
abstract fun sound()
}
class Cat : Animal() {
override fun sound() {
println("야옹!")
}
}
fun main() {
val cat = Cat()
cat.sound() // 야옹!
}
위 코드에서 Animal은 추상 클래스이기 때문에 직접 객체를 만들 수 없습니다.
대신 Cat이 sound()를 구현해서 실제 동작을 완성하고 있습니다.
즉, 추상 클래스는 “이 기능은 반드시 필요하지만, 구현은 자식에게 맡기겠다” 는 의도를 담을 때 사용합니다.
정리하면 클래스 안에는 보통 다음과 같은 요소들이 들어갑니다.
즉, 클래스는 단순히 변수만 모아두는 것이 아니라,
객체의 상태와 동작을 함께 정의하는 구조라고 볼 수 있습니다.
이번 글에서는 Kotlin에서 객체를 정의하는 기본 문법인 클래스에 대해 정리해보았습니다.
클래스는 객체를 만들기 위한 설계도이며,
그 안에는 객체의 상태와 동작을 표현하는 여러 요소가 들어갑니다.
정리하면 다음과 같습니다.
init 블록은 객체 생성 시 실행되는 초기화 코드입니다.this는 현재 객체 자기 자신을 가리킵니다.override는 부모 클래스의 기능을 자식 클래스에서 재정의할 때 사용합니다.클래스는 객체지향 프로그래밍의 가장 기본이 되는 개념인 만큼,
생성자와 초기화, 상속과 오버라이드까지 함께 이해해두면 이후 Kotlin 문법을 공부할 때도 훨씬 수월해집니다.