Skip to main content

우대사항: Kotlin 기반 서비스 개발 경험


Intro

채용 공고를 자주 본다.

최근 백엔드 채용 공고, 특히 빠르게 성장하는 스타트업에서 우대 사항에 Kotlin + Spring Boot 환경 개발 경험이 자주 보인다. Java 기반의 Spring Boot가 여전히 주류인데 왜 Kotlin이 같이 따라붙는 걸까.

Java와 Kotlin 코드를 직접 비교해보면 이유가 명확해진다.


1. 생산성의 차이 [Data Class]​

가장 먼저 체감되는 부분은 코드의 간결함이다. Java로 개발할 때 DTO 하나를 만들기 위해 꽤 많은 반복 노동이 필요했다. Lombok이라는 훌륭한 라이브러리가 존재하지만, 언어 차원에서 이를 지원하는 것과는 근본적인 차이가 있다.

Java (Lombok)​

@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
public class UserDto {
private String name;
private int age;
private String email;
}

Kotlin​

data class UserDto(
val name: String,
val age: Int,
val email: String
)

Kotlin의 data class는 생성자, Getter, equals(), hashCode(), toString()을 컴파일 시점에 자동으로 생성한다. 코드가 짧아진다는 것은 단순히 타자 칠 일이 줄어드는 것을 넘어, **"읽어야 할 코드가 줄어들어 비즈니스 로직에 더 집중할 수 있다"**는 큰 장점이 된다.


2. Null Safety​

Java로 짜다 보면 NullPointerException(NPE)이 꼭 한 번은 터진다. 컴파일 시점에 null 여부를 확신할 수 없어서 방어 로직이 늘어나거나, 그냥 런타임에서 터지거나 둘 중 하나다.

Kotlin은 이 문제를 컴파일 단계에서 원천 차단한다.

Java​

public void printLength(String str) {
// 실수로 null 체크를 누락했다면 NPE 발생 위험이 있다
System.out.println(str.length());
}

Kotlin​

// String? 은 null이 허용됨을 명시한다
fun printLength(str: String?) {
// 컴파일 에러 발생! null 가능성이 있는 변수는 직접 접근할 수 없다
// println(str.length)

// 안전한 호출 (?.)을 사용하여 null 체크와 실행을 동시에 처리한다
println(str?.length)
}


3. 비동기 처리 [Coroutines]​

Spring WebFlux의 Java Reactor 패턴(Mono, Flux)은 러닝커브가 가파르고 체인이 길어지면 읽기가 어렵다.

Java (Reactor)​

public Mono<Delivery> getDelivery(String userId) {
return userRepository.findById(userId)
.flatMap(user -> deliveryRepository.findByUserNo(user.getUserNo()))
.subscribeOn(Schedulers.boundedElastic());
}

Kotlin (Coroutines)​

suspend fun getDelivery(userId: String): Delivery {
val user = userRepository.findById(userId) ?: throw UserNotFoundException()
return deliveryRepository.findByUserNo(user.userNo)
}

Kotlin의 Coroutine은 suspend 키워드 하나로 비동기 코드를 동기처럼 쓸 수 있게 해준다. 코드가 훨씬 읽기 쉽고 디버깅도 편하다.


4. 마이그레이션 실습​

최근 진행한 토이 프로젝트의 코드를 직접 마이그레이션 해보았다. Github API를 통해 커밋 내역을 가져와 DTO로 변환하는 로직이다.

Java (Before)​

ArrayList를 생성하고 for 문을 돌며 데이터를 추가한다. Getter 호출이 중첩되어 코드가 길어진다.

public List<CommitResponse> getCommits(String token, String repoName) throws IOException {
GitHub github = new GitHubBuilder().withOAuthToken(token).build();
GHRepository repo = github.getRepository(repoName);

List<CommitResponse> commits = new ArrayList<>();
for (GHCommit commit : repo.queryCommits().pageSize(20).list()) {
commits.add(new CommitResponse(
commit.getSHA1(),
commit.getCommitShortInfo().getMessage(),
commit.getCommitShortInfo().getAuthor().getName(),
commit.getCommitDate().toString()
));
}
return commits;
}

Kotlin (After)​

Kotlin의 컬렉션 함수인 map을 활용했다. new 키워드가 사라졌고 프로퍼티 접근 문법을 통해 가독성이 향상되었다. 선언형 스타일 덕분에 데이터 흐름이 한눈에 파악된다.

fun getCommits(token: String, repoName: String): List<CommitResponse> {
val github = GitHubBuilder().withOAuthToken(token).build()
val repo = github.getRepository(repoName)

return repo.queryCommits().pageSize(20).list()
.map { commit ->
CommitResponse(
sha = commit.sha1,
message = commit.commitShortInfo.message,
author = commit.commitShortInfo.author.name,
date = commit.commitDate.toString()
)
}
}


5. Java와의 100% 상호 운용성​

기존 Java 코드를 다 갈아엎어야 했다면 도입 자체가 현실적이지 않았을 텐데, Kotlin은 Java와 100% 호환된다.

  • 하나의 프로젝트 내에서 .java와 .kt 파일이 공존 가능하다.
  • 기존의 방대한 Java 라이브러리를 그대로 사용할 수 있다.
  • 서비스 운영 중에도 점진적인 마이그레이션이 가능하다.

마치며​

나한테 Kotlin은 Android 전용 언어였다. 직접 코드를 비교해보고 나서야 왜 백엔드에서도 쓰는지 이해됐다.

Java도 계속 발전하고 있지만, Spring이 Kotlin을 First-class로 공식 지원하는 이상 선택지로는 충분히 고려할 만하다.