인프런/자바 개발자를 위한 코틀린 입

1) 코틀린에서 변수,null을 다루는 방법

backend dev 2025. 1. 6.

 

0. 기본

- 코틀린은 세미콜론이 필요없다.

- 함수 선언시 fun 함수명 

- 클래스 선언위에서 변수,함수 선언 가능 

 

 

1. 변수 선언 키워드 

1.1 var , val

 

 

코틀린

var ( variable ) -> 변경가능 ( 가변 ) 

val( value ) -> 변경불가 ( 불변 ) ( read-only )

읽는법 : var ( 발 ) val ( 밸 )

 

자바로 예시

long number1 = 10L; // (1)
final long number2 = 10L; // (2)

 

(1)은 코틀린으로

var number1 = 10L;

로 표현가능하고

 

(2)는 코틀린으로

val number2 = 10L;

으로 표현가능하다

 

number2를 변경하려고 하면 컴파일 에러가 발생한다.

 

 

코틀린에서는 모든 변수에 수정 가능 여부(var / val)를 명시해주어야 한다.

 

 

1.2 타입 생략 가능

코틀린은 

var number1 = 10L;

다음과 같이 타입을 생략할 수 있다.

 

컴파일러가 변수에 들어가있는 값을 이용하여 타입을 추론한다. 

 

하지만 원한다면 타입을 설정 할 수 있다.

var a: Long = 10L;

 

 

자바스크립트는 동적타입언어이기 때문에 

let name = "abc";
name = 5;

name이라는 변수의 타입을 계속 바꿔도 된다.

 

하지만

코틀린은 정적타입언어이다. 

var b = "a"

b = 5; // 컴파일 에러 발생

정적 타입언어이므로 변수선언시 타입이 고정된다. 

[ Any,Dynamic 과 같은 키워드를 이용해서 동적으로 사용하는 방법도 존재한다. ]

 

 


코틀린은 변수 타입 설정이 의무가 아니다.
컴파일러에서 해당 변수의 타입을 추론해서 넣어준다.
원한다면 타입을 직접 넣어 줄 수도 있다.
[ 기본적으로는 변수 선언시 타입이 고정된다. ]

 

1.3 초기값을 지정해주지 않는 경우에는?

 

var number1

값을 지정해주지않으면 타입을 추론할 수 없으므로 컴파일 에러가 발생한다.

[This variable must either have a type annotation or be initialized]

var number1: Long

하지만 타입을 지정해주면 컴파일 에러는 사라진다.

 

var number1: Long

print(number1)

그러나 값을 지정하지않은 변수를 사용하려고 하면 컴파일 에러가 발생한다.

[Variable 'number1' must be initialized]

 

 

 

1.4 val 컬렉션에는 element를 추가할 수 있다.

 

final List<String> list = new ArrayList<>();
list.add("1");
list.add("2");

자바에서 final 선언이 된 컬렉션 객체의 값은 못바꾸지만 element는 추가가 가능한것처럼

 

코틀린에서도 가능하다. [ 코틀린 리스트는 다음에 배운다. ]

 

Tip
모든 변수는 val(불변)으로 선언하고
필요한 경우에만 var로 변경한다.

val을 기본으로 사용: 값이 변경되지 않는 것을 보장하고 코드의 안정성과 유지보수성을 높임.
var는 필요한 경우만 사용: 예를 들어, 상태가 변경되는 변수(카운터, 반복 변수 등)에만 사용.
이 방식은 더 안전하고 직관적인 코드를 작성하는 데 도움이 됩니다.

 

 

 

2.Kotlin에서의 PrimitiveType

 

 

자바에서는 Primitive Type(원시 타입)과 

Reference Type(참조 타입)이 존재한다.

 

long number1 = 10L; // (1)

Long number3 = 1_000L; // (3)

[숫자에 _(언더스코어)가 붙은것은 가독성 때문이고 코드에는 영향 X ]

 

자바에서 원시타입은 빠르고 메모리 효율적이므로 사용되었다.

하지만 코틀린에서는 참조타입만 존재한다.

val number: Long = 1L;
val number2: Long = 2L;

 

공식문서에는 다음과 같이 나와있다.

 

숫자, 문자, 불리언과 같은 몇몇 타입은 내부적으로 특별한 표현을 갖는다.
이 타입들은 실행시에 Primitive Value로 표현되지만, 코드에서는 평범한 클래스 처럼 보인다

 

 

즉 Long으로 보이지만 원시타입이 필요할거 같다면 코틀린이 알아서 Primitive 타입으로 바꿔 처리해준다는것이다. 

 

코틀린 코드를 디컴파일해서 자바코드로 보면 원시타입으로 되어있는것을 확인할 수 있다.

 

 

즉, 프로그래머가 boxing / unboxing을 고려하지 않아도 되도록 Kotlin이 알아서 처리 해준다.

 

 

 

# 코틀린 디컴파일 방법

 

Show Kotlin ByteCode를 누르고

 

Decomplie을 누르면 Byte Code를 자바코드로 바꿔준다.

 

 

3. Kotlin에서의 nullable변수

 

자바에서는 Reference Type이라면 null이 들어 갈 수 있지만

var number = 1000L
number = null

코틀린에서 위와 같이 작성하면 컴파일 에러가 발생한다.

[Null can not be a value of a non-null type Long]

 

 

var number : Long? = 1000L
number = null

Kotlin에서 null이 변수에 들어갈 수 있다면 “타입?” 를 사용해야 한다.

 

코틀린은 변수를 선언할때 null이 들어갈 수 있는지 없는지를 알려줘야한다.

 

 

 

4. Kotlin에서의 객체 인스턴스화

 

Person.java

package com.lannstark.lec01;

public class Person {

  private final String name;

  public Person(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

}

 

Person person = new Person("최태현"); 

자바에서는 클래스의 인스턴스화를 위와 같이 진행한다.

 

코틀린에서는

 

var person = Person("gil")

new를 사용하지 않고 객체를 생성할 수 있다. [ new를 사용하면 안된다. ]

 

[ java로 작성된 클래스도 코틀린 문법으로 사용할 수 있다.
=> Person클래스는 자바로 작성된 클래스인데 코틀린 문법에서도 사용가능  ]

 

 

5. 코틀린에서 null을 다루는 방법

 

5.1 코틀린에서 null 체크

 

 

public boolean startsWithA(String str) {
  return str.startsWith("A");
}

자바에서 다음과 같은 함수를 만들었다고 했을때 

str에 null이 들어올 수 있으므로 위와 같은 코드는 위험한 코드이다.

 

그래서 자바에서는 null값에 대한 처리를 한다.

 

1. 

public boolean startsWithA1(String str) {
  if (str == null) {
    throw new IllegalArgumentException("null이 들어왔습니다");
  }
  return str.startsWith("A");
}

들어온값이 null 이라면 예외 발생

null이 아니라면 Primitive 타입 반환 

 

2. 

public Boolean startsWithA2(String str) {
  if (str == null) {
    return null;
  }
  return str.startsWith("A");
}

 

들어온값이 null이라면 null 반환

null이 아니라면 Boolean 이라는 Wrapper 타입 반환 

 

 

3. 

public boolean startsWithA3(String str) {
  if (str == null) {
    return false;
  }
  return str.startsWith("A");
}

들어온값이 null 이라면 서 false 반환

null이 아니라면 Primitive 타입 반환 

 

 

1,2,3 메소드를 각각 코틀린으로 변환해보면 아래와 같이 된다.

/*
함수 매개변수에는 var,val을 안붙여도 된다.
함수의 반환타입은 매개변수 뒤에 적어준다.
?를 붙여서 null도 허용해준다.

자바 boolean처럼 사용할거기 때문에 ?를 붙여주지않는다.
[자바에서 boolean 타입은 null을 가질수없으므로]

 */
fun startWithA1(str: String?) :Boolean {
    if (str == null) {
        // 코틀린에서는 객체생성시 new 를 붙이지 않아야한다.
        throw IllegalArgumentException("null이 들어왔습니다.")
    }

    return str.startsWith("A")
}

/*
자바코드상 반환타입이 Boolean이므로 , 즉 null이 들어갈수있는 Wrapper타입이니까
코틀린 함수 반환 타입에 ?를 붙여서 null허용해준다.
 */
fun startWithA2(str: String?): Boolean? {
    if (str == null) {
        return null
    }
    return str.startsWith("A")
}

fun startWithA3(str: String?): Boolean {
    if (str == null) {
        return false;
    }
    return str.startsWith("A")
}

 

 

 

 

5.2 SafeCall과 Elvis연산자

 

String name = "ABC";
int length = name.length();

자바인 경우 위의 코드를 에러가 발생하지않는다.

 

 

하지만 코틀린의 경우 다르다.

var name: String? = "ABC"
name.length

name은 String? 타입이므로 null이 들어갈 수 있는 변수이다.

 

name에 null이 들어갈 수 있으므로 

name.length라고 작성하면 컴파일 에러가 발생한다. null일 경우 저 코드에서 에러가 발생할것이 자명하기 때문이다.

var name: String? = "ABC"
if (name != null) {
    name.length
}

null에 대한 체크를 해준다면 컴파일 에러가 발생하지않는다.

 

코틀린은 null이 가능한 타입을 완전히 다르게 취급하기 때문에 이런 차이가 발생하는것이다.

그래서 코틀린은 null이 가능한 타입만을 위한 기능을 제공한다.

 

 


5.2.1 Safe Call

null이 아니면 실행하고, null이면 실행하지않는다.  [ 안전한 호출 ]

var name: String? = "ABC"
val length = name?.length

 

위의 코드는 ?. 라는 safe call을 이용한 예시이다.

 

?.앞에있는 변수가 null인지 확인하고 null이 아니라면 ?.뒤에있는 메서드를 호출하고

null이라면 ?.뒤에 있는 메서드를 호출하지않는다. [ => 그 변수값 그대로가 반환된다. 즉 null이 반환된다.]

 

var name: String? = null
print(name?.length)

위의 코드의 결과로

null이 출력된 모습

 

 

5.2.2 Elvis 연산자

앞의 연산 결과가 null이면 뒤에 연산을 진행하거나 값을 가지는 연산자이다.

var name: String? = "ABC"
var length = name?.length ?: 0
print(length)

name이 null이 아니므로 name?.length가 동작한 결과가 출력되었다.

 

 

?: 앞에 값이 null이라면 뒤에 연산을 진행하거나 값을 가지는 연산자이다.

 

null이 아니라면 ?: 앞의 값을 반환해준다. 즉 Elvis 연산자가 없었던것처럼 동작한다. (?: 뒤에는 동작하지않는다 )

 

var name: String? = null
var length = name?.length ?: 3
print(length)

name이 null이므로 ?: 앞에 결과가 null이니까 Elvis 연산자로 인해 ?: 뒤의값이 사용되어 출력된 모습이다.

 

 

배워본 Safe Call과 Elvis 연산자를 이용하여 위에서 만든 3가지 함수를 좀 더 코틀린스럽게 바꿔보도록 하자.

 

1. 기존코드

/*
함수 매개변수에는 var,val을 안붙여도 된다.
함수의 반환타입은 매개변수 뒤에 적어준다.
?를 붙여서 null도 허용해준다.

자바 boolean처럼 사용할거기 때문에 ?를 붙여주지않는다.
[자바에서 boolean 타입은 null을 가질수없으므로]

 */
fun startWithA1(str: String?) :Boolean {
    if (str == null) {
        // 코틀린에서는 객체생성시 new 를 붙이지 않아야한다.
        throw IllegalArgumentException("null이 들어왔습니다.")
    }

    return str.startsWith("A")
}

 

리팩토링 후

fun startWithA1Refact(str: String?): Boolean {
    return str?.startsWith("A") 
        ?: throw IllegalArgumentException("null이 들어왔습니다.")
}

str?. 라는 safe call을 이용하여

str이 null인 경우

str?.startsWith("A")

의 결과는 null로 되고

 

그렇게 되면 Evlis 연산자로 인해 ?: 뒤에 코드가 실행되어

throw IllegalArgumentException("null이 들어왔습니다.")

예외가 발생하도록 수정하였다.

 

 

2. 기존코드

/*
자바코드상 반환타입이 Boolean이므로 , 즉 null이 들어갈수있는 Wrapper타입이니까
코틀린 함수 반환 타입에 ?를 붙여서 null허용해준다.
 */
fun startWithA2(str: String?): Boolean? {
    if (str == null) {
        return null
    }
    return str.startsWith("A")
}

 

리팩토링 후

fun startWithA2Refac(str: String?): Boolean? {
    return str?.startsWith("A")
}

 

str은 null일수 있으므로 ?.라는 safe call을 사용해주고,

safe call은 str이 null일때 뒤의 함수를 실행하지않고 그냥 null을 반환하므로 이런식으로 리팩토링이 된다.

 

함수의 반환타입이 Boolean?이므로 null값도 반환가능해서 이렇게 작성이 가능하다. 

 

 

3. 기존코드

fun startWithA3(str: String?): Boolean {
    if (str == null) {
        return false;
    }
    return str.startsWith("A")
}

 

리팩토링 후

fun startWithA3Refac(str: String?): Boolean {
    return str?.startsWith("A") ?: false
}

 

 

 

 

 

 

5.3 널 아님 단언!!

nullable type이지만, 아무리 생각해도 null이 될 수 없는 경우

 

아무리 null이 들어갈 가능성이 없다고 해도 nullalbe type으로 선언된 이상

매번 safe call을 이용해줘야하고

null에 대한 처리를 Elvis 연산을 이용해서 해줘야한다.

 

그걸 안하기위해 이 값은 무조건 null이 아니라는것을 표현하는 방법이 있다.

!!를 이용하면 된다.

fun startsWith(str: String?): Boolean {
    return str!!.startsWith("A")
}

null이 아닐거라고 단언하는 변수앞에 붙여준다.

 

이렇게 해놨는데 str이 null이라면 null point Exception이 발생한다.

 

 

5.4 플랫폼 타입

 

코틀린은 자바와 100% 호환가능한 언어다보니까 한 프로젝트에서 코틀린과 자바를 병행해서 사용하는 경우도 있다.

 

코틀린에서 자바코드를 가져와서 사용할때 어떤 타입이 null이 될 수 있는지 , 될 수 없는지 어떻게 처리할까

[ Kotlin에서 Java 코드를 가져다 사용할 때 어떻게 처리될까? ]

 

 

Person.java

public class Person {

  private final String name;

  public Person(String name) {
    this.name = name;
  }

  @Nullable
  public String getName() {
    return name;
  }

}

 

fun main() {

    val person = Person("길")
    startsWithA(person.name)

}
fun startsWithA(str: String): Boolean {
    return str.startsWith("A")
}

 

다음과 같이 코드가 있다고 했을때

startsWithA(person)

 

이 코드에서 컴파일 에러가 발생한다.

Type mismatch

Required: String
Found: String?

 

startsWithA라는 함수는 null이 아닌 전달인자만 받게끔 정의했는데

Person 클래스의 getName()메소드에 @Nullable이 붙으면서 반환값이 null일 수도 있다는것을 명시했다.

 

그러므로 startsWithA메서드는 person.name이 null일 수 도있으므로 컴파일 에러가 발생하는것이다.

@NotNull
public String getName() {
  return name;
}

이렇게 변경하면 컴파일 에러가 사라진다. 

 

 

자바코드에서 null과 관련된 어노테이션을 사용하면 코틀린에서 해당 어노테이션을 읽어서 처리해준다.

 

 

그런데 만약 @Nuallble이든 @Notnull이든 null과 관련 어노테이션을 붙여놓지않았다면?

 

public String getName() {
  return name;
}

이렇게 해두면 컴파일에러는 발생하지않는다.

 

하지만 name에는 해당 메서드의 반환은 null일 수도있고 아닐 수도있으므로 

Kotlin에서는 이 값이 nullable인지 non-nullable인지 알 수가 없다

 

이런 코틀린이 null 관련 정보를 알 수 없는 타입을 플랫폼 타입이라고 부른다.

플랫폼 타입은 Rumtime시 Exception이 날 수 있다

[ null을 넣을 수 있으므로 ]

 

즉. 위의 예시에서는 name이라는 변수는 null일지 아닐지 모르니까 name의 타입이 플랫폼 타입인것이다.

 

 

 

그래서 코틀린에서 자바코드를 사용해야할때는 null관련 어노테이션을 꼼꼼하게 붙여주는것이 좋고 

자바로 된 라이브러리를 사용해야할때는 해당 코드를 열어서 null이 들어갈 수 있는지 없는지 확인하는게 좋고 

최초에 코틀린에서 자바 라이브러리를 사용한 지점을 래핑해서 단일지점으로 만들어두면 추후에 이슈가 났을때 대응하기 쉽게 할 수 있다. 

 

 

여기서 코틀린으로 자바 코드를 래핑해서 사용한다는것은 다음과 같다.

fun main() {

    val person = Person("길")
    startsWithA(safeGetName(person))

}
fun startsWithA(str: String): Boolean {
    return str.startsWith("A")
}

fun safeGetName(person: Person): String {
    return person.name ?: "noname"
}

 

자바클래스의 함수를 한번 래핑해서 null일 경우에 대한 처리를 미리 해준다.

 

 

댓글