Kotlin은 왜 파일 내에 class 선언이 없을까?
728x90
반응형

 

같이 공부하는 친구들끼리 얘기하다가 나온 이슈. Intellij에서 Java로 코드를 만들 때와 kotlin으로 코드를 만들 때, main 함수의 모습이 왜 다를까? 우선 아래 코드를 보자.

// Java : Sample.java 
public class Sample {
    public static void main(String[] args) {
        // blah blah
    }
}

// Kotlin : Sample2.kt
fun main(args: Array<String>) {
    // blah blah
}

자바 코드는 내부에 클래스를 선언하는 부분이 있다. 그 내부에 main 함수가 있다. 하지만 코틀린은 클래스를 별도로 선언하는 부분이 없다. 자바에서는 Sample을 객체화하려면 이 선언이 필수다. 그러면 코틀린은 이 선언이 없어도 되는 것인가? 아니면 없어야 하는 것인가? 이 차이를 알려면 우선 main 함수의 이해가 선행되어야 할 것 같다.

 

main 함수는 왜 정적(static) 함수로 선언되는가?

무의식적으로 사용하는 main 함수. 위 자바 코드를 보면 static으로 선언되어 있음을 볼 수 있다. 함수나 변수를 static으로 선언하면 자바 가상 머신(JVM)에서 객체 생성 없이 메모리에 할당시켜 호출 가능한 형태로 만든다. 그래서 static으로 선언된 변수나 함수는 어디서든지 호출이 가능하다. 하지만 static이 아닌 변수나 함수는 객체가 생성될 때, 런타임(Runtime)시에 메모리에 할당되어 참조값(Reference)를 통하여 접근할 수 있다. 즉, 일반적으로는 객체가 new 키워드를 이용해 생성이 되어야 객체가 메모리에 할당되어 접근 및 호출이 가능하다.

 

그렇다면 우리가 프로그램을 실행할 경우를 보자. 일반적으로 cmd 창에서 자바 프로그램을 실행할 경우, "java 메인 클래스"로 실행을 한다. 여기서 main() 함수에 static이 붙지 않는다는 가정을 해보자. 그러면 cmd창에 "메인 클래스"를 입력해 프로그램을 실행할 경우 내부적으로 아래와 같은 코드가 실행 되어야만 한다.

// 메인클래스 실행
java 메인클래스

// main 함수가 static이 아니라면
메인클래스 변수 = new 메인클래스(); 
변수.main();

하지만 실제로는 이렇게 실행하지 않는다. main 함수가 static으로 선언되어 있기 때문이다. main 함수는 자바 가상머신(JVM)에서 객체 생성 없이 메모리에 할당시켜 언제 어디서든 호출 가능한 형태로 만들어져 있다. 실제로 cmd창에 "java 메인 클래스"를 입력하면 JVM은 메인 클래스의 객체를 생성하는 것이 아니라, 클래스의 static으로 선언된 메서드를 객체 생성 없이 메모리에 할당시키고 할당된 메서드 중 "main"으로 네이밍된 메서드가 있는지 찾아 호출하게 된다. 즉 "(메인 클래스).main()"이 JVM 상에서 실행되는 것이다. 따라서 Entry Point 즉, main() 메서드는 static 이어야 한다. (static 멤버 호출 순서 : 정적 변수 -> 정적 초기화문 -> 정적 메서드)

 

코틀린의 main 함수를 설명하는 이미지

 

이것은 Kotlin도 마찬가지다. 패키지 내에 바로 함수 및 변수를 선언하면 정적으로 사용할 수 있다. 만약 클래스를 따로 선언하고 정적 함수나 변수를 사용하고 싶다면 "companion object"를 사용하면 된다.

class Sample {
    companion object {
        fun test() {
            println("Sample ::::")
        }
    }
}

// 실행
fun main(args: Array<String>) {
    Sample.test()
}

그렇다면 선언된 클래스 내부에 main 함수를 사용할 수 있을까? 사용할 수 있다. 대신 @JvmStatic을 사용해야 한다는 조건이 있다. 이 어노테이션은 자바에서 정적 메소드를 갖도록 한다. 또한 코드가 자바의 필드 및 메서드로 해석되도록 알려줘야 할 때 이 어노테이션을 사용한다. 즉 @JvmStatic을 사용하면 컴파일러는 객체를 둘러싸는 클래스의 정적 메서드와 객체 자체의 인스턴스 메서드를 모두 생성합니다. 그리고 해당 필드를 private으로 선언하고 getter과 setter을 작성한다. (참고로 Kotlin 1.3부터는 클래스가 아닌 인터페이스에서도 @JvmStatic를 사용할 수 있다. 인터페이스의 정적 메서드 사용은 Java 1.8에서도 사용 가능하다.)

class Sample {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            println("sample ----")
        }
    }
}

 

@JvmStatic을 쓰지 않으면 Sample.Companion.main()으로 디컴파일돼서 JVM이 찾지 못한다. (지인 YJ의 피드백으로 이 내용을 추가하였다.)

 


 

Plus~

 

코틀린에서 클래스 선언과 생성자를 헷갈려하는 경우가 있다. 이 기회에 정리를 해보자. 생성자는 두가지로 분류된다. 하나의 Primary constructor와 다수의 Secondary constructor.

 

Primary constructor는 constructor 키워드와 괄호 등을 생략할 수 있다. 실행문을 가질 수 없기 때문에 init 블럭을 사용하는 경우가 생길 수 있다. Secondary constructor는 Primary constructor와는 다르게 constructor 키워드를 생략할 수 없다. 신기하게도 Primary constructor가 없이 Secondary constructor를 사용할 수 있다. 만약 Primary constructor가 존재하는 경우, Secondary constructor는 this 키워드를 이용해 직간접적으로 Primary constructor에 위임해야 한다.

 

만약 추상 클래스가 아닌 클래스가 어떠한 생성자(primary or secondary)도 선언하지 않았더라도, 코틀린은 자동으로 아무런 파라미터를 갖지 않은 Primary constructor를 생성해준다. 이는 마치 자바에서 기본 생성자를 자동으로 생성해주는 것과 같다.

// 아래 2개 모두 Primary constructor
class Person constructor(firstName: String) { /*...*/ }

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}


// 클래스 선언 내부에 Secondary constructor가 있는 경우
// 이 때 Primary constructor는 존재하지 않음
class Person {
    var children: MutableList<Person> = mutableListOf()
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

// Secondary constructor가 Primary constructor에 위임한 경우
class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

 


 

Plus~~

 

관례상 Kotlin 프로그램의 진입점은 main(args : Array<String>)이라는 시그니처 함수이다. 여기서 "args"는 프로그램에 전달된 명령행 인자이다. 그러나 모든 응용 프로그램이 명령행 인자를 필요로 하는 것이 아니므로 매개 변수는 종종 사용되지 않는다. Kotlin 1.3은 매개 변수가 없는 더 간단한 형태의 main을 쓸 수 있다.

fun main() {
    println("Hello, world!")
}

 

 

참고자료 출처
- 왜 main()메소드는 static인가?
- 코틀린에는 static이 없다? - companion object
- 코틀린 생성자(Kotlin Constructor) 제대로 이해하기
- What's New in Kotlin 1.3
728x90
반응형