[기초] 코틀린에서 클래스를 다루는 방법

정리

- 코틀린에서는 필드를 만들면 getter와 (필요에 따라) setter가 자동으로 생긴다.
  → 이를 property 라고 부른다.
- 코틀린에서는 주생성자가 필수이다.
- 코틀린에서는 constructor 키워드를 사용해 부생성자를 추가로 만들 수 있다.
  → 단, default parameter나 정적 팩토리 메소드를 추천
- 실제 메모리에 존재하는 것과 무관하게 custom getter와 custom setter를 만들 수 있다.
  → 메모리에 존재하는 것과 무관하다는 의미는 함수로도 만들 수 있지만 property인 것처럼 사용하는 것을 말한다
- custom getter/setter에서 무한루프를 막기 위해 field라는 키워드를 사용하고, 이를 backing field라고 부른다.

 

Class와 Property

이름이 불변인 Person 객체를 만들어보자

 

Java

public class JavaPerson {
    private final String name;
    private int age;
    
    public JavaPerson(String name, int age) {
    	this.name = name;
        this.age = age;
    }
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public void setAge(int age) {
    	this.age = age;
    }
}

 

Kotlin

class Person constructor(name: String, age: Int){
    val name: String = name	// Type 생략 가능(val name = name)
    var age: Int = age		// Type 생략 가능(var age = age)
}

프로퍼티 = field + getter + setter 이기 때문에 Kotlin에서는 필드만 만들면 getter / setter를 자동으로 만들어준다.

     : .필드를 통해 getter / setter를 호출할 수 있다.

constructor 라는 지시어도 생략 가능하다.

→ 클래스의 필드 선언(프로퍼티 선언)과 생성자 선언을 동시에 할 수 있다

class Person (
    val name: String,
    var age: Int
)

최종적으로는 위와 같다.

println(pserson.name) 또는 perspon.age = 10 등을 통해서 값을 갖고오거나 넣어줄 수 있다.

→ Java 클래스에 대해서도 .필드를 통해 getter / setter를 사용할 수 있다.

 

생성자와 init

클래스가 생성되는 시점의 나이를 검증해보자

 

Java

public class JavaPerson {
    private final String name;
    private int age;
    
    public JavaPerson(String name, int age) {
    	if (this.age <= 0) {
    		throw new IllegalArgumentException(String.format("나이는 %s일 수 없습니다", age));
        }
    	this.name = name;
        this.age = age;
    }
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public void setAge(int age) {
    	this.age = age;
    }
}

 

Kotlin

class Person (
    val name: String,
    var age: Int
) {
	init {
    	if (age <= 0) {
        	throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
        }
    }
}

init : 이 클래스가 초기화되는 시점에 한 번 호출되는 블록

→ 값을 적절히 만들어주거나, validation 로직을 넣거나 하는 용도로 사용된다.

 

class Person (
    val name: String,
    var age: Int
) {
	init {
    	if (age <= 0) {
        	throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
        }
    }
    
    constructor(name: String): this(name, 1)
}

코틀린에서는 새로운 생성자를 constructor를 이용해 만들 수 있다.

 

fun main() {
	val person = Person("연이")
}

이를 이용해 Person 클래스를 이름만 놓고도 인스턴스화 할 수 있다.

 

  • 주생성자는 반드시 존재해야 한다.
    • 단, 주생성자에 파라미터가 하나도 없다면 생략 가능하다.
  • 부생성자는 있을 수도 있고 없을 수도 있다.
    • 최종적으로 주생성자를 this로 호출해야 한다.
    • body를 가질 수 있다.

🤔 Kotlin에서는 기본적으로 부생성자보다는 default parameter를 권장한다.

→ Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드가 더 추천한다.

 

커스텀 getter/setter

성인 확인 로직을 구현해보자

 

Java

public boolean isAdult(){
    return this.age >= 20;
}

 

Kotlin

class Pserson(
    val name: String = "연이",
    var age: Int = 1
){
  
    val isAdult: Boolean
    	get() = this.age >= 20
        // get() { return this.age >= 20 }
}

코틀린에서는 custom getter를 이용한 프로퍼티로 만드는 방식과

fun isAdult(): Boolean {
    return this.age >= 20
}

Java와 동일하게 함수로 만드는 방법이 있다.

 

이제는 이름을 무조건 대문자로 세팅해주는 로직을 구현해보자

class Pserson(
    name: String = "연이",
    var age: Int = 1
){
	var name = name
    	set(value) {
        	field = value.uppercase()
        }
}

setter를 써야하기 때문에 var로 선언해준다.

value는 밖에서 들어오는 파라미터를 의미한다.

backing field

name을 get할 때 무조건 대문자로 바꿔서 반환해보자

class Pserson(
    name: String = "연이",
    var age: Int = 1
){
    val name = name
    	get() = field.uppercase()
}

val로 클래스에 선언하게 되면 하나의 프로퍼티기 때문에 get을 자동으로 만들어준다.

그래서 자동으로 만들어주지 않기 위해(getter를 커스터마이징 할 것이기 때문에) val을 빼고 하단에 써주자.

 

주생성자 아래에 프로퍼티를 만들고 get() = name.uppercase()라고 작성해버리면,

코틀린에서 프로퍼티에 접근하는 행위(person.name)는 내부적으로 자동으로 생성된 게터(Getter) 메서드를 호출하는 것이다.
만약 주생성자 아래에 프로퍼티를 만들고 get() = name.uppercase()라고 작성하면, name을 읽기 위해 다시 get()을 호출하게 된다.
이 과정이 get()name 호출 → get()name 호출 순으로 반복되며 무한루프가 발생하게 된다.

 

이러한 상황을 방지하기 위해서, getter안에 프로퍼티 이름대신  자기 자신을 가리키는 field라는 예약어를 통해서 처리한다.

그래서 이러한 field를 자기 자신을 가리키는 보이지 않는 필드(백킹 필드)라고 한다.

 

그치만 backing field보다는 아래의 두가지 방법이 좀 더 많이 쓰인다.

class Person(
    val name: String = "연이",
    var age: Int = 1
){
    // 1. 
    fun getUppercaseName(): String = this.name.uppercase()
    
    // 2.
    val uppercaseNmae: String
    	get() = this.name.uppercase()
}