Java 개발자가 가장 먼저 익혀야 하는 Kotlin API - Spring Boot 실무에서 가장 많이 사용하는 기능들 (2)

donghyeoneom·2026년 7월 10일
post-thumbnail

이전 글에서는 Kotlin이 왜 등장했으며, 왜 Spring Boot 백엔드 개발에서 Kotlin을 많이 사용하는지 알아보았습니다.

이번 글에서는 실제 프로젝트에서 가장 많이 사용하는 Kotlin API를 Java 코드와 비교하면서 살펴보겠습니다.

단순히 API의 사용법을 나열하는 것이 아니라, Java에서는 어떻게 작성했고 Kotlin에서는 어떻게 달라지는지, 그리고 실무에서는 언제 사용하는지를 중심으로 설명하겠습니다.

들어가며

Java 개발자가 Kotlin을 처음 배우면 가장 많이 하는 질문이 있습니다.

"문법은 이해했는데 실제 프로젝트에서는 어떤 API를 많이 사용하나요?"

실제로 Kotlin을 사용하다 보면 사용하는 API는 생각보다 정해져 있습니다.

특히 Spring Boot 프로젝트에서는 다음과 같은 API를 거의 매일 사용합니다.

  • Scope Function
  • Collection API
  • Null Safety API
  • Extension Function

이번 글에서는 그중에서도 가장 많이 사용하는 Scope Function과 Collection API를 먼저 살펴보겠습니다.


1️⃣ Scope Function

Scope Function은 Kotlin을 대표하는 기능 중 하나입니다.

Java에서는 객체를 생성하거나 null을 확인하거나 특정 객체를 수정할 때 반복적으로 객체 이름을 작성하는 경우가 많습니다.

예를 들어 객체를 생성하는 경우입니다.

Java에서는 다음과 같이 작성합니다.

User user = new User();

user.setName("Kim");
user.setAge(30);
user.setActive(true);

객체 이름인 user를 계속 작성해야 합니다.

Kotlin은 Scope Function을 통해 이러한 반복을 줄일 수 있습니다.

대표적으로 사용하는 Scope Function은 다음과 같습니다.

API주요 목적
letNull 처리, 결과 변환
run객체 사용 후 결과 반환
apply객체 초기화
also부가 작업(Log 등)
with객체를 여러 번 사용할 때

처음에는 비슷해 보이지만 목적이 모두 다르기 때문에 적절한 상황에서 사용하는 것이 중요합니다.

let()

let()은 가장 많이 사용하는 Scope Function입니다.

주로 null 체크객체 변환을 위해 사용합니다.

Java에서는 보통 다음과 같이 작성합니다.

if (user != null) {
    System.out.println(user.getName());
}

Kotlin에서는 Safe Call과 함께 사용할 수 있습니다.

user?.let {
    println(it.name)
}

user가 null이면 let() 자체가 실행되지 않습니다.

별도의 if 문을 작성하지 않아도 되므로 코드가 더욱 간결해집니다.

Spring Boot에서는 Repository에서 조회한 Entity를 처리할 때 자주 사용합니다.

userRepository.findById(id)
    ?.let {
        return UserResponse.from(it)
    }

또는 nullable 객체를 DTO로 변환할 때도 자주 사용합니다.

언제 사용하면 좋을까요?

  • nullable 객체 처리
  • Entity → DTO 변환
  • 특정 객체를 잠시 사용할 때

주의할 점

let()은 매우 편리하지만 과도하게 중첩하면 오히려 가독성이 떨어질 수 있습니다.

user?.let {
    order?.let {
        ...
    }
}

이처럼 여러 단계로 중첩되는 경우에는 일반적인 if 문이 더 읽기 쉬운 경우도 있습니다.

apply()

apply()는 객체를 생성하면서 초기화할 때 가장 많이 사용합니다.

Java에서는 다음과 같이 작성합니다.

User user = new User();

user.setName("Kim");
user.setAge(30);
user.setActive(true);

Kotlin에서는 apply() 하나로 표현할 수 있습니다.

val user = User().apply {
    name = "Kim"
    age = 30
    active = true
}

객체 이름을 반복해서 작성할 필요가 없습니다.

또한 apply()는 자기 자신을 반환하기 때문에 Builder처럼 사용할 수 있습니다.

Spring Boot에서는 다음과 같은 상황에서 자주 사용합니다.

  • Entity 생성
  • DTO 생성
  • 테스트 데이터 생성
  • Builder 패턴 대체

예를 들어 Entity를 생성하는 경우입니다.

val user = User().apply {
    name = request.name
    age = request.age
    active = true
}

Java의 Builder보다 훨씬 간결한 코드를 작성할 수 있습니다.

also()

also()는 객체 자체를 변경하기보다 부가적인 작업을 수행할 때 사용합니다.

대표적인 예가 로그 출력입니다.

val user = userRepository.findById(id)
    ?.also {
        logger.info("User 조회 : {}", it.id)
    }

객체에는 영향을 주지 않고 로그만 남길 수 있습니다.

또는 디버깅할 때도 자주 사용합니다.

users
    .filter { it.active }
    .also {
        println(it.size)
    }
    .map { UserResponse.from(it) }

Stream의 peek()와 비슷한 느낌으로 사용할 수 있습니다.

하지만 also() 내부에서 객체를 수정하는 것은 권장하지 않습니다.

부가 작업(Log, Metric, Debug) 정도로 사용하는 것이 가장 적절합니다.

run()

run()은 객체를 사용한 후 결과를 반환할 때 사용합니다.

val adult = user.run {
    age >= 20
}

또는 계산 결과를 반환하는 경우에도 자주 사용합니다.

val message = user.run {
    "$name ($age)"
}

run()은 람다의 마지막 값을 반환하기 때문에 값을 계산하는 용도로 많이 사용됩니다.

with()

with()는 하나의 객체를 여러 번 사용할 때 가독성을 높여 줍니다.

Java에서는 같은 객체를 반복해서 사용하는 경우가 많습니다.

System.out.println(user.getName());
System.out.println(user.getAge());
System.out.println(user.getEmail());

Kotlin에서는 with()를 사용할 수 있습니다.

with(user) {
    println(name)
    println(age)
    println(email)
}

객체 이름을 반복해서 작성하지 않아도 되므로 코드가 훨씬 간결해집니다.

Scope Function은 어떻게 선택해야 할까요?

Scope Function을 처음 배우면 가장 헷갈리는 부분이 어떤 API를 선택해야 하는지입니다.

실무에서는 다음 기준으로 생각하면 대부분의 상황을 해결할 수 있습니다.

API언제 사용할까?
letnull 처리 및 객체 변환
apply객체 초기화
also로그 및 부가 작업
run계산 결과 반환
with같은 객체를 여러 번 사용할 때

모든 Scope Function을 사용하는 것이 중요한 것이 아니라, 상황에 맞는 API를 선택하는 것이 더 중요합니다.


2️⃣ Collection API

Java 8부터 Stream API가 등장하면서 Collection 처리 방식이 크게 개선되었습니다.

예를 들어 활성 사용자만 조회하는 경우를 살펴보겠습니다.

Java에서는 다음과 같이 작성합니다.

List<UserResponse> responses =
    users.stream()
         .filter(User::isActive)
         .map(UserResponse::from)
         .toList();

Kotlin은 Stream을 생성하지 않고 Collection에서 바로 사용할 수 있습니다.

val responses =
    users
        .filter { it.active }
        .map { UserResponse.from(it) }

처음에는 작은 차이처럼 보이지만, 프로젝트 규모가 커질수록 코드의 가독성 차이가 크게 나타납니다.

또한 Kotlin Collection API는 Java Stream보다 더 자연스럽고 직관적인 메서드들을 많이 제공합니다.

이번 글에서는 실무에서 가장 많이 사용하는 map(), filter(), flatMap()부터 살펴보겠습니다.


3️⃣ 실무에서 가장 많이 사용하는 Collection API

앞에서 map(), filter()와 같이 Collection을 변환하는 기본적인 API를 살펴보았습니다.

하지만 Spring Boot 프로젝트를 개발하다 보면 이보다 더 자주 사용하는 API들이 있습니다.

특히 Entity를 조회한 후 DTO로 변환하거나, 데이터를 그룹화하거나, Map 형태로 변경하는 작업은 거의 모든 프로젝트에서 반복적으로 수행하게 됩니다.

이번에는 실무에서 가장 많이 사용하는 Collection API를 살펴보겠습니다.

associateBy()

associateBy()는 List를 Map으로 변환할 때 가장 많이 사용하는 API입니다.

Java에서는 Collectors.toMap()을 사용해야 합니다.

Map<Long, User> userMap =
    users.stream()
         .collect(Collectors.toMap(
             User::getId,
             Function.identity()
         ));

Kotlin에서는 한 줄이면 충분합니다.

val userMap = users.associateBy { it.id }

결과는 다음과 같습니다.

{
    1 -> User(...),
    2 -> User(...),
    3 -> User(...)
}

특정 ID로 객체를 자주 조회해야 하는 경우 List보다 Map이 훨씬 효율적입니다.

Spring Boot 프로젝트에서는 다음과 같은 경우에 자주 사용합니다.

  • Entity를 ID 기준으로 캐싱
  • 조회 결과를 Map으로 변환
  • 반복 조회 제거
  • O(n) 탐색을 O(1) 조회로 변경

예를 들어 주문 목록이 있고, 사용자 정보를 반복해서 조회해야 하는 상황이라면 List를 계속 순회하는 대신 Map으로 변환하여 사용하는 것이 훨씬 효율적입니다.

groupBy()

groupBy()는 같은 조건의 데이터를 그룹으로 묶을 때 사용하는 API입니다.

Java에서는 다음과 같이 작성합니다.

Map<Role, List<User>> result =
    users.stream()
         .collect(Collectors.groupingBy(User::getRole));

Kotlin은 더욱 직관적입니다.

val result = users.groupBy { it.role }

결과는 다음과 같습니다.

ADMIN
 ├─ User1
 ├─ User2

USER
 ├─ User3
 ├─ User4

실무에서는 다음과 같은 상황에서 많이 사용합니다.

  • 상태(Status)별 데이터 분류
  • 사용자 권한(Role)별 그룹화
  • 날짜별 데이터 그룹화
  • 카테고리별 데이터 분류

Collection을 직접 순회하면서 Map을 만드는 것보다 훨씬 간결하고 가독성이 좋습니다.

partition()

partition()은 하나의 Collection을 두 개의 Collection으로 분리합니다.

Java에서는 Stream을 두 번 수행하거나 반복문을 작성해야 합니다.

Kotlin에서는 다음과 같이 사용할 수 있습니다.

val (activeUsers, inactiveUsers) =
    users.partition { it.active }

결과는

  • activeUsers
  • inactiveUsers

두 개의 List가 생성됩니다.

예를 들어 활성 사용자와 비활성 사용자를 동시에 분리해야 하는 경우 매우 유용합니다.

firstOrNull()

Java에서는 첫 번째 데이터를 조회하기 위해 다음과 같이 작성합니다.

users.stream()
     .findFirst()
     .orElse(null);

Kotlin에서는

val user = users.firstOrNull()

조건이 있는 경우에도 사용할 수 있습니다.

val admin =
    users.firstOrNull {
        it.role == Role.ADMIN
    }

결과가 없으면 null을 반환하기 때문에 안전하게 사용할 수 있습니다.

반대로 반드시 데이터가 존재해야 한다면 first()를 사용할 수도 있지만, 데이터가 없으면 예외가 발생하므로 대부분의 경우에는 firstOrNull()을 사용하는 것이 더 안전합니다.

any(), all(), none()

조건을 검사하는 API도 매우 자주 사용합니다.

any()

하나라도 만족하는 데이터가 있는지 확인합니다.

val exists =
    users.any {
        it.active
    }

all()

모든 데이터가 조건을 만족하는지 확인합니다.

val allActive =
    users.all {
        it.active
    }

none()

조건을 만족하는 데이터가 하나도 없는지 확인합니다.

val empty =
    users.none {
        it.active
    }

Java에서는 Stream과 Match API를 사용하지만 Kotlin에서는 훨씬 직관적으로 표현할 수 있습니다.


4️⃣ Sequence - 대용량 데이터를 위한 Lazy Collection

앞에서 살펴본 Collection API는 대부분 매우 편리합니다.

하지만 Collection의 크기가 커질수록 성능도 함께 고려해야 합니다.

예를 들어 다음 코드를 살펴보겠습니다.

users
    .filter { it.active }
    .map { UserResponse.from(it) }
    .sortedBy { it.name }

각 연산마다 새로운 Collection이 생성됩니다.

데이터가 수십만 건 이상이라면 불필요한 객체 생성 비용이 발생할 수 있습니다.

이럴 때 사용하는 것이 Sequence입니다.

users
    .asSequence()
    .filter { it.active }
    .map { UserResponse.from(it) }
    .sortedBy { it.name }
    .toList()

Sequence는 중간 Collection을 생성하지 않고 필요한 시점에 데이터를 하나씩 처리합니다.

이를 Lazy Evaluation(지연 연산) 이라고 합니다.

Java Stream도 Lazy 방식으로 동작하지만 Kotlin에서는 Collection에서 asSequence()만 호출하면 쉽게 사용할 수 있습니다.

Sequence는 언제 사용할까요?

  • 대용량 데이터 처리
  • 여러 Collection 연산이 이어지는 경우
  • 성능 최적화가 필요한 경우

반대로 데이터가 적은 경우에는 Sequence를 사용할 필요가 없습니다.

오히려 Sequence 객체를 생성하는 비용 때문에 일반 Collection보다 느릴 수도 있습니다.

즉, Sequence는 성능 최적화를 위한 도구이지 모든 Collection에서 사용하는 것이 정답은 아닙니다.


실무에서 자주 사용하는 패턴

Spring Boot 프로젝트에서는 다음과 같은 패턴을 매우 자주 사용합니다.

Entity 조회

필요한 데이터만 필터링

DTO 변환

Map 생성

Response 반환

예를 들어 다음과 같은 코드입니다.

val response =
    users
        .filter { it.active }
        .associateBy { it.id }
        .mapValues { (_, user) ->
            UserResponse.from(user)
        }

또는

val response =
    users
        .groupBy { it.role }
        .mapValues { (_, users) ->
            users.map(UserResponse::from)
        }

Collection API를 적절히 활용하면 반복문을 직접 작성하는 횟수가 크게 줄어들며, 코드의 의도도 더욱 명확하게 표현할 수 있습니다.


Java 개발자가 가장 먼저 익혀야 하는 Kotlin API

이번 글에서 소개한 API 중에서도 실무에서 가장 많이 사용하는 기능을 정리하면 다음과 같습니다.

API활용 예시
let()Null 처리
apply()객체 초기화
also()로그 출력
run()결과 반환
map()DTO 변환
filter()조건 필터링
associateBy()Map 변환
groupBy()데이터 그룹화
firstOrNull()안전한 조회
sequence()대용량 데이터 처리

이 API들만 익혀도 대부분의 Spring Boot 프로젝트에서 Kotlin 코드를 읽고 작성하는 데 큰 어려움이 없습니다.


마무리

Kotlin의 가장 큰 장점은 문법이 짧다는 것이 아닙니다.

언어 자체에서 개발자가 자주 수행하는 작업을 간결하게 표현할 수 있도록 지원한다는 점입니다.

특히 Collection API와 Scope Function을 적절히 활용하면 반복문이나 조건문을 직접 작성하는 횟수가 크게 줄어들고, 코드의 의도도 더욱 명확하게 표현할 수 있습니다.

하지만 중요한 것은 모든 API를 사용하는 것이 아니라 상황에 맞는 API를 선택하는 것입니다.

무조건 함수형 스타일을 사용하는 것이 좋은 코드가 아니라, 다른 개발자가 읽기 쉽고 유지보수하기 쉬운 코드를 작성하는 것이 더 중요합니다.

Java를 이해하고 Kotlin을 배우면 문법의 차이보다 언어가 문제를 해결하는 방식의 차이를 더욱 크게 느끼게 됩니다.

Kotlin은 Java를 대체하는 언어가 아니라, Java의 장점을 유지하면서 개발 생산성과 안정성을 높여 주는 언어입니다.

0개의 댓글