정적 언어란, 컴파일 시점에 데이터 타입이 지정되는 언어를 말한다. 반대로 동적 언어란 런타임 시점에 데이터 타입이 결정되며, 타입과 관계 없이 모든 값을 변수에 넣을 수 있다.
정적 언어의 장점은 다음과 같다.
- 성능
- 런타임에 어떤 메서드를 호출하는 것인지 알아 내는 과정이 필요 없다. 컴파일 시간은 더 소요되나, 메서드 호출 동작 등 런타임 동작은 더 빠르다.
- 신뢰성
- 컴파일러가 프로그램의 정확성을 검증한다. 따라서, 런타임 과정에서 프로그램이 오류로 인해 크래시 되는 현상을 더 줄일 수 있다.
- 유지보수성
- 코드에서 다루는 객체가 어떤 타입에 속하는지 알 수 있으므로, 처음 보는 코드를 다룰 때도 더 쉽다.
- 도구 지원
- 툴은 더 정확한 코드 완성 기능을 제공하며, 더 안전하게 리팩토링할 수 있다.
val x = 1
fun add(a: Int, b: Int) = a + b
class Chapter1 {
private lateinit var variable: String // 초기화 과정이 생략될 시 런타임 에러 발생
fun main() {
val a // 컴파일 에러
val b = 3
}
}
1-2. 함수형 프로그래밍과 객체지향 프로그래밍
함수형 프로그래밍의 핵심 개념을 살펴 본다.
일급 시민(객체) 함수 (First-Class / First Citizen)
- 함수를 변수처럼 다룰 수 있다. 함수를 변수에 저장할 수 있으며, 함수를 또 다른 함수의 인자로 전달할 수 있다. 함수에서 함수를 반환하는 것도 가능하다.
불변성
- 함수형 프로그래밍에서는 일단 만들어 지고 나면 내부 상태가 절대 바뀌지 않는 불변 객체를 사용해 코드를 작성한다.
부수 효과 (side effect) 없음
- 함수형 프로그래밍에서는 입력이 같으면 항상 같은 출력을 내놓는다. 다른 객체의 상태를 변경하지 않으며, 함수 외부 등 외부 환경과 상호작용하지 않는 순수 함수(pure function)를 사용한다.
순수 함수 (pure function)
순수 함수란, 어떤 함수에 같은 인자를 주었을 때 같은 출력 결과를 내뱉는 함수를 말한다. 즉, 외부 요소로 인해 같은 인자를 전달하였다 하더라도 결과가 달라진다면 그것은 순수 함수가 아니다.
그렇다면 함수형 프로그래밍을 통해 얻을 수 있는 장점은 무엇일까?
- 간결성
- 명령형 코드를 사용한다면 함수형 코드는 더욱 간결하다.
- 다중 스레드를 사용해도 안전함
- 멀티 스레딩 프로그램에서는 하나의 데이터를 여러 스레드에서 접근하고 변경할 때 가장 큰 문제가 발생한다. 따라서 불변 데이터 구조를 유지하고 순수 함수를 데이터 구조에 적용했다면 멀티 스레드 환경에서 하나의 데이터를 여러 스레드가 변경할 수 없게 된다.
- 테스트 용이성
- Side Effect를 가진 함수라면 테스트 케이스를 여러 가지로 준비해야 한다. 하지만, 순수 함수의 경우에는 그러한 준비 없이 하나의 테스트 케이스로 충분히 테스트 할 수 있다.
함수형 프로그래밍의 특징을 살펴 본다.
- 데이터 타입으로 함수 타입을 지원한다. 이로써 어떤 함수가 다른 함수를 파라미터로 받거나 함수가 새로운 함수를 반환할 수 있다.
- 람다 식을 지원함으로써 코드 블록을 쉽게 정의하고 전달할 수 있다.
- 대부분의 Kotlin 표준 라이브러리는 인자로 받은 람다 함수를 인라인한다. 따라서, 객체 증가로 인한 가비지 컬렉션으로 프로그램이 멈출 걱정을 하지 않아도 된다.
- Kotlin data class는 불변 객체를 간편하게 만드는 구문을 제공한다.
- Kotlin 표준 라이브러리는 객체와 컬렉션을 함수형 스타일로 나눌 수 있는 API를 제공한다.
실용성
간결성
코틀린은 boiler plate 코드를 자바에 비해 많이 줄여준다.
아래 코드에서 getter, setter 등의 불필요한 로직을 없애 주는 모습을 볼 수 있다.
class Person(
private val name: String,
private val age: Int
) {
fun getLastName(): String = invokeLastName()
private fun invokeLastName() = name.substring(0, 1)
fun getNextAge(): Int = invokeNextAge()
private fun invokeNextAge() = age + 1
}
fun main() {
val suYong = Person("홍길동", 26)
with(suYong) {
println("성 ${getLastName()}")
println("내년 나이 ${getNextAge()}")
}
}
안전성
코틀린은 JVM에서 실행된다. 이로써 Buffer Overflow를 예방하고 메모리 안전성을 보장한다.
코틀린은 Null Pointer Exception
을 어느 정도 예방한다. 코틀린은 기본적으로 Nullable 하지 않은 데이터 타입을 기초로 두며, ?
기호 추가를 통해 Nullable 한 데이터 타입을 추가할 수 있다.
val name: String? = null // nullable
val age: Int = 3 // non nullable
코틀린은 Class Cast Exception
또한 어느 정도 예방한다. 특정 객체를 다른 타입으로 캐스팅 하기 전에 타입을 미리 검사하지 않는다면 ClassCastException
이 발생하게 된다.
코틀린은 컴파일 과정에서 자주 발생할 수 있는 예외를 잡아줌으로써 개발 생산성(개발 비용)을 크게 떨어뜨리지 않고 안전성을 높였다고 볼 수 있다.
상호운용성
코틀린은 타입 추론을 지원하기 때문에, 타입을 생략할 수 있다.
fun max(a: Int, b: Int) = if (a > b) a else b
val question = "질문"
// val question: String = "질문"
val answer = 0
// val answer: Int = 0
val
은 Immutable, var
은 mutable 이다. 가급적 Immutable로 사용하는 것이 좋다.
다만, val
참조가 불변이라 하더라도 그 참조가 가리키는 객체 내부값은 변경될 수 있다.
val languages = arrayListOf("Kotlin")
languages.add("Java")
/*
val languages = listOf("Kotlin")
languages.add("Java") // Compile Error
*/
Kotlin은 문자열 템플릿 기능을 지원한다.
$
기호를 통해 문자열 변수를 사용할 수 있다.fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
print("Hello, $name")
}
$
문자를 문자열 내부에서 사용하고 싶다면 \
를 사용하여 이스케이프 시켜야 한다.간단하게 Person 클래스(value object
: 코드 없이 데이터만 저장하는 클래스)가 있다면, 아래와 같을 것이다.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
class Person(
val name: String, // 읽기 전용 프로퍼티
var age: Int // 읽기, 쓰기가 가능한 프로퍼티
)
boiler plate code
를 줄여줄 수 있다.Person person = new Person("Bob", 32);
System.out.println(person.getName());
System.out.println(person.getAge());
val person = Person("Bob", 32)
println(person.name)
println(person.age)
Kotlin은 커스텀 접근자를 제공한다.
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
fun main() {
val rectangle = Rectangle(40, 40)
print(rectangle.isSquare)
}
when
구문 같은 경우, Java switch
문과 대응된다. 하지만, Enum 클래스와 함께 사용할 때 Java보다 더 좋은 효율을 얻을 수 있다. 아래와 같다.
enum class Color {
RED, ORAGNGE, BLUE, GREEN
}
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Rechard"
Color.ORANGE -> "Of"
Color.BLUE, Color.GREEN -> "York"
else -> throw Exception("Unknown Color")
}
print(getMnemonic(Color.BLUE))
인자가 없는 when
구문을 사용해 불필요한 객체 생성을 막을 수도 있다.
when
구문에 아무 인자도 없으려면 각 분기의 조건이 반드시 Boolean 결과를 계산해야 한다. fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == ORANGE) -> ORANGE
(c1 == BLUE && c2 == GREEN) -> GREEN
}
Kotlin은 스마트 캐스트를 지원한다.
is
를 사용하여 변수 타입을 검사한다. Java의 instanceOf
와 유사하다. instanceOf
를 통해 값을 확인한 다음, 해당 타입을 사용하기 위해 명시적으로 타입을 캐스팅 해야 한다.if (e is Sum) {
return e.right + e.left
}
코틀린의 예외 처리는 기본적으로 자바와 유사하다.
하지만 코틀린이 클래스 인스턴스 변수를 생성하여 사용하듯, 예외를 던질 때에도 new
키워드를 사용할 필요가 없다.
아래와 같이 try catch
구문을 바로 반환할 수 있으며, try catch
구문을 변수에 대입하는 것도 가능하다.
fun getNumber(value: String): Int {
return try {
value.toInt()
} catch (e: Exception) {
0
}
}
인자가 없는 when 구문을 사용해 불필요한 객체 생성을 막을 수도 있다에서 불필요한 객체의 예를 들어주실수 있을까요? :)
코틀린은 처음 접해봤는데 굉장히 간결하고 편해보이네요!