본문 바로가기

Language/Kotlin

[Kotlin] 자바 개발자를 위한 코틀린 온보딩 (2) - 변수: val/var와 문자열 템플릿으로 이해하기

지난 편에서는 코틀린의 기본 단위인 함수를, 문과 식의 구분을 통해 살펴봤습니다. 이번 편의 핵심은 그 다음 기본 단위인 변수를 이해하는 것입니다. 코틀린에서 변수를 선언하는 두 가지 방법인 val/var를 짚어보고, 변수 값을 문자열 안에 끼워 넣는 문자열 템플릿까지 이어서 다뤄보겠습니다.

 

val과 var

val (immutable reference)

val name = "철수"
name = "영희"  // 에러! val은 재할당 불가

 

여기서 중요한 것은 값이 아니라 "참조"가 불변이라는 점입니다. val로 선언해도 그 안의 객체가 가변 타입이라면 내용은 얼마든지 바뀔 수 있습니다.

val list = mutableListOf(1, 2, 3)
list.add(4)                  // OK! 리스트 내용은 바꿀 수 있음
list = mutableListOf(5, 6)   // 에러! 참조 자체는 못 바꿈

 

 

자바의 final 변수와 비슷한 개념입니다.

// 자바
final List<Integer> list = new ArrayList<>();
list.add(4);        // OK
list = new ArrayList<>();  // 에러

 

자바 개발자가 걸려 넘어지는 포인트는 바로 이 지점입니다. val을 final과 동일시해서 "완전히 불변인 객체"라고 오해하기 쉬운데, val이 보장하는 건 참조의 고정뿐입니다. MutableList, MutableMap처럼 이름부터 가변임을 명시한 타입을 val로 선언해도 내용 변경은 막지 못합니다. 진짜 불변 컬렉션이 필요하다면 List, Map 같은 읽기 전용 타입을 쓰거나, 아예 불변 컬렉션 라이브러리를 고려해야 합니다.

 

코틀린에서는 기본적으로 val을 먼저 고려하는 것이 관례입니다. 재할당이 꼭 필요한 경우에만 var를 씁니다. 자바에서 습관적으로 변수를 선언하다 보면 var를 먼저 쓰게 되는데, 코틀린에서는 반대로 접근하는 것이 자연스럽습니다.

 

var (mutable reference)

var count = 0
count = 1   // OK
count += 1  // OK

언제든 재할당이 가능합니다.

 

타입 추론

val, var 모두 타입을 생략하면 컴파일러가 초기값으로부터 추론합니다.

val age = 20        // Int로 추론
val age: Int = 20   // 명시도 가능

 

초기값 없이 선언만 하려면 타입을 명시해야 합니다.

val age: Int   // 초기값 없이 선언만 하려면 타입 필수
age = 20        // 나중에 대입 (val이어도 "최초 한 번"은 허용)

val은 "재할당 불가"이지 "선언과 동시에 초기화 필수"는 아닙니다. 단, 정확히 한 번만 값을 대입할 수 있고, 컴파일러가 모든 실행 경로에서 한 번만 대입됨을 보장할 수 있어야 합니다.

 

문자열 템플릿 (String Template)

자바에서는 문자열에 변수를 끼워 넣으려면 + 연결이나 String.format을 써야 했습니다.

// 자바
String name = "철수";
System.out.println("안녕하세요, " + name);
System.out.println(String.format("안녕하세요, %s", name));

코틀린은 문자열 안에 변수나 식을 직접 삽입하는 문자열 템플릿을 제공합니다.

 

기본 형태: $변수명

val name = "철수"
println("안녕하세요, $name")  // 안녕하세요, 철수

 

복잡한 식: ${...}

프로퍼티 접근이나 연산, 함수 호출이 필요하면 중괄호로 감싸야 합니다.

val age = 20
println("내년엔 ${age + 1}살")           // 내년엔 21살

val list = listOf(1, 2, 3)
println("크기는 ${list.size}")           // 크기는 3

println("이름 길이: ${name.length}")     // 이름 길이: 2

 

단순 변수명은 $name처럼 중괄호 없이 써도 되지만, 점(.) 접근이나 연산이 들어가면 반드시 ${}로 감싸야 합니다.

println("$name.length")   // "철수.length" 그대로 출력됨! (의도와 다름)
println("${name.length}") // 2 (의도대로 동작)

 

템플릿은 내부적으로 toString()을 호출합니다

문자열 템플릿에 넣은 값이 문자열이 아니라면, 코틀린은 그 값의 toString()을 호출한 결과를 끼워 넣습니다. 커스텀 클래스를 템플릿에 넣었을 때 원하는 형태로 출력되지 않는다면, toString()을 오버라이드하지 않아서인 경우가 많습니다.

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

val user = User("철수", 20)
println("사용자: $user")  
// data class는 toString()이 자동 생성되어 있어서
// "사용자: User(name=철수, age=20)"처럼 출력됨

 

null인 값을 템플릿에 넣으면

자바였다면 null 참조에 + 연결이나 메서드 호출을 시도하다가 NullPointerException을 걱정해야 하는 지점입니다. 코틀린 템플릿은 null을 안전하게 처리해서, 문자 그대로 "null"이라는 문자열을 출력합니다.

val nickname: String? = null
println("별명: $nickname")  // 별명: null (예외 없이 문자열로 출력)

 

단, ${nickname.length}처럼 nullable 값에 점(.)으로 프로퍼티나 메서드에 바로 접근하려고 하면 컴파일 에러가 납니다. 코틀린은 nullable 타입(String?)의 멤버에 접근할 때 null인 경우를 처리하는 코드를 컴파일 시점에 강제하기 때문입니다. 이때 쓰는 게 안전 호출 연산자(?.)와 엘비스 연산자(?:)입니다.

  • ?.(안전 호출): 값이 null이 아니면 평소처럼 멤버에 접근하고, null이면 예외를 던지는 대신 그 자리에서 null을 반환합니다.
  • ?:(엘비스 연산자): 왼쪽 식이 null이면 오른쪽 값을 대신 사용합니다. "이 값이 없으면 저 기본값을 써라"는 뜻입니다.
println("길이: ${nickname?.length}")         // nickname이 null이면 -> 길이: null
println("길이: ${nickname?.length ?: 0}")    // nickname이 null이면 -> 길이: 0

자바에서 if (nickname != null) nickname.length() else 0 또는 Optional로 처리하던 걸, 코틀린은 nickname?.length ?: 0 한 줄로 표현하는 셈입니다. ?.와 ?:는 이번 글의 범위를 넘는 코틀린 null 안전성 체계의 일부라서 여기서는 템플릿과 관련된 최소한만 다뤘습니다. 자세한 내용은 별도 포스트로 다룰 만한 주제입니다.

 

특수문자와 함께 쓸 때

$를 리터럴로 쓰고 싶으면 이스케이프합니다.

println("가격: \$100")  // 가격: $100

 

삼중 따옴표 문자열(raw string)에서도 사용 가능

val path = """C:\Users\$name"""
println(path)  // C:\Users\철수 (백슬래시 이스케이프 불필요)

참고: 자바도 15부터(JEP 378) """로 감싸는 텍스트 블록을 지원해서, 여러 줄 문자열을 escape 없이 쓸 수 있게 됐습니다. 다만 자바의 텍스트 블록은 문자열 보간을 지원하지 않습니다. 변수를 끼워 넣으려면 여전히 String.format이나 formatted() 메서드를 따로 써야 합니다. 반면 코틀린은 일반 문자열이든 삼중 따옴표 문자열이든 $/${} 템플릿을 그대로 쓸 수 있다는 점이 다릅니다.

 

요약

구분 설명
val 재할당 불가 (참조 고정), 기본적으로 우선 사용
var 재할당 가능
$변수 단순 변수 삽입, 내부적으로 toString() 호출
${식} 프로퍼티 접근, 연산, 함수 호출 등 복잡한 식 삽입
$nullableVar 예외 없이 "null" 문자열로 출력

 

마무리

이번 편에서는 코틀린의 또 다른 기본 단위인 변수를 val/var로 선언하는 방식, 그리고 그 변수를 문자열에 끼워 넣는 문자열 템플릿까지 살펴봤습니다. val/var의 컴파일 결과나 문자열 템플릿의 내부 변환 방식(StringBuilder 활용 등)처럼 좀 더 깊이 들어갈 만한 주제도 있는데, 이는 다음 기회에 별도로 다뤄보겠습니다.

 


이 시리즈는 『코틀린 인 액션(Kotlin in Action)』을 참고하여 작성했습니다.