Java 개발자는 왜 Kotlin을 사용할까? - Spring Boot 백엔드 개발자의 시선으로 알아보기 (1)

donghyeoneom·2026년 7월 9일
post-thumbnail

Kotlin은 Android 개발 언어라는 이미지가 강하지만, 현재는 Spring Boot 백엔드 개발에서도 널리 사용되고 있습니다. 저 역시 Java로 백엔드 개발을 시작했지만, 현재는 Kotlin을 활용하여 Spring Boot 서비스를 개발하고 있습니다.

처음에는 "조금 더 간결한 Java" 정도로 생각했지만, 실제 프로젝트를 진행하면서 Kotlin은 단순히 문법을 줄여주는 언어가 아니라 개발 생산성과 안정성을 높이기 위해 설계된 언어라는 것을 체감하게 되었습니다.

이번 글에서는 Java 개발자의 관점에서 Kotlin이 왜 등장했으며, 왜 많은 Spring Boot 프로젝트에서 Kotlin을 선택하는지 알아보겠습니다.

들어가며

Java는 오랜 기간 동안 백엔드 개발의 표준 언어로 자리 잡았습니다.

Spring Framework와 Spring Boot의 성장으로 수많은 기업의 서비스가 Java 기반으로 개발되었으며, 안정성과 방대한 생태계는 지금도 Java의 가장 큰 장점입니다.

하지만 프로젝트 규모가 커질수록 Java를 사용하면서 반복적으로 작성하는 코드가 많다는 점을 느끼게 됩니다.

예를 들어 단순한 DTO 하나를 작성한다고 가정해 보겠습니다.

public class User {

    private Long id;
    private String name;
    private int age;

    public User(Long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        ...
    }

    @Override
    public int hashCode() {
        ...
    }

    @Override
    public String toString() {
        ...
    }
}

최근에는 Lombok을 많이 사용하면서 이러한 코드가 많이 줄어들었습니다.

하지만 생각해 보면 Lombok 역시 Java의 부족한 부분을 보완하기 위해 등장한 라이브러리입니다.

반면 Kotlin은 이러한 불편함을 라이브러리가 아니라 언어 자체에서 해결하도록 설계되었습니다.

그래서 Kotlin을 배우다 보면 "문법이 짧다."라는 느낌보다 개발자가 비즈니스 로직에 집중할 수 있도록 많은 부분을 언어가 대신 처리해 준다. 라는 점을 가장 크게 느끼게 됩니다.


1️⃣ Kotlin은 왜 등장했을까?

Kotlin은 2011년 JetBrains에서 처음 공개한 JVM 언어입니다.

JetBrains는 IntelliJ IDEA를 개발한 회사로, Java 개발 환경을 가장 잘 이해하고 있는 회사 중 하나입니다.

수많은 Java 프로젝트를 지원하면서 JetBrains는 Java가 가진 장점과 한계를 모두 경험하게 되었습니다.

Java는 다음과 같은 장점을 가지고 있습니다.

  • 뛰어난 안정성
  • 방대한 오픈소스 생태계
  • 높은 호환성
  • 오랜 기간 검증된 기술

반면 개발을 하다 보면 다음과 같은 불편함도 존재했습니다.

  • 반복적으로 작성해야 하는 Boilerplate Code
  • NullPointerException 발생 가능성
  • 장황한 Collection 처리 코드
  • Getter, Setter, Builder 등 반복되는 코드
  • Extension Function 부재
  • Optional 사용의 불편함

JetBrains는 이러한 문제를 해결하면서도 Java의 장점은 그대로 유지할 수 있는 언어를 만들고자 했습니다.

그 결과가 Kotlin입니다.

여기서 중요한 점은 Kotlin은 Java를 대체하기 위한 언어가 아니라는 것입니다.

Kotlin은 처음부터 Java와의 완벽한 상호 운용성(Interoperability)을 목표로 설계되었습니다.

즉,

  • 기존 Java 라이브러리를 그대로 사용할 수 있고
  • Java 프로젝트에 Kotlin을 함께 사용할 수 있으며
  • 필요한 부분부터 점진적으로 Kotlin으로 전환할 수도 있습니다.

실제로 많은 Spring Boot 프로젝트에서는 Java와 Kotlin이 함께 사용되기도 합니다.


2️⃣ Java와 Kotlin의 철학

Java와 Kotlin은 모두 JVM 위에서 동작하지만 언어가 추구하는 방향에는 차이가 있습니다.

Java는 명확성(Explicit)안정성을 중요하게 생각합니다.

반면 Kotlin은 생산성(Productivity)안전성(Safety) 을 동시에 추구합니다.

쉽게 말하면 Java는 개발자가 직접 작성하도록 설계되었고, Kotlin은 컴파일러가 대신할 수 있는 부분은 최대한 대신 처리하도록 설계되었습니다.

Java는 명시적인 언어

Java에서는 대부분의 타입을 직접 작성합니다.

List<User> users = new ArrayList<>();

Map<Long, User> userMap = new HashMap<>();

User user = new User();

코드만 보더라도 타입이 모두 명시되어 있습니다.

이러한 방식은 가독성이 좋다는 장점이 있지만, 프로젝트가 커질수록 같은 타입을 반복해서 작성하는 경우가 많습니다.

Kotlin은 타입 추론을 지원합니다.

Kotlin에서는 컴파일러가 충분히 추론할 수 있는 정보는 생략할 수 있습니다.

val users = mutableListOf<User>()

val userMap = mutableMapOf<Long, User>()

val user = User()

타입을 명시하지 않았지만 컴파일러가 자동으로 타입을 추론합니다.

이를 Type Inference(타입 추론) 라고 합니다.

코드의 길이는 짧아졌지만 의미는 그대로 유지됩니다.

Getter 대신 Property를 사용합니다.

Java에서는 객체의 값을 가져오기 위해 Getter를 호출합니다.

String name = user.getName();

Kotlin에서는 Property 형태로 접근합니다.

val name = user.name

내부적으로는 Getter를 호출하지만 개발자는 더욱 자연스러운 형태로 객체를 사용할 수 있습니다.

Spring Boot에서 Entity나 DTO를 사용할 때도 이러한 차이를 쉽게 느낄 수 있습니다.

switch 대신 when을 사용합니다.

Java에서는 조건 분기를 위해 switch를 사용합니다.

switch (status) {
    case READY:
        ...
        break;

    case COMPLETE:
        ...
        break;
}

Kotlin에서는 when을 사용합니다.

when (status) {
    READY -> ...
    COMPLETE -> ...
}

when은 단순한 조건문이 아니라 Expression으로도 사용할 수 있기 때문에 훨씬 강력한 기능을 제공합니다.


3️⃣ Java보다 Kotlin이 생산성이 높은 이유

많은 개발자가 Kotlin을 처음 접하면 가장 먼저 "코드가 짧다."라는 인상을 받습니다.

하지만 Kotlin의 진짜 장점은 코드 길이가 아닙니다.

반복적인 코드를 줄이고 개발자가 비즈니스 로직에 집중할 수 있도록 만들어 준다는 점이 가장 큰 장점입니다.

Data Class

Java에서는 DTO 하나를 만들기 위해 많은 코드를 작성해야 합니다.

Lombok을 사용하지 않는다면 생성자, Getter, Setter, equals(), hashCode(), toString() 등을 직접 구현해야 합니다.

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

data class User(
    val id: Long,
    val name: String,
    val age: Int
)

컴파일러가 자동으로 다음 메서드를 생성합니다.

  • equals()
  • hashCode()
  • toString()
  • copy()
  • componentN()

Spring Boot 프로젝트에서도 Response DTO나 Request DTO는 대부분 Data Class를 사용합니다.

String Template

Java에서는 문자열을 연결하기 위해 다음과 같이 작성합니다.

String message = "Hello " + user.getName();

Kotlin에서는 문자열 안에서 바로 변수를 사용할 수 있습니다.

val message = "Hello ${user.name}"

문자열이 길어질수록 가독성 차이는 더욱 커집니다.

Default Parameter

Java에서는 기본값을 지원하지 않기 때문에 생성자를 여러 개 만드는 경우가 많습니다.

new User("Kim");

new User("Kim", 20);

new User("Kim", 20, true);

Kotlin은 기본값을 지원합니다.

data class User(
    val name: String,
    val age: Int = 20,
    val active: Boolean = true
)

필요한 값만 전달하면 되므로 생성자 오버로딩이 거의 필요하지 않습니다.

Named Argument

매개변수가 많아질수록 생성자의 순서를 기억하기 어려워집니다.

Java에서는 순서를 잘못 입력해도 타입이 같다면 컴파일 오류가 발생하지 않는 경우가 있습니다.

new User("Kim", 20, true);

Kotlin에서는 매개변수 이름을 함께 작성할 수 있습니다.

User(
    name = "Kim",
    age = 20,
    active = true
)

코드를 읽는 것만으로도 어떤 값이 전달되는지 쉽게 이해할 수 있으며, 가독성과 유지보수성이 크게 향상됩니다.


4️⃣ Kotlin의 가장 큰 장점, Null Safety

Java 개발자가 Kotlin을 사용하면서 가장 크게 체감하는 변화는 Null Safety입니다.

Java에서는 모든 참조 타입이 null이 될 수 있습니다.

User user = userRepository.findById(id);

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

만약 usernull이라면 NullPointerException이 발생합니다.

그래서 Java에서는 항상 null 체크를 수행하거나 Optional을 사용해야 했습니다.

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

또는

Optional<User> optional = repository.findById(id);

처럼 Optional을 사용하여 null을 표현하기도 합니다.

하지만 Kotlin은 접근 방식 자체가 다릅니다.

Kotlin은 타입에 null 가능 여부를 포함하고 있습니다.

val user: User

위 변수는 절대 null이 될 수 없습니다.

반대로 null을 허용하려면 명시적으로 ?를 붙여야 합니다.

val user: User?

즉, 타입만 보더라도 해당 객체가 null이 될 수 있는지 바로 알 수 있습니다.

이러한 특징 덕분에 컴파일러는 null 관련 오류를 미리 검사할 수 있으며, Java에서 자주 발생했던 NullPointerException을 상당 부분 예방할 수 있습니다.

① Safe Call Operator (?.)

Kotlin에서는 Safe Call Operator를 통해 null 여부를 안전하게 처리할 수 있습니다.

val name = user?.name

user가 null이면 name 역시 null이 됩니다.

별도의 null 체크를 작성하지 않아도 되므로 코드가 훨씬 간결해집니다.

② Elvis Operator (?:)

null일 경우 기본값을 지정할 수도 있습니다.

val name = user?.name ?: "Unknown"

Java에서는 삼항 연산자나 Optional.orElse() 등을 사용해야 하지만 Kotlin에서는 훨씬 직관적으로 표현할 수 있습니다.

③ Not Null Assertion (!!)

!! 연산자는 해당 객체가 절대 null이 아니라는 것을 개발자가 직접 보장하는 연산자입니다.

val name = user!!.name

하지만 실제로 null이라면 NullPointerException이 발생합니다.

따라서 !!는 정말 필요한 경우가 아니라면 사용을 지양하는 것이 좋습니다.

Spring Boot 프로젝트에서도 !!보다 Safe Call(?.)이나 Elvis Operator(?:)를 사용하는 것이 일반적입니다.


5️⃣ Kotlin Collection API가 강력한 이유

Java 8 이후 Stream API가 등장하면서 컬렉션 처리가 매우 편리해졌습니다.

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

Kotlin 역시 동일한 기능을 제공하지만, Stream을 생성하지 않고 Collection 자체에서 바로 사용할 수 있습니다.

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

코드의 길이가 짧아질 뿐만 아니라 훨씬 읽기 쉬워집니다.

또한 Kotlin은 Collection API가 매우 풍부합니다.

예를 들어 Java에서는 Collectors.groupingBy()를 사용하지만,

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

Kotlin에서는 단순히 groupBy()만 호출하면 됩니다.

val result = users.groupBy { it.role }

Map으로 변환하는 경우도 마찬가지입니다.

Java

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

Kotlin

val userMap = users.associateBy { it.id }

언어 자체에서 Collection API를 적극적으로 지원하기 때문에 코드의 복잡도가 크게 줄어듭니다.

다음 글에서는 이러한 Collection API들을 실무 예제와 함께 자세히 살펴볼 예정입니다.


6️⃣ Extension Function

Kotlin을 사용하면서 가장 신기했던 기능 중 하나가 Extension Function입니다.

Java에서는 기존 클래스에 새로운 메서드를 추가하려면 Utility Class를 만드는 경우가 많았습니다.

public class StringUtils {

    public static boolean isEmail(String value) {
        ...
    }

}

사용할 때는 다음과 같이 호출합니다.

StringUtils.isEmail(email);

Kotlin에서는 기존 클래스를 수정하지 않고도 새로운 메서드를 추가할 수 있습니다.

fun String.isEmail(): Boolean {
    ...
}

사용하는 방법도 훨씬 자연스럽습니다.

email.isEmail()

Spring Boot 프로젝트에서도 Extension Function을 많이 활용합니다.

예를 들어 Entity를 DTO로 변환하거나 날짜를 포맷팅하는 기능 등을 Extension Function으로 작성하면 코드의 가독성이 크게 향상됩니다.


7️⃣ Spring Boot에서 Kotlin을 사용하는 이유

Spring Framework는 Kotlin을 공식적으로 지원하고 있으며, Spring Boot에서도 Kotlin을 위한 다양한 기능을 제공합니다.

특히 다음과 같은 이유로 Kotlin을 선택하는 프로젝트가 꾸준히 증가하고 있습니다.

코드량이 크게 줄어듭니다.

Data Class, Property, Type Inference 등으로 인해 Java보다 작성해야 하는 코드가 훨씬 적습니다.

Null 안정성이 높아집니다.

컴파일 단계에서 null 오류를 검사하기 때문에 런타임 오류를 줄일 수 있습니다.

Collection 처리가 간결합니다.

map(), filter(), groupBy(), associateBy() 등을 Collection에서 바로 사용할 수 있습니다.

Spring과의 궁합이 좋습니다.

Spring Boot는 Kotlin DSL, Coroutine, Kotlin Reflection 등 Kotlin 친화적인 기능을 지속적으로 지원하고 있습니다.

최근에는 새로운 Spring Boot 프로젝트를 시작할 때 Kotlin을 선택하는 사례도 점점 늘어나고 있습니다.


Java 개발자가 Kotlin을 사용하며 가장 크게 느끼는 변화

Java에서 Kotlin으로 넘어오면 가장 먼저 느끼는 변화는 문법이 아닙니다.

오히려 개발자가 작성해야 하는 불필요한 코드가 줄어든다는 점입니다.

처음에는 낯설게 느껴질 수도 있지만, Kotlin의 철학을 이해하고 나면 대부분의 기능이 개발자의 생산성을 높이기 위해 설계되었다는 것을 알 수 있습니다.

특히 다음 기능들은 실무에서도 거의 매일 사용하게 됩니다.

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

이러한 기능을 적절히 활용하면 코드의 길이뿐만 아니라 가독성과 유지보수성도 함께 향상됩니다.


마무리

Kotlin은 Java를 대체하기 위해 만들어진 언어가 아니라, Java의 장점을 유지하면서 생산성과 안정성을 높이기 위해 설계된 언어입니다.

특히 Spring Boot 백엔드 개발에서는 Null Safety와 간결한 Collection API, 그리고 다양한 언어 기능 덕분에 더욱 효율적인 개발이 가능합니다.

물론 Java를 잘 알고 있다고 해서 Kotlin을 바로 능숙하게 사용할 수 있는 것은 아닙니다.

Kotlin은 Java와 비슷해 보이지만 언어가 추구하는 철학과 코드를 작성하는 방식에는 분명한 차이가 있습니다.

다음 글에서는 Spring Boot 프로젝트에서 가장 많이 사용하는 Kotlin API들을 Java 코드와 비교하며 살펴보겠습니다.

let(), apply(), also(), run(), groupBy(), associateBy(), sequence() 등 실무에서 자주 사용하는 API를 중심으로 언제 사용하면 좋은지, 그리고 Java보다 어떤 점이 편리한지 자세히 알아보겠습니다.

0개의 댓글