2014-09-30

[Scala] implicit keyword (4) - 마치며

 지난 3번의 posting으로 Scala에서 implicit keyword의 3가지 사용법인 implicit converter, implicit classimplicit parameter에 대해서 알아봤다. 이 3가지 모두 약간씩 다르지만, 명시적으로 적어야 하는 코드를 줄여 코드를 간편하게 만들어 주는 역할을 해준다.
 그 때문에 적절하게 사용하면 verbose 코드를 줄여주어 가독성을 높여줄 뿐 아니라, Scala를 이용하여 domain-specific language(a.k.a. DSL)를 만들기 쉽게 만들어 준다. 실제로 많이 쓰이는 Play framework의 Configuration file이나 route file 또는 Scala와 Java 양쪽에서 많이 쓰이는 SBT는 Scala로 구현한 일종의 DSL로 Scala를 이용하여 static하게 compile 가능하게 되어있다.

 하지만 이런 power는 잘못 사용하면 해가 된다. 뭐 어떤 기능이 안 그러겠는가만은 특히 DSL을 만들기 쉽게 해준다는 것은 결국 기존의 언어가 아닌 새로운 언어를 정의하기 쉽게 된다는 것이고, 너무 많이 사용된 라이브러리는 Scala가 아닌 새로운 언어를 다시 배워야 한다는 문제가 생기기도 한다.
 실제로 SBT가 Scala로 컴파일되는 DSL이지만 필자는 아직도 SBT의 문법을 완벽히 이해하지 못했을 정도로 완전히 새로운 언어가 되었다. 또한, 많은 사람이 Scala를 처음 사용하면서 어려워하는 부분이 implicit converter와 implicit class로 인해 현재 class에 정의되지 않은 함수가 호출되는 것이다.1)

 Implicit keyword는 Scala의 난이도를 높여주는 장애물인 것은 분명하지만, 제대로 이해하고 있으면 새로운 세상을 보여주는 받침대가 된다.

1) 이전 글에서도 말했지만, 이러한 특성 때문에 Scala를 처음 사용할 때는 다른 언어보다 더욱더 IDE가 필요하다. 다시 한 번 IntelliJ IDEA를 추천한다.

Scala implicit keyword

[Scala] implicit keyword (3) - Implicit parameter

 implicit keyword의 3번째 사용법은 implicit parameter와 implicit value를 선언하는 것이다. implicit parameter는 함수를 호출할 때 인자를 생략할 수 있도록 해준다. 정확히는 함수를 호출할 때 인자를 생략하면 자동으로 그 인자를 채워서 실행시켜준다.
 이때 무엇으로 채워주는지를 정하는 규칙은 2가지가 있다. 1)

 첫 번째 규칙은 함수가 호출된 scope에서 prefix 없이 접근할 수 있는 implicit parameter와 같은 타입의 변수 중에 implicit label이 붙은 변수를 사용하는 것이다. 이 규칙에는 2가지 타입의 변수가 해당한다. 하나는 implicit parameter이고, 다른 하나는 해당 스코프에 선언된 implicit modifier가 붙은 local 변수이다. 이때 함수가 호출된 스코프에서 해당하는 규칙에 적용되는 변수가 2개 이상 있으면 "ambiguous implicit values"라는 메시지와 함께 컴파일 에러를 발생시킨다. 여기서 주의해야 할 점은 반드시 implicit parameter와 같은 타입의 변수여야 한다는 것이다. 설령 implicit converter로 변환 가능한 변수가 있어도 이는 implicit parameter로 넘어가지 않는다.

 두 번째 규칙은 companion object에 정의된 변수 중 implicit parameter와 같은 타입으로 선언된 implicit label이 붙은 변수를 사용하는 것이다. 이는 첫 번째 규칙에 해당하는 변수가 없을 때 사용되고, 첫 번째 규칙과 마찬가지로 두 번째 규칙에 해당하는 변수가 2개 이상 있으면 "ambiguous implicit values"라는 메시지와 함께 컴파일 에러를 발생시킨다. implicit converter를 적용하지 않는 것도 첫 번째 규칙과 같다.

 Scala library에서 implicit parameter를 잘 활용하는 대표적인 예는 Future class다. Future class의 method들은 언제나 자신이 실행될 ExecutionContext를 필요로 한다. 원래대로라면 함수를 호출할 때마다 ExecutionContext를 넘겨야 한다. 하지만 이를 implicit parameter로 선언하여 함수를 호출할 때 명시적으로 ExecutionContext를 넘기지 않아도 되도록 하였다.
 이는 boilerplate 코드를 간단하게 만든다는 장점도 있다. 하지만 다른 무엇보다도 서로 다른 symbol을 가지는 Option이나 List의 list comprehension method들2)과 같은 look and feel을 가지게 해준다는 장점이 크다. 같은 look and feel을 가지는 것이 단순히 감성적인 문제 때문에 좋은 게 아니다. Scala는 list comprehension method들에 대해서 for comprehension3)을 사용할 수 있게 해주기 때문에 좋은 것이다.

1) http://www.scala-lang.org/old/node/114
2) map, flatMap, filter 등...
3) http://www.scala-lang.org/old/node/111 for comprehension도 Scala의 중요 feature 중 하나이기 때문에 후에 따로 설명하도록 하겠다.

Scala implicit keyword

2014-09-07

[Scala] implicit keyword (2) - Implicit class

 Scala에서 implicit keyword를 사용하는 또 다른 예제는 Implicit class이다.

 Implicit class는 이미 존재하는 class에 새로운 함수를 추가하여 확장하기 위해서 사용된다.
 정확히 말하면 원래 있던 클래스 자체를 바꾸지는 않고, class를 implicit conversion해서 호출할 수 있는 함수를 추가할 수 있게 해준다.
 Standard library에 있는 implicit class의 대표적인 예는 Duration class들이다.

 위의 예제에서 보듯이 DurationIntDurationDouble이라는 implicit class가 각각  Int와 Double을 확장시켜서 seconds/milliseconds/nanoseconds 같은 method를 추가해서 Duration 객체를 만들 수 있게 해준다.

 이 설명을 보면 지난 글에서 설명해줬던 implicit converter가 해줄 수 있는 일과 크게 다르지 않아 보인다.
 사실 Implicit class는 implicit converter의 syntactic sugar에 불과하다. 내부적으로 implicit class로 정의된 class는 class의 이름과 같은 implicit converter를 같은 scope에 추가한다.
 즉, 아래와 같이 정의된 implicit class는

 아래와 같이 변환된다.


 내부적으로 implicit converter에 해당하는 함수를 정의하는 것이기 때문에 몇 가지 제약사항이 있다.
 첫 번째 제약사항으로 implicit class는 trait이나 classobject 안에 정의되어야 한다.
 이는 함수의 정의가 trait이나 class나 object 안에 정의되어야 하기 때문이다. 그래서 보통 implicit class들은 package object 안에 정의된다.
 두 번째 제약사항은 non-implicit인 parameter가 반드시 1개인 constructor를 가지고 있어야 하는 것이다.
 implicit parameter1)는 여러 개 가질 수 있으니 그 외에 넘기고 싶은 인자가 있으면 implicit parameter로 넘겨야 한다.
 세 번째 제약사항은 implicit class가 선언되는 scope 안에 implicit class와 같은 이름의 무언가가 있을 수 없다는 것이다.
 이것도 내부적으로 implicit converter에 해당하는 함수가 만들어지기 때문이다. 이 때문에 내부적으로 class와 같은 이름의 object를 만드는 case class는 implicit class가 될 수 없다.

1) implicit parameter에 대해서는 다음 글에서 설명하도록 하겠다.

Scala implicit keyword

2014-09-06

[Scala] implicit keyword (1) - implicit converter

 Implicit converter가 하는 일은 이름 그대로 value를 적절한 type으로 implicit하게 convert하는 것이다.
 Scala는 기본적으로 strong typed language이기 때문에 implicit conversion을 지원하지 않는다. 함수를 호출할 때 함수의 symbol에 맞지 않으면 바로 컴파일에러가 발생한다.
 implicit conversion을 하기 위해서는 해당 scope 안에 implicit converter를 구현해두어야 한다.

 Implicit converter의 선언은 다음과 같다.

 Implicit converter는 unary function에 implicit keyword를 붙여주는 것으로 정의된다.
 이렇게 Implicit converter를 구현해두면, sizeToRectangle 함수가 정의되어 있는 scope에서는 Size 객체가 Rectangle 객체로 implicit conversion이 가능해진다.

 예를 들어 Scala에서 자주 쓰이는 Option이라는 class를 보자.
 Option은 c#의 nullable과 유사한 type으로 원소가 없을 수 있는 객체이다. 이는 다른 관점에서 보면 최대 원소가 1개인 collection이라고 볼 수 있고, Option을 사용할 때 collection처럼 list comprehensive method1)를 사용하는 것을 권장한다.
 하지만 Option코드를 보면 Option은 Iterable이나 Traversable을 상속받지 않았고, 일부 함수를 제외하고 collection처럼 이용는데 필요한 모든 함수를 가지고 있지도 않다.
 하지만 Option을 Iterable로 변환해주는 option2Iterable이라는 implicit converter가 있기 때문에 iterable이 필요한 경우 자동으로 convert해서 넘겨준다.

 Implicit converter에 대응되는 개념이 다른 언어에 없는 것은 아니다.
 C++에서는 unary constructor를 구현하거나 assignment operator나 type case operator를 구현하여 implicit conversion을 구현한다. C#은 conversion operator를 구현하여 implicit conversion을 구현할 수 있다.
 하지만 implicit converter는 이들보다 더 표현력 있고 확장성 있는 개념이다.

 implicit converter가 다른 언어의 implicit conversion보다 표현력 있고 확장성 있다는 것을 보여주는 좋은 예제가 RichInt다.
 위의 예제는 Scala가 기본으로 제공해주는 Int타입의 확장함수 들이다. 하지만 Int 구현체에는 위의 함수들에 대한 구현이 없다. 왜냐하면 Scala는 정책상으로 Int class내에는 primitive operator2)에 대해서만 구현하도록 하고 있기 때문이다.
 그렇다면 위의 함수들은 어디에 정의되어 있을까?
 primitive operator가 아닌 함수들은 RichInt에 구현되어 있다. Int에서 RichInt로 implicit convert해주는 함수가 정의되어 있기 때문에 Int로 선언된 변수들에 대해서도 RichInt에 정의된 함수를 쓸 수 있는 것이다.

 여기서 Scala의 implicit converter가 C++에서 제공하는 implicit conversion보다 가지는 장점을 알 수 있다. C++에서 type case operator나 unary constructor를 구현하거나 C#에서 conversion operator를 구현하더라도 implicit type casting을 해서 함수의 symbol을 찾지 않는다.
 다시 말해서  C++이었다면 RichInt와 implicit conversion을 위한 operator들이 구현되어 있다고 해도 symbol을 찾기 위해서는 아래와 같은 추가 작업이 필요하다.
 하지만 Scala는 instance의 현재 class에 해당하는 symbol의 함수가 없어도 현재 scope에 있는 implicit converter를 1번 적용해서 나오는 class에서 해당하는 symbol의 함수를 찾을 수 있으면, 해당하는 함수를 부른다.

 또 기존의 implicit conversion에는 큰 문제가 있는데, implicit conversion의 정의가 class 내에 정의되어야 한다는 것이다. 즉, A에서 B로 가는 implicit conversion의 구현을 하고 싶으면 A와 B 둘 중 최소한 하나는 내가 수정할 수 있는 class여야 한다는 것이다.
 library에서 제공해주는 class 간의 implicit conversion을 구현할 방법이 없고 이 때문에 쓸데없이 Wrapper class를 만들어야 하는 일이 종종 발생한다.
 게다가 class에 implicit conversion이 정의되기 때문에 특정한 함수에서만 implicit conversion을 허용하거나, 반대로 특정 함수에서는 implicit conversion을 허용하지 않는 것을 할 수 없다.

 물론 이런 강대한 표현력은 가끔 과다해서 단점이 되기도 한다. 어떤 객체의 함수가 그 객체의 class 정의 밖에 존재하는 것이기 때문에 코드를 읽는데 어려움을 준다. 가끔은 다른 부분에서 자연스럽게 썼던 함수를 implicit converter를 import하지 않아서 사용하지 못하는 일도 있다.
 하지만 이 정도 문제는 좋은 IDE(Integrated Development Environment)가 있으면 쉽게 해결되는 문제다. 3)
 너무 과하지 않을 정도로 적당히4) 사용하면 한층 더 확장된 세계를 볼 수 있게 된다.

1) filter, map, flatMap ...
2) unary -, unary ~, +, * ...
3) 다시 한 번 IntelliJ IDEA를 추천한다.
4) 프로그래밍 언어는 결국 도구다. 어떤 도구를 쓰더라도 도구를 어떻게 사용할지는 그 도구를 사용하는 사람, 다시 말해 프로그래머가 잘 결정해야 한다.

Scala implicit keyword

2014-09-05

[Scala] implicit keyword (0)

 Scala의 가장 인상적인 keyword를 말하라고 하면 많은 사람이 implicit을 꼽을 것이다.
 implicit은 scala의 확장성에 무한한 힘을 주는 keyword임과 동시에 코드를 읽을 때 헬 게이트를 여는 주범이기도 하다. 1)

 implicit keywod는 Implicit converter, Implicit class, Implicit parameter의 3가지 목적으로 이용된다.
 앞으로 3번에 걸쳐서 각각에 대해 설명하도록 하겠다.

1) 개인적으로 코딩할 때 대부분 언어를 vim으로 작업해왔지만, scala를 배우면서 IntelliJ라는 IDE를 사용하기 시작했다.

Scala implicit keyword

2014-09-03

[MongoDB] ObjectId에 대해서

 지난번에 shard key를 설명한 글을 썼을 때 댓글ObjectId에 관한 얘기가 나왔었다.
 그래서 sharding과 큰 연관은 없지만, 이번 기회에 ObjectId에 관해서 먼저 설명하고 가는 게 좋을 것 같아서 sharding에 관한 것은 뒤로 미루고 이번에는 ObjectId에 대해 먼저 설명하고 가도록 하겠다.

ObjectId란 무엇인가

 ObjectId는 같은 document 내에서 유일함이 보장되는 12 byte binary data다.
 전통적인 centralized 되어 있는 시스템이라면 한 collection 내에서 유일함을 보장하는 것을 쉽게 할 수 있다. 하지만 sharding을 하는 MongoDB에서 유일함을 보장하는 것은 기존과는 다른 솔루션이 필요하다. 그리고 이 방법을 설명하는 게 사실상 ObjectId의 모든 것을 설명하는 것이다.

왜 ObjectId를 사용하는가

 전통적인 RDBMS에서 Primary key를 만들 때는 DB 서버로 data를 보내서 중복되지 않는 key를 골라서1) 그 값을 key로 저장하는 방식을 이용한다.
 하지만 MongoDB와 같은 분산 database에서는 key를 서버에서 만들지 않고 클라이언트에서 만든다.
 그 이유는 MongoDB가 query를 날릴 shard를 결정하는 방식을 보면 알 수 있다.
 MongoDB는 자신이 필요한 shard에게만 query를 요청한다. 다시 말해서 client에 해당하는 mongos2)config server의 data를 토대로 어떤 shard가 어느 범위의 값을 가졌는지를 저장하고 있다가 query를 요청할 때 자신이 필요로 하는 shard에게만 요청한다.
 따라서 shard key에 해당하는 data가 미완성인 상태로 서버에 저장하도록 요청할 수 없고, client가 ObjectId를 생성하여 값을 저장하도록 요청한다. 3)

ObjectId의 구성

 ObjectId는 크게 4부분으로 구성되어 있다.

 ObjectId의 처음 4 byte는 시간정보를 담고 있다.
 1970년 1월 1일 0시 0분 0초를 0으로 해서 4 byte의 정보를 담고 있으므로 2106년 2월 7일 6시 28분 15초까지 표현할 수 있다.

 다음 3 byte는 machine 별로 다른 고유한 id를 담고 있다.
 이것은 driver의 구현에 따라 다르지만 보통 mac address + ip address 혹은 hostname을 md5 hash한 값의 처음 3 byte를 따온다. 하지만 driver 별로 구현이 다르다. 같은 driver를 사용할 때 같은 머신이 같은 값을 가지는 방식 중에서 최대한 랜덤하게 분포하는 방식으로 구현된다.

 다음 2 byte는 process id 정보를 담는다.
 32-bit 머신에서 process id의 최댓값은 2^15이라서 2 byte로 문제없이 표현된다. 하지만 64-bit 머신에서는 최댓값이 2^22이 가능하므로 해당 범위에 들어가지 못한다. 이러면 어떻게 처리할지 문서에서 찾지 못했다. 하지만 실제로 process id가 필요한 것이 아니고 ObjectId의 유일함을 보장하기 위해 사용되는 bit들이므로 정확한 process id를 유지하고 있을 이유는 없다.
 때문에 mongodb가 가지고 있는 bson 구현체에서는 process id가 2 byte보다 크면 하위 2 byte만을 pid bits에 저장하고, 상위 1 byte는 machine number에 반영하지만 드라이버에 따라서는 하위 2 byte만을 process id로 저장하고 상위 1 byte는 버리는 드라이버도 있다.

 마지막 3 byte는 mongos가 생성한 값으로 inc라고 부른다.
 이것도 driver 별로 구현이 다르기는 하지만 보통 random 한 숫자에서 시작하여 auto increment되는 숫자다. 일부 구현체들은 0부터 시작하는 auto increment 되는 숫자를 쓰기도 하고, 단순히 random하게 고른 숫자를 쓰기도 하는데 이렇게 사용하면 원래의 방식보다 충돌할 확률이 늘어난다.

ObjectId의 unique성

 따라서 독립적으로 생성된 두 ObjectId가 같을 확률은 매우 낮다.
 일단 최소 조건은 두 ObjectId가 1초 내에 생성된 ObjectId이어야 한다. 4)
 그리고 같은 machine hash를 가지는 머신에서 같은 process id로 뜬 process id로 뜬 mongos에서 생성된 ObjectId이어야 한다.

 같은 머신에서 생성된 ObjectId는 기본적으로 이런 조건을 만족한다. 하지만 3 byte의 inc가 같은 머신에서 생성한 ObjectId가 중복되는 것을 막아준다. 하나의 mongos에서 1초에 4,096개 이상의 ObjectId를 생성하는 불가능한 시나리오가 아니면 중복된 ObjectId가 중복되지 않는다. 5)

 서로 다른 머신의 경우 같은 machine id와 process id를 가지는 확률은 매우 낮다.
 machine id로 3 byte를 사용하므로 machine id가 같을 확률은 1/4096이다. 거기에 같은 inc를 가질 확률 1/4096까지 더해져 실제로 machine id가 같은 mongos에서 같은 inc를 가질 확률은 천만분의 일이다. 여기에 서로 같은 process id로 process가 실행될 확률까지 더해져서 실제 확률은 더 낮아진다.

 하지만 이는 어디까지 확률적인 문제고 매우 낮은 확률로 혹은 의도적으로 같은 ObjectId가 생성되는 일도 있다. 이런 경우는 shard에서 E11000 duplicated key error를 발생시키고 insert가 실패한다.


1) 보통은 auto increment를 사용한다.
2) 혹은 query router
3) 사실 shard key 이외에 data가 미완성인 상태로 요청하여 server에서 완성하도록 하는 default 같은 방식을 지원하지 않는다. MongoDB는 자신이 요청받은 data를 그대로 저장한다.
4) 혹은 1970년 이전에 생성된 ObjectId거나 2106년 이후에 생성된 ObjectId이어야 하는데 MongoDB 개발자들은 이럴 경우는 없을 거라고 가정하고 있다.
5) 이것은 어디까지나 정상적인 드라이버에서의 얘기다. 만약 위에서 말한 대로 단순히 랜덤하게 생성하는 driver라면 4,096개를 생성하지 않더라도 1/4096의 확률로 충돌이 발생한다.