devcken.io

Thoughts, stories and ideas.

Redis Keys in RAM

원문: https://redislabs.com/blog/redis-keys-in-ram/

Dr. Seuss의 "Green Eggs and Ham"을 각색. 텍스트에 대한 링크. 그림은 Dr. Seuss에게 저작권이 있음.

I am San. 나는 San이야.
I am San. 나는 San이야.
San I am. San I am.

That San-I-am! 바로 San-I-am이라고!
That San-I-am! 바로 San-I-am이라고!
I do not like that San-I-am! 난 San-I-am을 좋아하지 않아!

Do you like Redis keys in RAM? 너는 RAM 안에 있는 Redis 키들을 좋아하니?

I do not like them San-I-am 난 그것을 좋아하지 않아, San-I-am
I do not like Redis keys in RAM. 나는 RAM 안에 있는 Redis 키들을 좋아하지 않아

Would you like them large [1] or small? 그들이 큰게 좋아 작은게 좋아?

I would not like them large or small. 그들이 크거나 작은 걸 좋아하지 않아.
I would not like them not at all. 난 그들을 전혀 좋아하지 않을거야.
I do not like Redis keys in RAM. 나는 RAM 안에 있는 Redis 키들을 좋아하지 않아.
I do not like them San-I-am. 나는 그들을 좋아하지 않아 San-I-am.

Would you like them as a String? 그것들이 문자열이길 바래?
Would you serialize everything? 모든 것을 직렬화하길 바래?

I do not like them as a String. 문자열을 좋아하지 않아.
I do not like to serialize things. 직렬화하는 것을 좋아하지 않아.
I do not like them large or small. 그들이 크든 작든 좋아하지 않아.
I do not like them not at all. 나는 그들을 전혀 좋아하지 않아.
I do not like Redis keys in RAM. 나는 RAM 안에 있는 Redis 키들을 좋아하지 않아.
I do not like them San-I-am. 나는 그들을 좋아하지 않아 San-I-am.

Would you like them in a Hash? 그들이 Hash 안에 있길 바라니?
Would you like a Hash as cache? cache 같은 Hash를 바라니?

Not in a Hash. Not as a cache. Hash 안에 있는 걸 바라지 않아. Cache도 바라지 않아.
Not as a String. No serialized, no anything. 문자열 같은 것도. 직렬화도, 어떤 것도 원치 않아.
I do not want them large or small. 난 그들이 크거나 작길 바라지 않아.
I do not want them, not at all. 난 그들을 전혀 원하지 않아.
I do not want Redis keys in RAM. 난 RAM 안에 있는 Redis 키를 원하지 않아.
I do not want them, San-I-am. 난 그들을 원하지 않아, San-I-am.

Would you want them as a List instead? 그러면 대신 List이길 바라니?
Do you want to access tail, body and head? 테일, 본문 그리고 헤드에 액세스하길 원하니?

Not as a List. Not as a Hash. 리스트를 원하지 않아. Hash를 원하지 않아.
Not as a String. Not as a cache. 문자열을 원하지 않아. cache를 원하지 않아.
Small or large I will have naught. 작든 크든 0을 가질 거야.
Goodbye San-I-am and thanks a lot. 안녕 San-I-am 그리고 고마워.

Would you? Could you? As a Set? Set는 어때? 해볼래? 해볼테야?
Get the difference! Store a union! Or just intersect… 차를 구해봐! 합집합을 구해봐! 아니면 교집합을...

I would not, could not, as a Set. 아니 해보고 싶지 않아

You may like them. 그들을 좋아할 수도 있어
You'll see for sure. 확실히 보게 될 거야.
You may like Sorted Sets by score? 스코어에 의한 Sorted Sets를 좋아할지도 몰라.

I would not, could not by a score. 스코어로 하고 싶지도, 할 수도 없어.
No more Sets! I say no more! 더 이상의 Sets는 없어! 더 이상 말하고 싶지 않아!
I do not like them as a List 난 List 같은 것도 좋아하지 않아
Stop this now – I do insist. 이제 그만둬. 분명히 말할게.
I do not like them as String or Hash 문자열이든 Hash든 좋아하지 않아
I do not like an in-memory database or cache. in-memory 데이터베이스든 cache든 좋아하지 않아.
I do not want Redis keys in RAM. RAM 안에 있는 Redis 키들을 좋아하지 않아.
I do not want them, San-I-am. 그들을 좋아하지 않아, San-I-am.

You do not like them. So you say. 넌 그들을 좋아하지 않는구나. 너가 말한대로.
http://try.redis.io! Try them! And you may. http://try.redis.io! 한 번 해봐! 좋아하질도 모른다.
Try them and you may, I say. 한 번 해봐, 내가 말하는데로 좋아할거야.

San! If you will let me be, San! 너가 날 놓아준다고 약속하다면,
I will try them. You will see. 한 번 해볼게. 두고봐.

Say! I like Redis keys in RAM! 우와! 난 RAM 안에 있는 Redis 키들이 좋아!
I do! I like them, San-I-am! 정말이야! 그들이 좋아, San-I-am!
So I will have them as a String. 그러니까 문자열을 가질거야.
And as a Hash, a List or anything. 그리고 Hash, List 어떤 것이든지.
And as a Set – both unordered and an ordered one. 그리고 Set, 정렬되지 않은 것이든 정렬된 것이든.
Say! Data structures are so much FUN! 우와! 데이터 구조는 정말 즐거워!

I do so like Redis keys in RAM 난 RAM 안에 Redis 키들이 정말 좋아
Thank you! Grazie, San-I-am 고마워! 고마워요, San-I-am!


후기 노트

저는 암스테르담에서 열린 Percona Live Europe 2015의 "Use Redis in Odd and Unusual Ways"라는 주제의 일부분으로 위와 같은 내용을 발표했습니다. 주요 주제는 MySQL로 이루어진 반면, 컨퍼런스의 프로그램에 Redis에 관한 내용은 4개 세션 밖에 없었습니다. 제 발표를 제외하고 말이죠. Redis 옹호자가 되는 것, 그것이 아마도 내가 바라는 최선의 방법일 것이나, 발표자로써 그것은 도전이었습니다. Redis에 대한 경험을 지닌 청중 뿐만 아니라 NoSQL에 대한 경험을 지닌 청중과 관련된 대화를 어떻게 준비해야 할까?

그래서 결론은 Redis와 대한 기초 주제와 고급 주제를 섞는 것이었고 Redis 베테랑 뿐만 아니라 Redis가 처음인 사람에게 도움이 되길 바랬습니다. DR.ediseuss라는 모티브는 약간의 배경에 가치를 둘 만 합니다. 나 이전의 많은 사람들이 그랬듯이, 내가 빨간 벽돌이 깔린 길에 첫발을 내디뎠을 때처럼, 무언가 API의 네이밍 스키마에 현혹되었습니다. [2] 모든 것을 흡수하면서, 나는 그 모든 것에 대한 첫 단계였던 작은 "시"를 생각해냈습니다. 다음은 여러분의 즐거운을 위한, "Dr. Seuss Reads Redis" 책의 첫번째 (그리고 유일한) 페이지입니다:

This is my friend
His name is ZADD
ZADD's a lad
Who's always SADD

It's really bad
that ZADD is SADD
I don't know why
And that makes me SCARD

I hope that ZADD
Will be someday glad
And that he'll get over
This stupid PFADD

문제가 있나요? 열렬히 환호해 주실건가요? 이메일이나 트윗하세요. 언제나 환영합니다 🙂


FN#1 Redis의 키는 512MB까지 가능하며 바이너리에 안전합니다. Simple string 값은 512MB까지 가능하며 역시 바이너리에 안전합니다. 다른 데이터 구조체들은 2^32개의 요소, 512MB까지 가능합니다.

FN#2 시작하는 이들을 위한 격려의 메모 - 금방 완벽하게 자리를 잡을 것이고 처음에 무엇때문에 그렇게 현혹되었는지를 염려하게 될 것입니다 🙂

Redis RAM Ramifications - Part 1

원문: https://redislabs.com/blog/redis-ram-ramifications-part-i/

... 우리가 알고 있듯, 알고 있다고 아는 것이 있다. 우리가 안다고 아는 것이 있다는 것이다. 우리는 또한 모른다고 알고 있는 것이 있다. 즉, 우리가 모르는 무언가가 있다고 알고 있다고 하는 것이다. 그러나 모르는 것을 모르는 것도 있다. 우리가 알지 못하는 것을 알지 못하고 있음을 말한다"

미국 국방 장관 도널드 럼스펠드, 2002년

Redis는 얼마나 많은 RAM을 필요로 할까?

Redis에 관해 가장 자주 반복되는 질문이 있다면, 이것일 겁니다. 이에 대한 대답은 너무도 많은 요인들 때문에 정확히 대답하기 가장 힘든 대답 중 하나입니다. 이 포스트에서 (그리고 내가 대충 얘기하고 중얼거리면서 장황하게 이야기하는 경향이 있기에 앞으로 쓸 글들에서) Redis의 RAM 소비에 대해 알아볼 것입니다. 모든 것에 대한 굉장한 답변은 보장하지는 못하지만, 다음과 같은 것들에 도움이 되길 바랍니다:

  • 알려진 것에 대해 알고 있다는 것(KKs)을 확인하기 위해 반복합니다
  • 가능하면 알고 있지 못하는 것을 아는 것으로 바꾸거나, 적어도 대충 이해하는 정도(KRUU)로 바꾸고자 합니다
  • 우리가 모르고 있는 것을 모르는 것에 대해 희망을 가지고 어떻게든 알아보면서, 모르고 있다는 사실을 아는 것, 대충이나마 이해하는 것, 혹은 알고 있는 것으로 바꿉니다

사이드 노트: RSS 피드를 구독해 이 시리즈에 대한 진행 과정을 추적할 수 있습니다. 아니면, Redis Watch에 가입해 메일함에 주간 업데이트가 오도록 할 수 있습니다. 그러나 그것만 보고 있다면, 실시간 정보를 위해 트위터를 팔로우하세요.

아무튼, Redis는 소프트웨어 중 하나이고 동작하는데 RAM을 요구합니다. 그러나, Redis는 그냥 보통의 소프트웨어가 아니라, in-memory 데이터베이스이며, 이는 Redis가 관리하는 모든 데이터 조각들 역시 RAM 안에 유지된다는 것을 뜻합니다. Redis가 운영에 필요로 하는 RAM을 Opernational RAM이라고 하고, 데이터 스토리지에 사용되는 RAM을 User Data RAM이라고 해보죠.

짐을 싣지 않은 제비의 비행 속도는 얼마인가?

Redis의 Operational RAM에 대해 얼른 알아보죠.

그것은 Redis가 수행하는 많은 목적과 작업을 위해 사용되며, RAM의 청크를 사용자의 데이터가 아닌 모든 것을 위해 Redis가 사용하는 전체 메모리와 같다고 생각하는 방법 중 하나입니다(나는 아마도 이후에 이것 때문에 천천히 진행할 것 같다). Redis의 RAM 사용 공간은 다음을 포함하는, 무수히 많은 배치 요인들에 영향을 받습니다:

  • 서버 프로세서의 아키텍처
  • 운영 시스템
  • Redis의 버전과 구성 정보
  • 수많은 아는 것, 무엇인지는 알지만 잘은 모르는 것, 아예 모르는 것

하지만, 전형적인 서버의 유휴 상태에서 Redis를 테스트하여 Redis의 operational RAM 요구 사항에 대한 기준을 쉽게 정할 수 있습니다. 예를 들어, 가상화된 우분투 14 64비트 서버의 upladen African swallow V3.0.0 인스턴스의 메모리 사용 공간은 7995392 바이트(또는 약 7.6MB)다. Redis 인스턴스가 얼마나 많은 RAM을 할당하는지 ps의 RSS 컬럼 혹은 Redis의 INFO 명령어를 사용해 빠르게 알아낼 수 있습니다:

foo@bar:~$ uname -a  
Linux bar 3.13.0-49-generic #81-Ubuntu SMP Tue Mar 24 19:29:48 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux  
foo@bar:~$ ps aux | grep redis-server  
foo 20139 0.0 0.1 42304 7808 pts/1 Sl+ 19:18 0:00 ./redis-server *:6379  
foo 20143 0.0 0.0 15940 944 pts/9 S+ 19:18 0:00 grep --color=auto redis-server  
foo@bar:~$ redis-cli INFO memory | grep used_memory_rss  
used_memory_rss:7995392  

사이드 노트: 위 두 메서드와 다른 것들의 결과가 항상 동일한 것은 아닙니다. 또한, 앞으로 보게 되겠지만 usedmemoryrss가 Redis의 used_memory와 상당히 차이가 난다는 점을 알아두시기 바랍니다.

이것이 최근에 초기화된 Redis 인스턴스라는 점 덕분에, 이 수치가 운영 상의 공정한 기준이라는 것을 추정할 수 있습니다. Redis의 operational RAM은 증가할 수 있으며, 심지어는 엄청 늘수도 있지만, 이제부터는 그 점에 대해 걱정할 필요가 없습니다.

뭐라고? 제비가 코코넛을 나른다고?

Redis는 아름답지만, 그저 아름다운 면 때문에 사용하고 있지는 않겠죠? 아닐 겁니다. 오늘 날 지구 상에서 가장 빠른 NoSQL를 필요로 하기 때문에 Redis를 관리하고 있을 겁니다. 당신은 Redis가 당신의 코코넛을 나르도록 하며 (거의) 매 초당 43번의 날개 짓을 하도록 합니다. 그래서 사용자 데이터가 차지하는 RAM은 얼마나 될까요? 그것은 코코넛에 달려있습니다 🙂

Redis의 schemaless schema*는 Key-Value 모델에 기반합니다. Redis가 관리하는 모든 사용자 데이터는 기본적으로 KV 쌍입니다. 키와 값이 더 길거나 클수록, Redis가 더 많은 RAM을 필요로 한다는 것을 이해하는 것은 대단한 일이 아닙니다. 그러나 Redis는 데이터를 간결하고 조직적으로 유지하기 위해 설계된 몇 가지 독창적인 트릭을 가지고 있습니다.

** 스키마가 없는 데이터베이스 같은 것은 없습니다. 대부분은 암시적인 데이터베이스만 가질 수 있죠.

가장 간단한 Redis 데이터 타입과 관련된 예제로 그것을 알아보죠:

127.0.0.1:6379> SET swallow coconut  
OK  

위에서 얼마만큼의 RAM을 사용했을까요? 모든 데이터가 키에 의해 구성되므로, 키의 이름이 우리가 알아내야 할 첫번째 요소입니다. Redis 키 이름은 바이너리에 안전한 문자열로 512MB까지 가능합니다. 좋아요. Redis에서 문자열 값 또한 바이너리에 안전하며 0.5GB까지 가능합니다. 그러므로 위 예제에서 "swallow"에 대해 7 바이트, "coconut"에 대해서 또 7바이트 라고 추정할 수 있는데... 틀렸습니다. 적어도 부분적으로는 말이죠. 다음을 보겠습니다:

127.0.0.1:6379> STRLEN swallow  
(integer) 7
127.0.0.1:6379> DEBUG SDSLEN swallow  
key_sds_len:7, key_sds_avail:0, val_sds_len:7, val_sds_avail:0  

STRLEN을 이용해 본대로, Redis는 "swallow"라는 키에 저장한 값("coconut")의 길이가 7바이트라고 합니다. 더욱이, "비밀의" DEBUG SDSLEN 명령어 역시 동일한 주장을 하고 있지만, 둘 다 데이터의 오버헤드에 대해서는 언급하고 있지 않은데, 모든 Redis 데이터 구조는 자신의 수화물을 가지고 있습니다. 즉, 실제 문자열 ("swallow"와 "coconut") 외에도 Redis는 그것을 관리하기 위한 약간의 RAM을 더 필요로 합니다.

Redis의 모든 key-value 튜플은 내부적인 장부를 위해 추가적인 RAM을 사용하며, 해당 RAM의 양은 데이터 구조와 데이터 자체에 달려 있으며, 나는 메타데이터가 아닌 이 오버헤드가 사용자 데이터의 일부라고 생각합니다. 달리 말하자면, 모든 X 바이트의 문자열에 대해 Redis는 X + Y 바이트를 요구하며, 그렇기에 분명히 Y는 잘 모르는 것에서 잘 아는 것이 되어야 할 겁니다.

문자열 이론

Redis의 문자열은 대부분은 Salvatore Sanfilippo @ antirez의 서브 프로젝트 중 하나인 sds (혹은 Simple Dynamic Strings 라이브러리)에 의해 구현됩니다. sds 문자열이 Redis 내부적으로 사용 상의 상당한 힘과 용이함을 가져다 주지만, sds 문자열이 만들어져 약간의 오버헤드가 발생합니다:

+--------+-------------------------------+-----------+
| Header | Binary safe C alike string... | Null term |
+--------+-------------------------------+-----------+
         |
         `-> Pointer returned to the user.

(안티레즈의 다이어그램 설명, https://github.com/antirez/sds/blob/master/README.md)

sds 문자열 헤더의 크기는 (당장은) 8바이트이며 추가 바이트로 null 문자가 들어가는데, 이는 문자열 당 총 9바이트의 오버헤드입니다. 7바이트의 "swallow"가 갑자기 그 두 배 이상인 16바이트 RAM으로 Redis에 저장되었습니다!

사이드 노트: 실제로는 더 사용합니다. 모든 키에 대한 참조가 Redis의 keyspace 해시 테이블에 저장되는데, 이는 더 많은 RAM을 요구합니다... 그리고 Redis가 LRU와 같은 일부 데이터들을 조성하기 위한 robj "객체" 또한 존재합니다... 그러나 그건 잘은 모르지만 아는 것으로, 키의 관리 RAM 오버헤드로 두고 당장은 operational RAM으로 던져두도록 하죠 🙂

실제로 한 라인에 코코넛이 있다면...

제비와 코코넛으로 돌아가보겠습니다. 현재 우리는 Redis가 위 예제에서 키와 값을 구성하는 두 개의 문자열을 저장하기 위해 36바이트를 사용한다는 것을 알게 되었습니다. 근데 그게 전부일까요? 코코넛에 대해 좀 더 알아보도록 하죠:

127.0.0.1:6379> OBJECT ENCODING swallow  
"embstr"

점점 더 복잡해지고 있다(또는 어쩌면 코코넛의 털이 자라고 있는걸지도). 이 암호같은 응답은 설명을 필요로 한다. 그런데 모든 코코넛이 동일하게 인코딩되는 걸까? 다른 모양의 문자열 코코넛들에 대해 살펴보자:

127.0.0.1:6379> SET swallow:0 "0"  
OK  
127.0.0.1:6379> SET swallow:1 "An oversized and thickly-haired coconut"  
OK  
127.0.0.1:6379> SET swallow:2 "Ok, this is the mother of all coconuts - it is something that would make Donkey Kong run back to his mama in tears"  
OK  
127.0.0.1:6379> OBJECT ENCODING swallow:0  
"int"
127.0.0.1:6379> OBJECT ENCODING swallow:1  
"embstr"
127.0.0.1:6379> OBJECT ENCODING swallow:2  
"raw"

각각의 코코넛이 모두 다릅니다. Redis가 서로 다른 인코딩을 사용했습니다. "int" 인코딩은 LONGMIN과 LONGMAX 사이의 정수 값을 효율적으로 저장하기 위해 사용되며 shared.integers 구조를 활용해 데이터 중복을 막습니다. 따라서 약간의 공간을 더 사용합니다. 39 바이트보다 긴 문자열은 "raw"로 저장되는 반면, 짧은 것은 "embstr" 인코딩을 사용합니다(이 마법의 숫자는 redis.h의 REDIS_ENCODING_EMBSTR_SIZE_LIMIT에 정의되어 있습니다).

Going (coco)nuts

다른 코코넛 구조들은 어떨까요? 문자열은 Redis가 제공하는 가장 간단한 데이터 구조이며 내부적으로 다른, 좀 더 진보된 구조를 만드는데 사용됩니다. Hash는 각 엔트리가 하나의 linked list인, 딕셔너리 데이터 구조와 함께 추가된 문자열 뭉치(필드와 값)로 구성됩니다... 그러나 완전히 ziplist로 인코딩될 수도 있습니다. 그리고 리스트에 대해 말하자면, Sets와 Sorted Sets에 의해 사용되는 linked list와 ziplist(그리고 아마 Matt Stancliff @ mattsta의 quicklist)가 있습니다.

Redis의 데이터 인코딩에 대한 미묘한 복잡함과 그들이 메모리 소비에 어떠한 영향을 주는지 계속해서 알아볼 수 있지만, 우리 모두를 지루하게 만들어 조만간 곧 죽게 만들 것 같아 두렵군요. 대신, 소스 코드를 체크아웃하여 계속 읽어볼 수 있습니다. 누군가는 그렇게 하리라는 것을 알지만, 어느 정도의 프로그래밍 스킬이 필요할 것입니다.

아직 알지 못하는, Redis가 필요로 하는 RAM은 얼마나 되는가라는 질문이 아직 남아 있습니다. 그리고 그보다는 작은 궁금증인 코코넛이 필요로 하는 RAM은 얼마나 되는가라는 질문도. 그러나 뜻이 있는 곳에 길이 있습니다. 저와 사용 가능한 채널을 통해 얼마든지 연락할 수 있습니다. 언제나 환영합니다 🙂

사이드 노트: 이어서 계속

Why should Java 8's Optional not be used in arguments

Java 8부터 java.util.Optional<T>이라는 클래스가 지원되기 시작했습니다. Scala에서 Option이라는 이름으로 지원되는 클래스와 거의 동일한 역할을 합니다.

자바의 공식 문서를 보면 다음과 같이 설명하고 있습니다.

null이 아닌 값을 포함하지 않거나 포함하는 컨테이너 객체

즉, null과 관련된 처리를 처리하기 위한 클래스로 보입니다.

Null Island is One of the Most Visited Places on Earth. Too Bad It Doesn’t Exist

Null 참조의 아버지(?)인 Tony Hoare는 2009년 한 컨퍼런스에서 Null 참조를 발명한 것에 대해서 사과했습니다.

저는 그걸 10억 달러짜리 실수라고 부릅니다. 1965년에 null 참조를 발명한 것 말이죠. 그 시대에, 저는 객체 지향 언어(ALGOL W)의 참조를 위한 첫번째 내포 타입 시스템을 설계하고 있었죠. 저의 목표는 모든 참조 사용이 절대적으로 안전하다는 것을 보장하는 것이었는데, 컴파일러에 의해 자동으로 수행되는 검사와 함께 말이죠. 그런데, null 참조를 부여하는 유혹을 견뎌내지 못했습니다. 단순히 구현하기 쉬웠기 때문이죠. 이는 무수한 오류, 취약점 그리고 시스템 충돌들을 야기시켰습니다. 필시 이것이 최근 40년 동안의 10억 달러 만큼의 고통과 상처를 안겨주었습니다.

그만큼 Null 참조는 문제가 많습니다. Null 참조로 인한 잠재적인 버그들이 개발자들을 괴롭혀왔고 여전히 그렇습니다.

이러한 고통과 상처를 해결하기 위한 것이 바로 Optional입니다.

제가 구현 중인 제품의 일부 코드를 예로 들어보겠습니다.

...
return Optional.ofNullable(Longs.tryParse(identifier))  
       .map(this::findOne)
       .orElse(this.repository.findOne(identifier));
...

보시면 아시겠지만, 저장소에서 특정 인스턴스를 가져오는 코드입니다. Optional.ofNullable을 사용했습니다. 그 이유는 identifier라는 변수가 String 타입이고 이를 Guava의 Longs.tryParse 함수를 사용해 구문 분석한 후 그 결과가 null이면 저장소의 findOne 메서드에 identifier 변수를 그대로 넘기고 Long 타입으로 잘 캐스팅되었다면 캐스팅된 값을 넘기도록 한 것입니다.

위 코드를 null을 사용해서 구현하면 다음과 같이 구현해야 할 겁니다.

Long x = Longs.tryParse(identifier)

return x == null ?  
  this.repository.findOne(identifier) :
  this.repository.findOne(x);

뭐 굉장히 평범한 코드이긴 합니다. null을 직접 언급한다는 것이 깨림직한 정도죠. Null 참조가 왜 나쁜가에 대한 논의는 여기서 직접하지 않겠습니다.

제가 이 포스트를 통해 알아보고자 하는 것은 사실 따로 있습니다.

저는 위 예제에서 Optional을 메서드의 반환 타입으로 사용했습니다. 이것이 바로 Optional의 존재 이유죠. 아, 참고로 findOne 함수 또한 Optional을 반환합니다. 만약 findOne의 반환 타입이 Optional이 아니고 Optional의 타입 파라메터 자체가 반환 타입이었다면 null이 반환될 가능성이 존재합니다. 요청된 식별자에 대한 값이 저장소 내에 없을 수도 있기 때문이죠.

만약 그럴 경우, 위 코드의 메서드를 실행하는 호출자는 null에 대한 대비를 해야 합니다. if (x == null) {...이나 x == null ? ... 류의 코드 말이죠.

음, 저는 이런 생각이 들었습니다. 메서드의 파라메터로 Optional 타입을 받으면 어떨까?

T doSomething(Optional<T> tOptional) {  
  tOptional.ifPresent(...);
  ...
}

뭐 대략 이런 식의 코드가 가능해질 겁니다.

저는 IDE로 IntelliJ를 사용 중인데요, 위와 같은 코드를 작성하면 IntelliJOptional<T>에 노란색 박스를 그립니다. 그리고는 아래와 같이 경고합니다.

Reports any uses of java.util.Optional, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.

말인즉슨, Optional 류의 타입을 파라메터에 적용하지 말라는 겁니다. 원래 그 용도가 반환 값으로 null이 반환될 가능성이 있는 경우, 이를 위한 명확한 방법을 제시하기 위한 것이고, 그것을 파라메터에 적용할 경우 대상 클래스가 Serializable해야 하는 경우 문제를 일으킬 수 있다는 것입니다.

즉, 객체를 직렬화할 때 Optional 자체가 문제가 될 수도 있다는 뜻입니다.

그 이유에 대해서 좀 더 찾아보던 중 StackOverflow에서 도움이 될 만한 질문과 대답을 찾았습니다. 이 포스트의 제목이 바로 그 질문의 제목입니다.

대답의 내용을 보면,

  1. (+) 어떤 의미론적 분석도 없이 Option 결과를 다른 메서드를 전달할 수 있다; 해당 메서드에 그것을 남기는 것(전달)은 꽤 괜찮다.
  2. (-) Optional을 파라메터로 사용하면, 메서드 내부의 조건부 로직이 일어나 비 생산적이다.
  3. (-) 인자를 Optional로 패키징해야 할 필요가 있는데, 이는 컴파일러에 대해 차선책이며, 불필요한 랩핑을 수행하게 됩니다.
  4. (-) null이 가능한 파라메터와의 비교에서 Optional이 좀 더 비용이 듭니다.

일반적으로: Optional은 해결해야 할 두 가지 상태를 통합한다. 따라서 데이터 흐름의 복잡도를 위해 입력보다는 결과(반환 값)에 더 잘 맞는다.

그 외에도 댓글들을 읽어보면 좀 더 도움이 될 것 같습니다. 그리고 채택되지는 않았지만 두번째 대답도 상당히 좋은 인사이트를 제공합니다.

RESTful API Designing guidelines - The best practices

이 포스트는 원문인 RESTful API Designing guidelines - The best practices을 번역한 것입니다. 심각한 오역이나 오글거리는 표현이 있을 수도 있으므로 주의하시기 바랍니다.

페이스북, 구글, 깃허브, 넷플릭스 그리고 다른 몇몇 기술 회사들은 개발자들과 제품에 그들의 데이터를 API를 통해 소비할 기해를 주어왔고 그들을 위한 플랫폼이 되었습니다.

여러분들이 다른 개발자들과 제품을 위한 API를 작성할 때 조차, 아름답게 만들어진 API를 갖는다는 것은 여러분의 애플리케이션에게 매우 유익합니다.

API 설계의 가장 좋은 방법에 관해 인터넷에서는 오랜 기간동안 논쟁이 있어 왔고, 가장 미묘한 것 중 하나입니다. 동일하게 정의된 공식적인 가이드라인은 없습니다.

API는 많은 개발자가 데이터와 상호 작용하는 하나의 인터페이스입니다. 잘 디자인된 API는 언제든지 사용하기 쉬우며 개발자들의 삶을 윤택하게 만들어줍니다. API는 개발자들을 위한 GUI로, 혼란스럽거나 자세하지 않다면 개발자는 대안을 찾거나 사용을 중단할 겁니다. 개발자의 경험은 API의 질을 측정하는데 있어 가장 중요한 지표입니다.

API는 무대 위에서 공연을 하는 예술가와 같으며, 사용자는 관중과 같다.

1) 용어

다음은 REST API와 관련된 가장 중요한 용어입니다

  • 리소스(resource)는 그와 관련된 데이터를 가지고 있고 그에 대해 동작하는 메서드 집합이 존재하는 것의 객체 혹은 표현입니다. 예를 들어, 동물, 학교 그리고 피고용자가 리소스가 될 수 있으며 delete, add, update는 이러한 리소스에 대해 실행되는 동작입니다.
  • 컬렉션(collections)는 리소스 집합으로, 예를 들어 CompaniesCompany 리소스의 컬렉션입니다.
  • URL(Uniform Resource Locator)는 리소스가 위치할 수 있고 어떤 동작이 리소스에 대해 실행될 수 있는 경로입니다.

2) API 엔드포인트

좀 더 알아보기 위해 Employees를 지닌 Companies에 대한 몇 가지 API를 작성해보겠습니다.

/getAllEmployess는 피고용자 목록으로 응답하는 API입니다. 다음과 같이 Company에 대한 좀 더 많은 API들이 있을 수 있습니다:

  • /addNewEmployee
  • /updateEmployee
  • /deleteEmployee
  • /deleteAllEmployees
  • /promoteEmployee
  • /promoteAllEmployees

그리고 다른 동작에 대한 이와 비슷한 다른 수 많은 API 엔트포인트들이 있을 수 있습니다. 그들 모두 많은 중복된 동작을 포함할 수 있습니다. 따라서, 이 API 엔드포인트는 API가 늘어날수록 유지하는데 부담이 될 것입니다.

무엇이 잘못됐는가?

URL은 동작이나 동사가 아닌 리소스(명사)만을 포함해야 합니다. /addNewEmployee라는 API 경로는 Employee라는 리소스 이름과 함께 addNew라는 동작을 포함하고 있습니다.

그러면 무엇이 옳은 방법인가?

/companies 엔드포인트는 동작을 포함하지 않는 좋은 예입니다. 그러나 companies라는 리소스에서 실행될 동작, 즉 추가, 삭제 혹은 갱신에 대해 서버에게 어떻게 알릴 수 있는지 의문입니다.

이 부분이 동사(verbs)라고도 불리는 HTTP 메서드(GET, POST, DELETE, PUT)가 활약할 곳입니다.

리소스는 API 엔드포인트 내에서 항상 복수(plural)여야 하며 리소스 중 하나의 인스턴스에 접근해야 하는 경우, URL 내에 아이디를 전달할 수 있습니다.

  • GET 메서드의 /companies는 전체 회사 목록을 가져와야 합니다
  • GET 메서드의 /companies/34는 34번 아이디의 회사의 상세 정보를 가져와야 합니다
  • DELETE 메서드의 /companies/34는 34번 아이디의 회사를 삭제해야 합니다

다음은 리소스 내에 리소스를 지닌 경우(예를 들어, 한 회사의 피고용자들)와 같은 다른 유스케이스의 예입니다:

  • GET /companies/3/employees는 3번 아이디 회사의 전체 피고용자 목록을 가져와야 합니다
  • GET /companies/3/employees는 3번 아이디 회사에 속하는 45번 아이디의 피고용자의 상세 정보를 가져와야 합니다
  • DELETE /companies/3/employess/45는 3번 아이디 회사에 속하는 45번 아이디의 피고용자를 삭제해야 합니다
  • POST /companies는 새로운 회사를 생성하고 생성된 새로운 회사의 세부 정보를 반환해야 합니다

이제 API가 좀 더 간결하고 조화롭지 않은가요?

결론: 경로는 리소스에 대한 복수 형태를 취해야 하며 HTTP 메서드는 리소스에 실행될 동작의 종류를 정의해야 합니다.

3) HTTP 메서드(verb)

HTTP는 리소스에 실행될 동작의 유형을 나타내기 위한 메서드 집합을 정의해왔습니다.

URL은 하나의 문장으로, 리소스는 명사이고 HTTP 메서드가 동사다.

중요한 HTTP 메서드는 다음과 같습니다:

  1. GET 메서드는 리소스의 데이터를 요청하며 어떠한 다른 부가 작용도 만들어내서는 안됩니다.
    예를 들어, /companies/3/employees는 3번 회사의 모든 피고용자 목록을 반환합니다.
  2. POST 메서드는, 대부분의 경우 웹 폼이 제출된 경우, 데이터베이스 내에 리소스를 생성하도록 서버에 요청합니다.
    예를 들어, /companies/3/employees는 3번 회사의 새로운 피고용자를 생성합니다. POST는 멱등적이지 않으며 이는 여러 번의 요청이 다른 효과를 만들어낸다는 것을 의미합니다.
  3. PUT 메서드는 리소스를 갱신하거나, 리소스가 존재하지 않는 경우 생성하도록 서버에 요청합니다.
    예를 들어, /companies/3/employees/john은 서버에 3번 회사의 피고용자 컬렉션 내 john이라는 리소스를 갱신하거나 존재하지 않는 경우 생성하도록 요청합니다. PUT은 멱등적이며 이는 여러 번 요청하더라도 동일한 효과를 갖는다는 것을 의미합니다.
  4. DELETE 메서드는 리소스 혹은 그것의 인스턴스가 데이터베이스에서 제거되도록 요청합니다.
    예를 들어, /companies/3/employees/john/은 3번 회사의 피고용자 컬렉션에서 john이라는 리소스를 삭제하도록 서버에 요청합니다.

다른 포스트에서 알아볼 몇 가지 다른 메서드들도 있습니다.

4) HTTP 응답 상태 코드

클라이언트가 API를 통해 서버에 요청을 할 때, 클라이언트는 실패했는지, 전달되었는지 혹은 요청이 잘못되었는지 등 피드백을 받아야 합니다. HTTP 상태 코드는 다양한 시나리오에서의 다양한 원인을 지닌 표준화된 코드 뭉치입니다. 서버는 항상 올바른 상태 코드를 반환해야 합니다.

다음은 HTTP 코드에 대한 중요한 카테고리입니다:

2xx (성공 범주)

이 상태 코드들은 요청된 동작이 수신되었고 서버에 의해 성공적으로 처리되었다는 것을 나타냅니다.

  • 200 OK GET, PUT 혹은 POST에 대한 성공을 나타내는 표준 HTTP 응답
  • 201 Created 새로운 인스턴스가 생성된 경우라면 이 상태 코드가 반환되어야 합니다. 예를 들어, POST 메서드를 사용해 새로운 인스턴스 생성 시, 201 상태 코드를 반환해야 합니다.
  • 204 No Content 요청이 성공적으로 처리되었지만 반환할 내용이 없다는 것을 나타냅니다. DELETE가 좋은 예입니다.
    DELETE /companies/43/employees/2는 2번 피고용자를 삭제하는데, 명시적으로 시스템에 삭제를 요청한 것이므로, API의 응답 본문 내에 어떤 데이터도 반환할 필요가 없습니다. 만약 employee 2가 데이터베이스 내에 존재하지 않는 것처럼, 오류가 있다면, 응답 코드는 2xx Success 범주가 아닌 4xx Client Error 범주가 되어야 합니다.

3xx (리다이렉션 범주)

  • 304 Not Modified 클라이언트가 캐시 내에 이미 응답을 지니고 있다는 것을 의미합니다. 따라서 동일한 데이터를 다시 전송할 필요가 없습니다.

4xx (클라이언트 오류 범주)

이 상태 코드들은 클라이언트가 잘못된 요청을 했다는 것을 나타냅니다.

  • 400 Bad Request 클라이언트가 요청한 바를 서버가 이해하지 못해 요청이 처리되지 않았다는 것을 의미합니다.
  • 401 Unauthorized 클라이언트가 리소스에 접근할 수 없다는 것을 의미하며, 요구되는 자격과 함께 다시 요청해야 합니다.
  • 403 Forbidden 요청이 유효하며 클라이언트가 인증되었지만, 클라이언트가 어떤 이유로 페이지나 리소스에 접근할 수 없다는 것을 의미합니다. 예를 들어, 인증된 클라이언트는 때때로 서버의 디렉토리에 접근할 수 없을 때가 해당됩니다.
  • 404 Not Found 요청된 리소스가 현재 이용 불가능하다는 것을 나타냅니다.
  • 410 Gone 요청된 리소스가 의도적으로 옮겨져서 더 이상 이용할 수 없다는 것을 나타냅니다.

5xx (서버 오류 범주)

  • 500 Internal Server Error 요청은 유효하나 서버가 요청을 전혀 처리할 수 없으며 예상치 않은 상태에 대응할 수 없다는 것을 의미합니다.
  • 503 Service Unavailable 서버가 다운되었거나 수신할 수 없어서 요청을 처리할 수 없다는 것을 의미합니다. 대부분 서버를 유지보수 중인 경우에 해당됩니다.

5) 필드 이름의 대소문자 관례

어떠한 대소문자 관례든지 따를 수 있지만, 애플리케이션 상에서는 일관성을 유지해야 합니다. 요청 본문 혹은 응답 타입이 JSON이라면 일관성을 유지하기 위해 camelCase를 따르시기 바랍니다.

6) 검색, 정렬, 필터링 그리고 페이지네이션

이 동작 모두 하나의 데이터셋에 대한 쿼리입니다. 이 동작을 처리하기 위한 새로운 API 집합은 존재하지 않습니다. GET 메서드 API에 쿼리 파라메터를 추가해야 합니다.

이러한 동작들의 구현 방법에 대한 예를 알아보도록 하죠.

  • 정렬 클라이언트가 정렬된 회사 목록을 얻으려고 하는 경우, GET /companies 엔드포인트는 쿼리 내에서 여러 정렬 파라메터에 접근해야 합니다. 예를 들어, GET /companies?sort=rank_asc는 회사 목록을 순위에 의해 올림차순으로 정렬합니다.
  • 필터링 데이터셋을 필터링하기 위해, 쿼리 파라메터를 통해 다양한 옵션을 전달할 수 있습니다. 예를 들어, GET /companies?category=banking&location=india는 인도에 위치한 은행 카테고리의 회사로 회사 목록 데이터를 필터링합니다.
  • 검색 회사 목록 내에서 회사 이름으로 검색할 때 API는 GET /companies?search=Digital Mckinsey가 되어야 합니다.
  • 페이지네이션 데이터셋이 너무 큰 경우, 데이터를 더 작은 조각을 나누어, 성능을 향상시키는데 도움을 주고 응답을 처리하기 더 쉽도록 해야 합니다. 예를 들어, GET /companies?page=23는 23번째 페이지의 회사 목록을 가져온다는 것을 의미합니다.

GET 메서드 내에 많은 쿼리 파라메터가 추가되어 URI가 너무 길면, 서버가 401 URI Too long이라는 HTTP 상태로 응답할 수 있으며, 이런 경우 파라메터를 POST 메서드의 요청 본문 내로 전달할 수 있습니다.

7) 버저닝

여러분의 API가 세계 곳곳에서 소비되고 있고, 어떤 큰 변화로 API가 업그레이드되었다면, 여러분의 API를 사용 중인 기존의 제품 혹은 서비스에 장애를 일으킬 수 있습니다.

http://api.yourservice.com/v1/companies/34/employees는 경로 내에 API의 버전 번호를 지닌 좋은 예입니다. 주요 업데이트가 있는 경우, 새로운 API 집합을 v2 혹은 v1.x.x로 이름지을 수 있습니다.

이 가이드라인은 개발에 대한 저의 경험으로 만들어졌습니다. 위에서 언급한 조언들에 대한 여러분들의 관점을 알고 싶습니다. 댓글을 남겨 제가 알도록 해주세요!

이 포스트가 좋다면, 공유하고 댓글을 남겨주시기 바랍니다.

Scala Type Bounds: Upper Bounds, Lower Bounds and View Bounds

원문: Scala Type Bounds: Upper Bounds, Lower Bounds and View Bounds

이전 포스트에서, 스칼라 변성에 관해 자세히 알아보았습니다. 이번 포스트에서는, "스칼라 타입 바운드"에 대해서 알아보도록 하겠습니다.

스칼라의 타입 바운드란 무엇인가?

스칼라의 타입 바운드는 타입 파라메터 혹은 타입 변수에 대한 제한을 말합니다. 타입 바운드를 사용하여, 타입 변수의 범위(limit)을 정의할 수 있습니다.

스칼라 타입 바운드의 이점

스칼라 타입은 다음과 같은 이점을 제공합니다:

  • 타입에 안전한 애플리케이션 개발

스칼라 타입 바운드

스칼라는 다음과 같은 타입 변수의 타입 바운드를 지원합니다:

  • 스칼라 상위 바운드
  • 스칼라 하위 바운드
  • 스칼라 뷰 바운드

다음 섹션에서 예제를 가지고 이런 개념들에 대해 좀 더 자세히 알아보도록 하겠습니다.

스칼라 상위 바운드

스칼라에서, 아래 그림처럼 타입 파라메터에 상위 바운드를 정의할 수 있습니다.

설명

여기서 T는 타입 파라메터이고 S는 타입입니다. [T <: S]와 같이 상위 바운드를 선언한다는 것은 타입 파라메터 TS와 동일하거나 S의 하위 타입이 되어야 한다는 것입니다.

예제1

[T <: Ordered[T]]

여기서 우리는 타입 파라메터 T에서 Ordered[T] 타입으로의 상위 바운드를 정의했습니다. 그러면 TOrdered 혹은 Ordered 타입의 하위 타입됩니다.

예제2

다음은 스칼라 상위 바운드를 설명하기 위한 스칼라 프로그램입니다.

class Animal  
class Dog extends Animal  
class Puppy extends Dog

class AnimalCarer{  
  def display [T <: Dog](t: T){
    println(t)
  }
}

object ScalaUpperBoundsTest {  
  def main(args: Array[String]) {

    val animal = new Animal
    val dog = new Dog
    val puppy = new Puppy

    val animalCarer = new AnimalCarer

    //animalCarer.display(animal)
    animalCarer.display(dog)
    animalCarer.display(puppy)
  }
}

이 프로그램은 다음 라인을 주석 처리해야 잘 동작합니다.

해당 라인의 주석을 제거하고 다시 해보면, 컴파일 오류가 발생할 겁니다. 왜냐하면, 아래와 같이 상위 바운드를 정의했기 때문이죠:

class AnimalCarer{  
  def display [T <: Dog](t: T){
    println(t)
  }
}

여기서 [T <: Dog]를 정의했는데 이것은 display 메서드가 Dog 클래스의 객체 혹은 Dog 클래스의 하위 타입(즉, Puppy)만 받아들인다는 것을 의미합니다. 그것이 Dog 슈퍼 클래스를 전달하면 "Type Mismatch" 컴파일 오류가 발생하는 이유입니다.

스칼라 하위 바운드

스칼라에서, 아래 보이는 것처럼 타입 파라메터에 하위 바운드를 정의할 수 있습니다:

설명

여기서 T가 타입 파라메터고 S가 타입입니다. [T >: S]처럼 하위 바운드를 설정한다는 것은 해당 타입 파라메터 TS와 같거나 S의 상위 타입이어야 한다는 것을 의미합니다.

예제1

[T >: Ordered[T]]

여기서 타입 파라메터 T에서 Ordered[T] 타입으로의 하위 바운드를 정의했습니다. 그러면 TOrdered 혹은 Ordered 타입의 상위 타입이 되어야 합니다.

예제2

class  Animal  
class Dog extends Animal  
class Puppy extends Animal

class AnimalCarer{  
  def display [T >: Puppy](t: T){
    println(t)
  }
}

object ScalaLowerBoundsTest {  
  def main(args: Array[String]) {

    val animal = new Animal
    val dog = new Dog
    val puppy = new Puppy

    val animalCarer = new AnimalCarer

    animalCarer.display(animal)
    animalCarer.display(puppy)
    animalCarer.display(dog)
  }
}

여기서 DogPuppy의 하위 타입이 아니지만, DogAnimal의 항위 타입이고 아래 보이는 것처럼 타입 파라메터 T에 "하위 바운드"를 정의했기 때문에 이 프로그램은 여전히 잘 동작합니다.

class AnimalCarer{  
  def display [T >: Puppy](t: T){
    println(t)
  }
}

이 클래스에서 하위 바운드 정의를 제거하면, 컴파일 오류가 발생하게 됩니다.

스칼라 뷰 바운드

스칼라에서, 뷰 바운드는 기존의 암시적인 변환을 자동으로 사용하고자 할 때 사용됩니다. 아래 보이는 것처럼 타입 파라메터에 뷰 바운드를 정의할 수 있습니다:

예제

다음은 (Int10 > 12와 비슷한) 관계 연산자를 가지고 문자열을 비교하는 스칼라 프로그램입니다.

class Person[T <% Ordered[T]](val firstName: T, val lastName: T) {  
  def greater = if (firstName > lastName) firstName else lastName
}

object ScalaViewBoundsTest {  
  def main(args: Array[String]) {
    val p1 = new Person("Rams","Posa")
    val p2 = new Person("Chintu","Charan")

    println(p1.greater)
    println(p2.greater)
  }
}

출력

Rams  
Chintu  

만약 스칼라 뷰 바운드 연산자인 <%를 사용하지 않는다면, 다음의 에러 메시지를 보게 될 겁니다.

error: value > is not a member of type parameter T  

이것이 스칼라의 상위 바운드, 하위 바운드 그리고 뷰 바운드의 전부입니다. 다가올 포스트에서 스칼라의 컨셉에 대해서 좀 더 알아보도록 하겠습니다.

포스트에 대해 이슈나 제안이 있다면 코멘트를 남겨주시기 바랍니다.