우대사항: Kotlin 기반 서비스 개발 경험
채용 공고를 자주 본다.
최근 백엔드 개발자 채용 공고, 특히 빅테크 기업이나 빠르게 성장하는 스타트업의 자격 요건을 보면 눈에 띄는 공통점이 있다. Java 기반의 Spring Boot가 여전히 주류이지만, 우대 사항이나 자격 요건에 Kotlin + Spring Boot 환경 개발 경험이 매우 자주 등장한다는 점이다.
단순히 새로운 언어가 유행하기 때문일까, 아니면 확실한 기술적 이점이 있기 때문일까? Java와 Kotlin의 코드를 직접 비교해보며 왜 기업들이 점점 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로 공식 지원하는 이상 선택지로는 충분히 고려할 만하다.