Kim-Baek 개발자 이야기

Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기 본문

개발/java basic

Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기

김백개발자 2025. 12. 22. 10:42
반응형

이 글을 읽으면: Kotlin의 변수 선언 방식인 val과 var의 차이를 완벽하게 이해하고, Java에는 없는 강력한 타입 추론과 Null Safety 시스템을 실전 예제로 배울 수 있습니다.


📌 목차

  1. 들어가며 - Java 변수 선언의 불편함
  2. val vs var - 언제 무엇을 쓸까?
  3. 타입 추론 - 타입을 안 써도 되는 마법
  4. 명시적 타입 선언 - 언제 필요할까?
  5. 기본 타입 완벽 정리
  6. Nullable 타입 - NPE와의 전쟁
  7. 타입 변환과 체크
  8. 마무리 - 다음 편 예고

들어가며 - Java 변수 선언의 불편함

Java로 개발하면서 이런 경험 있으신가요?

// Java - 매번 타입을 명시해야 함
String name = "규철";
int age = 30;
List<String> hobbies = new ArrayList<String>();  // 타입 중복!
Map<String, Integer> scores = new HashMap<String, Integer>();  // 너무 길다!

// 불변 변수 만들려면 final 붙여야 함
final String company = "우리회사";
// company = "다른회사";  // 컴파일 에러

Kotlin은 이렇게 간단합니다:

// Kotlin - 타입 추론 + 간결한 불변 선언
val name = "규철"                    // 타입 자동 추론
val age = 30                         // Int로 추론
val hobbies = listOf("테니스", "개발")  // List<String> 추론
val scores = mapOf("수학" to 95)     // Map<String, Int> 추론

오늘은 이 마법 같은 기능들을 하나씩 파헤쳐보겠습니다.


val vs var - 언제 무엇을 쓸까?

val - 불변 변수 (Immutable)

val은 "value"의 약자로, Java의 final과 같습니다.

fun main() {
    val name = "규철"
    // name = "철규"  // ❌ 컴파일 에러!
    // Val cannot be reassigned
    
    println(name)  // 규철
}

var - 가변 변수 (Mutable)

var는 "variable"의 약자로, 일반 변수입니다.

fun main() {
    var age = 30
    println(age)  // 30
    
    age = 31      // ✅ 가능
    println(age)  // 31
}

실전 비교 - 사용자 정보 관리

fun main() {
    // 변하지 않는 정보 - val
    val userId = "user123"
    val birthYear = 1990
    
    // 변할 수 있는 정보 - var
    var nickname = "코틀린초보"
    var point = 1000
    
    // 포인트 적립
    point += 500
    println("적립 후 포인트: $point")  // 1500
    
    // 닉네임 변경
    nickname = "코틀린중수"
    println("변경된 닉네임: $nickname")
    
    // userId = "user456"  // ❌ 에러!
}

언제 val을 쓰고 언제 var를 쓸까?

기본 원칙: 무조건 val을 먼저 쓰고, 필요할 때만 var로 변경

상황 선택 예시

상수 val val PI = 3.14
설정값 val val maxRetry = 3
함수 파라미터 val (자동) fun test(name: String)
반복문 카운터 var var i = 0
누적 계산 var var sum = 0
상태 변경 var var isLoading = false

Java와 비교

// Java - final을 깜빡하기 쉬움
String name = "규철";  // 실수로 변경 가능
name = "철규";        // 의도치 않은 변경

// 불변으로 만들려면 명시적 final
final String safeName = "규철";
// safeName = "철규";  // 컴파일 에러
// Kotlin - 기본이 val (불변 권장)
val name = "규철"
// name = "철규"  // 컴파일 에러 (안전!)

var changeable = "규철"
changeable = "철규"  // 명시적으로 var를 선택했으므로 OK

타입 추론 - 타입을 안 써도 되는 마법

기본 타입 추론

Kotlin은 초기값을 보고 타입을 자동으로 추론합니다.

fun main() {
    val number = 42           // Int로 추론
    val pi = 3.14             // Double로 추론
    val message = "안녕"       // String으로 추론
    val isValid = true        // Boolean으로 추론
    
    // 타입 확인하기
    println(number::class.simpleName)   // Int
    println(pi::class.simpleName)       // Double
    println(message::class.simpleName)  // String
}

컬렉션 타입 추론

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    // List<Int>로 추론
    
    val names = listOf("규철", "영희", "철수")
    // List<String>으로 추론
    
    val scores = mapOf(
        "국어" to 90,
        "영어" to 85,
        "수학" to 95
    )
    // Map<String, Int>로 추론
}

Java와 비교

// Java - 타입 중복의 고통
List<String> names = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();

// Java 10+ 에서 var 추가 (로컬 변수만)
var names = new ArrayList<String>();  // 그래도 타입 명시 필요
// Kotlin - 타입 한 번도 안 씀
val names = listOf("규철", "영희")
val scores = mapOf("수학" to 95)

명시적 타입 선언 - 언제 필요할까?

타입 명시가 필요한 경우

1. 초기화하지 않을 때

fun main() {
    val name: String  // 타입 명시 필수
    
    if (조건) {
        name = "규철"
    } else {
        name = "손님"
    }
    
    println(name)
}

2. 더 넓은 타입으로 선언할 때

fun main() {
    val number: Number = 42        // Int를 Number로
    val list: List<Any> = listOf(1, "문자", true)  // 여러 타입 섞기
}

3. 인터페이스 타입으로 받을 때

fun main() {
    val list: MutableList<String> = arrayListOf()
    // ArrayList가 아닌 MutableList 인터페이스 타입
}

타입 선언 형식

// 형식: val/var 변수명: 타입 = 값
val name: String = "규철"
var age: Int = 30
val price: Double = 19.99
val isValid: Boolean = true

기본 타입 완벽 정리

숫자 타입

fun main() {
    // 정수
    val byte: Byte = 127                    // 8bit: -128 ~ 127
    val short: Short = 32767                // 16bit
    val int: Int = 2147483647              // 32bit (기본)
    val long: Long = 9223372036854775807L  // 64bit (L 접미사)
    
    // 실수
    val float: Float = 3.14f               // 32bit (f 접미사)
    val double: Double = 3.14              // 64bit (기본)
    
    // 자동 추론
    val autoInt = 42        // Int
    val autoLong = 42L      // Long
    val autoDouble = 3.14   // Double
    val autoFloat = 3.14f   // Float
}

문자 타입

fun main() {
    val char: Char = 'A'
    val string: String = "안녕하세요"
    
    // 여러 줄 문자열
    val multiLine = """
        첫 번째 줄
        두 번째 줄
        세 번째 줄
    """.trimIndent()
    
    println(multiLine)
}

Boolean 타입

fun main() {
    val isTrue: Boolean = true
    val isFalse: Boolean = false
    
    val result = isTrue && !isFalse
    println(result)  // true
}

Java와 차이점

Java Kotlin 차이점

int Int Kotlin은 객체 (클래스)
Integer Int 통합됨 (박싱/언박싱 자동)
long Long L 접미사 동일
String String 동일하지만 더 강력한 기능

중요: Kotlin은 모든 타입이 객체입니다!

val number = 42
println(number.toString())     // 메서드 호출 가능
println(number.toDouble())     // 42.0

Nullable 타입 - NPE와의 전쟁

Java의 악몽, NullPointerException

// Java - 언제 터질지 모르는 시한폭탄
String name = getName();  // null일 수도 있음
int length = name.length();  // 💥 NPE 발생!

Kotlin의 해결책 - Nullable 타입

기본 원칙: Kotlin은 기본적으로 null을 허용하지 않습니다!

fun main() {
    var name: String = "규철"
    // name = null  // ❌ 컴파일 에러!
    // Null can not be a value of a non-null type String
}

Nullable 타입 선언 - ? 연산자

fun main() {
    var name: String? = "규철"  // ?를 붙이면 null 가능
    name = null                 // ✅ OK
    
    println(name)  // null
}

Nullable 타입 안전하게 다루기

1. Safe Call 연산자 (?.)

fun main() {
    val name: String? = null
    
    // Java 스타일 (위험)
    // val length = name.length()  // 컴파일 에러!
    
    // Kotlin 스타일 (안전)
    val length = name?.length  // null이면 null 반환
    println(length)  // null
}

2. Elvis 연산자 (?:)

fun main() {
    val name: String? = null
    
    // null이면 기본값 사용
    val length = name?.length ?: 0
    println(length)  // 0
    
    val displayName = name ?: "손님"
    println(displayName)  // 손님
}

3. Not-null 단언 (!!)

fun main() {
    val name: String? = "규철"
    
    // 확실히 null이 아닐 때만 사용 (위험!)
    val length = name!!.length
    println(length)  // 2
    
    // 주의: null이면 NPE 발생
    val nullName: String? = null
    // val badLength = nullName!!.length  // 💥 NPE!
}

실전 예제 - 사용자 정보 처리

data class User(
    val id: String,
    val name: String,
    val email: String?,      // 이메일은 선택사항
    val phoneNumber: String? // 전화번호도 선택사항
)

fun main() {
    val user = User(
        id = "user123",
        name = "규철",
        email = null,
        phoneNumber = "010-1234-5678"
    )
    
    // Safe Call로 안전하게 접근
    println("이메일 길이: ${user.email?.length ?: 0}")  // 0
    
    // Elvis로 기본값 제공
    val displayEmail = user.email ?: "이메일 없음"
    println("표시용 이메일: $displayEmail")  // 이메일 없음
    
    // let으로 null이 아닐 때만 실행
    user.phoneNumber?.let {
        println("전화번호: $it")  // 전화번호: 010-1234-5678
    }
}

Java와 비교

// Java - null 체크 지옥
public void processUser(User user) {
    if (user != null) {
        String email = user.getEmail();
        if (email != null) {
            int length = email.length();
            System.out.println(length);
        }
    }
}
// Kotlin - 한 줄로 해결
fun processUser(user: User?) {
    println(user?.email?.length ?: 0)
}

타입 변환과 체크

is - 타입 체크 (instanceof)

fun main() {
    val value: Any = "문자열"
    
    if (value is String) {
        // 자동으로 String으로 캐스팅됨 (Smart Cast)
        println(value.length)  // 3
    }
}

as - 타입 캐스팅

fun main() {
    val value: Any = "문자열"
    
    val str = value as String  // 강제 캐스팅
    println(str.length)
    
    // 안전한 캐스팅 (실패하면 null)
    val number = value as? Int
    println(number)  // null
}

숫자 타입 변환

fun main() {
    val intValue = 42
    
    val longValue = intValue.toLong()
    val doubleValue = intValue.toDouble()
    val stringValue = intValue.toString()
    
    println("Long: $longValue")      // 42
    println("Double: $doubleValue")  // 42.0
    println("String: $stringValue")  // "42"
}

Java와 비교

// Java - 명시적 캐스팅 필요
Object value = "문자열";

if (value instanceof String) {
    String str = (String) value;  // 수동 캐스팅
    System.out.println(str.length());
}
// Kotlin - Smart Cast로 자동 변환
val value: Any = "문자열"

if (value is String) {
    println(value.length)  // 자동 캐스팅!
}

마무리 - 다음 편 예고

오늘 배운 것 ✅

  • val (불변) vs var (가변) 완벽 이해
  • 타입 추론으로 코드 간결하게 작성하기
  • Nullable 타입 (?)으로 NPE 방지
  • Safe Call (?.), Elvis (?:), Not-null (!!) 연산자
  • 타입 체크 (is)와 Smart Cast

다음 편에서 배울 것 📚

3편: 함수 완벽 가이드 | fun으로 시작하는 간결한 코드

  • 함수 선언과 반환 타입
  • 단일 표현식 함수 (Single Expression Function)
  • 기본 매개변수 (Default Parameters)
  • Named Arguments로 가독성 높이기
  • 확장 함수 (Extension Functions)

실습 과제 💪

// 1. 다음 변수를 val 또는 var로 선언하세요
// - 사용자 이름 (변경 가능)
// - 생년월일 (변경 불가)
// - 현재 포인트 (변경 가능)

// 2. Nullable 타입 연습
// - 이메일을 받아서 길이를 출력 (null이면 0)
// - 전화번호를 받아서 포맷팅 (null이면 "없음")

// 3. 타입 변환 연습
// - 문자열 "123"을 Int로 변환
// - Int 값을 Double로 변환

자주 묻는 질문 (FAQ)

Q: val을 써야 할까요, var를 써야 할까요?
A: 기본은 val! 꼭 변경이 필요할 때만 var를 쓰세요. 불변 변수가 버그를 줄입니다.

Q: nullable 타입은 언제 쓰나요?
A: 값이 없을 수 있는 경우 (선택 정보, API 응답 등). 하지만 최대한 피하는 게 좋습니다.

Q: !! 연산자는 언제 쓰나요?
A: 거의 안 씁니다! 정말 확실할 때만 쓰고, 가능하면 ?.나 ?:를 쓰세요.

Q: Java의 primitive 타입과 다른가요?
A: Kotlin은 모두 객체지만, 컴파일 시 최적화되어 성능 차이는 거의 없습니다.


관련 글


💬 댓글로 알려주세요!

  • nullable 타입 사용 시 어려운 점이 있나요?
  • val과 var 중 어떤 걸 주로 쓰시나요?
  • 이 글이 도움이 되셨나요?

 

반응형
Comments