본문 바로가기
  • 개발하는 곰돌이
Programming Language/Kotlin & Java

Kotlin 기본 문법 6 : 클래스와 접근 지정자

by 개발하는 곰돌이 2022. 12. 2.

목차

    개요

    Kotlin은 JVM 기반이면서 객체지향 프로그래밍을 추구하는 언어인 만큼 클래스를 통해 객체를 생성하여 사용한다. 클래스와 객체의 차이는 아래와 같다.

    • 클래스 : 객체들의 공통된 특성을 정의해놓은 틀
    • 객체 : 클래스를 이용하여 생성하여 각자의 속성을 가진 개체

    클래스와 객체의 예를 들면 다음과 같다. 자동차라는 클래스가 있으면 차종, 자동차번호 등은 모든 자동차가 공통적으로 갖는 특성이다. 이렇게 공통적으로 갖는 특징적인 값을 필드(field)라고 한다. 또한 모든 자동차는 운행, 세차 등의 행위를 할 수 있는데, 이렇게 공통적으로 갖는 동작 행위를 메소드(method)라고 한다. 이러한 자동차라는 클래스로 차종, 자동차번호 등의 각각의 필드값을 갖는 개체가 바로 객체가 된다.

     

    Kotlin은 Java와 클래스를 작성하는 방법이 조금 다르다. 이번 포스트는 이에 대해 정리하고자 한다.

    클래스 작성

    Kotlin에서는 2가지 방법으로 클래스를 작성할 수 있다. 첫 번째 방법은 Java의 클래스와 같은 형태로 작성하는 것이고, 두 번째 방법은 메소드의 파라미터를 지정하는 것처럼 클래스의 이름 뒤에 클래스의 필드를 작성하는 것이다. 아래 예시 코드를 보자.

    // 첫 번째 방법
    class Person {
        val socialSecurityNumber = "주민등록번호"
        var name = "이름"
    }
    // 두 번째 방법
    class Person(val socialSecurityNumber: String, var name: String)

    첫 번째 방법의 경우에는 필드를 선언할 때 null인 경우를 포함하여 반드시 초기화를 해야한다. 필드값을 초기화하기 위해선 기본값을 입력하여 초기화를 할 수도 있고 생성자를 통해 객체를 생성하면서 초기화를 할 수 있다. 두 번째 방법의 경우에는 필드의 초기값을 선언할 필요가 없다. 또 다른 차이점은 생성자에 관한 것인데 이는 아래의 Java 코드를 보면서 이야기 하겠다.

    // 첫 번째 방법
    class Person {
        final String socialSecurityNumber = "주민등록번호";
        String name = "이름";
    }
    // 두 번째 방법
    class Person {
        final String socialSecurityNumber;
        String name;
    
        public Person(String socialSecurityNumber, String name) {
            this.socialSecurityNumber = socialSecurityNumber;
            this.name = name;
        }
    }

    각 방법의 경우를 Java 코드로 변환한 것이다. 이를 보면 첫 번째 방법의 경우에는 별도의 생성자가 선언되어 있지 않지만 두 번째 방법의 경우에는 각 필드를 초기화하는 생성자가 선언되어 있다. 즉, 첫 번째 방법으로 클래스를 생성하면 비어있는 기본 생성자만 사용할 수 있지만 두 번째 방법으로 클래스를 생성하면 각 필드를 초기화하는 기본 생성자가 자동으로 만들어진다고 볼 수 있다. 물론 이 경우에는 Java처럼 파라미터를 받지 않는 생성자를 사용하려면 별도의 생성자를 작성해야 한다.

     

    클래스의 메소드를 작성하는 방법은 함수를 작성하는 방법과 동일하다. 메소드 이름과 파라미터를 지정한 후 코드 블럭 내부에 동작을 작성하면 된다.

    class Person(val socialSecurityNumber: String, var name: String) {
        fun walk() = println("${name}이 걸어갑니다.")
        fun rename(name: String) {
            println("${this.name}의 이름이 ${name}으로 바뀌었습니다.")
            this.name = name
        }
    }
    
    fun main() {
        val person = Person("123456-1234567", "지나가던 사람")
        person.walk()
        person.rename("서있는 사람")
    }
    
    /*
    지나가던 사람이 걸어갑니다.
    지나가던 사람의 이름이 서있는 사람으로 바뀌었습니다.
    */

    Kotlin의 생성자

    상기한 바와 같이 Kotlin에서는 클래스를 작성하는 방법에 따라 다른 생성자가 만들어진다. 하지만 하나의 클래스에 둘 이상의 생성자가 필요할 수 있다. 이 경우에는 constructor() 키워드를 사용하여 추가 생성자를 작성할 수 있다. 이 경우에도 클래스를 작성한 방법에 따라 사용 방법이 조금 다르다.

     

    첫 번째 방법으로 클래스를 작성한 경우에는 Java에서 생성자를 작성하는것과 비슷한데 클래스 명으로 생성자를 작성하던 것만 constructor()로 바꿔주면 된다. 앞서 이야기한 것 처럼 첫 번째 방법을 사용하게 되면 기본값을 지정하지 않은 필드는 반드시 생성자를 통해 초기화해야 한다.

    // Kotlin
    class Person {
        val socialSecurityNumber = "주민등록번호"
        var name = "이름"
        
        constructor(name: String) {
            this.name = name
            println("생성자 호출")
        }
    }
    // Java
    class Person {
        final String socialSecurityNumber = "주민등록번호"
        String name = "이름"
        
        public Person(String name) {
            this.name = name
            println("생성자 호출")
        }
    }

    두 코드의 동작은 완벽히 일치한다.


    두 번째 방법의 경우에는 클래스 이름 뒤의 괄호가 실제로 필드와 생성자를 동시에 선언하고 있는데,  추가적인 생성자를 선언하려면 기본 생성자를 상속하는 보조 생성자를 생성해야 한다. 

    class Person(val socialSecurityNumber: String, var name: String) {
        constructor(name: String): this("주민등록번호", name) {
            println("보조 생성자 호출")
        }
    }

    위 코드에서 this()는 Person 클래스의 기본 생성자를 의미한다. name만 파라미터로 갖는 생성자를 호출하면 "주민등록번호"와 name을 파라미터로 갖는 기본 생성자를 호출하면서 코드 블럭 내부의 동작을 수행하게 되어 보조 생성자 호출이라는 문자열이 출력된다.

     

    두 번째 방법의 또 다른 특징으로는 함수가 파라미터의 기본값을 지정할 수 있던것과 같이 필드의 기본값을 지정할 수 있다는 것이다. 이렇게 필드의 기본값을 지정해놓으면 생성자를 호출할 때 기본값을 지정해놓은 필드를 파라미터로 전달하지 않으면 기본값이 해당 필드에 할당된다.

    fun main() {
        val person = Person("주민등록번호")
    }
    
    class Person(val socialSecurityNumber: String, var name: String = "기본 이름")

    생성자를 호출할 때 다른 동작을 함께 수행할 수도 있다. 이 때는 클래스 내부에 init 키워드를 사용하여 생성자가 호출될 때 수행하고자 하는 동작을 작성하면 된다. init 블럭 내부의 동작은 기본 생성자와 보조 생성자 상관 없이 모두 실행된다.

    fun main() {
        val person1 = Person("123456-1234567", "사람1")
        val person2 = Person("사람2")
    }
    
    class Person(val socialSecurityNumber: String, var name: String) {
        init {
            println("$name 등장")
        }
        constructor(name: String): this("주민등록번호", name) {
            println("보조 생성자 호출")
        }
    }
    
    /*
    사람1 등장
    사람2 등장
    보조 생성자 호출
    */

    Kotlin의 객체

    Kotlin도 Java와 마찬가지로 생성자를 통해 객체를 생성한다. Java와의 차이점은 Kotlin은 new 키워드가 존재하지 않아 생성자와 파라미터만 작성하면 객체를 생성할 수 있다는 점이다. 이 때 식별자 앞에 붙는 valvar의 차이는 객체의 참조 주소를 바꿀 수 있는지에 대한 것인데, 예를 들면 새로 생성자를 호출하여 전혀 다른 객체를 할당할 수 있는지의 차이다. valvar 모두 객체의 필드값은 제한 없이 변경할 수 있다.

    fun main() {
        val person1 = Person("123456-1234567", "사람1")
        var person2 = Person("234567-1234567", "사람2")
        person1.name = "새 이름"	// OK
        person1 = Person("123456-2345678", "새로운 사람1")	// 컴파일 에러
        person2 = Person("234567-2345678", "새로운 사람2")	// OK
    }
    
    class Person(val socialSecurityNumber: String, var name: String)

    객체의 멤버 변수나 메소드에 접근하고 싶을 때는 객체의 식별자에 마침표를 찍고 멤버 변수의 이름이나 메소드를 호출하면 된다. 다만 멤버 변수가 val 타입일 경우에는 접근하여 값을 가져올 수는 있지만 값을 변경할 수는 없다.

    접근 지정자

    접근 지정자는 클래스나 클래스의 필드와 메소드에 접근 가능한 범위를 지정해주는 키워드로, 접근 제한자라고도 한다. Kotlin도 Java와 마찬가지로 접근 지정자가 존재하는데 약간의 차이가 있다. 가령, Java에서는 접근 지정자를 따로 붙이지 않으면 default라는 별도의 상태가 되지만 Kotlin에서는 접근 지정자를 붙이지 않으면 기본적으로 public 상태가 된다. 또한 Kotlin에는 internal이라는 별도의 접근 지정자가 있다. 각 접근 지정자의 접근 가능 범위는 아래의 표와 같다.

      Kotlin Java
    private 동일 클래스 내부 동일 클래스 내부
    protected 동일 클래스와 상속 관계의 클래스 동일 패키지 또는 동일 클래스와 상속 관계의 클래스
    internal 동일 모듈 내부 -
    default - 동일 패키지 내부
    public 모든 영역 모든 영역
    모듈(Modules)

    Kotlin 공식 문서 상에서 정의하는 모듈은 함께 컴파일되는 파일들의 집합이다. 예를 들어 test.kt에 internal 속성의 함수가 있으면 동일한 프로젝트에 있는(=함께 컴파일되는) example.kt에서 해당 함수를 호출할 수 있다.

    소스 파일에 반드시 클래스를 작성할 필요가 없는 Kotlin의 특성상 접근 지정자는 클래스 뿐만 아니라 해당 파일 내부, 즉 Top-level에서도 동작한다. 이 때는 아래의 표와 같이 접근 지정자의 동작에 약간의 차이가 있다.

      Top-level 클래스
    private 동일 파일 내부 동일 클래스 내부
    protected - 동일 클래스와 상속 관계의 클래스
    internal 동일 모듈 내부 동일 모듈 내부
    public 모든 영역 모든 영역

    최상위 레벨에서는 protected를 사용할 수 없고, private의 제한이 동일 클래스 내부가 아니라 동일 파일 내부로 바뀐 것을 볼 수 있다. 만약 최상위 레벨의 함수에 protected를 사용하려고 하면 아래와 같은 오류가 발생한다.

    주의할 점은 최상위 레벨에서 private 클래스는 동일 파일 내부의 함수에서는 아무 제한 없이 접근할 수 있지만 동일 파일 내부에 있는 다른 클래스나 함수밖에서는 접근할 수 없다. 최상위 레벨의 private 함수는 동일 파일 내부라면 어디서든 접근할 수 있다.

    private val a = add(1, 3)	// OK
    fun main() {
        val person = Person("123456-1234567", "이름")	// OK
        println(add(123, 456))	// OK
        println(a)	// OK
    }
    
    private fun add(a: Int, b: Int) = a + b
    
    private class Person(val socialSecurityNumber: String, var name: String) {
        fun foo() {
            println(add(10, 20))	// OK
        }
    }
    
    class Example(val person: Person)	// 컴파일 에러

    마무리

    Kotlin의 클래스에 대한 기본적인 부분을 정리해보았다. 그 외의 추상 클래스, 인터페이스, 클래스의 상속, 오버로딩과 오버라이딩에 대한 내용은 2개의 포스트에 걸쳐 정리하고자 한다.

     

    댓글 피드백은 언제나 환영합니다.

    댓글