# Table of Contents
# 객체지향 프로그래밍
객체지향 프로그래밍
은 모든 대상을 객체(Object)
로 바라본다. Kotlin 역시 객체지향 프로그래밍 언어이며, 객체지향 프로그래밍을 이해하려면 클래스와 인스턴스에 대해 알아야 한다.
# 클래스와 인스턴스
클래스와 인스턴스를 설명할 때 와플 기계와 와플을 예로 많이 든다.
클래스(Class)
는 인스턴스를 만드는 틀, 인스턴스(Instance)
는 클래스로 만든 무언가다. 즉 클래스는 와플 기계, 인스턴스 와플이다. 클래스를 코드로 표현하면 다음과 같다.
class Waffle {
// ...
}
인스턴스는 다음과 같이 생성한다. 클래스 이름 뒤에 ()
를 붙여주면 된다.
var waffle = Waffle()
이렇게 만든 인스턴스는 객체(Object)
라고도 한다.
# 생성자
생성자(Constructor)
는 클래스의 인스턴스를 생성할 때 호출되는 구문이며, 보통 초기화 작업을 수행한다. 생성자는 키워드constructor
를 사용하여 선언한다.
class Person {
// 멤버변수
String name
// 생성자
constructor() {
// 멤버변수 초기화 작업 수행
this.name = "John"
println("This is constructor.")
}
}
이 생성자는 클래스의 인스턴스를 생성할 때 호출된다.
Person person = person()
// This is constructor.
Kotlin에는 두 종류의 생성자가 있다.
# 기본 생성자
기본 생성자
는 클래스 이름 뒤에 키워드constructor
를 붙여서 만든다.
class Person constructor(name: String) {
// ...
}
기본 생성자는 별도의 초기화 구문이 존재하지 않는다. 이 때는 init
구문을 사용할 수 있다.
class Person constructor(name: String) {
// 멤버변수 선언
var name: String
// 초기화 작업 수행
init {
println("This is constructor.")
}
}
// 클래스의 인스턴스 생성
var person = Person("Paul")
print(person.name) // Paul
키워드constructor
는 생략할 수 있다.
class Person(name: String) {
// 멤버변수 선언
var name: String
// 초기화 구문
init {
this.name = name
}
}
var person = Person("Paul")
print(person.name)
생성자 안에서 멤버변수를 선언할 수도 있다.
class Person (var name: String) {
// var name: String
}
위처럼 코드를 작성하는 경우, 클래스를 생성할 때 전달한 값 전달인자가 매개변수에 자동으로 초기화된다.
var person = Person("Paul")
print(person.name) // Paul
# 보조 생성자
기본 생성자를 정의하지 않고 아래와 같이 보조 생성자
만을 사용할 수도 있다.
class Person {
// 멤버변수
var name: String
// 보조 생성자
constructor(name: String) {
this.name = name
}
}
var person = Person("Paul")
println(person.name)
# 멤버 변수와 메소드
멤버 변수
와 메소드
는 클래스 내부에 다음과 같이 선언한다.
class Person {
// 멤버 변수
var name: String
// 생성자
constructor(name: String) {
this.name = name
}
// 메소드
fun printName() {
println("My name is ${this.name}")
}
}
이렇게 선언한 클래스의 인스턴스를 다음과 같이 생성한다.
var person = Person("Ross")
인스턴스의 메소드는 다음과 같이 호출할 수 있다.
person.printName()
인스턴스의 멤버 변수는 다음과 같이 접근할 수 있다.
var name = person.name
# 상속
실생활에서의 상속은 자식의 부모의 재산을 물려받는 행위를 뜻한다. Kotlin에서도 상속은 비슷한 의미로 사용된다. 상속(Inheritance)
은 부모 클래스의 멤버 변수나 메소드를 자식 클래스가 그대로 물려받는 것을 의미한다. 이를 통해 코드의 중복을 제거할 수 있다.
# final
Kotlin에서 클래스는 기본적으로 상속이 불가능하다.
class Person(var name: String) {
fun work() {
println("work!");
}
}
위와 아래 코드는 동일하다.
final class Person(var name: String) {
final fun work() {
println("work!");
}
}
클래스는 키워드 final
가 기본값이며, 이 키워드가 붙은 클래스는 상속이 불가능하다.
# open
부모 클래스를 상속하려면 부모 클래스에 키워드 open
을 붙여야한다.
우선 부모 클래스가 기본 생성자를 사용하는 경우에 대해 살펴보자.
// 부모 클래스에서 기본 생성자를 사용하는 경우
open class Person(val name: String)
자식 클래스에서는 생성자 뒤에 :
를 붙이고 부모 클래스의 이름을 작성한다. 이후 부모 클래스의 생성자를 반드시 호출해야한다.
// 자식 클래스
class Programmer(name: String, val nation: String): Person(name)
이제 부모 클래스에서 보조 생성자를 사용하는 경우에 대해 알아보자.
open class Person {
val name: String
// 부모 클래스에서 보조생성자를 사용
constructor(name: String) {
this.name = name
}
}
자식 클래스에서는 다음과 같이 생성자를 선언한다. 그리고 super()
를 통해 부모 클래스의 생성자를 호출해야 한다.
// 자식 클래스
class Programmer: Person {
val nation: String
constructor(name: String, nation: String): super(name) {
this.nation = nation
}
}
# 메소드 오버라이드
부모 클래스에 정의된 메소드를 자식 클래스에서 재정의할 수 있다. 이를 오버라이드(override)
라고 한다.
// 부모 클래스
open class Person(var name: String) {
open fun work() {
println("work!")
}
}
자식 클래스에서는 재정의하려는 메소드 앞에 키워드 override
를 붙인다.
// 자식 클래스
class Footballer (name: String): Person(name) {
// 메소드 오버라이드
override fun work() {
println("play soccer!")
}
// 자식 클래스에서 새로운 메소드 정의
fun exercise() {
println("exercise!")
}
}
자식 클래스의 인스턴스는 다음과 같이 생성한다.
var footballer = Footballer("Ronaldo")
footballer.work()
# Nested Class vs. Inner Class
Kotlin에서는 클래스 안에 클래스를 정의할 수 있다.
Nested Class
는 외부 클래스의 멤버변수에 접근할 수 없다.
class OuterClass {
private val outerVariable: Int = 1
// Nested class
class NestedClass {
fun printSomething() {
// Nested Class에서는 외부 클래스의 멤버변수에 접근할 수 없다.
println(outerVariable) // Error
}
}
}
Nested Class
의 인스턴스는 다음과 같이 생성할 수 있다.
val nestedClass = OuterClass.NestedClass()
반면 Inner Class
는 외부 클래스의 멤버변수에 접근할 수 있다.
class OuterClass {
private val outerVariable: Int = 1
// Inner class
inner class InnerClass {
fun printSomething() {
// Inner Class에서는 외부 클래스의 멤버변수에 접근할 수 있다.
println(outerVariable) // 1
}
}
}
Inner Class
의 인스턴스는 다음과 같이 생성한다.
val outerClass = OuterClass()
val innerClass = outerClass.InnerClass()
# 추상 클래스
선언만 있고 구현부는 없는 메소드를 추상 메소드(Abstract Method)
라고 하며, 추상 메소드를 포함하는 클래스를 추상 클래스(Abstract Class)
라고 한다. 추상 메소드와 추상 클래스는 앞에 키워드 abstract
를 붙인다.
abstract class Person {
// 구현부가 없는 추상 메소드
abstract fun work()
// 구현부가 있는 일반 메소드
fun eat() {
println("eat something.")
}
}
추상 클래스는 인스턴스를 생성할 수 없다.
val person = Person() // Error. Cannot create an instance of an abstract class
추상 클래스를 상속하는 자식 클래스에서는 추상 메소드를 오버라이드할 수 있다.
class Programmer: Person() {
override fun work() {
println("do programming")
}
}
추상 클래스의 모든 추상 메소드를 구현한 자식 클래스는 다음과 같이 인스턴스를 생성할 수 있다.
var programmer = Programmer()
programmer.work()
programmer.eat()
# 인터페이스
모든 메소드가 선언만 있고 구현부가 없는 클래스를 인터페이스(Interface)
라고 한다. 인터페이스를 선언할 때는 키워드 interface
를 사용한다.
interface Person {
fun work()
fun eat()
}
인터페이스는 인스턴스를 만들 수 없으며, 이를 구현한 클래스를 작성해야한다. 인터페이스를 구현한 클래스를 구현체(Implementation)
라고 한다.
// 구현체
class Footballer: Person {
override fun work() {
println("play soccer.")
}
override fun eat() {
println("Eat something.")
}
}
인터페이스를 구현한 클래스는 인스턴스를 생성할 수 있다.
// 구현체의 인스턴스 생성
var footballer = Footballer()
footballer.work()
footballer.eat()
# 데이터 클래스
데이터만을 담기 위한 클래스를 데이터 클래스(data class)
라고 하며, 선언할 때 키워드data
를 붙여야 한다. 주의할 점은 멤버 변수를 반드시 기본 생성자에 추가해야한다.
data class Person constructor(val name: String, val nation: String)
데이터 클래스는 데이터를 처리하는데 유용한 메소드들을 자동으로 생성해준다.
# equal()
equal()
메소드는 두 인스턴스가 동일한지 비교하는데 사용한다.
data class Person constructor(val name: String, val nation: String)
val p1 = Person("Ronaldo", "Portugal")
val p2 = Person("Ramos", "Spain")
println(p1.equals(p2)) // false
보통 equal()
은 개발자가 정의하여 사용한다.
data class Person constructor(val name: String, val nation: String) {
override fun equals(other: Any?): Boolean {
return (this.name == (other as Person).name) && (this.nation == (other as Person).nation)
}
}
val p1 = Person("John", "USA")
val p2 = Person("John", "USA")
val p3 = Person("John", "France")
p1.equals(p2) // true
p1.equals(p3) // false
# toString()
toString()
메소드는 인스턴스를 문자열로 변환하는데 사용한다.
data class Person constructor(val name: String, val nation: String)
val person = Person("Ronaldo", "Portugal")
println(person.toString()) // Person(name=Ronaldo, nation=Portugal)
보통 toString()
도 개발자가 정의하여 사용한다.
data class Person constructor(val name: String, val nation: String) {
override fun toString(): String {
return "${name} lives in ${nation}"
}
}
val p = Person("John", "USA")
p.toString() // John lives in USA
# copy()
copy()
메소드는 인스턴스를 복사하는데 사용한다.
data class Person constructor(val name: String, val nation: String)
val p1 = Person("Paul", "England")
val p2 = p1.copy()
println(p2.name) // Paul
# enum class
열거 클래스(enum class)
는 서로 관련 있는 상수들을 모아 심볼릭한 명칭의 집합으로 정의하는 것이다.
예를 들어 요일은 월요일부터 일요일까지로 데이터의 범위가 한정되어 있다. 이러한 경우 열거 클래스를 유용하게 사용할 수 있다. 열거 클래스는 키워드 enum
을 붙여 정의한다.
enum class Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
}
열거 클래스의 인스턴스는 다음과 같이 생성한다.
val today: Day = Day.MONDAY
열거 클래스 역시 타입 추론이 가능하므로 타입을 생략할 수 있다.
val today = Day.MONDAY
열거 클래스는 when()
구문과 함께 유용하게 사용할 수 있다.
when(today) {
Day.MONDAY -> println("It's monday today.")
Day.TUESDAY -> println("It's tuesday today.")
Day.WEDNESDAY -> println("It's Wednesday today.")
Day.THURSDAY -> println("It's thursday today.")
Day.FRIDAY -> println("It's friday today.")
else -> println("Weekend.")
}
열거 클래스는 내부에 데이터를 담을 수도 있다.
enum class Day(val color: String) {
SUNDAY("Red"),
MONDAY("Black"),
TUESDAY("Black"),
WEDNESDAY("Black"),
THURSDAY("Black"),
FRIDAY("Black"),
SATURDAY("Blue"),
}
다음과 같이 값에 접근할 수도 있다.
val today = Day.SUNDAY
today.color // Red
# sealed class
Sealed Class
는 열거 클래스의 확장판이라고 보면 된다. 열거 클래스와 마찬가지로 서로 관련 있는 상수들을 모아 심볼릭한 명칭의 집합으로 정의할 수 있다.
Sealed Class는 키워드 Sealed
를 사용하여 선언한다.
sealed class Color {
object Red: Color()
object Blue: Color()
object Green: Color()
}
Sealed Class의 인스턴스는 다음과 같이 생성할 수 있다.
// Sealed Class의 인스턴스 생성
var backgroundColor: Color = Color.Blue
Sealed Class는 when 구문
과 함께 유용하게 사용할 수 있다.
when(backgroundColor) {
is Color.Red -> {
println("Background color is red.")
}
is Color.Blue -> {
println("Background color is blue.")
}
is Color.Green -> {
println("Background color is green.")
}
}
# enum class vs. sealed class
Sealed Class
는 인스턴스 안에 다른 타입의 데이터를 포함할 수도 있다. 아래 코드를 살펴보자.
sealed class Manager {
data class Programmer(var school: String) : Manager()
data class Marketer(var major: String) : Manager()
}
매니저는 프로그래머 출신일 수도 있고 마케터 출신일 수도 있다. 이에 따라 다른 구문이 실행되도록 구현할 수 있다.
fun printInformation(manager: Manager) {
when (manager) {
is Manager.Programmer -> {
println("Manager studied at MIT")
}
is Manager.Marketer -> {
println("Manager majored in Economics")
}
}
}
var itManager = Manager.Programmer("MIT")
var marketingManager = Manager.Marketer("Economics")
printInformation(itManager)
// Manager studied at MIT
printInformation(marketingManager)
// Manager majored in Economics
# 로그인 예제
서버에 로그인을 요청하는 코드가 있다고 가정하자. 로그인에 성공하면 다음과 같이 데이터를 반환한다.
data class LoginData(val code: Int, var message: String)
로그인에 실패하면 다음과 같이 에러를 반환한다.
enum class LoginError {
INVALID_EMAIL,
INVALID_PASSWORD
}
이처럼 상황에 따라 다른 타입의 데이터를 반환할 때 Sealed Class를 유용하게 사용할 수 있다.
sealed class LoginResponse {
object OnProgress : LoginResponse()
data class OnSuccess(val data: LoginData) : LoginResponse()
data class OnFailure(val error: LoginError) : LoginResponse()
}
로그인을 요청하는 함수는 아래와 같이 LoginResponse
를 반환한다.
fun login(id: String, password; String): LoginResponse {
// 로그인
}
Sealed Class when()
구문과 함께 유용하게 사용될 수 있다.
var response = login("Paul@gmail.com", "12345")
when(response) {
is LoginResponse.OnProgress -> {
println("${response}")
}
is LoginResponse.OnSuccess -> {
println("${response.data.code}")
println("${response.data.message}")
}
is LoginResponse.OnFailure -> {
when(response.error) {
SignUpError.INVALID_EMAIL -> {
println("invalid email")
}
SignUpError.NETWORK_ERROR -> {
println("network error")
}
SignUpError.INVALID_PASSWORD -> {
println("invalid password")
}
}
}
}
# 키워드 object
키워드 object
는 크게 세 가지 용도로 사용된다.
# 싱글톤
키워드 object
는 싱글톤을 만드는데 사용할 수 있다. 싱글톤(Singleton)
은 오직 하나의 인스턴스만 존재하는 클래스를 의미한다. 싱글톤은 다음과 같이 정의한다.
object Counter {
var count = 0
fun countUp() {
count ++
}
fun clear() {
count = 0
}
}
싱글톤은 별도의 인스턴스를 생성하지 않고 사용할 수 있다.
Counter.count // 0
Counter.countUp()
Counter.countUp()
Counter.countUp()
Counter.count // 3
Counter.clear()
Counter.count // 0
물론 싱글톤도 인스턴스를 생성할 수 있다. 이 때 모든 인스턴스가 값을 공유한다.
var myCounter: Counter = Counter
myCounter.countUp()
myCounter.countUp()
var yourCounter: Counter = Counter
yourCounter.countUp()
yourCounter.countUp()
myCounter.count // 4
yourCounter.count // 4
Counter.count // 4
# 익명 클래스
키워드 object
는 익명 클래스(Anonymous Class)
에도 사용된다. 아래 예제는 안드로이드에서 버튼을 클릭했을 때 특정 작업을 수행하는 코드다.
var button: Button
button.setOnClickListener(OnButtonClickedListener())
class OnButtonClickedListener: View.OnClickListener {
@override
override fun onClick(v: View?) {
// 클릭 시 처리
}
}
Button
클래스의 setOnClickListener()
메소드는 인터페이스 View.OnClickListener
의 구현체를 인자로 전달받는다. 따라서 OnButtonClickedListener
라는 구현체에서 View.OnClickListener
를 구현하고 있다.
익명 클래스를 사용하면 위 코드를 다음과 같이 단축할 수 있다.
var button: Button
button.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
// 클릭 시 처리
}
})
# companion object
키워드 companion object
는 자바의 static
과 유사하다. companion object
구문 안에 선언된 멤버 변수와 메소드는 인스턴스를 별도로 생성하지 않고 접근할 수 있다.
class Poll(val subject: String) {
companion object {
var total = 0
fun printTotal() {
println(total);
}
}
var count = 0
fun vote() {
total++
count++
}
}
companion object
구문 안에 선언된 멤버 변수는 클래스의 모든 인스턴스가 공유한다.
var melon = Poll("Melon")
melon.vote()
melon.vote()
var apple = Poll("Apple")
apple.vote()
apple.vote()
apple.vote()
Poll.printTotal() // 5
println("${Poll.total}") // 5
println("${melon.name}: ${melon.count}") // Melon: 2
println("${apple.name}: ${apple.count}") // Apple: 3
키워드 const
와 함께 컴파일 타임에 생성되는 상수를 선언할 수 있다.
class Person(val name: String) {
companion object {
const val MAX_AGE = 200
}
}
Person.MAX_AGE
companion object
에는 이름을 붙일 수도 있다.
class Person(val name: String) {
companion object Constant {
const val MAX_AGE = 200
}
}
Person.Constant.MAX_AGE
# 접근 제한자
접근 제한자(Access Modifiers)
는 외부에서 클래스 내부의 메소드나 멤버변수에 접근하는 것을 제한하는 것이다. Kotlin은 네 개의 접근 제한자를 지원한다.
# private
private
이 붙은 멤버변수, 메소드, 생성자는 클래스 내부에서만 접근할 수 있다. 클래스에는 붙일 수 없다.
class Person(private val name: String) {
fun printName() {
// 해당 클래스 내부에서 private 변수 name에 접근할 수 있다.
println("Name: ${name}.")
}
}
var person = Person("Paul")
var name = person.name // (에러) 클래스 내부가 아니므로 접근 불가능하다.
private
으로 선언된 변수는 상속받는 자식 클래스에서도 접근이 불가능하다.
open class Person(private val name: String) {
fun printName() {
println("Name: ${name}.")
}
}
class Player(name: String, private var team: String): Person(name) {
fun printTeam() {
println("Team: ${team}")
}
fun printInformation() {
println("${name} works in ${team}")
// (에러) 부모클래스에 선언된 private 변수 name에 접근할 수 없고 컴파일되지 않는다.
}
}
# protected
protected
가 붙은 멤버변수, 메소드, 생성자는 해당 클래스와 자식 클래스에서만 접근할 수 있다. 클래스에는 붙일 수 없다.
open class Person(protected val name: String) {
fun printName() {
println("Name: ${name}.")
}
}
class Player(name: String, private var team: String): Person(name) {
fun printTeam() {
println("Team: ${team}")
}
fun printInformation() {
println("${name} works in ${team}")
// 부모클래스에 선언된 protected 변수 name에 접근할 수 있다.
}
}
# internal
internal
이 붙은 멤버변수, 메소드, 생성자는 같은 모듈 안 어디에서든 접근할 수 있다. 이 접근 제한자는 클래스 앞에도 붙일 수 있다. Kotlin 공식 문서에서 말하는 같은 모듈
은 아래 상황을 의미한다.
- Android Studio Module
- IntelliJ IDEA Module
- Maven Project
쉽게 말하면 안드로이드 스튜디오 프로젝트의 같은 모듈에서는 접근이 가능하다고 보면 된다.
# public (default)
접근 제한자를 따로 붙이지 않으면 이 접근 제한자가 적용된다. public
이 붙은 멤버변수, 메소드, 생성자는 다른 모듈에서도 접근할 수 있으며, 클래스에도 붙일 수 있다.
# Custom Getter, Setter
Java와 Kotlin은 Getter와 Setter를 정의하는 방법에서 차이가 있다.
# Java의 Getter, Setter
Java에서는 보통 클래스의 멤버 변수를 private
으로 선언한 후 Getter
와 Setter
를 정의한다.
class Person {
private String name;
private int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getter
String getName() {
return this.name;
}
// Getter
int getAge() {
return this.age;
}
// Setter
void setName(String name) {
this.name = name;
}
// Setter
void setAge(int age) {
this.age = age;
}
}
Person person = new Person("Paul", 35);
person.setName("Johb");
person.setAge(20);
String hisName = person.getName();
String hisAge = person.getAge();
# Kotlin의 Getter, Setter
Kotlin에서는 Getter
와 Setter
를 자동으로 만들어준다. 따라서 직접 정의할 필요가 없다.
class Person {
var name: String
var age: String
constructor(name: String, age: String) {
this.name = name
this.age = age
}
}
위 구문은 다음과 같이 단축할 수 있다.
class Person(var name: String, var age: String)
이제 클래스의 속성에 직접 접근하면 Getter
와 Setter
가 호출된다.
val person = Person("Paul", 35)
person.name = "John"
person.age = 35
var hisName = person.name
var hisAge = person.age
이처럼 Kotlin에서는 Getter
와 Setter
가 자동으로 생성된다. 하지만 직접 Getter
와 Setter
를 구현할 수 있다.
# Kotlin의 Custom Getter
아래 Java 코드를 살펴보자.
class Person {
private String name;
private int age;
// ...
String getInformation() {
return this.name + " is " + this.age
}
}
Person person = new Person("Paul", 35);
String information = person.getInformation(); // Paul is 35
Kotlin에서는 위 코드를 Custom Getter
로 쉽게 구현할 수 있다.
class Person(val name: String, val age: Int) {
var information: String
get() {
return "${this.name} is ${this.age}"
}
}
val person = Person("Paul", 35);
val information = person.information
get()
의 실행 구문이 한줄일 때는 다음과 같이 단축할 수 있다.
class Person(val name: String, val age: Int) {
var information: String
get() = "${this.name} is ${this.age}"
}
Custom Getter
의 실행 구문은 속성에 접근할 때 마다 매번 다시 계산되며, 값 검증 같은 추가적인 작업에 활용할 수 있다.
class Rectangle(val width: Int, val height: Int) {
val isSquare: Boolean
get() {
return this.width == this.height
}
}
var rectangle = Rectangle(5, 10)
println(rectangle.isSquare) // false
var square = Rectangle(10, 10)
println(square.isSquare) // true
# Kotlin의 Custom Setter
Custom Setter
역시 값 검증 같은 추가적인 작업에 활용할 수 있다.
class Person(var name: String) {
var height: Double = 0.0
set(value) {
if (value < 0) throw Exception("Wrong height range.")
field = value
}
}
var person = Person("Monica")
person.height = -1.1