Kotlin

코틀린 스프링을 사용하는 이유

mayleaf 2023. 11. 20. 22:33

코틀린 스프링을 백엔드 개발할때 쓰는 이유를 적은 글을 쓰며

백엔드 개발을 시작할때 최근에 가장 많이 논의되는 언어가 코틀린과 타입스크립트라고 생각을 하고 있는데, 개인적으로는 서로의 장단점이 다르다고 느껴져서 이런 글을 적어두고 싶었다.특히 Nest.js에 익숙하신 분들에게 도움이 되길 바라며 글을 쓴다.

글의 접근 방식

이 글은 코틀린과 다른 언어를 많이 비교하면서 진행될 예정이다. 자바랑 비교하게 되는 내용도 있고, 개인적인 경험을 빗대기 위해서 Dart같은 약간 생소할 수 있는 언어도 사용했다.

 

글의 내용

  • 코틀린에 대한 소개
  • 코틀린의 강점이라고 느껴지는 부분
  • 백엔드 개발에서 코틀린을 쓰는 이유

코틀린이란?

코틀린. 모던 랭귀지의 대표적인 주자중 하나이다. 대표적인 삼대장을 뽑아보라고 하면 Rust, Kotlin, Go 이렇게가 현대 언어의 중심으로 활동하고 있다고 느껴진다. 코틀린 같은 경우는 특히 백엔드 개발과 안드로이드에서 많이 사용되고 있다. 그리고 최근에는 코틀린 멀티플랫폼을 통해서 더 다양한 필드로 나가고 있다. 멀티플랫폼은 아직 시기상조라고 느껴지긴한다.

 

코틀린은 젯브레인사에서 만들어진 제품이고 개발자들이 가장 좋아하는 언어중 하나라고 생각한다. 시작에 불을 부은 건 안드로이드 개발에 대한 공식지원이었다. 그 전까지는 다들 자바만 주구장창썼고, 안드로이드 문서도 자바로 나왔다.

그래서 코틀린을 쓰는 이유가 뭐야?

이런 물음을 받으면 개인적으론 참 어렵게 느껴진다. JVM위에서 동작하는 언어이다보니, 코틀린의 장점을 아무리 이야기해도 그거 자바에서 돌아가잖아? 로 귀결이 되는데, 개인적으로는 이걸 납득시키기는 참 어려운 것 같다. 그 코틀린의 장점을 말해보자면 몇가지가 있다.

Null safety와 간결성

자바 버전 유저 도메인

public class User {
    private final String name;
    private final int age;

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

    // Getters, Setters, hashCode(), equals(), toString()...
}

코틀린 버전 유저 도메인

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

위 예시는 코틀린의 간결성을 보이기 위한 예시이다. 자바는 타입단위에서 null safety에 대한 보장이 없기 때문에 Null이 아님을 보이기 위해서 코드의 양이 방대해진다. Null을 아예 관리하지 못한다는 의미가 아니라 Optional을 사용했을때 값이 있을때는 어떻게, 없을때는 어떻게 이런 코드 블록을 계속 만들어 내야하고, 그러한 코드 블럭이 인지 능력을 해치기 때문에 쓰다보면 피곤해진다. 그리고 절대적인 코드의 양이 비교가 안된다. 

 

감사하게도 자바에서도 위의 예시에 나온 게터, 세터, toString의 경우 Lombok을 사용해서 직접 구현하지 않아도 된다. 그런데 롬복을 사용하는게 좋다고 생각하진 않아서 비빌만한게 아니라고 생각한다. 롬복때문에 사이드 이펙트가 발생하는 경우가 생길수도 있고, 롬복 자체도 러닝커브다. 만약에 자바 개발을 한다면 롬복에서 쓸만한 기능은 로깅용 어노테이션정도가 아닐까? 나머지는 IDE에서 generate해주는 생성자와 필수 메소드들을 써내쓰는 것이 좋다고 생각한다.

 

이런 코틀린의 간결성과 Null safety한 특성은 현대 언어에서 많이 발견되는 특성이다. 개인적으로 타입스크립트나 플러터, 스위프트를 해보신 분들이라면 아주 익숙하게 코틀린에서 null을 잘 다루실 것이라고 생각한다. nullish coalesce 만 잘써도 문제는 안생긴다. 코드 블록도 자바만큼은 안 늘기도 한다.

자바와의 호환

자바랑 100%호환됨이 코틀린의 캐치프레이즈였다. 실제로 코틀린에서 자바 도메인 뽑아서 쓰는 경우도 많고, jar를 안 건들이고 코틀린만으로 이런저런 작업을 다 쳐낼 수 있다.

// Kotlin code calling Java libraries
val calendar = java.util.Calendar.getInstance()
println(calendar.time)

보면 알겠지만 자바에서 구현된 내용을 import해서 쓸 수 있다. 

 

비동기 처리를 위한 코루틴

코루틴, 아마 코틀린 이야기를 주구장창 외치고 다니시는 분들은 코루틴이야기를 많이 하실 것 같다.

우선 예시를 하나 보면서 이야기해보면 좋을 것 같다.

타입스크립트버전 http 호출

  await fetch('http://localhost:3000/api/...', {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  })

코틀린 버전 비동기적 http 호출(Reactor Netty 활용)

runBlocking {
    withContext(Dispatchers.IO) {
        var a = HttpClient.create() //실제 코드에서는 이렇게 길게 쓰는 경우는 별로 없다. 간결할 것이다.
            .get()
            .uri("http://localhost:8080/api...")
            .responseContent()
            .aggregate()
            .asString()
            .doOnNext { println(it) }
            .awaitLast()
    }
}

코틀린이 훨씬 길고 뭔가 알수 없는 블록으로 감싸고 있는 걸 알 수 있다. runBlocking과 withContext는 CoroutineScope라는 범위를 잡아준다. 이 코드만 봤을때는 코틀린이 TS보다 더 읽기 어려워보이는건 자명하다. 그런데 왜 코틀린을 쓸까?

사실 이런 api호출하는 코드들은 대부분 추상화되어서 코드를 보는 과정에서 config 파일을 파헤치지 않는 이상 이렇게 짤 일은 없다

쓰레드풀의 선택 여부

가장 큰 차이점은 쓰레드풀을 고를 수 있냐 없냐이다. 이 차이는 언어의 근본적인 차이에서부터 느껴지는 부분이다. TS의 경우 이벤트루프를 통해서 병목지점이 되는 IO와 CPU Bound작업을 LibUV의 쓰레드에 던져준다. 문제는 던질 수 있는 작업이 Node.js 에서 정해준 작업뿐이라는 것이다. HTTP 요청이라던가 암호화라던가 명확하게 병목이 되는 부분은 잘 처리를 해주는데, 큰 객체를 파싱해서 변경해야한다던가, 한달치 송장을 뽑기 위해서 몇십만개의 객체를 변환하는 작업을 하면 몇초동안 먹통이 될 수 있다. 먹통되는 것을 막기 위해서 setTimeOut이나 setImmediate에 계속 값을 던져서 변환할수도 있겠지만, 그렇게 한다고 해서 객체 변환 속도가 빨라지는 것은 아니다.

그러나 코틀린에서는 쓰레드풀을 지정해서 던져줄 수 있기 때문에 시간이 오래걸리는 단순작업들을 멀티쓰레딩해서 처리할 수 있게 하기에 더 적합하다. 이런 모습을 생각해보면 자바, 코틀린이 General Purpose 한 언어에 더 가깝고, JS,TS는 웹기반에서 돌아가기에 더 적합하다는 생각이 강하게 든다.

추가적으로 더 이야기하면 위에서 언급한 그런 송장뽑기 이슈들을 운영으로 해결할 수 있는 상황도 있지만, 요구사항이 변하지 않는 상황에서도 어떻게 해서든 문제를 해결해야한다. 이런 점을 생각해보면, 이런 상황에서도 어떻게든 최적화를 노릴 수 있는 코틀린이 복잡한 요구사항을 요구하는 개발에는 더 좋다고 생각이 된다.

 

정리: 간결하고 Null safety한 코드 작성 가능, 자바와의 호환, 제네럴한 상황에서 비동기처리 가능 + 쓰레드풀의 할당을 마음대로 할 수 있음

코틀린을 백엔드에서 쓰는 이유

스프링 사용이 가능하다

코틀린을 백엔드에서 쓰는 이유는 스프링 프레임워크를 사용할 수 있기 때문이다. 자바와의 100%호환덕분에 DI를 손쉽게 해주는 스프링을 쓸 수 있게 된것이다.

@RestController
class UserController {

    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        // Fetch user
    }
}

스프링을 만져보신분들을 코틀린을 모르시더라도 어노테이션들이 어떻게 처리되는지 알기 때문에, 이 코드가 어떻게 호출되는지, 어떤 형식으로 응답을 내려주는지 이해할 수 있을 것이고, 그만큼 빠르게 적응할 수 있다는 장점이 있다.

 

이게 어떻게 장점이 돼? 라고 생각할 수 있을 것 같다.

그래서 스프링을 쓰는 이유, 코틀린으로 개발하는 이유에 대해서 조금 더 언급을 해보고 이 글을 마무리하고자한다.

백엔드에서 스프링을 쓰는 이유와 코틀린으로 짜는 이유

1. 스프링을 쓰는 이유: 확장 가능한 구조

스프링 MVC, WebFlux만 사용할거면 사실 굳이 스프링 안써도 된다. Nest.js가 구조도 훨씬 더 잘 잡아주고 코드 생성도 빌드도 더 빠르다. TS 특성상 리터럴 오브젝트를 사용해서 프로그래밍하면, Intellisense의 강력한 도움을 받으면서 객체지향 프로그래밍과 함수형 프로그래밍의 장점만 취한 프로그래밍도 가능하다. 그러나 스프링 배치와 쓰레드풀을 지정할 수 있다는 장점이 있다. Nest.js도 테스크 스케쥴링 있는데요? 라고 하기에는 주로 스케쥴잡은 거대한 배치를 다루게 되는 경우가 많아서 적합하지 않다. 난 거대한 배치를 다루는 작업이 노드 계열 엔진에서 쉽고, 고성능으로 돌릴 수 있다고 생각하지 않는다. 이유는 위에서 이야기했다.

그렇기 때문에 어떻게보면 반쪽짜리 스케쥴러를 사용하고 있는 셈이고, 그래서 스크립트 코드들을 따로 빼서 돌리는 일이 계속 있었다.

그러나 JVM 위에서는 대부분의 대용량 데이터도 효과적으로 처리할 수 있고, 이러한 이유로 스케쥴러를 통해서 돌아가는 배치와 엔드포인트 서버를 둘다 개발해야하는 서비스가 있다면 스프링을 선택할 것 같다. 그리고 배압처리라던가 하는 부분들도 있는데 다른 프레임워크에서도 배압을 처리하는 방법이 있지만, 배압처리 관련된 부분도 내가 아는 선에서는 스프링이 제일 자연스럽고, 커스텀하게 사용할 수 있어서 백엔드 개발이 스프링을 통해서 많이 이뤄지는 것 같다. 그리고 사실 대부분의 서비스, 아무리 작은 서비스여도 배치 스케쥴잡과 엔드포인트 서버가 둘다 필요해지기 때문에 국내에서 사람들이 스프링 개발을 많이 하게 되는 부분도 있지 않나 싶다.

 

2. 코틀린으로 짜는 이유

처음부터 설명해왔기 때문에 경험적인 부분을 좀 이야기하겠다.

개발을 하다보면 읽기 좋은 코드가 항상 중요하다. 그런데, 자바는 읽기 좋은 코드를 짜고 싶어도 불변성과 널 안정성을 방어하는 프로그래밍을 하려다보면 어느샌가 수 많은 코드 블럭이 생성되는 걸 보게 된다. 내 짧은 지식으로 이것은 피할 수가 없다. 만약 선언형으로 개발하려고 한다면 머리가 더 복잡해진다. 개념하나를 선언하려고 하는데 Optional과 Stream을 덕지덕지 붙혀놓은 코드블럭을 보면 선언형은 내려놓고 싶어지기도한다. 하지만 코틀린은 Nullish coelasce를 잘 처리해주고 if문도 expression취급 되어서 선언을 한눈에 보기 좋게 개발할 수 있게 해주고, 이런 부분에서 높은 생산성을 유지할 수 있게 해준다.

그래서 자바 대신 코틀린을 점점  많은 사람들이 선택해서 쓰게 되는 것 같다.

 

마무리

지금 회사에서 쓰는 기술셋을 스스로 정리해보는 시간을 가져보려고 하고 있습니다.

이번에는 좀 제대로 시리즈를 마무리해보고 싶습니다. 시리즈물을 매번 쓰다가 말아서 연중 전문 웹소설 작가가 되는 기분이네요.

이번 시리즈는 오늘 내용 포함해서 9개의 게시물로 작성 될 것 같은데요. 매주 하나씩 올려볼게요.

다음 포스팅 일자는 2023년 11월 27일이겠습니다.

 

모두 좋은 하루 보내시길 바라요 :)