1. 클래스와 프로퍼티 , 필드와 프로퍼티 차이
코틀린에서는 함수,클래스 기본 접근제어자가 public이다.
자바의 한 클래스를 코틀린으로 바꿔보자
public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name) {
this(name, 1);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
name은 불변인 특징을 가진 클래스이다.
코틀린으로 바꿔보자
class Person constructor(name: String, age: Int) {
val name: String = name
var age: Int = age
}
클래스 옆에 constructor를 이용하여 생성자를 정의해줄수 있다.
생성자의 매개변수를 필드에 바로 주입도 가능하다.
또한 주입되는 값을 이용하여 타입이 추론 되므로 타입 또한 생략 가능하다.
class Person constructor(name: String, age: Int) {
val name = name
var age = age
}
Property(프로퍼티) => 필드 + Getter + Setter
코틀린은 필드만 작성하면 프로퍼티의 나머지인 Getter, Setter를 자동으로 생성해준다.
그리고 constructor 라는 keyword(지시어)는 생략 가능하다.
class Person (name: String, age: Int) {
val name = name
var age = age
}
그리고 코틀린에서는 생성자를 만들어줄때 프로퍼티로 선언과 초기화를 동시에 해줄 수 있다.
class Person (val name: String, var age: Int) {
}
동작 방식
- val name: String과 var age: Int가 클래스의 프로퍼티로 바로 생성되고 초기화됩니다.
- 주 생성자의 파라미터가 자동으로 클래스의 프로퍼티로 설정됩니다.
- 따로 프로퍼티를 초기화하거나, 값을 다시 할당할 필요가 없습니다.
Kotlin에서 프로퍼티(property)는 필드(field)와 Getter/Setter 메서드를 결합한 개념입니다.
이를 통해 Kotlin은 더 간결하고 직관적인 방식으로 객체의 데이터를 다룰 수 있습니다.
프로퍼티란?
- 프로퍼티는 클래스 내부에서 값을 저장하고, 읽거나 수정할 수 있는 속성을 의미합니다.
- Kotlin의 프로퍼티는 자동으로 Getter/Setter를 제공하여, 필드 접근 및 제어를 간소화합니다.
예시
class Person(val name: String, var age: Int)
- val name: String
- 읽기 전용 프로퍼티.
- 내부적으로 private final String name 필드와 public String getName() Getter를 자동 생성.
- var age: Int
- 읽기 및 쓰기 가능한 프로퍼티.
- 내부적으로 private int age 필드와 public int getAge() 및 public void setAge(int age)를 자동 생성.
필드(Field)란?
- 필드는 객체의 데이터를 실제로 저장하는 저수준 메모리 공간입니다.
- Kotlin에서는 직접 필드에 접근하지 않고, 프로퍼티를 통해 데이터를 읽거나 수정합니다.
- 필드는 프로퍼티의 일부로 캡슐화되며, 일반적으로 private으로 선언되어 외부에서 접근할 수 없습니다.
예시
class Person(var age: Int)
컴파일 시 생성되는 Java 코드
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
필드와 프로퍼티의 관계
- 프로퍼티는 필드를 감싸는 역할을 하며, 필드의 읽기/쓰기 동작을 캡슐화합니다.
- Kotlin에서는 기본적으로 프로퍼티를 사용하며, 개발자가 직접 필드를 정의할 필요가 거의 없습니다.
- 필드는 내부 구현에 숨겨져 있고, 프로퍼티를 통해 제어됩니다
프로퍼티와 필드 관계 예
class Person(var age: Int) {
init {
age = if (age > 0) age else 0 // 프로퍼티를 통해 초기화
}
}
컴파일된 Java 코드
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age > 0 ? age : 0; // Getter/Setter로 필드를 제어
}
결론
- 프로퍼티: Kotlin의 고수준 개념으로, 데이터를 저장하고 관리하며 Getter/Setter를 자동 생성.
- 필드: 프로퍼티가 내부적으로 데이터를 저장하는 저수준 메커니즘.
- 차이점: 프로퍼티는 Kotlin이 제공하는 추상화된 개념이며, 필드는 이를 구현하기 위한 실제 저장소.Kotlin에서는 프로퍼티를 사용하는 것이 일반적이며, 필드를 직접 조작할 필요는 없습니다.
그리고 클래스 body에 아무것도 없기 때문에 중괄호는 생략 가능하다.
class Person (
val name: String,
var age: Int
)
그리고 getter의 setter를 .필드를 통해 쉽게 호출 할 수 있다.
fun test() {
val person = Person("gil", 10)
println(person.name)
person.age = 1111
}
그리고 자바로 작성된 클래스도 getter의 setter를 .필드를 통해 쉽게 호출 할 수 있다.
val javaPerson = JavaPerson("gil", 10)
println(javaPerson.age)
javaPerson.age = 100
2. 생성자와 init , 주 생성자 ,부 생성자
자바 코드에서는 다음 코드와 같이 생성자에서 검증을 하고 필드값을 초기화 해줄 수 있었다.
public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name, int age) {
if (age <= 0) {
throw new IllegalArgumentException(String.format("나이는 %s일 수 없습니다", age));
}
this.name = name;
this.age = age;
}
}
코틀린에서는 다음과 같이 작성할 수 있다.
class Person(
val name: String,
var age: Int
) {
init {
if (age > 0) {
throw IllegalArgumentException("no")
}
}
}
init 블록을 사용하는데 init은 이 클래스가 초기화 되는 시점에 한번 호출되는 블록이다.
그러므로 init 블록안에 필드값을 처리해서 넣어주거나 ,검증하거나 하는 로직들을 넣어주면 된다.
init 블록의 용도
- init 블록은 객체가 생성될 때 주 생성자에서 전달된 값을 사용해 초기화 작업을 수행하는 데 사용됩니다.
- 클래스의 주 생성자와 연결되어 있으며, 객체가 생성될 때 한 번만 호출됩니다.
주 생성자(Primary Constructor)
Kotlin에서 주 생성자는 클래스 정의와 함께 선언되는 생성자입니다.
주 생성자는 클래스 이름 바로 뒤에 선언되며, 클래스 인스턴스를 생성할 때 반드시 호출되는 기본 생성자입니다.
주 생성자의 선언 방법
주 생성자는 class 키워드 뒤에 선언되며, 클래스 이름과 함께 파라미터를 정의합니다.
이 파라미터들은 클래스의 프로퍼티로 바로 사용할 수 있습니다.
class Person(val name: String, var age: Int)
주 생성자의 특징:
- 클래스 정의와 함께 선언됩니다.
- 자동으로 프로퍼티가 생성됩니다 (주 생성자의 파라미터가 자동으로 val 또는 var로 변환).
- init 블록을 통해 초기화 작업을 수행할 수 있습니다.
부 생성자(Secondary Constructor)
Kotlin에서는 부 생성자도 정의할 수 있습니다.
부 생성자는 주 생성자 외에 추가로 선언되는 생성자로, 주 생성자에서 할 수 없는 다른 초기화 작업을 추가할 때 사용됩니다.
부 생성자 선언 방법
부 생성자는 constructor 키워드를 사용하여 선언하며, 주 생성자 없이 사용할 수 있습니다.
부 생성자는 여러 개를 정의할 수 있습니다.
주생성자, 부생성자 ,init 사용 예시
class Person(
val name: String,
var age: Int
) {
init {
if (age > 0) {
throw IllegalArgumentException("no")
}
}
constructor(name: String) : this(name, 0) // this를 이용하여 주 생성자를 호출
//두번째 부 생성자에서는 첫번째 부 생성자를 this(name,11)를 통해 부른다.
//첫번째 부 생성자가 주 생성자를 호출하므로 두번째 부 생성자도 결론적으로는 주 생성자를 호출하게 된다.
constructor(name: String,gender: String) : this(name,11){
println("성별 : $gender")
}
}
부 생성자에서 this를 사용하여 주 생성자를 호출한다.
부 생성자는 있을 수도 있고 없을 수도 있다, 부 생성자는 최종적으로 주생성자를 this로 호출해야한다.
주 생성자는 반드시 존재해야한다.
단, 주 생성자에 파라티머가 하나도 없다면 생략 가능하다.
생략 예시)
class Student
fun main() {
val student = Student()
}
부생성자는 결국 주 생성자를 호출하면 된다. 예시)
class Person(
val name: String,
var age: Int
) {
init {
println("초기화 블럭 실행")
if (age > 0) {
throw IllegalArgumentException("no")
}
}
constructor(name: String) : this(name, 0) {
println("첫번째 부 생성자")
}
constructor() : this("홍길동"){
println("두번째 부 생성자")
}
}
fun main() {
val person = Person()
}
두번째 부 생성자를 사용했지만
두번째 부 생성자가 this("홍길동")을 통해 첫번째 부 생성자를 호출하므로
첫번째 부 생성자의 this(name,0)을 통해 주 생성자가 실행되고
주 생성자가 실행됨에 따라 초기화 블럭이 실행되고... 의 역순으로 실행된다.
class Person(
val name: String,
var age: Int
) {
init {
println("초기화 블럭 실행")
if (age > 0) {
throw IllegalArgumentException("no")
}
}
constructor(name: String) : this(name, 0) {
println("첫번째 부 생성자")
}
constructor() : this("홍길동",11){
println("두번째 부 생성자")
}
}
fun main() {
val person = Person()
}
두번째 부 생성자도 this를 통해 주 생성자를 바로 호출해도 된다.
class Person(
val name: String,
var age: Int
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
constructor(name: String) : this(name, 0) {
println("첫번째 부 생성자")
}
constructor() : this("홍길동",11){
println("두번째 부 생성자")
}
}
fun main() {
val person = Person()
}
하지만 부 생성자보다는 Default Parameter를 권장한다.
class Person(
val name: String = "gil",
var age: Int = 10
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
}
constructor에 기본 파라미터값을 넣어줘서 쓰는것이 더 깔끔하다.
Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천 한다.
예시)
class Person private constructor(
val name: String,
var age: Int
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
companion object {
// 정적 팩토리 메소드
fun createWithDefaultValues(): Person {
return Person("gil", 10)
}
fun createWithName(name: String): Person {
return Person(name, 10)
}
}
}
fun main() {
val person1 = Person.createWithDefaultValues() // 기본값을 사용하는 생성
val person2 = Person.createWithName("Alice") // 이름만 지정하는 생성
}
companion object는 Kotlin에서 클래스 내부에 정의할 수 있는 동반 객체를 의미합니다.
companion object는 클래스의 인스턴스 없이도 접근할 수 있는 정적 객체입니다.
이를 사용하면 자바의 static 메소드처럼, 클래스 수준에서 동작하는 메소드나 속성들을 정의할 수 있습니다.
3. 커스텀 getter, setter , backing field
성인인지 확인하는 코드를 짤때 자바에서는 다음과 같이 했다.
public class JavaPerson {
private final String name;
private int age;
public boolean isAdult() {
return this.age >= 20;
}
}
코틀린에서는
fun isAdult(): Boolean {
return this.age >= 20
}
이렇게 작성할 수 도 있지만
isAdult함수 대신 isAdult 프로퍼티로도 만들 수 있다.
isAudult 프로퍼티의 Getter또한 Custom 할 수 있다. [ Custom Getter ]
예시1)
class Person(
val name: String = "gil",
var age: Int = 10
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
val isAdult: Boolean
get() = this.age>=20
}
isAdult라는 읽기전용(val,read-only) 프로퍼티(속성)을 만들고
get()를 이용하여 해당 프로퍼티의 getter를 커스텀했다.
fun main() {
val person = Person()
val adultFlag = person.isAdult
}
person.isAdult를 통해 isAdult라는 프로퍼티에 접근하게되고
그때 getter를 호출하는데 get()으로 만들어둔 custom getter가 호출된다.
예시2)
class Person(
val name: String = "gil",
var age: Int = 10
) {
init {
println("초기화 블럭 실행")
if (age < 0) {
throw IllegalArgumentException("no")
}
}
val isAdult: Boolean
get() {
return this.age>=20
}
}
커스텀 게터는 중괄호를 이용하여 구현할 수 도 있다.
모두 동일한 기능이고 표현 방법만 다르다
첫번째꺼는 함수인것처럼 접근하는 방식이고
두번째,세번째꺼는 프로퍼티처럼 접근하는 방식이다.
객체의 속성을 나타내는것이라면 custom getter가 활용되는 프로퍼티를 쓰고
그렇지 않다면 함수를 쓰는것을 추천한다.
이 예시에서 isAdult는 Person 객체가 성인인지 속성을 확인하는것처럼 보인다.
그때는 custom getter가 활용되는 프로퍼티를 쓰는게 좋을거 같다.
자바였으면 isAdult라는 필드가 있을것이고
생성자에서 age값을 체크해서 isAdult에 어떤값을 초기화해줄게 결정됬을것이다.
public class JavaPerson {
private final String name;
private int age;
private boolean isAdult = false;
public JavaPerson(String name, int age) {
if (age > 20) {
this.isAdult = true;
}
this.name = name;
this.age = age;
}
}
코틀린에서는 프로퍼티와 custom getter를 사용해서 자바코드에서 작업을 간소화할 수 있다.
class Person(
val name: String = "gil",
var age: Int = 10
) {
val isAdult: Boolean
get() = this.age >= 20
}
isAdult라는 프로퍼티를 val(자바에서 final, read-only,읽기전용)으로 만들어서 isAdult라는 프로퍼티에 어떤값도 저장되지않게 만든다.
그후 get()을 통해 게터를 재정의해서 custom getter를 만들고
이후
person객체를 통해 person.isAdult 와 같이 프로퍼티 접근법을 사용하면
isAdult의 getter가 동작하므로
this.age >= 20
의 로직이 동작한다.
person.isAdult가 호출될때 마다 위의 로직이 동작하고
age의 값이 달라질때 반환되는값은 달라진다.
Custom getter 예시2)
이번에는 name 프로퍼티를 접근할때 대문자로 변환해서 반환하도록 바꿔보자
class Person(
name: String = "gil",
// 여기에 val을 붙여주면 코틀린이 알아서 name을 프로퍼티로 만들어준다.
// 그러므로 val을 빼서 그냥 생성자의 매개변수로 취급되게 만들어준다.
var age: Int = 10
) {
val name = name
get() = name.uppercase()
}
custom getter를 생성하기 위해 주 생성자의 매개변수에서 val를 제거해준다.
[ 주 생성자 매개변수에 val을 붙임으로서 코틀린이 name 프로퍼티를 자동 생성해줬기 때문
( 필드,getter,setter를 자동생성해줬다.) ]
위의 코드는 문제가 있는 코드이다.
외부에서 person.name 을 하게되면 name 프로퍼티의 getter()가 실행된다 [name의 값을 가져와야하니까]
즉 get() 뒤의 로직인 name.uppercase()가 실행되고 반환된다.
그때 getter의 로직인 name.uppercase()를 실행하려고할때 name또한 get()를 호출하게된다.
[ name의 값을 가져오고 uppercase()를 적용해야하니까 ]
즉 get()을 실행하려고할때 name부분에서 name을 가져와야하니까 name이라는 프로퍼티에 접근하게 되고
프로퍼티 접근할때 다시 getter를 호출하게되고 또 getter를 실행하려고 보니 name이 있으니까 ....
무한으로 getter를 호출하게된다.
그걸 방지하기 위해
class Person(
name: String = "gil",
var age: Int = 10
) {
val name = name
get() = field.uppercase()
}
자기자신을 가리키는 filed라는 예약어(keywords)를 사용한다.
[ 예약어, 지시어 둘다 영어로 키워드라고 하는거 같다. ]
이러한 filed를 뒤에있는,보이지않는 filed라는 의미로 backing filed라고 부른다.
field는 자바에서 흔히 말하는 그 필드 (저수준 메모리 저장소)를 의미한다.
name이라는 프로퍼티를 직접사용하면 getter가 호출되서 무한루프가 되니까
getter를 통하지않고 field를 직접가져와서 무한루프를 해결하는것이다.
[ property => filed + getter + setter ]
하지만 custom getter에서 backing field를 사용하는 경우는 적다고 한다.
위의 요구사항을 다음과 같이 처리 가능하다.
class Person(
var name: String = "gil",
var age: Int = 10
) {
val upperCaseName: String
get() = this.name.uppercase()
}
왜 두 번째 방식이 더 좋은가?
- 직관성: upperCaseName은 명확하게 변환된 값을 제공하는 프로퍼티로, name 프로퍼티와는 독립적인 성격을 가집니다. name은 원본 데이터를 저장하고, upperCaseName은 변환된 데이터를 제공합니다.
- 의도 명확성: name을 대문자로 변환하는 기능은 name의 값과는 별개로 제공되어야 하므로, upperCaseName을 별도의 프로퍼티로 두는 것이 코드의 의도를 더 명확하게 나타냅니다.
- 불필요한 field 사용 방지: 첫 번째 방식에서는 field를 사용하여 name의 값을 대문자로 변환하는데, 이는 결국 name과 동일한 값을 변형한 결과를 제공하는 것이므로 field를 직접 다루는 것이 약간 부자연스러울 수 있습니다.
결론:
두 번째 방식 (upperCaseName을 별도로 정의하는 방식)이 더 나은 방식입니다. name을 직접적으로 변형하는 대신, 변형된 값을 별도의 프로퍼티로 제공하는 것이 코드의 의도를 더 잘 표현하며, 유지보수성과 가독성 측면에서도 더 좋습니다.
정리:
val,var로 선언되는 프로퍼티는 filed(=값,backing field,값을 저장하는 변수)를 가진다.
하지만 val로 선언된 프로퍼티에 custom getter를 선언할때는 조심해야한다.
예시1) backing field가 생성되지않는 경우
val t1:String = "A"
get() = "B"
이 경우 컴파일 에러가 발생한다. -> Initializer is not allowed here because this property has no backing field
그 이유는 getter가 "B"(단일 값)을 반환하기때문에 backing filed가 필요없다.
그래서 backing field가 없는데 "A"로 초기화를 하려고 하니까 에러가 발생한다.
예시2) backing field가 생성되는 경우
val t2:String = "A"
get() = field+ "B"
이 경우에는 컴파일 에러가 발생하지 않는다.
getter에서 field를 사용하기때문에 backing field가 존재하고, 그래서 초기값을 저장할 수 있게된다.
Custom Setter
class Person(
name: String = "gil",
var age: Int = 10
) {
var name = name
set(value) {
field = value.uppercase()
}
}
setter 무한루프를 막기위해 field (backing filed)를 사용해주었다.
var name = name
set(value) {
name = value.uppercase()
}
이런식으로 작성하면 name = 가 setter를 호출하게되고 set()이 동작하면서 또 name = 를 만나게되서 sette가 무한 호출된다.
사실 setter 자체를 지양하기 때문에 custom setter도 잘 안쓴다고 한다.
[ setter 보다는 update같은 함수를 만들어서 제공하기 때문이다. ]
'인프런 > 자바 개발자를 위한 코틀린 입문' 카테고리의 다른 글
6) 코틀린에서 접근 제어를 다루는 방법 (0) | 2025.01.09 |
---|---|
5) 코틀린에서 상속을 다루는 방법 (0) | 2025.01.08 |
3) 코틀린에서 조건문,반복문,예외,함수를 다루는 방법 (0) | 2025.01.07 |
2) 코틀린에서 Type, 연산자를 다루는 방법 (0) | 2025.01.06 |
1) 코틀린에서 변수,null을 다루는 방법 (0) | 2025.01.06 |
댓글