Overview

Kotlin 에서 엔티티를 정의할 때 @Id 값을 정의하는 방법에는 여러가지가 있습니다.

Java 에서는 기본적으로 모든 변수가 nullable 하기 때문에 딱히 의견이 갈릴 일이 없었는데요.

Kotlin 에서 많이 사용하는 대표적인 @Id 정의 스타일과 장단점을 알아보겠습니다.


1. val + nullable 정의

@MappedSuperclass
abstract class BaseEntity1 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
}

아마 Kotlin + JPA 를 사용하면서 가장 많이 보이는 스타일일 것 같습니다.

특징

  • 엔티티의 특성상 DB 에 저장되기 전까지는 null 값이므로 nullable 타입을 사용

이 스타일의 단점이라고 하면 id 가 nullable 이기 때문에 도메인 로직에서 사용할 때는 id!!requireNotNull(id) 등을 사용해야 할 수 있습니다.


2. id 기본값을 0L 으로 지정

@MappedSuperclass
abstract class BaseEntity2 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L
}

이 스타일 역시 Kotlin + JPA 에서 많이 보이는 스타일입니다.

특징

  • Notnull 타입으로 정의했기 때문에 매번 !! 을 붙이지 않아도 됨 (NPE 방지)
  • DDD 관점에서는 도메인 객체가 "불완전한 상태(null ID)"를 가지는 것이 자연스럽지 않기 때문에 이를 해결

가장 큰 특징으로는 nullable 을 지양하는 Kotlin 의 철학을 지킬 수 있다는 점입니다.

엔티티의 id 는 DB 에 저장되기 전까지 null 인데 어떻게 0L 을 사용할 수 있는걸까요?


2.1. Repository#save

@Override
@Transactional
public <S extends T> S save(S entity) {

    Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);

    if (entityInformation.isNew(entity)) {
        entityManager.persist(entity);
        return entity;
    } else {
        return entityManager.merge(entity);
    }
}

JPA 의 Repository saveisNew 라는 함수를 사용해서 엔티티가 새로 생성된건지 기존 데이터인지 검사합니다.

새로 생성된 데이터라면 persist 를 진행해서 데이터를 저장, id 에 값을 주입하고 기존 데이터라면 merge 를 사용해서 업데이트 합니다.


2.2. isNew

@Override
public boolean isNew(T entity) {

    ID id = getId(entity);
    Class<ID> idType = getIdType();

    if (!idType.isPrimitive()) {
        return id == null;
    }

    if (id instanceof Number n) {
        return n.longValue() == 0L;
    }

    throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}

Spring Data JPA 내부의 Repository 구현을 보면 isNew 함수에서 id == null 뿐만 아니라 id == 0L 또한 "새로운 객체" 라고 판단해줍니다.

그래서 null 대신 0L 을 입력해도 JPA 가 정상적으로 동작하는 겁니다.

하지만 JPA 의 구현체로 Hibernate 가 아닌 다른 걸 사용한다면 0L 을 저장되지 않은 구현체로 보장하지 않기 때문에 문제가 발생할 수 있습니다.

그리고 id 를 0L 로 정의한다는 것 자체가 어색하거나 불편하게 느껴질 수도 있습니다.


3. var + nullable 정의

@MappedSuperclass
abstract class BaseEntity3 {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
}

Java 와 가장 유사하게 동작하는 스타일입니다.

Jetbrain 이나 Spring 에서 제공하는 예제 코드에서 등장하는데 오픈소스 예제에서는 테스트 시 직접 id 값을 설정하거나 유연하게 다루기 위해 때문에 사용하는 것 같습니다.


4. 개인적인 사용 방식

저는 위의 스타일들 중에서 마음에 안드는 부분이 하나씩 있었습니다.

  • 저장되지 않은 id0L 을 넣는 것
  • nullable 하게 정의하면 호출할 때 id!! 를 사용해야 하는 것
  • 그렇다고 별도의 id() 메서드를 정의하는 것도 마음에 안듬

그래서 Backing Field 를 사용했습니다.


@MappedSuperclass
abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected val _id: Long? = null

    val id: Long = _id ?: throw IllegalStateException("엔티티의 ID 값이 존재하지 않습니다.")
}

id 정의하는데 이렇게까지 해야하나? 하는 생각이 들수도 있지만 BaseEntity 에만 정의해두면 다시 건들 일이 없긴 합니다.

  • @Id 가 사용되는 _id 변수는 nullable 이라 저장되지 않았을 때 null
  • protected 로 정의하여 상속 후 프록시객체로 필드 주입 가능
  • 외부에서 id 호출시에도 !! 를 붙이거나 함수처럼 id() 형태로 사용하지 않아도 됨

Conclusion

그래서 뭘 써야하나?

정답은 없습니다.

팀 컨벤션이나 개인적인 선호도에 따라 1번을 쓰기도 하고 2번을 쓰기도 합니다.

현재 대중화된 Spring Boot + JPA Hibernate 기준으로는 어떤 스타일을 해도 정상 동작합니다.

가장 많이 쓰는 방식은 1번 (val id: Long? = null) 으로 알고 있습니다.

Overview

Kotlin 에서 제공하는 유용한 기능 중에 JPA 에서는 권장되지 않거나 사용하면 안되는 기능들이 있습니다.


1. data class

Kotlin 에서 제공하는 data class는 JPA의 @Entity에서는 사용하지 않는 것이 좋습니다.

data class 는 불변성과 값 기반 비교를 지향하지만, JPA의 엔티티는 보통 가변 상태를 가지며 식별자(ID) 기반으로 관리되기 때문에 둘의 지향점이 다릅니다.

또한, data class 가 자동 생성하는 equals, hashCode 구현은 모든 필드를 기반으로 비교하기 때문에 id 가 아직 할당되지 않은 상태에서는 동일한 엔티티임에도 불구하고 서로 다른 객체로 인식될 수 있습니다.

그래서 Kotlin 에서 엔티티 객체를 정의할 때는 기본 class 를 사용해야 합니다.


2. init

Kotlin 에서는 init 블럭을 사용할 수 있습니다.

init 블럭은 "객체 생성 직후" 실행되지만, JPA 는 리플렉션을 통해 엔티티를 "빈 객체로 생성 -> 필드 값을 주입" 하는 순서로 동작합니다.

따라서 init 블럭에서 필드 값을 참조하면 아직 값이 설정되지 않아 NullPointerException 또는 예상치 못한 로직 오류가 발생할 수 있습니다.

엔티티 객체 생성 이후에 뭔가를 처리하고 싶다면 외부에서 처리하거나, 내부에서 처리해야 한다면 @PostLoad 등을 활용해야 합니다.


Conclusion

Kotlin 은 여러 편의 기능을 제공하지만, JPA 와 함께 사용할 때는 특정 Kotlin 기능이 JPA 의 동작 방식과 충돌할 수 있습니다.

그 중 대표적인 예로 data classinit 블럭 사용을 지양해야 하는 이유를 알아보았습니다.

Overview

Kotlin 으로 Spring Boot 를 만들다보면 JPA 를 함께 사용하는 일이 많습니다.

JPA 는 대표적인 Spring Boot 의 ORM 이지만 Kotlin 과 함께 사용하려면 몇가지 불편한 점이 존재합니다.

이런 불편한 점들을 해결하기 위해 Kotlin 측에서는 JPA 에서 사용하기 적합한 몇가지 플러그인을 제공합니다.


1. Kotlin JPA plugin

Kotlin 에서 제공하는 plugin.jpa 에는 다음 두 가지 플러그인이 포함되어 있습니다.

  • plugin.allopen
  • plugin.noarg

1.1. all-open plugin

Kotlin 에서는 기본적으로 모든 클래스와 메서드가 final 입니다.

이 말은 클래스나 메서드를 상속/오버라이드 할 수 없다는 뜻입니다.

Java 에서는 기본적으로 open 이라 상속에 열려있는 반면에 Kotlin 은 의도된 상속 구조를 사용하기 위해 기본적으로 final 이고 상속 가능한 클래스만 명시적으로 open 을 선언합니다.

JPA 에서는 지연 로딩 (lazy loading), 더티 체킹 (dirty checking) 등에 사용되는 프록시 객체를 만들기 위해서는 클래스가 상속 가능해야 합니다.

그래서 Kotlin 에서는 JPA 클래스를 open 으로 변경시켜주는 "all-open" 플러그인을 제공합니다.


1.2. no-arg plugin

JPA 에서는 엔티티 객체를 리플렉션 (Reflection) 으로 생성햐는데 구현상 반드시 파라미터 없는 기본 생성자가 필요합니다.

Kotlin 에서는 정의된 생성자를 통해 모든 필드를 초기화하도록 요구하므로, 별도로 기본생성자를 만들어주지 않으면 JPA 에서 인스턴스를 생성할 수 없습니다.

사실 기본생성자가 없는 문제는 Kotlin 만의 문제는 아니고 Java 에서도 @Entity 객체는 항상 기본생성자를 직접 만들어주거나 Lombok 의 @NoArgConstructor 를 붙여줘야 합니다.

Kotlin 에서는 JPA 클래스에 기본생성자를 붙여주는 "no-arg" 플러그인을 제공합니다.


2. 적용 방법

plugins {
    val kotlinVersion = "2.1.20"
    kotlin("plugin.jpa") version kotlinVersion
}

Kotlin 2.1.20 버전 사용 기준으로 위와 같이 추가해주시면 됩니다.

allopen, noarg 플러그인을 별도 사용시 원래는 allOpen, noArg 블럭을 추가하여 적용 대상을 지정해야 했는데 plugin.jpa 에는 해당 설정 또한 내장되어 있기 때문에 따로 추가할 필요가 없습니다.


Conclusion

Spring Initializr 에서는 Kotlin + JPA 선택 시 plugin.jpa 를 자동으로 추가해줍니다.

그래서 Spring Initializr 나 Gradle 설정을 통해 무심코 적용되는 경우가 많지만 실제로 Kotlin 에서 JPA 를 안정적으로 사용하기 위해 중요한 역할을 하는 플러그인들을 정리해봤습니다.

Overview

Java8 에서는 Collection 을 다루기 위해 Stream 을 사용합니다.

Kotlin 은 Collections 자체에서 filter, map 등의 여러 가지 API 를 제공하기 때문에 매번 .streams() 를 붙이지 않아도 사용 가능하다는 장점이 있습니다.

하지만 비슷해보이는 두 코드 사이에는 큰 차이점이 하나 있는데요.

바로 Lazy Evaluation 입니다.


1. Lazy Evaluation

Lazy Evaluation 이란 쉽게 말해서 필요하지 않은 연산을 하지 않는다 라고 이해할 수 있습니다.

어떤 로직이나 연산을 그 즉시 수행하지 않고 실제로 사용되기 전까지 미루는 걸 의미합니다.

반대의 의미인 Eager Evaluation 은 연산이 있으면 그때그때 수행하는 것을 의미합니다.

Java Stream 의 예시와 함께 보면 쉽게 이해할 수 있습니다.


2. Java Streams

Stream.of(1, 2, 3, 4, 5, 6)
        .filter(e -> {
            System.out.println("filter: " + e);
            return e < 3;
        })
        .map(e -> {
            System.out.println("map: " + e);
            return e * e;
        })
        .anyMatch(e -> {
            System.out.println("anyMatch: " + e);
            return e > 2;
        });
  • (1, 2, 3, 4, 5, 6) 의 숫자 묶음 존재
  • filter: 3 보다 작은 수만 추출
  • map: 제곱으로 변환
  • anyMatch: 2 보다 큰 수가 있는지 확인

조금 지저분해 보이지만 이해를 돕기 위해 연산 중간중간마다 print 문을 추가했습니다.

위 코드를 있는 그대로 나열하면 6 개의 숫자 묶음에서 3 보다 작은 수만 뽑아서 제곱한 뒤 그 중에서 2 보다 큰 수가 있는지 확인하는 겁니다.

위 코드의 결과값은 아래와 같습니다.


filter: 1
map: 1
anyMatch: 1
filter: 2
map: 2
anyMatch: 4

총 6 개의 숫자가 있었지만 실제로 연산된 것은 두 개 뿐입니다.

anyMatch 조건에 해당하는 숫자가 나오자 이후 숫자들은 볼 필요가 없다고 판단하여 전부 생략했습니다.

이게 바로 Lazy Evaluation (필요하지 않는 연산은 하지 않는다) 입니다.


3. Kotlin Collections

listOf(1, 2, 3, 4, 5, 6)
    .filter {
        println("filter: $it")
        it < 3
    }
    .map {
        println("map: $it")
        it * it
    }
    .any {
        println("any: $it")
        it > 2
    }

그럼 이제 같은 로직을 Kotlin 으로 작성해보았습니다.

위 로직을 실행하며 어떻게 될까요?


filter: 1
filter: 2
filter: 3
filter: 4
filter: 5
filter: 6
map: 1
map: 2
any: 1
any: 4

한 눈에 봐도 결과가 다른 것을 알 수 있습니다.

Kotlin Collections 는 매 연산마다 모든 원소에 대해서 수행합니다.

데이터의 양이 많으면 많을수록 성능 차이는 더욱 벌어질 겁니다.


4. Kotlin Sequences

Kotlin 에서도 Lazy Evaluation 을 수행하게 하는 방법이 있습니다.

바로 Sequence 를 사용하는 겁니다.

위 코드에서 한줄만 추가하면 됩니다.


listOf(1, 2, 3, 4, 5, 6)
    .asSequence()   // 이 부분을 추가해서 Sequence 로 변경
    .filter {
        println("filter: $it")
        it < 3
    }
    .map {
        println("map: $it")
        it * it
    }
    .any {
        println("any: $it")
        it > 2
    }

Collection 에서 수행하지 말고 asSequence() 를 사용해서 Sequence 로 변경한 뒤에 연산을 수행하면 됩니다.

위 코드의 결과는 다음과 같습니다.


filter: 1
map: 1
any: 1
filter: 2
map: 2
any: 4

이제 불필요한 연산을 하지 않는 것을 볼 수 있습니다.


5. 왜 그럴까?

Lazy Evaluation 에 대해 좀더 설명하면 중간 단계 (intermediate step) 의 결과를 바로 리턴하냐 아니냐의 차이에 있습니다.

Kotlin Collections 은 매 연산을 수행할 때마다 결과 Collection 을 반환합니다.

이에 비해 Kotlin Sequences 또는 Java Streams 는 종료 (terminate) 함수가 호출되기 전까지는 연산을 수행하지 않습니다.

위에서 사용한 any() 함수 또한 종료 함수입니다.

이 차이를 쉽게 알려면 종료 함수가 없는 Sequences 를 사용해보면 됩니다.


5.1. Kotlin Sequences 의 Lazy Evaluation 확인

val sequence: Sequence<Int> = listOf(1, 2, 3)
    .asSequence()
    .filter {
        println("filter: $it")
        it < 2
    }
    .map {
        println("map: $it")
        it * it
    }

println("종료함수를 아직 호출하지 않음")
sequence.toList()

Sequences 는 매 함수의 결과로 Sequence 를 반환합니다.

그래서 최종적으로 Collection 으로 변환하려면 다시 toList() 를 호출해야 합니다.

toList() 역시 종료함수라서 호출되는 순간에 모든 연산이 수행됩니다.

Java Streams 의 collect(Collectors.toList()) 와 같다고 생각하시면 됩니다.


종료함수를 아직 호출하지 않음
filter: 1
map: 1
filter: 2
filter: 3

5.2. Kotlin Collections 의 Eager Evaluation 확인

val list: List<Int> = listOf(1, 2, 3)
    .filter {
        println("filter: $it")
        it < 2
    }
    .map {
        println("map: $it")
        it * it
    }

println("Collection 은 매번 List 를 반환하기 때문에 이미 연산됨")

Sequences 와 다르게 Collections 은 매 함수의 결과로 Collection 을 반환합니다.

사실상 매 함수가 모두 종료 함수라고 볼 수 있으며, 그래서 결과를 다음 단계로 넘기지 못하고 매번 전부 연산을 하는겁니다.


filter: 1
filter: 2
filter: 3
map: 1
Collection 은 매번 List 를 반환하기 때문에 이미 연산됨

Conclusion

이제 Kotlin Sequences 는 Lazy Evaluation 때문에 불필요한 연산을 생략한다는 점을 알았습니다.

하지만 Sequences 가 항상 좋은 것은 아닙니다.

Sequences by Kotlin Reference 를 보면 다음과 같은 문구가 있습니다.

So, the sequences let you avoid building results of intermediate steps, therefore improving the performance of the whole collection processing chain. However, the lazy nature of sequences adds some overhead which may be significant when processing smaller collections or doing simpler computations. Hence, you should consider both Sequence and Iterable and decide which one is better for your case.


요약하자면 Sequences 는 중간 단계의 결과를 생략하기 때문에 성능 향상이 되지만, 오버헤드가 있기 때문에 데이터가 적거나 연산이 단순한 컬렉션을 처리할 때는 오히려 안좋을 수가 있다고 합니다.

그러므로 각자 상황에 맞춰 적절한 방법을 선택하는게 가장 좋습니다.


Reference

1. Overview

Java Enum 에 이어 Kotlin Enum 사용법에 대해서도 알아봅니다.

사용법과 이유에 대해서는 Java 에서 알아보았으니 간단하게 코드만 작성합니다.


2. 기본 사용법

enum class Day {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

3. 필드값 추가

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;
}

Kotlin 은 원래 ; 을 안쓰지만 Enum 클래스에 필드값을 추가하는 경우 마지막에 꼭 추가해야합니다.


4. 필드값 캐싱

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;

    companion object {
        private val LABEL_CACHE: Map<String, Day> =
            values().associateBy { it.label }

        fun findByLabel(label: String) = LABEL_CACHE[label]
    }
}

5. 상수별 메소드 구현

enum class Operation {
    PLUS {
        override fun apply(x: Double, y: Double): Double {
            return x + y
        }
    },

    MINUS {
        override fun apply(x: Double, y: Double): Double {
            return x - y
        }
    },

    TIMES {
        override fun apply(x: Double, y: Double): Double {
            return x * y
        }
    },

    DIVIDE {
        override fun apply(x: Double, y: Double): Double {
            return x / y
        }
    };

    abstract fun apply(x: Double, y: Double): Double
}

Reference

1. Collection Sort

Kotlin 에서는 Collection 을 정렬하기 위한 여러가지 유틸리티들을 제공합니다.


1.1. Sort, Sorted

가장 쉬운 방법은 sort 메소드를 호출하는 겁니다.

기본적으로 오름차순으로 정렬합니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
list.sort()
println(list)  // [1, 2, 5, 6, 6, 7]

sort 메소드는 해당 Collection 의 원소 위치가 변경됩니다.

기존 Collection 은 그대로 둔 채 새로운 Collection 으로 받길 원한다면 sorted 메소드를 사용해야 합니다.

sorted 메소드를 사용하면 기존 Collection 은 변하지 않습니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
val sorted = list.sorted()
println(sorted)  // [1, 2, 5, 6, 6, 7]
println(list)    // [1, 2, 7, 6, 5, 6] (sorted 를 사용했기 때문에 변하지 않음)

내림차순으로 정렬하고 싶다면 sortByDescending 를 사용하거나 reverse 메소드를 사용하면 됩니다.

마찬가지로 sortedByDescending 를 사용하면 원래 Collection 의 변경 없이 내림차순으로 정렬된 값을 구할 수 있습니다.

// 1. sortByDescending 로 내림차순 정렬
list.sortByDescending { it }

val sorted = list.sortedByDescending { it }

// 2. reverse 사용해서 정렬 후 뒤집기
list.sort()
list.reverse()

val sorted = list.sorted().reversed()

1.2. SortBy

만약 Object 의 특정 Property 들을 기준으로 정렬하고 싶다면 sortBy 메소드를 사용하면 됩니다.

sortBy 메소드는 Object 를 받아서 Property 를 반환하는 Lamdba 식을 파라미터로 받습니다.

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortBy { it.second }
println(list)  // [(1, a), (2, b), (7, c), (5, c), (6, d), (6, e)]

sort 와 마찬가지로 기존 Collection 의 변경 없이 정렬된 값을 받고 싶다면 sortedBy 를 사용하면 됩니다.

그리고 내림차순을 지원하는 sortByDescending 도 있습니다.


1.3. SortWith

sortWith 메소드를 사용하면 여러 가지 조건을 섞어서 정렬할 수 있습니다.

sortWith 메소드는 Comparator 를 파라미터로 받습니다.

(Kotlin 에서 Comparator 를 생성하는 여러가지 방법은 다음 챕터에서 다룹니다)

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortWith(compareBy({it.second}, {it.first}))
println(list)  // [(1, a), (2, b), (5, c), (7, c), (6, d), (6, e)]

위 Collection 은 it.second(문자) 로 먼저 정렬된 후에 it.first(숫자) 로 정렬됩니다.

그리고 역시 sortedWith 메소드가 존재하며, 역순으로 정렬할때는 reverse 를 사용하거나 Comparator 를 반대로 수정하면 됩니다.


2. Comparison

Kotlin 은 Comparator 를 만들기 위해 kotlin.comparisons 라는 유용한 패키지를 제공합니다.

이 챕터에서는 아래 컨텐츠를 다룹니다.

  • Comparator creation
  • Handling of null values
  • Comparator rules extension

2.1. Comparator Creation

Kotlin 은 Comparator 를 생성하는 여러 팩토리 메서드를 제공합니다.


2.1.1. naturalOrder

가장 간단한 생성 메서드는 naturalOrder() 입니다.

아무런 파라미터를 필요로 하지 않으며 오름차순을 기본으로 합니다.

val ascComparator = naturalOrder<Long>()

2.1.2. compareBy

여러 개의 속성을 사용하고 싶다면 compareBy 메소드를 사용하면 됩니다.

파라미터로는 Comparable 를 리턴하는 정렬 규칙을 여러 개 사용할 수 있습니다.

그럼 넘겨진 규칙들은 순차적으로 호출 되며 원소들을 정렬합니다.

만약 먼저 나온 규칙에서 원소의 우열이 가려져 정렬 처리가 되었다면 뒤의 규칙들은 확인하지 않습니다.

val complexComparator = compareBy<Pair<Int, String?>>({it.first}, {it.second})

위 코드에서 it.first 값을 사용해 먼저 비교를 하고 값이 같은 경우에만 it.second 비교까지 이루어집니다.


2.1.3. Comparator

간단하게 new Comparator 를 선언해서 만들 수도 있습니다.

자바와 마찬가지로 두 원소에 대한 비교 조건을 넣어줘야 합니다.

val caomparator = Comparator<Int> { a, b -> a.compareTo(b) }

2.2. Handling of null Values

정렬하려는 Collection 이 null 값을 갖고 있을 수도 있습니다.

nullsFirst 또는 nullsLast 와 함께 Comparator 를 사용하면 null 값을 가장 처음 또는 가장 마지막에 위치하도록 설정할 수 있습니다.

val list = mutableListOf(4, null, 1, -2, 3)

list.sortWith(nullsFirst())  // [null, -2, 1, 3, 4]

list.sortWith(nullsLast())  // [-2, 1, 3, 4, null]

list.sortWith(nullsFirst(reverseOrder()))  // [null, 4, 3, 1, -2]

list.sortWith(nullsLast(compareBy { it }))  // [-2, 1, 3, 4, null]

2.3. Comparator Rules Extension

Comparator 오브젝트는 추가적인 정렬 규칙과 혼합되거나 확장할 수 있습니다.

kotlin.comparable 패키지에 있는 then 키워드를 활용하면 됩니다.

첫 번째 비교의 결과가 동일할 때만 두번째 비교가 이루어집니다.

val students = mutableListOf(21 to "Helen", 21 to "Tom", 20 to "Jim")

val ageComparator = compareBy<Pair<Int, String?>> {it.first}
val ageAndNameComparator = ageComparator.thenByDescending {it.second}

// [(20, Jim), (21, Tom), (21, Helen)]
println(students.sortedWith(ageAndNameComparator))

위 코드는 나이가 어린 순으로 먼저 정렬하고 나이가 같으면 이름을 알파벳 역순으로 정렬합니다.


Reference

'Language > Kotlin' 카테고리의 다른 글

Kotlin Collections 와 Sequences 의 차이점 (feat. Java Stream)  (1) 2022.01.27
Kotlin Enum  (0) 2021.10.06
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11
[Kotlin] String - drop, dropLast, dropWhile, dropLastWhile  (0) 2020.09.25

Swap

Python 만큼은 아니지만 Kotlin 에서도 문법을 활용하여 값의 Swap 을 쉽게 짤 수 있다.

var a = 1
var b = 2

a = b.also { b = a }

println(a) // print 2
println(b) // print 1

For

Kotlin 에서의 for 문은 자바와 마찬가지로 iterator 를 사용합니다.

따라서 iterator(), next(), hasNext() 을 제공하는 모든 컬렉션에서 for 문을 사용할 수 있습니다.

기본 for 문은 아래와 같습니다.

for (item in collection) {
    print(item)
}

특정 범위 (range) 를 지정해서 사용할 수도 있습니다.

.. 을 사용하면 마지막 숫자까지 포함하until 을 사용하면 마지막 숫자는 포함하지 않습니다.

// 1, 2, 3
for (i in 1..3) {
    println(i)
}

// 1, 2
for (i in 1 until 3) {
    println(i)
}

downTo 를 이용하면 감소하는 for 문을 만들 수 있고 step 을 사용하면 증가되는 양을 수정할 수 있습니다.

// 6, 4, 2, 0
for (i in 6 downTo 0 step 2) {
    println(i)
}

배열을 순회할 때는 index 만 뽑아내거나 (index, value) 형태로 순회 가능합니다.

for (i in array.indices) {
    println(array[i])
}
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

컬렉션 타입에서는 forEach, forEachIndexed 를 사용하여 람다식으로 표현할 수 있습니다.

// 1, 2, 3
listOf(1, 2, 3).forEach { println(it) }

/**
 * index: 0, value: 4
 * index: 1, value: 5
 * index: 2, value: 6
 */
listOf(4, 5, 6).forEachIndexed { index, value -> 
    println("index: $index, value: $value")
}

+ Recent posts