class Student(name: String) {
val studentName: String
init {
this.studentName = name
}
fun getStudentName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.studentName)
println(student.studentName)
}
위 코드를 실행하니, 다음과 같은 오류가 발생했다.
흠🤔 시그니처가 같은 메서드가 두 개 이상 선언된 것 같은데...
잘 모르겠으니 위 Kotlin 코드를 Java 코드로 변환하여 뜯어보자.
참고로, Kotlin 코드를 Java 코드로 변경하는 방법은 아래를 참고하자.
https://velog.io/@hunzz/intellij-1
엥 나는 Student 클래스에서 하나의 getStudentName() 메서드만 만들었는데,
왜 Java 코드에서는 2개의 getStudentName() 메서드가 만들어진걸까?
public class Student {
private String studentName;
private int studentAge;
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public int getStudentAge() {
return studentAge;
}
public void setStudentAge(int studentAge) {
this.studentAge = studentAge;
}
}
Java에서는 보통 멤버 변수를 private로 선언하여 외부에서 멤버 변수로의 직접 접근을 막고(= 캡슐화), 그 대신 간접적으로 접근할 수 있도록 getter/setter 메서드를 개발자가 직접 생성해야만 했다. 이 때, (물론 setter를 제공해주는 경우는 흔치 않지만) 대부분의 멤버 변수에 getter/setter를 제공해주다보면, 코드가 길어져 가독성이 떨어지는 큰 문제점이 있다.
class Student() {
var studentName: String = ""
}
fun main() {
val student = Student()
student.studentName = "John" // setter
println(student.studentName) // getter
}
이를 보완하고자 Kotlin에서는, 클래스 내부에서 멤버 변수를 선언하게 되면 위와 같이 getter/setter를 개발자가 따로 만들어주지 않아도 자동으로 생성해준다.
참고로, val로 멤버 변수를 선언했을 때에는 getter만 자동으로 생성해준다.
class Student(name: String) {
val studentName: String
init {
this.studentName = name
}
fun getStudentName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.studentName)
println(student.studentName)
}
아무튼, 맨 처음에 오류가 났던 그 코드로 돌아가보자.
Student 클래스에서 멤버 변수 studentName는 val로 선언되었고, 그렇기 때문에 JVM에서 자동으로 getter를 만들어 줄 것이다. 이 때, 자동으로 만들어지는 getter의 메서드명은 get+[멤버변수명]으로 만들어진다.
이미 getter가 JVM에 의해 생성되었음에도 불구하고, 내가 임의로 getStudentName() 메서드를 또 만들었으니... 시그니처가 같은 메서드가 두 개가 선언된 꼴이 되어 오류가 발생한 것이다!
이를 해결하기 위해서는 어떻게 해야할까?
1) 멤버 변수의 접근 제한자를 private로 설정한다.
class Student(name: String) {
private val studentName: String
init {
this.studentName = name
}
fun getStudentName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.getStudentName())
}
클래스 내에서 멤버 변수를 private var과 private val로 선언하면 JVM은 getter/setter를 제공해주지 않는다. 따라서 접근 제한자를 private로 선언한 후에 개발자가 임의로 getter/setter를 만들어서 사용하면, 메서드명이 겹칠 일이 없어서 오류가 발생하지 않는다.
2) 메서드명을 변경한다.
class Student(name: String) {
val studentName: String
init {
this.studentName = name
}
fun getName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.getName())
}
정말 간단한 해결방법이다. JVM이 getStudentName()이라는 getter를 자동으로 생성해주니깐, 나는 getStudentName을 제외한 다른 이름으로 메서드명을 변경하는 것이다. getName()으로 변경해보는 것이 그나마 차선책일 듯 하다.
하지만, 방법 2)는 그다지 권장하는 방법은 아니다.
메서드명은 해당 메서드의 기능을 직관적으로 파악할 수 있어야 하는데, 메서드명을 getName보다는 getStudentName로 선언했을 때 '학생의 이름을 받는 메서드'라는 의미가 더 부각되기 때문이다.
3) @JvmName 어노테이션을 사용한다.
class Student(name: String) {
val studentName: String
init {
this.studentName = name
}
@JvmName("getName")
fun getStudentName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.getStudentName())
}
개발자가 임의로 만든 getter 위에 @JvmName 어노테이션을 붙인다. @JvmName 어노테이션이 붙어진 메서드는, Java 바이트코드(.class)로 컴파일될 때 사용자가 정의해놓은 이름으로 컴파일된다.
그런데 이 경우에는 getStudentName 메서드와 getName 메서드가 두 개가 생기게 된다. 큰 영향은 없지만, 굳이 메서드명을 바꿔가면서 쓸데없는 메서드를 하나 더 만들어내는게 불편하다면 아래와 같은 방법을 사용해보자.
4) @JvmField 어노테이션을 사용한다.
class Student(name: String) {
@JvmField
val studentName: String
init {
this.studentName = name
}
fun getStudentName() = this.studentName
}
fun main() {
val student = Student("John")
println(student.getStudentName())
}
클래스 내 멤버 변수에 @JvmField 어노테이션을 붙이면, 컴파일 시 JVM이 getter와 setter를 만들어주지 않는다. 따라서 개발자가 임의로 만든 getter 메서드만 만들어지게 되는 것이다.