getter/setter에 대한 질문에 답변하고자 블로그 글을 작성합니다!
현재 제가 코틀린 프로그래밍에 대한 책을 쓰고 있는 중이라 홈페이지나 미흡한 동영상 강의 부분을 업데이트 하지 못했는데요. 책을 쓰면서 댓글에 지적된 내용이나 동영상에서 미흡한 부분을 최대한 쉽게 설명하도록 하고 있습니다. 책이 출간되면 동영상도 업데이트할 예정이니 많은 관심 부탁합니다. ^^
게터 세터가 예전부터 궁금했었는데 이해가 잘 되지 않았는데 어느정도 이해가 됐습니다. 하지만 분명한 차이를 느끼지 못하겠습니다. 게터도 30이상이면 old라는 값을 반환하고 30이하면 young이라는 값을 반환합니다. 세터도 0 이상이면 그냥 반환하고 0 이하면 Exception값으로 오류를 발생시키는 것 같습니다. 이렇게 보면 차이가 너무 모호한 것 같습니다. 제가 이해한 바로는 둘다 한 값의 기준을 주고 기준 a면 A값을 b면 B값을 리턴하는 거로밖에 안보이는데 차이점이 무엇인가요? 또 프로퍼티가 var 값이면 커스텀 게터 세터가 가능한데 val 이면 세터가 선언이 안된다고 알고 있습니다. 세터는 (setter)니까 프로퍼티 값을 setting하는 개념으로 프로퍼티의 값의 영향을 줄 수도 있어서 안되는 건가요?
먼저 간단히 답변드리고 소스코드를 다시 단계적으로 설명해 보겠습니다.
var
로 선언하면 값을 변경할 수 있는(mutable) 프로퍼티로 getter/setter 둘 다 접근 가능해 집니다. 코틀린에서 선언되는 프로퍼티는 보이진 않지만 기본적으로 getter/setter를 가지고 있고 사용자가 동작 방식을 변경할 수 있습니다. val
로 선언되면 변경할 수 없는(immutable)이기 때문에 값을 변경하는 setter는 허용되지 않으니 getter만 사용할 수 있는 것이죠. 물론 사용자가 변경하고 싶어도 getter만 변경할 수 있습니다.
getter 의 작동은 다음 코드에 의해 작동하죠.
kindong.look // 이것은 사실 look 프로퍼티의 게터인 get() 을 호출한다.
30이상이면 old, 아니면 young를 반환하는 것은 수정된 getter의 역할입니다. getter의 변경된 구현부는 다음과 같죠.
var look: String = "unknown" get() { if(age > 30) { ... } else { ... }
이제 setter가 작동되는 main
블록의 코드를 봅시다.
kildong.age = <인자값> // 인자값은 프로퍼티 age의 세터인 set(value)의 매개변수 value에 전달 된다.
값 설정시 kildong.age = <인자값>
이 setter의 value
로 들어오게 되고 값이 0이하면 예외를 발생 하도록 의도적으로 setter의 기능을 변경 한 겁니다. setter 구현 코드를 봅시다.
var age: Int = _age set(value) { field = if(value > 0) value // 인자로 받은 value를 backing field인 field에 할당 else throw ... (...) // 그렇지 않으면 의도적으로 예외를 발생 }
여기엔 특수한 역할을 하는 변수인 보강 필드(backing field)라고하는 field
가 있는데 이것은 프로퍼티 age
에 값을 넣기 위한 일종의 임시 저장소로 이해하시면 좋습니다. 만일, age
를 그대로 쓰게 되면 age
의 셋터가 무한정 호출되는 재귀에 빠져서 스택이 넘치게 됩니다. ^^;
먼저 다음 선언을 봅시다.
class Person(name: String, age: Int, isMarried: Boolean)
Person 클래스의 주 생성자의 매개변수 3개를 정의했으나 해당 변수는 프로퍼티가 되지 않습니다. 클래스 안에 값을 넘기지 않았기 때문이죠. 클래스의 멤버 프로퍼티가 되려면 다음과 같이 할당합니다.
class Person(_name: String, _age: Int, _isMarried: Boolean) { val name = _name var age = _age var isMarried = _isMarried }
보통 언더라인을 사용해 매개변수를 구성하고 프로퍼티에 할당합니다. 이것을 좀 더 간단하게 표현하면,
class Person(val name: String, var age: Int, var isMarried: Boolean)
이렇게 주 생성자 매개변수 선언 시 val
혹은 var
을 붙여 주는 것이죠. 이렇게 하면 프로퍼티와 함께 정의되 위와 동일하며 간단하게 표현한 것이죠.
이해를 돕기 위해 위의 코드에 생략된 getter와 setter를 넣어 보도록 하겠습니다.
class Person(_name: String, _age: Int, _isMarried: Boolean) { val name = _name get() = field var age = _age get() = field set(value) { field = value } var isMarried = _isMarried get() = field set(value) { field = value } } fun main(args: Array<String>) { val kildong = Person("KilDong", 30, true) // getter의 사용 println(kildong.name) println(kildong.age) println(kildong.isMarried) //setter의 사용 // kildong.name = "Dooly" // val 선언이므로 setter가 없어 할당 불가 kildong.age = 40 kildong.isMarried = false }
결국 val
은 get()
만 가지고 있고, var
은 get()
과 set()
이 지정될 수 있는 것을 알 수 있습니다. 결국 모든 프로퍼티는 다음과 같은 기본 get()
, set()
을 가지게 되는 거죠.
var|val <프로퍼티명> = <기본값> get() = field // getter, field는 프로퍼티를 가르킨다 set(value) { // setter, 받아들인 인자 value를 field에 할당하면서 프로퍼티가 설정됨 field = value }
결국 위의 코드의 get()
, set()
의 표현은 코틀린에서 기본적으로 구현되어 있기 때문에 생략되어 있는 것입니다.
따라서, 코틀린에서는 "프로퍼티 = 필드 + 게터/세터" 라고 말하고 있는 것이죠.
코드를 IntelliJ 의 바이트 코드 변환 후 역컴파일 해보면 자바로 변환된 코드가 나오는데 더 쉽게 이해할 수 있습니다.
Tools > Kotlin > Show Kotlin Bytecode 이후 Decompile 버튼을 눌러 봅니다.
// ... 자바로 변환된 역컴파일 코드
public final class Person {
@NotNull
private final String name;
private int age;
private boolean isMarried;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int value) {
this.age = value;
}
public final boolean isMarried() {
return this.isMarried;
}
public final void setMarried(boolean value) {
this.isMarried = value;
}
// 주 생성자에 해당하는 부분
public Person(@NotNull String _name, int _age, boolean _isMarried) {
Intrinsics.checkParameterIsNotNull(_name, "_name");
super();
this.name = _name;
this.age = _age;
this.isMarried = _isMarried;
}
}
//...
public final class ClassPropertyTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Person kildong = new Person("KilDong", 30, true);
String var2 = kildong.getName(); // getName() 이라는 getter 를 통해 이름을 가져오고 있다.
System.out.println(var2);
int var3 = kildong.getAge(); // getter
System.out.println(var3);
boolean var4 = kildong.isMarried(); // getter
System.out.println(var4);
kildong.setAge(40); // setAge()라는 setter를 통해 40을 지정하고 있다. (40은 value가 됨)
kildong.setMarried(false); // setter
}
}
결국에는 자바에서 보는 것처럼 각 필드에 get...()
/set...()
으로 이루어진 메서들이 있고 이것은 접근 메서드라고도 불립니다.
우리가 코틀린에서 사용한 kildong.name
은 결국 kindong.getName()
을 호출하는 코드와 같은 것이고,
kildong.age = 40
은 kildong.setAge(40)
과 같은 코드가 되는 것이죠.
그래서 setter와 getter의 동작을 때로는 바꾸고 싶을 때 필요로 하는 부분만 프로퍼티에 구현을 추가해 주면 되는 것입니다. var
에서 둘다 필요하면 set()
/get()
두개를 다 구현해도 되고 하나만 구현해도 되는 것이죠. 다음과 같이 필요한 부분만 변경 가능합니다.
class Person(_name: String, _age: Int, _isMarried: Boolean) {
val name = _name
var age = _age
get() = field - 1
set(value) {
field = if (value > 0) value
else throw IllegalArgumentException("Age must be greter than zero")
}
var isMarried = _isMarried
}
fun main(args: Array<String>) {
val kildong = Person("KilDong", 30, true)
// getter의 사용
println(kildong.name)
println(kildong.age)
println(kildong.isMarried)
//setter의 사용
// kildong.name = "Dooly" // val 선언이므로 setter가 없어 할당 불가
kildong.age = 40
kildong.isMarried = false
println(kildong.age)
println(kildong.isMarried)
}
4라인의 age
의 getter를 커스텀으로 넣어 기존 값에 1 감소한 값이 반환 되도록 했습니다. 5-7라인에서 age
의 setter를 구현해 0 이상이 값만 허용하도록 하고 이하인 경우 예외 에러를 내도록 했습니다.
여기 까지 입니다. getter/setter을 이해하는데 도움이 되었길 바랍니다.
"어떤 것을 완전히 알려거든 그것을 다른 이에게 가르쳐라."
- Tryon Edwards -