-
최근 코틀린의 Kotest 로 작성된 테스트 코드를 보고 코드가 간결하고 깔끔하다고 생각되어 한번 공부해보고 싶단 생각이 들었습니다.
https://developers.hyundaimotorgroup.com/blog/137Kotest를 통한 Kotlin Spring에서의 테스트 코드 작성
Kotest를 통한 Kotlin Spring에서의 테스트 코드 작성
developers.hyundaimotorgroup.com
그래서 코틀린 공식문서를 참고하여 자바와 코틀린 문법의 차이를 위주로 정리했습니다.컴파일
Java와 Kotiln 은 각각의 컴파일러가 .java, .kt 파일을 JVM 이 실행할 수 있는 .class 파일로 만든다.
Java, Kotiln 같이 사용한 프로젝트를 컴파일 한다면 다음과 같은 과정이 일어난다.- Kotiln 컴파일러가 .kt 파일을 컴파일한다. 이 과정에서 Java 파일을 참조하고 있는 코드도 Kotiln 컴파일러가 로딩한다.
- Java컴파일러가 .java 파일을 컴파일한다. 이 과정에서 Java코드에서 Kotiln 코드를 참조하고 있더라도 Kotiln 파일은 이미 .class 파일로 컴파일 되었기에 참조 해결이 가능하다.
예외
Java 에서 checked exception 은 어딘가에서는 잡아서 처리해야한다.
이를 처리하지 않으면 컴파일 시점에 에러가 발생한다.
하지만 Kotiln 에서는 모든 예외는 unchecked exception 으로 처리되며, 에러를 잡지 않아도 컴파일 오류가 발생하지 않는다.
접근제한자
Java 에서 접근제한자를 생략하게 되면 자동으로 default 접근제한자가 적용된다.
default 접근제한자는 같은 패키지 내에서의 호출이 가능하고, 다른 패키지에서는 호출이 불가하다.
Kotiln 은 접근제한자를 생략하게 되면 자동으로 public 접근제한자가 적용되어 어디서나 호출을 할 수 있다.Variables
Java 에서 변수를 선언할 때 아래와 같이 사용했다.
int num = 10;
Kotlin 에서는 변수를 선언할 때 아래와 같이 선언할 수 있다.
val immutableNumber = 10; // val 로 선언된 변수는 불변이며, 변경이 일어나면 에러가 발생합니다. var num = 10; // var 로 선언된 변수는 불변이 아닙니다.
Kotlin 에서는 Type Inference 타입 추론을 하여 변수에 들어올 값의 타입을 추론한다.
따라서 위의 예시 같은 경우에서는 컴파일러가 값을 보고 자동으로 int 형을 할당한다.
하지만 구체적인 타입을 지정해야 하는 경우라면 명시적으로 선언해야 한다.var num : Int = 10;
Collections
Java 에서는 아래와 같이 컬렉션을 선언한다.
List<String> fruits = List.of("apple", "banana", "grape");
Kotiln 에서는 아래와 같이 컬렉션을 선언한다.val fruits = List.of("apple", "banana", "grape"); // val 로 선언되었기에 다른 컬렉션으로 재할당 X
Kotiln 의 List 는 불변타입이다. 한번 선언된 컬렉션에 대해서 변경할 수 없다.
변경이 일어나야 하는 컬렉션이라면 아래와 같이 MutableList 로 선언해야 한다.val fruits : MutableList<String> = mutableListOf("apple", "banana", "grape") fruits.removeAt(0) // 에러가 발생하지 않는다.
컬렉션과 관련된 메소드들은 최상위 함수로 _Collections.kt 파일에서 찾아볼 수 있다. Kotlin 에서는 클래스 파일 안이 아니더라도 함수를 정의할 수 있다. 이렇게 패키지 수준에서 정의된 함수들을 최상위 함수라고 한다.
예기치 못한 변경을 제어하기 위해서는 아래와 같이 읽기 전용 뷰를 제공할 수 있다.
val fruits : MutableList<String> = mutableListOf("apple", "banana", "grape") val fruitsReadOnly : List<String> = fruits
- fruits 는 MutableList 로 선언된 변경 가능한 컬렉션이다.
- fruitsReadOnly 는 Call By Reference 로 참조되어, fruits 의 주소를 가리키고 있다.
- 하지만 fruitsReadOnly 는 변경 불가한 컬렉션 타입인 List로 선언되었기에 예기치 못한 변경이 일어날 수 없게 된다.
정리하자면 fruits, fruitsReadOnly 모두 같은 주소를 가르키고 있고, 그 주소에는 컬렉션이 있다.
하지만 fruits 를 통해서만 변경이 일어날 수 있고, fruitsReadOnly 에서는 변경이 일어나지 못한다.Map
Map 도 특별히 다를 건 없고 위의 내용들이 적용된다.
아래는 Kotlin 에서 Map 을 선언하고 사용하는 간단한 예시코드이다.val menu : MutableMap<String, Int> = mutableMapOf("apple" to 100, "banana" to 200) // to 를 사용해 편하게 설정할 수 있다. println(menu["apple"]) // 결과 : apple=100 println(menu["grape"]) // 결과 : null menu["grape"] = 300
Map 에서 없는 키에 접근한다면 null 이 나오게 되며, Map 에 값을 할당하는 것은 아래처럼 가능하다.
Builder
Builder 패턴은 클래스의 필드가 많아 생성자의 파라미터가 많을 경우 쓰이는 패턴이다.
아래는 Lombok 의 @Builder 어노테이션을 사용한 코드이다.@Builder public Apple(int price, String name, LocalDateTime createAt) { this.price = price; this.name = name; this.createAt = createAt; } Apple.Builder() .price(1000) .name("apple) .createAt(LocalDateTime.now()) .build();
생성자의 파라미터 갯수가 많아지면 Builder 패턴은 사용하는 것이 편리하다.
결론부터 말하면 Kotlin 은 Builder 패턴이 필요없다.
아래는 Kotlin 에서 Apple 을 생성하는 동일한 코드이다.class Apple(var price: Int, var name: String = "apple", var craetedAt: LocalDateTime) Apple( price = 100, name = "apple", createdAt = LocalDateTime.now() ) Apple( price = 100, createdAt = LocalDateTime.now() )
파라미터가 많더라도 파라미터의 순서 걱정 없이 원하는 필드에 값을 할당할 수 있다.
Default 값을 선언하여 오버로딩으로 여러 개의 생성자를 만들 필요도 없다.
이는 뒤에 나올 Named arguments 과 Default parameter values 을 사용하기에 가능한 것이다.When
Kotlin 에는 when 문법이 있다.
사용하며 느낀 점은 Java 의 switch 문과 유사하다고 느꼈다.val fruit = "apple" when (fruit) { "apple" -> println("apple") "banaba" -> println("banana") "grape" -> println("grape") }
when 의 결과를 변수에 바로 저장하는 것도 가능하다.
val fruit = "apple" val result = when { fruit == "apple" -> "apple" fruit == "banana" -> "banana" fruit == "grape" -> "grape" else -> "not found" }
삼항연산자
Java 를 사용하며 아래처럼 삼항연산자를 사용한 경험이 많다.
int num = 100; int max = num <= 100 ? num : 0;
Kotlin 에서는 삼항연산자를 지원하지 않는다.
위와 같은 코드는 아래와 같이 if-else 를 사용해 처리할 수 있다.val num = 100; val max = if(num <= 100) num else 0
개인적으로 Java 의 코드에 익숙하지만 가독성은 Kotlin 이 좋아보인다.
Loop
Kotlin 의 for 문은 다음과 같이 사용할 수 있다.
for(i in 0..5) { print(i) // 012345 } for(i in 5 downTo 0) { print(i) // 543210 } for(i in 0..5 step 2) { print(i) // 024 }
Function
아래는 Java 에서 파라미터로 들어온 값을 출력하는 간단한 코드이다.
public void printParam(String text) { System.out.println(text); }
아래 코드는 Kotlin 코드로 바꾼 코드이다.
fun printParam(text: String) { println(text) }
Kotlin 에서는 함수를 선언할 때 fun 키워드를 사용하여 함수를 선언한다.
파라미터의 변수는 모두 val 타입으로 불변성을 보장한다.
또한 위에서 언급된 Type inference 타입 추론은 함수를 정의할 때는 적용되지 않는다.
따라서 Java 와 같이 Type 을 필수로 명시해야 한다.
아래는 Kotlin 에서 함수를 정의하는 예시 코드이다.fun sum(a: Int, b: Int): Int { return a + b } fun sum(a: Int, b: Int): Int = a + b
Named arguments
일반적으로 Java 에서 다음과 같은 코드는 의도와는 다르게 동작한다.
public static void printParam(String message, String prefix) { System.out.println(prefix + message); } public static void main(String[] args) { String message = "world"; String prefix = "Hello"; printParam(prefix, message); }
parameter 의 이름과는 관계 없이 파라미터의 타입이 같다면 순서에 따라 결정된다.
코드를 사용하는 쪽인 main 함수에서는 Hello world 를 기대하고 함수를 호출했지만 실제 실행결과는 world Hello 가 출력될 것이다.
Kotlin 은 Named arguments 를 사용하여 다음과 같이 함수를 호출할 수 있다.fun printNamedParams(message: String, prefix: String) { println("$prefix $message") } fun main() { printNamedParams(prefix = "Hello", message = "world") }
Return from a function
Kotlin 에서는 편리하게 함수에서 Lambda 표현식을 반환할 수 있다.
다음은 파라미터인 text 에 따라 Int 형을 입력 받고 Int 형을 반환하는 Lambda 표현식을 반환하는 함수이다.fuc calculateTime(text: String): (Int) -> Int = when { "hour" -> { value -> value * 60 * 60 } "minute" -> { value -> value * 60 } "second" -> { value -> value } else -> { value -> value } } fun main() { val timesInMinutes = listOf(2, 10, 15, 1) val min2sec = toSeconds("minute") val totalTimeInSeconds = timesInMinutes.map(min2sec).sum() println("Total time is $totalTimeInSeconds secs") // Total time is 1680 secs }
- main 함수에서 minute 를 파라미터로 toSeconds 를 실행
- 반환 값은 value -> value * 60 인 Lambda 표현식이 반환
- Collections 의 map 으로 컬렉션 내에 있는 요소들에 value -> value * 60 을 적용하여 컬렉션 내에 있는 모든 요소를 Lambda 표현식에 맞게 연산
Invoke separately
Lambda 는 독립적으로 실행될 수 있다.
val num = {a: Int -> a + 100}
위와 같은 람다 표현식은 아래처럼 바로 실행이 가능하다.
val num = {a: Int -> a + 100}(100) // num = 200
Data Class
Kotlin 에는 data 타입의 클래스가 있다.
data 타입으로 클래스를 선언하게 되면 다음과 같은 것을 따로 설정하지 않아도 구현되어 있다.- toString()
- equals()
- copy()
이러한 기능들이 필요한 클래스라면 data 타입으로 클래스를 선언하면 편리할 거 같다.
data class Apple(var id: Int, val price: Int)
Nullable Type
Kotlin 은 null 에 엄격하다.
아래와 같은 Java 코드는 Java 에서는 괜찮지만 Kotlin 에서는 에러를 발생한다.String text = null
다음은 Kotlin 으로 옮긴 코드이다.
val notNullText: String = null // 컴파일 에러가 발생한다. null 은 들어갈 수 없음. val nullableText: String? = null // ? 가 붙었기에 컴파일 에러가 발생하지 않는다.
safe calls, Elvis operator
안전 호출이란 접근하는 값이 null 일 경우를 고려하여 함수를 호출하는 것이다.
다음 코드를 보며 이해를 해보자class Apple(val price: Int, val owner: String?, val createdAt: LocalDateTime) val apple = Apple( price = 1000, owner = null, createdAt = LocalDateTime.now() ) println(apple.owner.length) // apple.owner 는 null 이기에 NPE 발생 println(apple.owner?.length) // ?. 로 안전호출 했기 때문에 null 이라면 null 을 반환 println(apple.owner?.length?: 0) // apple.owner 가 null 일 경우 0을 반환. Elvis 연산자
Companion object
Java 에서는 static 키워드로 정적 클래스, 변수, 메서드를 관리한다.
하지만 Kotlin 에는 static 키워드가 없다.
Java 의 static 처럼 사용하는 것이 Companion object (동반 객체)이다.class MyClass { // 동반 객체 정의 companion object { const val CONSTANT_VALUE = 42 fun printHello() { println("Hello from companion object") } } } // 동반 객체의 메서드와 변수에 접근 fun main() { println(MyClass.CONSTANT_VALUE) // 42 MyClass.printHello() // "Hello from companion object" }
참고 자료
Welcome to our tour of Kotlin! | Kotlin
Welcome to our tour of Kotlin! | Kotlin
kotlinlang.org
'Java' 카테고리의 다른 글
JVM 클래스로더 알아보기 (0) 2025.01.11 JVM 클래스 로딩 과정 알아보기 (0) 2025.01.09 JVM 바이트코드 알아보기 (1) 2025.01.04 커스텀 어노테이션 (0) 2024.02.13 OpenAPI 데이터베이스 저장 (0) 2023.05.02