본문 바로가기

Programming/Kotlin

[4일차] 깡샘의 안드로이드 앱 프로그래밍 with 코틀린(Do it!) / 04 코틀린 객체지향 프로그래밍

해당 내용은 이지스 퍼블리싱 카페에서 공부하며 올렸던 글을 다듬었습니다.

< 클래스 선언 >

- 클래스 키워드 : class

class User { }
 

- 생성자를 본문이 아닌 선언부에 작성할 수 있어서 본문이 없는 클래스도 의미가 있음

  * 코틀린에서는 이런 클래스가 자주 등장!

- 클래스의 멤버 : 생성자, 변수, 함수, 클래스

- 생성자 : constructor() 키워드로 사용

- 클래스 안에 다른 클래스 선언 가능

- 객체 생성 구문

  * 코틀린에서는 new 키워드 사용 안 함

  * 클래스 이름과 같은 함수로 객체를 생성함

val user = User("kim")

 

- 객체를 생성할 때 생성자가 자동으로 호출

- 전달한 인자는 클래스에 선언된 생성자의 매개변수와 들어맞아야 함

- 코틀린에서 생성자는 주 생성자보조 생성자로 구분

 

[ 주 생성자 ]

  * constructor 키워드로 클래스 선언부에 선언 (생략 가능)

  * 한 클래스에 하나만 가능

  * 컴파일러가 매개변수가 없는 주 생성자를 자동으로 추가

class User constructor() { } // 주 생성자 선언
class User() { } // constructor 키워드 생략 예 
class User { } // 매개변수가 없는 주 생성자 자동 선언 
class User(name: string, count: Int) { } // 주 생성자의 매개변수
 
  * 주 생성자를 선언할 때 필요에 따라 매개변수를 선언 가능

    => 객체를 생성할 때 매개변수의 타입과 개수에 맞는 인자를 전달해야 함

class User(name: String, count: Int) { }

  * init : 주 생성자를 이용해 객체를 생성할 때 특정한 로직을 수행할 수 있음

    => 주 생성자의 본문을 구현하고 싶을 때 사용

    => init 키워드로 지정한 영역은 객체를 생성할 때 자동으로 실행

class User(name: String, count: Int) {
    init {
        println("i am init...")
    }
}

fun main() {
    val user = User("kkang, 10)
}

---------- < 실행 결과 > ----------
i am init...
-----------------------------------

  * 생성자의 매개변수는 기본적으로 생성자에서만 사용할 수 있는 지역 변수

// 생성자의 매개변수를 다른 함수에서 사용하는 예 (첫번째 방법)
class User(name: String, count: Int) {

    // 생성자의 매개변수를 다른 함수에서 사용하는 예
    // 클래스 멤버 변수 선언
    var name: String
    var count: Int

    init {
        // 클래스 멤버 변수에 생성자 매개변숫값 대입
        this.name = name
        this.count = count
    }

    fun someFun() {
        println("name: $name, count: $count")
    }
}

fun main() {
    val user = User("kkang", 10)
    user.someFun()
}

---------- < 실행 결과 > ----------
name: kkang, count: 10
----------------------------------
 

    => 주 생성자의 매개변수는 생성자 안에서만 사용할 수 있는 지역 변수지만

         매개변수를 var나 val 키워드로 선언하면 클래스의 멤버 변수가 됨

         ※ 주 생성자만 유일하게 매개변수에 var나 val 키워드로 선언 가능!!!

// 생성자의 매개변수를 클래스의 멤버 변수로 선언하는 방법 (두번째 방법)
class User(val name: String, val count: Int) {
    fun someFun() {
        println("name: $name, count: $count")
    }
}

fun main() {
    val user = User("kkang", 10)
    user.someFun()
}

---------- < 실행 결과 > ----------
name: kkang, count: 10
----------------------------------

[ 보조 생성자 ]

   * 클래스의 본문에 constructor 키워드로 선언하는 함수

     => 여러 개 추가 가능

     => 객체를 생성할 때 자동으로 호출

     => 생성자 본문을 중괄호 { }로 묶어서 객체 생성과 동시에 실행할 영역 지정 가능

 
class User {
    constructor(name: String) {
        println("constructor(name: String) call...")
    }
    constructor(name: String, count: Int) {
        println("constructor(name: String, count: Int) call...")
    }
}

fun main() {
    val user1 = User("kkang")
    val user2 = User("kkang", 10)
}

---------- < 실행 결과 > ----------
constructor(name: String) call...
constructor(name: String, count: Int) call..."
----------------------------------

  * 주 생성자와 보조 생성자를 모두 선언한다면 반드시 생성자끼리 연결해야 함

    => 주 생성자가 있다면 this() 구문을 이용해 주 생성자를 호출해야 함

class User(name: String) {
    constructor(name: String, count: Int): this(name) {
        println("class User in constructor(name: String, count: Int): this(name) call...")
    }
}

  * 보조 생성자가 여러 개라면 어떤 식으로든 주 생성자가 호출되게 해야 함 (규칙!!!)

class User(name: String) {
    constructor(name: String, count: Int): this(name) {
        println("First constructor(name, count) call...")
    }
    constructor(name: String, count: Int, email: String): this(name, count) {
        println("Second constructor(name, count, email) call...")
    }
}

- 주 생성자, 보조 생성자 모두 클래스의 생성자이며, 객체를 생성할 때 호출되는 건 똑같음

- 단지 주 생성자와 보조 생성자를 모두 선언한다면 보조 생성자로 객체를 생성할 때에는

  반드시 주 생성자가 실행되게 만들어야 한다는 규칙이 있을 뿐

- 필수 매개변수와 실행 구문은 주 생성자에 작성하고

  보조 생성자가 실행될 때 주 생성자도 함께 실행하려는 의도

- 결국 객체를 여러 형태로 생성할 수 있도록 매개 변수를 다양하게 구성할 때

  생성자의 공통된 코드는 주 생성자에 작성하라는 의미

 

 

 

 

 

< 클래스를 재사용하는 상속 >

- 상속 : 클래스를 선언할 때 다른 클래스를 참조해서 선언하는 것

- 선언부에 콜론(:)과 함께 상속받을 클래스 이름을 입력해야 함

// 클래스 상속 형식
// 상위 클래스 : open 키워드 이용
open class Super {

}

// 하위 클래스 : 상위 클래스(Super)를 상속받아 하위 클래스(Sub) 선언
class sub: Super() {

}
{ }

- 상위 클래스 : 상속의 대상이 되는 클래스

- 하위 클래스 : 상속을 받는 클래스

- 코틀린의 클래스는 기본적으로 다른 클래스가 상속할 수 없음

  * 상위 클래스 => open 키워드 사용

    하위 클래스 => 이름: 상위 클래스명()

 

- 상위 클래스를 상속받은 하위 클래스의 생성자에서는 상위 클래스의 생성자를 호출해야 함 (규칙)

// 매개변수가 있는 상위 클래스의 생성자 호출
open class Super(name: String) {

}

class Sub(name: String): Super(name) {

}

- 상위 클래스의 생성자 호출문을 꼭 클래스 선언부에 작성할 필요는 없음

// 상위 클래스의 생성자 호출문을 꼭 클래스 선언부에 작성할 필요는 없음
// 하위 클래스에 보조 생성자만 있을 때 상위 클래스의 생성자 호출
open class Super(name: String) {

}

class Sub: Super {
    constructor(name: String): super(name) {

    }
}
){ } }

- 상속이 주는 최고의 이점 : 상위 클래스에 정의된 멤버(변수, 함수)를 하위 클래스에서 자신의 멤버처럼 사용 가능

- 오버 라이딩 (재정의) : 상위 클래스에 선언된 변수나 함수를 같은 이름으로 하위 클래스에서 다시 선언하는 것

  * 규칙 : 상위 클래스에서 오버 라이딩을 허용할 변수나 함수 선언 앞에 open 키워드를 추가하는 것

             open 키워드로 선언한 변수나 함수를 하위 클래스에서 재정의 시 override라는 키워드를 추가해야 함

// 오버라이딩의 예
// 오버라이딩: 상위 클래스에 선언된 변수나 함수를 같은 이름으로 하위 클래스에서 재정의하는 것
// 주로 함수 재정의할 때 사용
open class Super {
    open var someData = 10
    open fun someFun() {
        println("i am super class function: $someData")
    }
}

class Sub: Super() {
    override var someData = 20
    override fun someFun() {
        println("i am sub class function: $someData")
    }
}

---------- < 실행 결과 > ----------
i am sub class function : 20
----------------------------------

- 접근 제한자 : 클래스의 멤버를 외부의 어느 범위까지 이용하게 할 것인지를 결정하는 키워드

- 접근 제한자의 종류

접근 제한자
최상위에서 이용
클래스 멤버에서 이용
public
모든 파일에서 가능
모든 클래스에서 가능
internal
같은 모듈 내에서 가능
같은 모듈 내에서 가능
protected
사용 불가
상속 관계의 하위 클래스에서만 가능
private
파일 내부에서만 이용
클래스 내부에서만 이용

  * 접근 제한자를 생략하면 public이 기본

  * internal의 모듈은 그래들이나 메이븐 같은 빌드 도구에서 프로젝트 단위 또는 같은 세트 단위를 가리킴

  * private는 클래스에서 이용할 때는 해당 클래스 내부에서만 접근할 수 있음.

    최상위에서 private로 선언하면 해당 파일 내부에서만 접근 가능

// 접근 제한자 사용 예
open class Super {
    var publicData = 10
    protected var protectedData = 20
    private var privateData = 30

    protectedData++     // 오류
    privateData++       // 오류
}

class Sub: Super() {
    fun subFun() {
        publicData++
        protectedData++
        privateData++   // 오류
    }
}

fun main() {
    val obj = Super()
    obj.publicData++
    obj.protectedData++ // 오류
    obj.privateData++   // 오류
}
 

< 코틀린 클래스 종류 >

- 데이터 클래스 : data 키워드 사용. 자주 사용하는 데이터를 객체로 묶어줌

  * 주목적: 데이터를 다루는데 편리한 기능을 제공하는 것

- VO(Value-Object) 클래스를 편리하게 이용할 수 있게 해 줌

class NonDataClass(val name: String, val email: String, val age: Int)   // 일반 클래스

data class DataClass(val name: String, val email: String, val age: Int) // 데이터 클래스

 

- equals() : 객체의 데이터를 비교하는 함수

  * VO 클래스는 데이터를 주요하게 다루므로 객체의 데이터가 서로 같은지 비교할 때가 많음

    => 객체가 같은지(X), 객체의 데이터가 같은지(O)

// 데이터 클래스의 기본 사용법 + equals 함수 예제

class NonDataClass(val name: String, val email: String, val age: Int)   // 일반 클래스

data class DataClass(val name: String, val email: String, val age: Int) // 데이터 클래스

fun main() {
    val non1 = NonDataClass("kkang", "a@a.com", 10)
    val non2 = NonDataClass("kkang", "a@a.com", 10)

    val data1 = DataClass("kkang", "a@a.com", 10)
    val data2 = DataClass("kkang", "a@a.com", 10)

    println("non data class equals: ${non1.equals(non2)}")
    println("data class equals: ${data1.equals(data2)}")
}


---------- < 실행 결과 > ----------
non data class equals: false
data class equals: true
----------------------------------

 

- 단, equals()는 주 생성자에 선언한 멤버 변수의 데이터만 비교 대상으로 삼는다.

// 데이터 클래스의 equals() 함수 예
// equals() 함수는 주 생성자에 선언한 멤버 변수의 데이터만 비교함!!!!
data class DataClass(val name: String, val email: String, val age: Int) {
    lateinit var address: String
    constructor(name: String, email: String, age: Int, address: String): this(name, email, age) {
        this.address = address
    }
}

fun main() {
    // equals() 예제
    val obj1 = DataClass("kkang", "a@a.com", 10, "seoul")
    val obj2 = DataClass("kkang", "a@a.com", 10, "busan")
    println("obj1.equals(obj2) : ${obj1.equals(obj2)}")
}

---------- < 실행 결과 > ----------
obj1.equals(obj2) : true
----------------------------------

- toString() : 객체가 가진 값을 확인해야 할 때

  * 자주 사용!!

  * 데이터 클래스와 일반 클래스의 toString() 반환 값이 다름

  * toString() 역시 주 생성자의 매개변수에 선언된 데이터만 출력 대상

fun main() {
    // toString 함수 예제
    class NonDataClass(val name: String, val email: String, val age: Int)
    data class DataClass(val name: String, val email: String, val age: Int)
    val non = NonDataClass("kkang", "a@a.com", 10)
    val data = DataClass("kkang", "a@a.com", 10)
    println("non data class toString : ${non.toString()}")
    println("data class toString : ${data.toString()}")
}

---------- < 실행 결과 > ----------
non data class toString : com.example.a211113test.DataClassKt$main$NonDataClass@17f6480
data class toString : DataClass(name=kkang, email=a@a.com, age=10)
----------------------------------

- 오브젝트 클래스 : 익명 클래스를 만들 목적

  * 클래스를 선언하면서 동시에 객체를 생성해야 함

  * 보통은 타입까지 함께 입력해서 선언

 
// 오브젝트 클래스의 사용 예
// 오류 원인 : 타입을 명시하지 않아 코틀린 최상위 타입인 Any로 취급
// => Any 타입 객체에는 data, some()이라는 멤버가 없어서 오류 발생
val obj = object {
    var data = 10
    fun some() {
        println("data : $data")
    }
}

fun main() {
    obj.data = 30  // 오류
    obj.some()     // 오류
}

- 컴패니언 클래스 : companion 키워드 사용. 멤버 변수나 함수를 클래스 이름으로 접근하고자 할 때 사용

  * 객체를 생성하지 않고서도 클래스 이름으로 특정 멤버를 이용 가능

  * 자바의 static 대체

  * 컴패니언 클래스를 지원하는 이유

    ① 자바는 모든 것을 클래스 내부에 선언하므로 객체로 묶일 필요학 없는 멤버는 static으로 선언하지만,

       코틀린은 최상위에 함수나 변수를 선언할 수 있고

       객체로 이용할 필요가 없는 멤버를 굳이 클래스에 선언하지 않아도 됨

    ② 코틀린에서는 클래스에 선언하는 멤버를 대부분 객체로 이용해야 함

// 컴패니언 클래스 예제
class MyClass {
    companion object {    // companion 키워드로 선언해야 함!!
        var data = 10
        fun some() {
            println(data)
        }
    }
}

fun main() {
    MyClass.data = 30
    MyClass.some()
}

---------- < 실행 결과 > ----------
30
----------------------------------