쾌락코딩

코틀린에서 Unix 시간 사용 시 주의점

|

Unix 시간 타입은 숫자로만 이루어져 있다. 1970-01-01 00:00:00 시간을 0으로 시작하여 1초마다 1씩 증가하는 값이다.

현재 시간 2019-03-22 19:54:23 을 기준으로 Unix 시간은 1553252063이 된다. 1970-01-01 00:00:00 이후로 1553252063초가 지난 것이다.

http://chongmoa.com/unixtime 이 링크에서 날짜를 unix 타입으로 쉽게 변환 할 수 있다.

코틀린에서 사용하기

그런데 위 사이트에서 얻어온 unix 값을 가지고 kotlin 또는 Java에서 날짜로 변경하면 원하는 값이 나오지 않는다. 아래의 코드를 보자.

fun main(args: Array<String>) {
    println(Date(1553252063)) // Mon Jan 19 08:27:32 KST 1970
}

1553252063은 웹 사이트에서 변환했을 때 날짜로 2019-03-22 19:54:23 이지만 코틀린에서는 한국 시간 1970년대라고 한다. 연도 뿐만 아니라 모든 값이 엉터리로 나온다. 이와 같이 출력되는 이유는, 일반적인 UNIX 시간은 초단위로 1씩 증가하는 반면 코틀린과 자바같은 경우는 밀리세컨드 단위로 1씩 증가하는 규칙을 가지고 있기 때문이다(컴퓨터는 0.001초도 의미 있는 값이기 때문). 코틀린과 자바에서는 밀리세컨드(0.001초)마다 unix 시간이 1씩 증가하므로 당연히 일반적인 유닉스 타임보다 표면적으로 보이는 유닉스 타임 값이 클 수 밖에 없다. 일반적인 유닉스 값은 1초에 1씩 증가하는 반면, 프로그램에서는 1초에 1000씩 증가한다(1000 밀리세컨드가 1초다).

따라서 외부로부터 일밬적인 유닉스 값을 받아와서 프로그램에 적용하고 싶다면, 일반적인 유닉스 타임에 자바, 코틀린 형식에 맞도록 1000L을 곱해주면 된다. 주의 할 점은 Int로 1000을 곱하는게 아니라 Long타입으로 곱해야 한다. Int로 1000을 곱해주면 overflow가 일어나 또 한번 원하지 않는 값이 출력될 수 있다.

fun main(args: Array<String>) {
    println(Date(1553252063 * 1000L)) // Fri Mar 22 19:54:23 KST 2019
    println(Date(1553252063 * 1000)) // Sun Dec 14 17:05:01 KST 1969
}

Long 타입으로 곱하면 정상적인 값이 나오지만 Int 1000을 곱해주면 이상한 값이 출력되는 것을 볼 수 있다.

코틀린 invoke 함수(람다의 비밀)

|

invoke 란?

코틀린에는 invoke라는 특별한 함수, 정확히는 연산자가 존재한다. invoke연산자는 이름 없이 호출될 수 있다. 이름 없이 호출된 다는 의미를 파악하기 위해 아래의 코드를 보자.

object MyFunction {
    operator fun invoke(str: String): String {
        return str.toUpperCase() // 모두 대문자로 바꿔줌
    }
}

MyFunction이라는 오브젝트 하나가 있다. obeject키워드로 만들었기 때문에 MyFunction은 하나의 객체처럼 사용될 수 있다. 즉, 하나의 객체이기 때문에 객체 안의 메서드를 호출하기 위해서 아래와 같이 호출하고 싶을 것이다.

MyFunction.invoke("hello") // HELLO

물론 잘 동작하지만, kotlin에서 invoke라는 이름으로 만들어진 함수는 특별한 힘을 갖는다. 이름 없이 실행될 수 있는 힘이다. 즉, 아래와 같이 호출이 가능하다.

MyFunction("hello") // HELLO

MyFunction은 객체다. 그렇기 때문에 MyFunctionprint해보면 MyFunction의 주소값만 출력될 뿐이다. 그런데 MyFunction안에 invoke()함수가 정의 되어있으므로 MyFunction에서 메서드 이름 없이 바로 호출한 것이다. 물론 파라미터를 받을 창구가 있어야 하므로 ()안에 파라미터를 넣어서 실행이 가능하다.

연산자

이렇듯 분명히 invoke와 같이 이름을 부여한 함수임에도 불구하고 실행을 간편하게 할 수 있게 하는 것들을 연산자라고 부른다. 그런 연산자들 몇 개를 코틀린에서 미리 정해놓았다. 대표적으로 + 연산자가 있다. 간단히 예제 하나를 보면서 연산자가 무엇인지 알고 다음으로 넘어가자.

object Sample {
    operator fun plus(str: String):String {
        return this.toString() + str
    }
}

main() {
    Sample + " Hello~!" // [Sample의 주소값] Hello~!
}

실행해보면 [Smaple의 주소값]부분에는 실제로 주소값이 들어가고, 그 뒤에 바로 Hello~!라는 글자가 붙을 것이다. 즉 plus라는 이름으로 함수를 만들었지만 plus는 코틀린에서 연산자로 정의해 놓았으므로 plus연산자를 호출하기 위해 틀별히 + 기호를 사용해서 호출한다.

람다는 invoke 함수를 가진 객체다.

코틀린은 람다를 지원한다. 예를 들어 아래와 같은 람다 함수가 있다고 하자. 이미 코틀린이 기본으로 제공하는 함수를 한 번 감싸는 의미 없는 코드이지만 이해를 위해 작성해보았다.

val toUpperCase = { str: String -> str.toUpperCase() }

모든 값, 또는 값을 담는 변수에는 타입이 있다. Int일 수도, String일 수도, 또는 우리가 만든 클래스 타입일 수도 있다. 그렇다면 위의 toUpperCase는 타입이 무엇일까? Int도, String도 아니다. String을 받고, 다시 String을 반환하는 (String) -> String 타입이다.

그러나 (String) - String은 좀 생소하다. 더 정확히 어떤 걸 의미하는 것일까? 사실 위 타입은 코틀린 표준 라이브러리에 정의된 Function1<P1, R>인터페이스 타입이다. Function<P1, R>의 구현을 살펴보면 invoke(P1) : R 연산자 하나만 달랑 존재한다. 위에서 언급했듯이 invoke연산자는 이름 없이 호출할 수 있다. 이름 없는 함수인 람다와 연관성이 조금 보인다.

결국 위에서 작성한 toUpperCase는 아래 코드와 같다.

val toUpperCase = object : Function1<String, String> {
    override fun invoke(p1: String): String {
        return p1.toUpperCase()
    }
}

실제로 코틀린에서 작성한 람다는 위의 코드와 같기 때문에 결국 람다도 컴파일 시간에 람다가 할당된 객체로 변환된다는 뜻이다. 개발자는 그것의 invoke를 호출하는 셈이다.

toUpperCase는 현재 invoke라는 연산자 하나를 가진 객체이다. 이제 위에서 언급 알아본대로 toUpperCase.invoke("hello")가 아닌 toUpperCase("hello")와 같이 호출할 수 있음을 기억한 채로 toUpperCase를 사용해 보자.

fun main() {
    val strList = listOf("a","b","c")
    println(strList.map(toUpperCase)) // [A,B,C]
}

map함수는 strList의 요소들을 순회하면서 각각의 요소(a,b,c)마다 toUpperCase(요소)를 실행할 것이다. toUpperCaseinvoke연산자를 가지고 있기 때문에 이렇게 편하게 사용 가능한 것이다. 또한 위의 코드는 아래와 같이 작성할 수 있다.

fun main() {
    val strList = listOf("a","b","c")
    println(strList.map {str: String -> str.toUpperCase()}) // [A,B,C]
}

{str: String -> str.toUpperCase()} 이 부분이 결국 런타임 시 invoke를 하나 가지는 오브젝트로 변환된다.

코틀린 코루틴 사용법 맛보기

|

코루틴이 완전히 처음이라면 코틀린 코루틴 개념익히기 를 읽고 돌아오자!

클라이언트 앱을 만들기 위해서 비동기 처리는 필수적이다. 따라서 클라이언트를 개발할 수 있는 언어, 라이브러리, 프레임워크에는 비동기 처리를 쉽게 도와주는 도구들이 존재한다. 물론 RxKotlin, coroutine 같은 도구들을 사용하지 않고도 처리할 순 있겠지만 상당히 번거로운 작업이 될 것이며, 앞으로 수 많은 비동기 처리를 해야한다면 이런 도구를 적절히 사용할 줄 아는게 유리할 것이다.

Coroutine

코틀린의 코루틴을 사용하기위해 검색해보면 runBlocking, launch, async/awatit, job 등등의 키워들이 있고 예제들이 나온다.

launch & async

launchasync는 코루틴 빌더로써 쓰레드 처럼 코루틴을 시작하는 것이라고 한다. 코루틴 빌더라고 뭔가 있어보이지만 그냥 코루틴을 생성해주는 정도?로 생각하면 될 것 같다. launch와 async는 사실상 같은 일을 한다. 유일한 차이점은 launch가 Job을 반환하는 반면 async는 Defferd를 반환한다는 점 뿐이다. 따라서 항상 launch대신 async를 사용해도 문제는 없다고 한다.

launch라는 단어는 보통 미사일을 발사 할 때도 쓴다. 즉, 미사일을 위로 발사하고나면, 돌아오기 전까지 그 사실을 잊어버린다는 것. 즉 비동기가 실행된다는 것이다. 미사일이 다시 원래의 자리로 돌아오든, 아니면 목표물에 도착했든 완료 신호가 오면 그에 맞는 처리를 할 수도 있다.

launch를 예로 들어보자.

fun main() {
    runBlocking { // start main coroutine
        launch { // launch new coroutine in background and continue
            delay(1000L)
            println("world")  // #2
        }
        println("Hello")      // #1
    }
    println("end")            // #3
}

위의 코드에서 runBlocking은 간한히 { } 블록 안의 비동기 처리들이 끝날 때 까지 코드를 블로킹 시킨다라고만 알고 넘어가자. 아래에서 더 다룬다.

위의 코드를 실행 시키면 출력 순서는 #1, #2, #3이다. main() 함수가 실행되면 곧바로 runBlocking을 만나게 되는데, runBlocking을 만났기 때문에 runBlocking 블록 안의 코드들이 모두 끝나야 그 다음 줄인 println("end") 코드가 실행된다. 따라서 #3 이 가장 마지막에 출력되는 것이다.

runBlocking 블록 안에서 가장 먼저 만나는게 비동기의 시작을 알리는 launch함수다. 위에서도 말했듯이 launch를 만나면 launch 블록의 코드들이 미사일에 담겨 비동기로 수행된다고 생각하자. 간단히 가벼운 백그라운드 쓰레드(코루틴)로 수행된다고 생각해도 좋을 것 같다.

launch블록 안에서 가장 먼저 만나는 delay라는 함수가 있는데 이것은 코루틴 스코프 안에서만 실행할 수 있는 시간 지연 함수다. 여기서는 delay를 사용하여 시간이 걸리는 것(미사일이 제 역할을 하고 다시 돌아오는 시간)을 나타냈지만, 실제로는 외부 api를 요청하거나 데이터베이스 IO작업처럼 시간이 걸리는 작업이라고 생각해도 된다. 아무튼 delay바로 밑 줄인 #2는 delay(1000L)이 끝나야 실행된다. launch 블록에 같이 묶여있기 때문이다.

아까 말햇듯이 launch는 코드 블럭을 마치 백그라운드 쓰레드에서 실행시키는 것과 비슷하게 비동기 식으로 동작시킨다. 따라서 launch블락 다음 줄 #1 이 바로 호출된다. 미사일을 발사시키는 사람이 미사일을 발사시켜 놓고 나서 미사일이 돌아올 동안 정지하는게 아니라 다른 할일을 하는 것 처럼. 따라서 luanch블럭을 먼저 만났음에도 불구하고 1초의 소요시간이 걸리는 비동기 코드(delay) 때문에 #1이 먼저 출력되고, 아까 발사시켰던 미사일이 1초뒤에 돌아오면 아까 멈추었던 launch 블럭으로 돌아가 #2가 출력되고, 마지막으로 #3이 출력되는 것이다.

runBlocking

위의 코드에서 왜 #1 -> #3 -> #2 순서대로 실행되지 않는지 의문이 생길 수도있다. 그 것은 runBlocking때문이다. 위에서 언급했듯이 runBlocking은 간단히 { } 블록 안의 내부 코루틴(launch든 async든)이 종료되기 전 까지는 작업 중인 경량 쓰레드를 블로킹 시킨다.

쉽게 말해서 runBlocking은 { } 블록 안에 비동기 처리 코루틴들이 들어 있음에도 불구하고 마치 일반적인 하나의 함수처럼 작동하는 것이다. 즉, runBlocking블록안의 모든 비동기 작업이 끝나고,첫번째 줄에서 마지막 줄까지 모두 끝나야 그 다음 줄 코드가 실행된다. 코루틴이 아닌 일반 서브루틴(우리가 아는 기본적인 함수)처럼 말이다.

job.join() & defferd.await()

fun main() {
    runBlocking{
        launch {
            delay(1000L)
            println("world")  // #2
        }
        println("Hello")      // #1
    }
    println("end")            // #3
}

이 코드는 위의 코드를 그대로 가져온 것인데, 실행 순서를 아래처럼 바꾸고 싶다.

fun main() {
    runBlocking{
        launch {
            delay(1000L)
            println("world")  // #1 이거부터 출력하고 싶다.
        }
        println("hello")      // #2 이건 두 번째
    }
    println("end")            // #3 마지막으로 출력하고 싶다.
}

이럴땐 코드에 job.join()을 넣어주면 된다.

fun main() {
    runBlocking{
        var job = launch {
            delay(1000L)
            println("world")  // #1
        }
        job.join()
        println("Hello")      // #2
    }
    println("end")            // #3
}

위처럼 하면 world -> Hello -> end 순서로 출력이된다. 즉 job의 join() 함수를 만나게 되면, 그 순간 job을 확인한 후 job이 아직 완료상태가 아니라 비동기 처리중인 상태일 경우 join()이후의 코드들을 실행시키지 않고 대기시킨다. 그리고 job들이 완료(미사일이 돌아옴)상태가 되면 그때서야 join() 아랫줄 코드들을 실행시킨다.

이제 비동기 요청으로 서버에 데이터를 요청했다고 가정하자. 서버에 데이터를 요청하고 받아오는 시간은 1초이고, 받아온 데이터를 가지고 어떤 연산을 하고싶다. 위의 코드에서 job.join()아랫줄에 연산 코드를 넣으면 되겠지만, 서버로 부터 받아온 데이터가 어디있는가? 그것을 받아오도록 코드를 바꿔보자.

fun main() {
    runBlocking{
        var deffered = async {
            delay(1000L)
            println("world")  // #1
            50 // 서버로 부터 받아온 데이터를 리턴해주는 부분 return은 적지 않는다.
        }
        var dataFromServer = deffered.await()
        println(`Hello $dataFromServer`)      // #2 Hello 50
    }
    println("end")            // #3
}

await()을 통해 async블록에서 넘겨준 값을 가져왔다. await()join()과 같이 deffered들이 완료 상태가 되면 가지고 있던 값을 뽑아내준다.

마무리

코루틴을 이해한다기 보다는 최소한의 사용법을 익히는 글이었다. 코루틴 스코프까지 이해하고 각각의 Dispatcher들을 모두 이해하면 좋겠지만, 계속 이론만 보면 이해에 발전이 없을 것 같아 이렇게 써보면서 이해 중이다.

runBlocking을 이해하고 launch, async만 사용할 줄 알아도 어느정도 즐거운 코딩은 가능하다. 여러가지 상황에 부딪혀 더 깊게 알아야 될 때도 있겠지만 우선은 이것들 부터 익숙해지자. 더 깊게 이해가 되면 그때 이론적인 부분까지 포스팅해야겠다.

맥 한영 전환 속도 개선하기 (capslock 사용)

|

mojave에서는 한영 전환을 capslock으로 한다. capslock으로 한영 전환을 하다보면 윈도우와는 달리 딜레이가 존재한다. 한글로 타이핑을 하다가 capslock을 누른 직후 영어 타이핑을 쳐도 여전히 한글이 쳐질때가 많다. 자세히는 모르겠지만 capslock 고유의 기능과 한영 전환 기능 두 개가 존재하기 때문에 생기는 딜레이인것 같다. 이 딜레이를 없애는 방법은 capslock고유의 기능을 없애는 것이다(대부분 capslock고유의 기능은 잘 사용하지 않으므로 쿨하게 없애주자). 물론 익숙해지면 약간의 딜레이를 고려해 타이핑을 한다는 분도 계시지만 성질 급한 나는 그 딜레이가 너무 싫었다.

Karabiner-Elements

그냥 capslock 기능을 없앨 수는 없는 것 같다. karabiner-Elements의 도움을 받아야한다. 링크를 클릭하고 다운로드를 받아 설치하자. command_line

설치를 완료했다면 시스템 환경 설정에서 키보드 메뉴를 클릭하고 보조키를 눌러 caps Lock키의 기능을 없애자. command_line command_line

보조키를 눌렀을 때 가끔씩 상단부분에 “키보드 선택”부분이 있는 경우도 있다. 그럴때는 Apple Internal Keyboard / Trackpad를 선택해서 위의 사진과 같이 capslock을 설정해 주면 된다.

capslock 기능을 작업 없음으로 바꾸고 나면 capslock을 눌러도 한영전환이 이뤄지지 않는다. 이제 karabiner-Elements를 사용해 capslock의 기능을 추가해주자. command_line From key 부분에 caps_lock을 설정해주자. 그리고 caps_lock을 클릭했을 때 f9(fn9)키를 실행시키도록 To key를 f9로 설정하자. 꼭 f9일 필요는 없다. 자신이 거의 사용하지 않는 키중 아무거나 넣으면 된다.

이제 capslock을 누르면 f9가 실행되지만, f9는 한영 전환키가 아니다. 다시 키보드 설정으로 들어가서 f9를 한영 전환으로 바꿔주자. command_line 이렇게 해주면 capslock을 눌렀을 때 한영 전환이 이뤄진다. 이제 capslock은 최초 맥 설정과 다르게 한영키 전환 역할 딱 한 가지만 수행하기 때문에 딜레이가 없어진다.

읕! 성공적으로 끝마쳤다면 감사의 의미로 Karabiner-Elements github 저장소에 가서 star 눌러주자 :D command_line

코틀린 동등성 연산 (== vs === vs equals)

|

두 개의 원시 타입(int, char 등등)이든 두 개의 객체든 서로를 비교해야할 경우는 반드시 생긴다. 언제는 두 개의 값이 같은지를 구분하고 싶을 때가 있고, 언제는 두 개의 주소값이 같은지를 알고 싶을 때가 있다. 코틀린에서는 이들을 어떻게 구분할까?

자바와 다르다

자바에서는 원시 타입을 비교하기 위해 ==를 사용한다. 이 경우 ==는 두 피연산자의 값이 같은지 비교하는데, 이를 동등성이라고 한다. 값이 동등하다는 것이다.

int a = 1
int b = 2

System.out.println(a == b) // false

한편 참조 타입인 두 피연산자 사이에 ==를 사용할 경우 주소값으로 비교를 하게 된다. 두 피연산자의 주소값이 같은 곳을 가리키고 있다면 true를 반환하는 것이다. String의 경우 원시 타입이 아닌 참조 타입이기 때문에, 겉으로 보이는 문자가 똑같아 보여도 주소값이 다를경우 false가 출력된다.

String a = "hi" // 주소값 : 1번지
String b = "hi" // 주소값 : 2번지

System.out.println(a==b) // false

따라서 자바에서는 두 객체(참조 타입)의 동등성을 알기 위해서 equals를 호출해야 한다.

String a = "hi" // 주소값 : 1번지
String b = "hi" // 주소값 : 2번지

System.out.println(a.equals(b)) // true

코틀린

코틀린에서도 == 연산자가 기본이다. 그러나 자바와는 동작 방식에 조금 차이가 있다. 원시 타입 두개를 비교할 때는 == 연산자가 동일하게 동작하지만, 참조 타입을 비교할 때 다르게 동작한다.

==는 내부적으로 equals를 호출한다. 따라서 참조 타입인 두 개의 String을 ==연산으로 비교하면 주소값이 아닌 값(동등성)비교를 한다.

val a: String = "hi"
val b: String = "hi"

println(a == b) // true

참조 타입의 주소 값을 비교(reference comparision)하고 싶다면?

코틀린은 자바에는 없는 ===연산자를 지원한다. 참조 비교를 위해서 === 연산자를 사용하면 된다. 즉, 자바의 주소 값 비교인 ==와 코틀린의 ===가 동일한 역할을 한다.