ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4-1. 클래스와 객체 다루기 : 클래스 정의하기
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 9. 29. 13:51

    4장

    4-1. 클래스와 객체 다루기 : 클래스 정의하기

    4-2. 클래스와 객체 다루기 : 널 가능성

    4-3. 클래스와 객체 다루기 : 단순한 변수 이상의 프로퍼티

    4-4. 클래스와 객체 다루기 : 객체


    클래스에 대해 찾아보면, 객체지향 소프트웨어 작성의 가장 기본적인 단위로써,

    객체를 구현/기술하는 문법이라고 나온다. 

    3장의 함수, 4장의 클래스는 실제로 가장 많이 쓰면서도 관련된 내용들을 물어보면

    명확하게 대답하기 어려운 내용들이 많은 것 같다.

    클래스를 공부하며 자세히 알아보자.


    4-1. 클래스 정의하기

    1) 클래스 내부 구조

    코틀린의 클래스는 class 키워드 다음에 클래스 이름이 오고,

    그 다음에 클래스 본문이 오는 형태로 정의된다.

    클래스 본문은 멤버 정의가 들어가있는 블록이다. 

    어떤 사람에 대한 정보를 저장하는 클래스를 정의해보자.

    class Person {
        var fistName: String = ""
        var familyName: String = ""
        var age: Int = 0
    
        fun fullName() = "$fistName $familyName"
        fun showMe(){
            println("${fullName()}: $age")
        }
    }

    이 정의는 모든 Person 클래스의 인스턴스마다 firstName, familyName, age 라는 프로퍼티와

    fullName() 및 showMe()라는 두 함수가 들어있음을 알려준다.

    즉 메인함수에서 아래와 같이 사용할 수 있다.

    class Person {
        var firstName: String = ""
        var lastName: String = ""
        var age: Int = 0
    
        fun fullName() = "$firstName $lastName"
        fun showMe(){
            println("${fullName()}: $age")
        }
    }
    
    fun main() {
        var person1 = Person()
        person1.firstName = "김"
        person1.lastName = "자반"
        person1.age = 20
    
        println(person1.fullName()) // 김 자반
        person1.showMe() // 김 자반: 20
    
        var person2 = Person()
        person2.firstName = "유"
        person2.lastName = "퀴즈"
    
        println(person2.fullName()) // 유 퀴즈
        person2.showMe() // 유 퀴즈: 0
    }

    다음과 같이 변수처럼 프로퍼티를 사용하는 참조 구문도 있다.

    fun showAge(p: Person) = println(p.age) // 프로퍼티 읽기
    fun readAge(p: Person){
        p.age = readLine()!!.toInt() // 프로퍼티 쓰기
    }

    프로퍼티는 어떤 클래스의 구체적인 인스턴스와 엮여 있기 때문에 이 인스턴스를 식으로 지정해야 한다.

    (위 코드에서는 p가 인스턴스이다.)

    이런 인스턴스를 수신 객체(receiver oject)라고 부르고, 이는 프로퍼티에 접근할 때 사용해야 하는 객체를 지정한다.

    멤버 함수의 경우에도 똑같이 수신 객체가 있고, 이 경우 멤버 함수를 메서드라고 부른다.

    fun showFullName(p: Person) = println(p.fullName()) // 메서드 호출

    클래스 내부에서는 this 식으로 수신 객체를 참조할 수 있다.

    대부분의 경우 this를 기본값으로 가정하므로 생략해도 된다.

    class Person {
        var firstName: String = ""
        var lastName: String = ""
        var age: Int = 0
    
        fun fullName() = "${this.firstName} ${this.lastName}"
        fun showMe(){
            println("${this.fullName()}: ${this.age}")
        }
    }

    하지만 때로는 this가 꼭 필요한 경우가 있다.

    예를 들어 어떤 클래스의 프로퍼티와 메서드 파라미터 이름이 같은 경우,

    이 둘을 구분하기 위해 프로퍼티 이름 앞에 this를 써야 한다.

    class Person {
        var firstName: String = ""
        var lastName: String = ""
        var age: Int = 0
    
        fun setName(firstName: String, lastName: String){
            this.firstName = firstName
            this.lastName = lastName
        }
    }

    기본적으로 코틀린 클래스는 공개 가시성(public visibility)이다

    최상위 함수와 마찬가지로 최상위 클래스를 internal이나 private으로 설정할 수 있다.

    이렇게 지정하면 클래스의 가시성 범위를 제한할 수 있다.


    2) 생성자

    생성자는 클래스 인스턴스를 초기화해주고, 인스턴스 생성 시 호출되는 특별한 함수다.

    코드를 통해 좀 더 자세히 살펴보자. 아래 두 코드의 기능은 동일하다.

    class Person() {
        var firstName: String = ""
        var lastName: String = ""
    
        fun setName(firstName: String, lastName: String){
            this.firstName = firstName
            this.lastName = lastName
        }
    }
    
    fun main() {
        var person1 = Person()
        person1.setName("김", "자반")
    }
    
    

    class Person(firstName: String, lastName: String) {
        var firstName = firstName
        var lastName = lastName
    }
    
    fun main() {
        var person1 = Person("김", "자반")
    }

    클래스 헤더의 파라미터 목록을 주생성자(primary constructor) 선언이라고 부른다.

    주생성자는 함수와 달리 본문이 하나가 아니다.

    대신 주생성자는 클래스 정의 내에서 프로퍼티 초기화와 초기화 블록이 등장하는 순서대로 구성된다.

     

    초기화 블록이란 init이라는 키워드가 앞에 붙은 블록이다.

    이 블록 안에서 클래스 초기화 시 필요한 간단하지 않은 초기화 로직을 수행할 수 있다.

    클래스 안에 init 블록이 여럿 들어갈 수 있으며, 이 경우 각 블록은 프로퍼티 초기와와 함께 순서대로 실행된다.

    class Person(firstName: String, lastName: String) {
        val firstName = firstName
        val lastName = lastName
        val fullName = "$firstName $lastName"
        init{
            println("Person Name : $fullName")
        }
    }

    초기화 블록에는 return문이 들어갈 수 없다.

     

    코를린은 init 블록 안에서 프로퍼티를 초기화 하는 것을 허용한다.

    이는 하나의 식으로 표현하기 복잡한 초기화 로직을 실행할 때 유용하다.

    class Person(fullName: String) {
        var firstName: String = ""
        var lastName: String = ""
    
        init{
            val names = fullName.split(" ")
            if(names.size != 2){
                throw IllegalArgumentException("Invalid name: $fullName")
            }
            firstName = names[0]
            lastName = names[1]
        }
    }

    주생성자 파라미터를 프로퍼티 초기화나 init 블록 밖에서 사용할 수는 없다.

    예를 들어 멤버 함수 내부에서는 firstName을 사용할 수 없기 때문에 다음 코드는 잘못된 코드이다.

    class Person(firstName: String, lastName: String) {
    
        val fullName = "$firstName $lastName"
        fun printFirstName() = println("${firstName}")
    }

    이에 대한 해법은  생성자 파라미터의 값을 지정할 멤버 프로퍼티를 정의하는 것 이다.

    하지만 코틀린은 간단하게 생성자 파라미터의 값을 멤버 프로퍼티로 만들 수 있는 방법을 제공한다.

     

    class Person(firstName: String, lastName: String) {
    
        val firstName = firstName
        val fullName = "$firstName $lastName"
        fun printFirstName() = print(firstName)
    }

    class Person(val firstName: String, lastName: String) {
    
        val fullName = "$firstName $lastName"
        fun printFirstName() = print(firstName) // lastName은 호출 x
    }

    var / val 파라미터를 사용하면 단순하지 않은 멤버가 포함되지만, 본문은 비어있는 클래스를 정의할 수 있다.

    class Person(val firstName: String, val lastName: String = "") {
    }

    이 경우 본문을 아예 생략할 수 있으면 intellij 코틀린 플러그인은 이런 코드 스타일을 권장한다고 한다.

    class Person(val firstName: String, val lastName: String = "")

     

    함수와 마찬가지로 디폴트 값과 vararg를 생성자 파라미터에 사용할 수 있다.

    class Person(val firstName: String, val lastName: String = ""){
        var fullName = "$firstName $lastName"
    }
    
    class Room(vararg val persons: Person){
        fun showNames(){
            for (person in persons) println(person.fullName)
        }
    }
    
    fun main() {
        val room = Room(Person("김", "자반"), Person("유", "퀴즈"))
        room.showNames()
       // 김 자반
       // 유 퀴즈
    }

    3) 멤버 가시성

    앞에서 정리한 내용이 여기 나와서 그대로 작성하겠다.

     

    자바의 접근 제한자(Access modifiers)는 코틀린에서 가시성 변경자(Visibility modifiers)로 불린다.

    아마 function visibility scope를 그대로 번역해서 함수의 영역과 가시성 으로 제목이 적힌것 같은데,

    처음 들으면 무슨 내용일지 감이 잡히지 않아 번역이 살짝 아쉽다.

    좀 더 다양한 예시를 보기 위해 코틀린 공식 문서를 참고해 정리해 보았다.

    https://kotlinlang.org/docs/visibility-modifiers.html#class-members

    정리내용

    더보기

    클래스, 오브젝트, 인터페이스, 생성자, 함수, 프로퍼티, 세터(Setters)는 Visibility modifiers를 가진다.

    코틀린에는 네가지 Visibility modifiers가 있다

    private, protected, internal, public이 있으며, 기본값(명시하지 않은 경우) public이 적용된다

     i ) 패키지

    함수, 프로퍼티, 클래스, 오브젝트, 인터페이스는 패키지 내부의 top-level에 선언할 수 있다.

    package foo
    
    fun baz() { ... }
    class Bar { ... }

    만약 modifier를 명시하지 않는다면, public이 기본값으로 사용된다.

    이 다음 설명에서 선언은 위에 작성한 함수, 프로퍼티, 클래스 등을 의미한다.

    public : 선언이 어디서든 어디서든 호출할 수 있다.

    (declarations will be visible everywhere)

    private : private선언이 포함된 파일 내부에서만 호출할 수 있다.

    (visible inside the file that contains the declaration)

    internal : 동일한 모든 모듈이 호출할 수 있다.

    (visible inside the file that contains the declaration)

    protected : top-level에서 선언할 수 없다

    (is not available for top-level declarations)

    다른 패키지의 top-level 선언을 호출하고 싶은 경우 반드시 해당 패키지를 import 해야한다.

    (To use a visible top-level declaration from another package, you should import it.)

    // file name: example.kt
    package foo
    
    private fun foo() { ... } // visible inside example.kt
    
    public var bar: Int = 5 // property is visible everywhere
        private set         // setter is visible only in example.kt
    
    internal val baz = 6    // visible inside the same module

     ii) 클래스 멤버

    private : 모든 멤버를 포함해 이 클래스 내에서만 호출할 수 있다.

    (the member is visible inside this class only (including all its members))

    protected : 모든 멤버는 private과 동일하지만, 하위 클래스에서도 protected 멤버를 호출할 수 있다.

    (the member has the same visibility as one marked as private, but that it is also visible in subclasses)

    internal : 이 선언을 본 모듈 내부의 모든 클라이언트는 internal 멤버를 호출할 수 있다.

    (any client inside this module who sees the declaring class sees its internal members)

    public : 클래스 선언을 본 모든 클라이언트는 해당 클래스의 public 멤버를 호출할 수 있다.

    (any client who sees the declaring class sees its public members)

    코틀린에서 외부 클래스는 내부 클래스의 private 멤버를 볼 수 없다(호출할 수 없다)

    (In Kotlin, an outer class does not see private members of its inner classes)

    open class Outer {
        private val a = 1
        protected open val b = 2
        internal open val c = 3
        val d = 4  // public by default
    
        protected class Nested {
            public val e: Int = 5
        }
    }
    
    class Subclass : Outer() {
        // a is not visible
        // b, c and d are visible
        // Nested and e are visible
    
        override val b = 5   // 'b' is protected
        override val c = 7   // 'c' is internal
    }
    
    class Unrelated(o: Outer) {
        // o.a, o.b are not visible
        // o.c and o.d are visible (same module)
        // Outer.Nested is not visible, and Nested::e is not visible either
    }

    iii) 모듈

    모듈은 internal modifier를 설명하기 위한 내용으로, 동일한 모듈 내에서 호출가능하다. 

    여기서 모듈은 함께 컴파일된 Kotlin 파일 세트이다. 예를들면 다음과 같다.

    • An IntelliJ IDEA module.
    • A Maven project.
    • A Gradle source set (with the exception that the test source set can access the internal declarations of main).
    • A set of files compiled with one invocation of the <kotlinc> Ant task.

    4) 내포된 클래스

    함수, 프로퍼티, 생성자 외에 코틀린 클래스는 다른 클래스도 멤버로 가질 수 있다.

    이를 내포된 클래스(nested class)라고 부른다.

    다른 멤버와 마찬가지로 내포된 클래스에도 여러 가지 가시성을 지정할 수 있다.

    class Room(private vararg val persons: Person){
        class Person(val firstName: String, val lastName: String = ""){
            var fullName = "$firstName $lastName"
        }
        fun showNames(){
            for (person in persons) println(person.fullName)
        }
    }
    
    fun main() {
        val room = Room(Room.Person("김", "자반"), Room.Person("유", "퀴즈"))
        room.showNames()
    }

    내포된 클래스를 둘러싸고 있는 본문 밖에서는(위 코드에서는 main) Room.Person 처럼

    내포된 클래스의 이름 앞에 바깥쪽 클래스의 이름을 덧붙여야만 내포된 클래스를 참조할 수 있다.

     

    내포된 클래스에 inner를 붙이면 자신을 둘러싼 외부 클래스와 현재 인스턴스에 접근할 수 있다.

    class Person(val firstName: String, val lastName: String){
        inner class Possession(val description: String){
            fun showOwner() = println(fullName())
        }
        private fun fullName() = "$firstName $lastName"
    }
    
    fun main() {
        val person = Person("John", "Deo")
        val wallet = person.Possession("Wallet")
        wallet.showOwner()
    }

    여기서 내부 클래스 생성자를 호출할 때, person.Possession("Wallet") 처럼 외부 클래스 인스턴스를 지정해야 한다

     

    일반적으로 this는 항상 내부 클래스 인스턴스를 가리킨다.

    따라서 내부 클래스 본문에서 this는 내부 클래스 자신을 가리킨다.

    내부 클래스 본문에서 외부 클래스 인스턴스를 가리켜야 한다면 한정시킨(qualified) this 식을 사용해야 한다.

    한정시킨 this 식에서 @ 기호 다음에 오는 식별자는 외부 클래스의 이름이다

    class Person(val firstName: String, val lastName: String){
        inner class Possession(){
            fun getOwner() = this@Person
        }
    }

    5) 지역 클래스

    코틀린 함수에서도 함수 본문에서 클래스를 정의할 수 있다.

    이런 지역 클래스는 자신을 둘러싼 코드 블록 안에서만 쓰일 수 있다.

    fun main() {
        class Point(val x: Int, val y: Int){
            fun shift(dx: Int, dy: Int): Point = Point(x + dx, y + dy)
            override fun toString() = "$x $y"
        }
        val p = Point(10, 10)
        println(p.shift(-1, 3))
    }
    
    fun foo(){
        println(Point(0, 0))
        // compile error
    }

    지역 함수와 유사하게 코틀린 지역 클래스도 자신을 둘러싼 코드 선언에 접근할 수 있다.

    특히 지역 클래스는 본문 안에서 자신이 접근할 수 있는 값을 접근할 수 있고, 변경할 수도 있다.

    fun main() {
        var x = 10
        class Counter{
            fun plusTwo(){
                x += 2    
            }
        }
        Counter().plusTwo()
        println(x) // 12
    }

    내포된 클래스와 지역 클래스의 차이점

    • 내포된 클래스와 달리 지역 클래스에는 가시성 변경자를 붙일 수 없다.
    • 지역 클래스의 영역은 항상 자신을 둘러싼 블록으로 제한된다.
    • 지역 클래스도 다른 클래스가 포함할 수 있는 모든 멤버(함수, 프로퍼티, 생성자, 내포된 클래스 등)를 포함할 수 있다.
    • 내포된 클래스는 반드시 inner 클래스여야만 한다.

    다음글

    4-2. 클래스와 객체 다루기 : 널 가능성

    반응형

    댓글

Designed by Tistory.