자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)
강의를 들으며 내용 정리
코틀린에서 클래스를 다루는 방법
클래스와 프로퍼티
<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;
}
}
Person 객체에 생성자와 getter, setter를 만들어줬다. (name은 불변이라 setter가 없다)
프로퍼티: 필드 + getter + setter (ex. name, age)
<kotlin>
class Person constructor(name: String, age: Int) {
val name = name
var age = age
}
코틀린에서 생성자는 constructor를 사용한다.
코틀린은 val, var로 필드를 선언하면 getter, setter를 자동으로 만들어준다.
여기서 조금 더 코드를 다듬어보자.
1. contructor를 생략하고,
2. 클래스의 필드 선언과 생성자를 동시에 선언할 수 있다.
class Person(val name: String, var age: Int)
자바에 비해 코드 양이 크게 줄은 것을 볼 수 있다.
'.' 필드를 통해 getter와 setter를 바로 호출한다.
fun main() {
val person = Person("한영규", 100) // constructor
println(person.name) // getter
person.age = 10 // setter
}
생성자와 init
클래스가 생성되는 시점에 나이를 검증해보자.
<java>
public JavaPerson(String name, int age) {
if (age <= 0) {
throw new IllegalArgumentException(String.format("나이는 %s일 수 없습니다", age));
}
this.name = name;
this.age = age;
}
<kotlin>
class Person(
val name: String,
var age: Int,
) {
init {
if (age < 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
}
}
}
init(초기화) 블록은 생성자가 호출되는 시점에 호출한다.
보통 객체 생성 시 필드 값을 적절히 만들어주거나, validation하는 로직에 사용한다.
생성자를 하나 더 만들자.
<java>
public JavaPerson(String name, int age) {
if (age <= 0) {
throw new IllegalArgumentException(String.format("나이는 %s일 수 없습니다", age));
}
this.name = name;
this.age = age;
}
public JavaPerson(String name) {
this(name, 1);
}
<kotlin>
// 주생성자(primary constructor)
// 반드시 존재해야 한다(단, 파라미터 없는 기본 생성자라면 생략 가능)
class Person(
val name: String,
var age: Int,
) {
init {
if (age < 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
}
}
// 부생성자(secondary constructor)
// 최종적으로 주생성자를 this로 호출해야 한다
constructor(name: String) : this(name, 1)
}
this(name, 1)로 주생성자를 호출한다.
부생성자는 body를 가질 수 있다.
class Person(
val name: String,
var age: Int,
) {
init {
if (age < 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
}
println("초기화 블록")
}
constructor(name: String) : this(name, 1) {
println("부생성자 1")
}
constructor() : this("한영규") {
println("부생성자 2")
}
}
만약 Person()으로 파라미터 없는 생성자를 호출한다면,
1. constructor()가 this("한영규") 호출
2. constructor(name: String)이 this(name, 1) 호출
3. 주생성자 호출
여기서 주의할 점은 본문은 역순으로 실행된다는 점이다.
출력 순서
1. 초기화 블록
2. 부생성자 1
3. 부생성자 2
그런데 사실 부생성자보다는 default parameter를 권장한다.
class Person(
val name: String = "한영규",
var age: Int = 1,
) {
init {
if (age < 0) {
throw IllegalArgumentException("나이는 ${age}일 수 없습니다")
}
}
}
객체를 converting(ex. Ailen -> Person)하는 경우 부생성자를 사용할 수 있지만, 이 경우에는 정적 팩토리 메서드를 추천한다.
커스텀 getter, setter
성인인지 확인하는 기능을 추가하자.
<java>
public boolean isAdult() {
return this.age >= 20;
}
<kotlin>
fun isAdult(): Boolean {
return this.age >= 20
}
함수 대신 프로퍼티로도 만들 수 있다.
// 함수
fun isAdult(): Boolean {
return this.age >= 20
}
// 프로퍼티 -> 커스텀 getter
val isAdult: Boolean
get() = this.age >= 20
// 프로퍼티 -> 커스텀 getter
val isAdult: Boolean
get() {
return this.age >= 20
}
모두 동일한 기능이고 표현 방법만 다르다.
실제로 자바 코드로 decompile해보면 모두 isAdult()라는 함수로 표현된다.
그렇다면 어떤 방법이 나을까?
-> 객체의 속성이라면 커스텀 getter, 그렇지 않다면 함수
isAdult의 경우 사람의 나이를 판별하는 기능이기 때문에 프로퍼티로 사용하는 것을 권장한다.
커스텀 getter를 사용하면 자기 자신을 변형해 줄 수 도 있다.
name을 get할 때 무조건 대문자로 바꿔주자.
class Person(
name: String,
var age: Int,
) {
val name: String = name
get() = field.uppercase()
}
1. 주생성자에서 name 필드 선언 시, getter가 생기지 않도록 val을 제거해준다.
2. 주생성자에서 받은 name을 불변 프로퍼티 name에 대입해준다.
3. name에 대한 커스텀 getter를 만들어준다. 이 때, field를 사용한다.
backing field
Q: 왜 name.uppercase()를 안쓰고 field를 사용할까?
A:get() = name.uppercase()에서 name은 다시 name에 대한 getter를 호출한다 -> 여기서 무한루프가 발생한다.
field는 이러한 무한루프를 막기 위한 예약어로, 자기 자신을 가리킨다. 이를 backing field라고 한다.
사실 커스텀 getter에서 backing field를 쓰는 경우는 드물다.
일반적으로 person.name하면 그대로 name을 get 하는 것을 기대하지, 대문자로 get하는 것을 기대하지 않는다.
따라서 위 요구사항은 다음과 같이 의미 있는 커스텀 getter로 바꿀 수 있다.
val upperCaseName: String
get() = this.name.uppercase()
이번엔 name을 set할 때 대문자로 바꿔 보자.
var name: String = name
set(value) {
field = value.uppercase()
}
그러나 이 역시 backing field를 쓰는 것보다 updateNameToUpperCase와 같은 의미 있는 메서드를 사용하는 것을 권장한다.
게다가 setter 자체를 지양하기도 한다.
코틀린에서 상속을 다루는 방법
추상 클래스
여기 Animal 추상 클래스가 있다.
<java>
public abstract class JavaAnimal {
protected final String species;
protected final int legCount;
public JavaAnimal(String species, int legCount) {
this.species = species;
this.legCount = legCount;
}
abstract public void move();
public String getSpecies() {
return species;
}
public int getLegCount() {
return legCount;
}
}
<kotlin>
abstract class Animal(
protected val species: String,
protected val legCount: Int,
) {
abstract fun move()
}
Animal을 구현한 Cat, Penguin 클래스가 있다.
먼저 Cat을 정의하자.
<java>
public class JavaCat extends JavaAnimal {
public JavaCat(String species) {
super(species, 4);
}
@Override
public void move() {
System.out.println("고양이가 사뿐 사뿐 걸어가~");
}
}
<kotlin>
class Cat(
species: String,
) : Animal(species, 4) { // 상위 클래스의 생성자를 바로 호출한다
override fun move() { // override를 필수적으로 붙여 주어야 한다
println("고양이가 사뿐 사뿐 걸어가~")
}
}
다음 Penguin을 정의하자.
<java>
public final class JavaPenguin extends JavaAnimal {
private final int wingCount;
public JavaPenguin(String species) {
super(species, 2);
this.wingCount = 2;
}
@Override
public void move() {
System.out.println("펭귄이 움직입니다~ 꿱꿱");
}
@Override
public int getLegCount() {
return super.legCount + this.wingCount;
}
}
wingCount를 추가했다.
getLegCount()를 오버라이드 한다.
<kotlin>
class Penguin(
species: String,
) : Animal(species, 2) {
private val wingCount: Int = 2
override fun move() {
println("펭귄이 움직인다~ 꽥꽥")
}
override val legCount: Int
get() = super.legCount + this. wingCount
}
여기서 주의할 점은 legcount가 추상 프로퍼티가 아니라면 상속을 위해 open을 꼭 붙여야 한다.
abstract class Animal(
protected val species: String,
protected open val legCount: Int, // open
) {
abstract fun move()
}
추상 프로퍼티: abstract가 붙어 있는 경우, 혹은 인터페이스의 추상 메서드
인터페이스
두 인터페이스를 정의한다.
<java>
public interface JavaSwimable {
default void act() {
System.out.println("어푸 어푸");
}
}
public interface JavaFlyable {
default void act() {
System.out.println("파닥 파닥");
}
}
<kotlin>
interface Swimable {
fun act() {
println("어푸 어푸")
}
}
interface Flyable {
fun act() {
println("파닥 파닥")
}
}
코틀린은 default 키워드 없이 기본 메서드 구현이 가능하다.
구현체가 없는 경우에는 추상 메서드로 간주된다.
interface Flyable {
fun foo() // 추상 메서드
fun act() { // 기본 메서드
println("파닥 파닥")
}
}
이제 인터페이스를 구현해보자.
<java>
public final class JavaPenguin extends JavaAnimal implements JavaSwimable, JavaFlyable {
...
@Override
public void act() {
JavaSwimable.super.act();
JavaFlyable.super.act();
}
}
<kotlin>
class Penguin(
species: String,
) : Animal(species, 2), Swimable, Flyable {
...
override fun act() {
super<Swimable>.act()
super<Flyable>.act()
}
}
인터페이스 구현도 ':'를 사용한다.
중복되는 인터페이스를 특정할 때 "super<타입>.함수"를 사용한다.
클래스를 상속할 때 주의할 점
여기 두 클래스가 있다.
<java>
public class JavaBase {
public JavaBase() {
System.out.println(getMember());
}
public int getMember() {
return 1;
}
}
public class JavaDerived extends JavaBase {
public JavaDerived() {
super();
}
@Override
public int getMember() {
return 10;
}
}
public static void main(String[] args) {
new JavaDerived();
}
Q: JavaDerived() 생성자를 호출하면 어떤 값이 찍힐까?
1. JavaDerived()에서 JavaBase() 호출
2. JavaBase()에서 getMember() 호출
3. 여기서 getMember는 JavaDerived의 getMember()이기 때문에 최종적으로 10 출력
이번에는 각 클래스에 필드가 있는 경우를 생각해보자.
<java>
public class JavaBase {
private final int number;
public JavaBase(int number) {
this.number = number;
System.out.println("Base Class");
System.out.println(getNumber());
}
public int getNumber() {
return number;
}
}
public class JavaDerived extends JavaBase {
private final int number;
public JavaDerived(int number) {
super(number);
this.number = number;
System.out.println("Derived Class");
}
@Override
public int getNumber() {
return number;
}
}
public static void main(String[] args) {
new JavaDerived(300);
}
Q: JavaDerived(300) 생성자를 호출하면 어떤 값이 찍힐까?
1. JavaDerived()에서 JavaBase() 호출
2. JavaBase()에서 "Base Class" 출력
3. JavaBase()의 getNumber() 호출
4. 이 때 getNumber()는 JavaDerived의 getNumber()인데 아직 JavaDerived의 number에 값이 할당되지 않았기 때문에 int 기본값인 0을 출력한다.
5. JavaDerived()의 생성자에서 "Derived Class" 출력
결과
Base Class
0
Derived Class
사실 부모 클래스에 이미 number가 있기 때문에 굳이 자식 클래스에 number를 또 정의할 필요가 없는 경우가 많다.
실제로 위 코드에서 Derived의 number 필드를 지워주면 number 프로퍼티는 부모에서만 정의되어 있기 때문에 의도한대로 300이 출력된다.
그러나 저렇게 코드를 짠 이유는 아래 코틀린 코드를 설명하기 위해서이다.
위 코드를 그대로 코틀린으로 작성해보면 다음과 같다.
<kotlin>
open class Base(
open val number: Int = 100,
) {
init {
println("Base Class")
println(number)
}
}
class Derived(
override val number: Int,
) : Base(number) {
init {
println("Derived Class")
}
}
부모 클래스의 추상 프로퍼티가 아닌 프로퍼티에는 open을, 자식 클래스에는 해당 프로퍼티에 override를 붙여야 한다.
fun main() {
Derived(300)
}
Q: Derived() 생성자를 호출하면 어떤 값이 찍힐까?
1. Derieved()에서 Base() 호출
2. Base()의 초기화 블록에서 "Base Class" 출력
3. Base()의 초기화 블록에서 number 출력
4. 이 때 number는 Derived의 number인데 아직 Derived의 초기화 블록이 실행되지 않았기 때문에 Int 기본값인 0을 출력한다.
5. Derieved()의 초기화ㅣ 블록에서 "Derived Class" 출력
결과
Base Class
0
Derived Class
해당 결과는 number가 Derieved()인자인 300도 아닌, 부모 클래스의 기본값인 100도 아닌 0이 찍히므로, 의도한대로 동작하지 않았을 확률이 높다.
-> 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티는 open을 피해야 한다.
그 이유는 부모 클래스에서 open으로 특정 필드를 열어 놓으면 자식 클래스에서 override할 가능성을 열어 놓는 것이고, 해당 open 프로퍼티를 부모의 초기화 블럭에서 사용하게 되면 위 예시처럼 부모의 초기화 블록에서 자식의 프로퍼티를 사용함으로써 정상적으로 동작하지 않을 수 있다.
-> 즉, 부모의 초기화 블록에서 사용할 프로퍼티는 open을 피함으로써, 자식이 상속받는 것을 방지해 의도한 대로 동작할 것을 기대할 수 있다.
Q: 만약 Derived에서 override를 붙이지 않았다면?
class Derived(
number: Int,
) : Base(number) {
init {
println("Derived Class")
}
}
A: 프로퍼티에 override를 붙이지 않게 되면 number라는 이름은 같더라도 부모의 프로퍼티가 아닌 자식의 프로퍼티가 되기 때문에, 부모의 초기화 블록에서의 number는 부모의 프로퍼티를 부르게 되고, 그 값은 Derived(300)에서 부모까지 넘어온 값인 300이 출력되게 된다.
상속 관련 지시어 정리
final: override를 할 수 없게 한다.
open: override를 열어 준다.
abstract: 반드시 override 해야 한다.
override: 상위 타입을 override 하고 있다.
코틀린에서 접근 제어를 다루는 방법
자바와 코틀린의 가시성 제어
<java>
public: 모든 곳에서 접근 가능
protected: 같은 패키지 또는 하위 클래스에서만 접근 가능
default: 같은 패키지에서만 접근 가능
private: 선언된 클래스 내에서만 접근 가능
<kotlin>
public: 모든 곳에서 접근 가능 (자바와 동일)
protected: 선언된 클래스 또는 하위 클래스에서만 접근 가능 (가시성 제어에서 패키지 개념이 사라졌다)
internal: 같은 모듈에서만 접근 가능 (default가 사라지고, 모듈 관련 가시성 제어가 추가)
private: 선언된 클래스 내에서만 접근 가능 (자바와 동일)
+) 자바의 기본 접근 지시어는 default, 코틀린의 기본 접근 지시어는 public
코틀린 파일의 접근 제어
public: 기본값, 어디서든 접근할 수 있다.
protected: 파일(최상단)에는 사용 불가능 -> 클래스 내부에서만 사용할 수 있다.
internal: 같은 모듈에서만 접근 가능
private: 같은 파일 내에서만 접근 가능
다양한 구성요소의 접근 제어
클래스 안의 멤버
public: 모든 곳에서 접근 가능
protected: 선언된 클래스 또는 하위 클래스에서만 접근 가능
internal: 같은 모듈에서만 접근 가능
private: 선언된 클래스 내에서만 접근 가능
생성자
멤버와 동일하다. 단, 생성자에 접근 지시어를 붙이려면, constructor를 써줘야 한다.
class Bus internal constructor(
val price: Int,
)
자바에서 유틸성 코드를 만들 때, abstract class + private contructor를 사용해서 인스턴스화를 막을 때가 있다.
<java>
public abstract class StringUtils {
private StringUtils() {}
public boolean isDirectoryPath(String path) {
return path.endsWith("/");
}
}
<kotlin>
코틀린도 비슷하게 가능하지만, 그냥 파일 최상단에 바로 유틸함수를 작성하면 편하다.
// StringUtil.kt
fun isDirectoryPath(path: String): Boolean {
return path.endsWith("/")
}
해당 코드를 자바로 디컴파일하면 static 메서드로 바꿔주기 때문에, StringUtil.isDirectoryPath() 처럼 사용할 수 있다.
프로퍼티
멤버와 동일하다. 단, 가시성을 제어하는 방법으로,
class Car(
internal val name: String, // 1. getter, setter 한 번에 접근 지시어를 정하거나
_price: Int
) {
var price = _price
private set // 2. getter 혹은 setter에만 추가로 가시성을 부여할 수 있다.
}
Java와 Kotlin을 함께 사용할 경우 주의할 점
1. internal은 바이트 코드 상 public이 된다. 때문에 자바 코드에서는 코틀린 모듈의 internal 코드를 가져올 수 있다.
2. 코틀린의 protected와 자바의 protected는 다르다. 자바는 같은 패키지의 코틀린 protected 멤버에 접근할 수 있다.
즉, 코틀린의 internal과 protected는 코틀린 내에서만 유효하다.
코틀린에서 object 키워드를 다루는 방법
static 함수와 변수
<java>
public class JavaPerson {
private static final int MIN_AGE = 1;
public static JavaPerson newBaby(String name) {
return new JavaPerson(name, MIN_AGE);
}
private String name;
private int age;
private JavaPerson(String name, int age) {
this.name = name;
this.age = age;
}
}
<kotlin>
class Person private constructor(
private val name: String,
private val age: Int,
) {
companion object {
private const val MIN_AGE = 1
fun newBaby(name: String): Person {
return Person(name, MIN_AGE)
}
}
}
static 대신 companion object를 사용할 수 있다.
companion object: 클래스와 동행하는 유일한 오브젝트
또한, 상수 MIN_AGE에 const를 붙임으로써 컴파일 시에 변수가 할당되게 할 수 있다. (const가 없으면 런타임 시에 할당된다)
여기서 자바와 다른 점 한 가지는 companion object는 하나의 객체로 간주되기 때문에 이름을 붙일 수도 있고, interface를 구현할 수도 있다.
interface Log {
fun log()
}
class Person private constructor(
private val name: String,
private val age: Int,
) {
companion object Factory : Log {
private const val MIN_AGE = 1
fun newBaby(name: String): Person {
return Person(name, MIN_AGE)
}
override fun log() {
println("LOG")
}
}
}
Factory라는 이름을 붙이고, Log라는 인터페이스를 구현했다.
추가로, 자바에서 코틀린 companion object를 사용하려면 @JvmStatic을 붙여야 한다.
<kotlin>
class Person private constructor(
private val name: String,
private val age: Int,
) {
companion object Factory : Log {
private const val MIN_AGE = 1
@JvmStatic // 추가
fun newBaby(name: String): Person {
return Person(name, MIN_AGE)
}
override fun log() {
println("LOG")
}
}
}
<java>
Person.Companion.newBaby("ABC"); // 이름이 없으면 기본적으로 Companion을 사용한다
Person.newBaby("ABC"); // Companion은 생략할 수 있다
Person.Factory.newBaby("ABC"); // companion object에 이름이 있으면 해당 객체의 이름을 직접 사용한다
싱글톤
<java>
public class JavaSingleton {
private static final JavaSingleton INSTANCE = new JavaSingleton();
private JavaSingleton() { }
public static JavaSingleton getInstance() {
return INSTANCE;
}
}
<kotlin>
object Singleton
이거면 끝난다.
코틀린에서 싱글톤은 이런식으로 직관적으로 사용할 수 있다.
object Singleton {
var a: Int = 0
}
fun main() {
println(Singleton.a)
Singleton.a = 10
println(Singleton.a)
}
// 결과
0
10
익명 클래스
익명 클래스: 특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스
<java>
public interface Movable {
void move();
void fly();
}
public static void main(String[] args) {
moveSomething(new Movable() {
@Override
public void move() {
System.out.println("움직인다~~");
}
@Override
public void fly() {
System.out.println("난다~~");
}
});
}
private static void moveSomething(Movable movable) {
movable.move();
movable.fly();
}
<kotlin>
fun main() {
moveSomething(object : Movable {
override fun move() {
println("움직인다")
}
override fun fly() {
println("난다~~")
}
})
}
private fun moveSomething(movable: Movable) {
movable.move()
movable.fly()
}
코틀린에서 중첩 클래스를 다루는 방법
중첩 클래스의 종류
중첩 클래스는 크게 2가지 종류가 있다.
1. static을 사용하는 클래스
2. static을 사용하지 않는 클래스
자바 코드로 두 예시를 살펴보자.
<static을 사용하는 클래스>
public class JavaHouse {
private String address;
private LivingRoom livingRoom;
public JavaHouse(String address) {
this.address = address;
this.livingRoom = new LivingRoom(10);
}
public LivingRoom getLivingRoom() {
return livingRoom;
}
public static class LivingRoom { // static 키워드
private double area;
public LivingRoom(double area) {
this.area = area;
}
public String getAddress() {
return JavaHouse.this.address; // 컴파일 에러
}
}
}
static을 사용하는 내부 클래스는 바깥 클래스를 직접 참조 불가능하다.
<static을 사용하지 않는 클래스>
public class JavaHouse {
private String address;
private LivingRoom livingRoom;
public JavaHouse(String address) {
this.address = address;
this.livingRoom = new LivingRoom(10);
}
public LivingRoom getLivingRoom() {
return livingRoom;
}
public class LivingRoom {
private double area;
public LivingRoom(double area) {
this.area = area;
}
public String getAddress() {
return JavaHouse.this.address; // 바깥 클래스와 연결되어 있다
}
}
}
static을 사용하지 않는 내부 클래스는 바깥 클래스를 직접 참조 가능하다.
그렇다면 필요에 따라 두 방식 중 하나를 선택하면 될까? -> 그렇지 않다.
1. static을 사용하지 않는 중첩 클래스는 외부 클래스의 정보를 가지고 있어, 참조를 해지하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅하기 어렵다.
2. 내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 있어 제한이 있다.
(Effective Java 3rd Edition - Item24, Item 86)
"따라서 클래스 안에 클래스를 만들 때는 static 클래스를 사용하자"
코틀린의 중첩 클래스와 내부 클래스
코틀린에서는 이러한 가이드를 충실히 따르고 있다.
<권장되는 중첩 클래스>
class House(
private var address: String,
private var livingRoom: LivingRoom = LivingRoom(10.0)
) {
class LivingRoom(
private var area: Double,
)
}
코틀린은 기본적으로 바깥 클래스에 대한 연결이 없는 중첩 클래스가 만들어진다.
<권장되지 않는 중첩 클래스>
class House(
private var address: String,
) {
private var livingRoom: LivingRoom = this.LivingRoom(10.0)
inner class LivingRoom(
private var area: Double,
) {
val address: String
get() = this@House.address
}
}
중첩 클래스에서 바깥 클래스를 참조하고 싶다면, 중첩 클래스에 inner 키워드를 붙이면 된다.
이 때, 자바와 다른 점은 바깥 클래스 참조를 위해 "this@바깥클래스"를 사용한다.
-> 물론 앞서 말한듯이 중첩 클래스에서 바깥 클래스를 참조하지 않는 방식이 좋다.
코틀린에서 다양한 클래스를 다루는 방법
Data Class
여기 DTO가 하나 있다.
<java>
public class JavaPersonDto {
private final String name;
private final int age;
public JavaPersonDto(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JavaPersonDto that = (JavaPersonDto) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "JavaPersonDto{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
필드, 생성자, getter, equals, hashCode, toString을 정의한다.
lombok을 활용할 수도 있지만, 클래스가 장황해지거나 클래스 생성 이후 추가적인 처리를 해줘야 하는 단점이 있다.
코틀린의 data class를 사용하면 훨씬 간단해진다.
<kotlin>
data class PersonDto(
val name: String,
val age: Int,
)
class 앞에 data 키워드를 붙여주면 eqauls, hashCode, toString을 자동으로 만들어준다.
+) 자바에서도 JDK16부터 코틀린의 data class 같은 record class를 도입했다.
Enum Class
<java>
public enum JavaCountry {
KOREA("KO"),
AMERICA("US"),
;
private final String code;
JavaCountry(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
<kotlin>
enum class Country(
private val code: String,
) {
KOREA("ko"),
AMERICA("US"),
;
}
자바와 크게 다른게 없다.
추가 팁으로 when을 enum class, sealed class와 함께 사용할 경우 더욱 진가를 발휘한다.
<java>
private static void handleCountry(JavaCountry country) {
if (country == JavaCountry.KOREA) {
// 로직 처리
}
if (country == JavaCountry.AMERICA) {
// 로직 처리
}
}
코드가 많아지면 else 로직 처리 등 복잡도가 증가한다.
<kotlin>
fun handleCountry(country: Country) {
when (country) {
Country.KOREA -> TODO()
Country.AMERICA -> TODO()
}
}
코틀린의 when을 사용하면 조금 더 읽기 쉬운 코드가 만들어진다.
또한, 컴파일러가 country의 모든 타입을 알고 있어서 else 로직을 작성하지 않아도 된다.
Sealed Class, Sealed Interface
sealed class는 추상클래스를 만들고 싶은데, 외부에서는 이 클래스를 상속받지 못하게 할 때 사용한다.
즉, 하위 클래스를 봉인한다. (sealed -> 봉인을 한)
sealed class HyundaiCar(
val name: String,
val price: Long,
)
class Avante: HyundaiCar("아반떼", 1_000L)
class Sonata: HyundaiCar("소나타", 1_000L)
class Grandeur: HyundaiCar("그렌저", 1_000L)
sealed class는 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다.
즉, 런타임 때 클래스 타입이 추가될 수 없다.
+ 하위 클래스는 같은 패키지에 있어야 한다.
sealed class와 when을 활용하면 다음과 같다.
fun handleCar(car: HyundaiCar) {
when (car) {
is Avante -> TODO()
is Sonata -> TODO()
is Grandeur -> TODO()
}
}
+) 추가로 JDK17에서도 sealed class가 추가되었다.
'kotlin' 카테고리의 다른 글
[Kotlin] JPA 플러그인 정리 (0) | 2024.07.19 |
---|---|
[Kotlin] 추가적으로 알아두어야 할 코틀린 특성 (0) | 2023.06.20 |
[Kotlin] 코틀린에서의 FP (0) | 2023.06.04 |
[Kotlin] 코틀린에서 코드를 제어하는 방법 (4) | 2023.05.07 |
[Kotlin] 코틀린에서 변수와 타입, 연산자를 다루는 방법 (0) | 2023.05.07 |