[구글스터디잼] Kubernetes in the Google Cloud #1

해당 글은 현재 하고 있는 구글 스터디잼 Kubernetes in the Google Cloud 을 학습하는 과정에서 배우는 것을 정리하는 글입니다.

총 10개의 챕터가 있고 일단은 가장 처음 두 개인, Introduction to Docker, Hello Node Kubernetes 를 공부했는데…

1] Introduction to docker
-> 실제로 Docker 책을 보면 쉽게 보게되는 명령들을 소개해줍니다. 즉 docker 를 실행시키거나, 현재 수행중인 container 에 접속한다거나, 현재의 수정본을 docker hub 등의 외부 registry에 등록하는 방법(사실 여기서는 gcloud 를 이용해서 gcp내의 registry에 등록하더군요.)

/node-app:0.2

사실 이미 docker 를 사용해 보신 경험이 있다면, 특별한 차이를 느끼지 못하실꺼 같습니다. 다른 차이는 실습을 하게 되는 환경에서 이미 docker/kubenetes 등이 다 설치되어 있어서… 아주 편하게 실습을 할 수 있다는 것 정도… 일단 제목이 Kubernetes in the Google Cloud 인거 처럼 kubenetes 내용은 2장 부터 시작됩니다.

2] Hello Node Kubernetes

Kubernetes 는 Container Orchestration Tool 이라고 볼 수 있습니다.(즉 컨테이너를 관리해주는?) 여기서는 간단히 kubenetes 위에서 node application을 배포하고 Rolling update 하는 것을 보여주게 됩니다.

2-1] Cluster 의 생성
다음과 같은 과정을 통해서 클러스터를 생성하게 됩니다. 클러스터가 생성된다고 이미지가 돌고 있거나 그러지는 않고, 클러스터만 실행된다고 생각하면 됩니다.

#project 이름 설정
gcloud config set project PROJECT_ID

#hello-world 라는 이름으로 cluster 생성, 노드는 2개, 머신 타입은 n1-standard-1, 생성되는 존은 us-centrall-a 입니다.
gcloud container clusters create hello-world \
                --num-nodes 2 \
                --machine-type n1-standard-1 \
                --zone us-central1-a


2-2] Pod 의 생성
생성된 클러스터에서 이제 실제로 container를 실행하게 됩니다.

#hello-node 라는 이름으로 컨테이너 실행
kubectl run hello-node \
    --image=gcr.io/PROJECT_ID/hello-node:v1 \
    --port=8080

#kubectl get deployments 로 현재 deployment 상황을 볼 수 있고
kubectl get deployments

#kubectl get pods 로 현재 수행중인 pod를 볼 수 있습니다.
kubectl get pods

2-3] container 외부에 노출하기
갓 만들어진 컨테이너를 외부에 서비스하기 위해서는 외부에서 접속을 할 수 있어야 합니다. 하지만 처음 생성되었을때는 kubenetes 내부에서만 연결이 되고 외부에서는 접속이 안될 것입니다. 이럴 때 특정 pod는 외부에서 접속할 수 있어야 하는데, 이러면 외부 IP를 가지거나 , 외부에서 해당 pod에 연결할 수 있는 proxy가 실행되어야 할껍니다.(아니면 기존 proxy의 설정이 바뀌거나…)

#외부에 노출하기
kubectl expose deployment hello-node --type="LoadBalancer"

#어떤 서비스들이 있는지 확인
kubectl get services
#결과의 EXTERNAL-IP를 통해서 외부 IP를 확인할 수 있습니다.

2-3] container scaling(개수 변경)
최초에는 container 가 하나만 실행되는데, 이 개수를 바꾸거나 하고 싶을때 어떻게 해야하는지 설명합니다.

#실행되는 컨테이너의 개수를 4개로 바꾸기
kubectl scale deployment hello-node --replicas=4

#kubectl get deployment를 하면 아까와 다른 결과를 볼 수 있습니다.

다만 이 과정과정가 눈깜짝할 사이에 끝나지는 않습니다. 저는 실험결과 몇초에서 몇분 정도 지나야 이제 제대로 적용되는 걸 볼 수 있습니다.

이제 쿠버네티스의 상태는 다음과 비슷합니다.
kube1

실제로 다음에 해보면 좋을것 같은 부분…
scale 은 실제로 서비스를 할 때, 중요한 부분이므로, 자신들의 서비스를 넣고, 롤링 업데이트나 scale을 바꿀때, 실제로 영향이 어떻게 되는지를 확인하시는게 좋을듯 합니다.

Advertisements

[입 개발] EXT4 에서 달라진 부분들 #2 – 데이터 영역의 관리

EXT4가 들어오면서 크게 달라진 부분 그 두 번째는, 데이터 영역을 관리하는 방법입니다. 일단 기존에서 사용하던 방식을 알아보도록 하겠습니다. 파일시스템의 특성을 볼때는, 그 시기에 있던 기술의 한계를 알면 도움이 되는데, 최초에 ext가 나오던 시기는 그렇게 큰 파일이 많지는 않던 시기입니다. 즉 엄청 큰 파일을 처리할 일이 많지는 않던 시기입니다. 그리고 기본적으로 ext2~3 까지는 하나의 파일의 최대 크기는 4 bytes 변수를 사용하기 때문에 최대 4기가 한계입니다.(블럭의 크기와 상관없이 일단 최대한 가졌을 때 표현할 수 있는 한계가 4기가입니다. 파일 크기를 나타내는 변수가 4 bytes 이기 때문이죠.)

여기서 보통 파일의 종류에 따라서 File/Directory 에서 File이면, 실제 데이터 영역에 파일의 내용이 있고, 디렉토리면, 해당 디렉토리에 있는 파일 목록에 대한 정보가 데이터 영역에 관리가 되게 됩니다. 즉, “I am a boy” 라는 내용을 가진 파일이 있다면, 그 파일의 크기는 10이고, block 크기가 4k라면, 한 개의 block만 할당이 되어있을것입니다. 그래서 데이터 영역에 가면 처음 10바이트가 “I am a boy”가 들어있게 됩니다. 이걸 관리하는 정보는 EXT는 inode에 있고, ext2~3에서는 그 크기는 총 60바이트입니다. 그리고 블럭을 가리키는 정보는 하나가 4 bytes 입니다. 즉, 60바이트의 공간이 할당되어 있는데, 한 칸이 4 bytes 면 총 15개의 정보를 저장하는 공간이 들어갈 수 있습니다.(FAT32 를 안다면 Fat Table 을 생각하시면 쉽습니다.)

그림1

그런데 여기서 잘 생각해보면, 그림1 처럼 한칸이 4 bytes라 하나의 블럭을 가리키면… 60바이트로는 총 15개… 한 칸이 하나의 블럭을 가리키므로, block 크기가 4k면 4k * 15 = 60k 밖에 안됩니다.(아까는 4기가가 최고라며!!!) 어떻게 된 것일까요? 사실 EXT2/3의 특징 중에 하나가 direct, indirect block, double indirect block, triple indirect block 으로 구성이 된다는 것입니다.

먼저 시작하기 전에 inode에 들어있는 15 개의 칸은… 12개의 direct와 각각 하나씩의 indirect, double indirect, triple indirect 로 구성이 되어있습니다. 먼저 direct 는 그 칸 하나가 그림2처럼 실제 디스크의 블럭하나를 가리키는 것입니다. direct 로는 최대 60k 밖에 파일이 가질 수가 없으므로, 이 사이즈를 더 크게 사용하기 위한 것들이 위한 indirect, double indirect, triple indirect 방식입니다.

그림2

12개니 각 블럭이 4k면 48k의 공간을 지정할 수 있습니다. 그렇다면 13번째 칸의 indirect 는 뭐냐, indirect 가 가리키는 블럭으로 가면, 아까 12개의 direct block을 가리키는 정보가 있던 것과 같은 형태로 실제 디스크를 가리키는 블럭이 direct block이 나옵니다. 블럭 크기가 4k면 여기에 실제 디스크의 블럭을 가리킬 수 있는 정보가 각각 4 byte니 1024개가 들어가게 됩니다. 즉, indirect block은 실제로 direct 블럭 1024개를 가리키므로 4M의 공간을 지정할 수 있습니다. 그림3을 참고합시다.

그림3

그럼 이제 슬슬 이해가 가기 시작할 것입니다. indirect 블럭은 해당 블럭이 direct block을 어드레싱 하는 주소 정보가 들어가 있고, double indirect 블럭이 가리키는 정보는 indirect 블럭을 가리키는 블럭 1024개를 가리키는 블럭에 대한 정보가 됩니다. 그림4를 참고하면, 실제로 double indirect 블럭의 내용은 indirect block 정보들입니다.

그림4

여기서는 4M 짜리 1024개를 가리키므로 4G 의 디스크 정보를 가리킬 수 있습니다. 이쯤되면 이제 triple indirect 의 성격도 눈치 채실 껍니다.
triple indirect block이 가리키는 정보는 double indirect block 1024개를 가리키는 정보입니다. 그림5를 참고하면 됩니다. 그런데 여기서
의아한 것은 사실 double indirect block 만으로도… 실제 파일의 최대 크기인 4G가 넘어간다는 것입니다.(물론 블럭 크기가 4K 일 경우입니다. 1K로 설정되면 이 마지막 블럭을 사용하게 되겠지만… 블럭 크기가 4k면… 마지막 triple indirect 는 사용할 이유가 없습니다.)

블럭 크기가 4k 일때, triple indirect block은 4G * 1024 = 4TB 의 데이터 영역을 가리킬 수 있습니다. 그림5를 참고합시다.

그림5

그런데 ext4로 가면서 왜 이런 구조를 버리는 것일까요? 간단하게 생각해보면 해당 구조는 낭비가 너무 심합니다. 혹시나 FAT32에서 NTFS로 가면서 내부구조가 어떻게 바뀌었는지 이해한다면, 실제로 EXT4의 extend를 이해하는 것은 굉장히 수월합니다.

먼저 간단하게 생각해봅시다. 1000, 1001, 1002, 1003 4개의 블럭이 있다고 할 때 이걸 가리키는 방법으로 ext2에서의 기존 방식은 하나에 한칸을 가리켜야 하므로 총 4칸 16 바이트가 필요합니다. 그런데 이렇게 표현할 수 있지 않을까요? (시작위치, 블럭 개수), 이런 형태면 다음과 같은 (1000, 4) 형태로, 표현이 가능해집니다. 데이터 공간도 각각 4 바이트를 쓴다고 하더라도 8바이트면 줄어듭니다. 물론 매번 필요한 공간이 Fragmentation 이 발생한다면 (1000, 1), (1001, 1), (1002, 1), (1003, 1) 형태로 공간을 낭비하게 됩니다. 즉 연속된 공간이 많이 필요할 수록, 새로운 표현 방법이 공간을 절약할 수 있습니다. 또한 추가 분석할 필요 없이도 쉽게 뒤에 얼마만큼 읽어야 할지도 알 수 있게 됩니다.

이런 방식을 적용한것이, ntfs의 cluster runs 나, ext4의 extent 라는 구조입니다. 그런데 extent는 (시작위치, 블럭 개수) 의 구조에서 블록 개수에는 2 byte만 할당되어 있습니다. 그래서 block 크기가 4k 기준일 때 무조건 32768 즉 4k * 32768 = 128MB 이상되면 아무리 디스크에 연속적으로 할당된 공간이 있더라도 extend가 추가로 생기게 됩니다. 즉 1GB 파일이면 8개는 extent가 생겨야 합니다.

그런데 extent구조를 보면 헤더가 일단 12 바이트이고, 리프 노드(실제 파일의 정보를 가리키는)냐, 인덱스 노드(리프 노드나 다른 인덱스 노드의 정보를 가지는)냐에 따라 가지는 정보들이 각각 12 bytes입니다. 즉… 60바이트에서 헤더를 빼고나면 일단 inode 안에는 4개의 리프 노드만 들어갈 수 있습니다. 즉 512MB 보다 파일 사이즈가 커지거나 fragmentation 이 많이 나면… 결국 tree 형태로 관리되는 정보가 실제 다른 블럭에 들어가야만 합니다.

그림6

그래서 실제 구조는 그림6처럼 되게 됩니다. 그리고 이 extent도 실제로는 해당 파일의 inode의 flag에 USE_EXTENT 가 설정되어 있어야 사용하게 됩니다. 즉 ext4 내에 있는 파일이 어떤 건 extent 형태로, 어떤건 옛날 방식으로도 주소 지정이 가능하다라는 것입니다. 다음번에는 기존에 왜 한 디렉토리에 파일이 많으면, 파일 열기등이 느려지는지, ext4에서는 어떻게 풀고 있는지 가볍게 살펴보도록 하겠습니다.

[입 개발] EXT4 에서 달라진 부분들 #1 – Flexible Block Group

사실 EXT4는 나온지 굉장히 굉장히 오래된 파일 시스템입니다. EXT의 역사를 보면 간략히 다음과 같습니다.

  • EXT, 1992년 출시
  • EXT2, 1993년 출시
  • EXT3, 2001년 커널 2.4.15에 포함
  • EXT4, 2006년 커널 2.6.19에 포함
  • 2008년 커널 2.6.28에 EXT4 안정 버전 포함

한마디로 굉장히 오래되었습니다. 거의 10년이 지났네요. 그런데 아는 분들은 다 아시지만, 그래도 모르는 사람들은
EXT2 -> EXT3 -> EXT4로 넘어가면서 무엇이 변했는지 잘 모릅니다. 그냥 좋아졌다?

EXT4로 들어오면서 크게 바뀐것들이 좀 있는데, 그 중에서도 Flexible Block Group, Extent를 위한 HTree,
많은 수의 파일 목록에서 쉽게 파일을 찾기 위한 Hash Directories 등이 있습니다.

일단 생각은 해당 EXT4 시리즈에서는 크게 위의 세 가지만 다뤄볼려고 합니다. 자세한 것은 당연히 다루지 않습니다.
왜냐… 잘모르니깐…

Flexible Block Group 을 설명하기 위해서는 먼저 EXT에서 Disk Layout 을 어떻게 구성하는가 부터 알아야
합니다. EXT에서는 Block Group 이라고 해서 특정 사이즈 단위로 디스크를 나눠서 관리 정보를 가지고 있다고
보시면 됩니다.

그림1

즉 그림1 처럼 하나의 디스크를 여러개의 Block Group 으로 나누게 됩니다. 보통 하나의 Block Group 은 128MB
할당 됩니다. 그럼 하나의 Block Group 은 어떻게 구성되는가? 그림2 처럼 각 Block Group 은 전체 정보를 가지는
Super Block 과 GDTs, Reserved GDTs를 가지게 됩니다. 이 정보들은 주로 Block Group 0번의 것을 사용하지만
백업용으로 다른 Block Group 들에도 존재합니다. 이 것 이외에 Block Bitmap, Inode Bitmap, Inode Table,
Data 영역은 각 Block Group 단위로 관리되는 정보들입니다. 이 때 보면 Block Bitmap, Inode Bitmap은 하나의
Block 만 할당 받기 때문에 Block의 크기가 4K 라면 bit 단위로 총 32k 개를 관리할 수 있습니다. 그래서 32k * 4K
해서 하나의 Block Group 의 크기가 128MB로 보통 설정되게 됩니다.

그림2

그런데 이 구조의 단점은 무엇일까요? 작은 크기의 파일은 큰 문제가 없는데, 128MB(실제로는 이것보다 조금 더 작은 크기의)
를 넘어가는 파일부터는 Block Group 하나에 저장되지 않아서, 무조건 Disk 에 물리적으로 Fragmentation 이 발생하게 됩니다.
즉 연속적으로 읽을 수 없고, Random Access 가 발생해야 한다는 것이죠. 그리고 현재에는 사실 128MB 단위는 생각보다
많이 작습니다.

그럼 결국 Flexible Block Group 은 뭐냐? 몇개의 Block Group 들을 하나로 묶자는 것이 메인 아이디어 입니다.
그래서 그림3 처럼 각 Block Group 의 Block Bitmap, Inode Bitmap 등이 쭈욱 연결되서 하나처럼 관리되게 됩니다. 여기서는
8개의 Block Group 이 하나처럼 관리되는 것으로 가정했습니다.

그림3

정리하자면, 각 Block Group 에 있던, Block Bitmap, Inode Bitmap, Inode Table 영역들이 쭈욱 연결되는 형태로 들어가게 됩니다.
0번 Block Group 에 다른 Block Group 들의 메타정보(Block Bitmap, Inode Bitmap, Inode Table) 들이 다 넘어오게 되는 것이죠.
그림2와 비교해보시면 그 차이가 명확하게 보이실 겁니다.

주의 할 것은 EXT4에서 Flexible Block Group 은 무조건 사용하는 것이 아니라, 헤더에 FLEX_BG 라는 Flag가 설정되어 있어야만 Flexible Block Group 을 사용 가능합니다. 즉, 이 Flag가 꺼져있다면, 예전 방식대로 처리해야한 합니다.

다음번에는 EXT4에서 extents를 어떻게 관리하는지, 살펴보도록 하겠습니다.

EXT4 에 대해서 Ext4 Disk Layout 이라는 매우 좋은 자료가 있습니다. 자세한 건 이걸 참고하세요.

[입 개발] Redis 에서 zadd 와 zincrby 의 차이

안녕하세요. 입개발 CharSyam입니다. 오래간만에 포스팅을 하게 되네요. 오늘은 아주 간단한 것을 포스팅 할려고 합니다. 가끔씩 Redis의 sorted set 을 사용하는 명령중에 zadd 와 zincrby 가 있습니다. 과연 이 두 개의 명령은 어떤 차이가 있을까요?

결론부터 말하자면, zadd 와 zincrby 는 사실 같은 기능을 사용하는 아주 유사한 명령입니다. 먼저 zadd 는 다음과 같이 사용하니다.

zadd key score member

그러면 sorted set 에 member 가 해당 score를 가지게 됩니다. 여기서 가지게 됩니다를 중요하게 봐주세요.

그리고 zincrby 는 특정 값 increment 만큼 score를 증가시키는 명령입니다.

zincrby key increment member

그렇다면 다음 두 가지 경우로 나눠보도록 하겠습니다. 현재 해당 key에 member가 존재하지 않는 경우와 존재하는 경우입니다. 먼저 존재하지 않을 경우, 두 개의 명령 zadd 와 zincrby 는 모두 해당 member를 생성하게 됩니다. 그리고 zadd 는 해당 score로 설정하고 zincrby 는 0에 해당 increment를 추가한 것 처럼 동작합니다.(즉 둘 다 해당 값으로 설정하게 되는거죠.)

그렇다면 해당 member가 존재할 경우는 어떻게 될까요? zadd는 해당 member의 score를 현재 넘겨준 값으로 변경시켜 버립니다. 즉 기존에 3이 었는데 zadd key 1 member 라면 해당 member 의 score 는 3에서 1로 변경이 됩니다. zincrby 의 경우는 zincrby key 1 member 라면 기존 값이 3이 있었다면, 이제 3에 1을 더하게 되어서 4가 되게 됩니다.

코드를 보면 zadd 와 zincrby 는 Flag 하나만 다르고 동일한 함수를 사용합니다.

void zaddCommand(client *c) {
    zaddGenericCommand(c,ZADD_NONE);
}

void zincrbyCommand(client *c) {
    zaddGenericCommand(c,ZADD_INCR);
}

그리고 zaddGenericCommand를 보면 zsetAdd 를 호출하게 되는데… zadd는 모두 기존 값을 가져와서, score 를 비교합니다. 그래서 zincrby 에서 ZADD_INCR 설정되면 new_score 로 기존값 + increment로 설정하고, zadd 에서는 new_score 를 넘겨준 설정값으로 설정하게 됩니다.

            /* Prepare the score for the increment if needed. */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    return 0;
                }
                if (newscore) *newscore = score;
            }

            /* Remove and re-insert when score changed. */
            if (score != curscore) {
                zobj->ptr = zzlDelete(zobj->ptr,eptr);
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                *flags |= ZADD_UPDATED;
            }

그리고 score가 변경되면 sorted set 은 기존 member를 지우고 다시 insert 하는 식으로 동작하게 됩니다. 결국 zadd 와 zincrby 는 거의 다른게 없습니다.

[입 개발] spring-security-oauth의 RedisTokenStore의 사용은 서비스에 적합하지 않습니다.

안녕하세요. 입개발 CharSyam 입니다. 저의 대부분의 얘기는 한귀로 듣고 한귀를 씻으시면 됩니다.(엉?) 일단 제목만 보면, 많은 Spring 유저들에게, 저넘의 입개발, 스프링도 모르면서라는 이야기를 들을듯 합니다.(아, 아이돌 까던 분들이 집단 따돌림을 당할 때의 느낌을 미리 체험할 수 있을듯 합니다. – 강해야 클릭율이 올라가는!!!)

먼저, 내용을 시작하기 전에 저는 Spring 맹, Java 맹으로 무식하다는 걸 미리 공개하고 넘어갑니다. 흑흑흑 (나의 봄님이 이럴 리 없어!!!)

일단 아시는 분이, Redis 와 oauth가 궁합이 안맞느냐라는 이야기를 들으면서 시작하게 됩니다. 아니, Redis와 oauth는 철자부터 다른데 무슨 얘기십니까? 라는 질문을 하다보니, Redis 에 과부하가 발생되서 처리가 잘 안된다는 얘기였습니다. 물론 수 많은 이유로 Redis가 느려질 수 있기 때문에, 자세한 정보를 요청했더니, spring-security-oauth 를 이용하고 계시다는 이야기를 들었습니다. 시간이 지나갈 수록 점점 느려진다는 느낌을 받고, 어떨 때는 Redis가 처리를 못한다고… 일반적인으로 아주 일반적으로 Redis는 아주 짧은 get/set 등의 요청은 초당 8~10만 정도는 가볍게 처리가 가능합니다.(일단 어떤식으로 문제에 접근했는가는 다음 번 주제로 미르고)

결론부터 얘기하자면, 당연히 사용하는 사람의 어느정도 실수가 발생하기 때문이긴 하지만, spring-security-oauth 의 RedisTokenStore 는 서비스에서 장애를 일으키기 쉽습니다.(보통은 라이브러리보다는 우리의 문제를 확인하는 것이 첫번째, 두번째도 우리문제, 세번째는 내 잘못인지 확인이… 정답입니다.)

먼저 Redis 는 Single Threaded 입니다. 즉, 하나의 명령이 많은 시간을 소모하면 그 동안은 아무런 작업을 하지 못합니다. 즉 Redis를 잘 쓰고 적합하게 이용하는 것은, 명령어를 빨리 수행해서 결과를 빨리 줄 수 있는 상황에서 이용하는 것이 적합하다는 것입니다.

사실 spring-security-oauth 자체가 큰 문제라기 보다는, 이 코드를 작성한 분은 사용자가 이런식으로 사용하게 되면 문제가 될 것이라는 고려를 하지 않은 것이 가장 큰 이슈입니다. 내부적으로 RedisTokenStore는 단순히 키 자체를 저장하는 get/set 커맨드와, 현재까지 발급되었던 키 정보를 저장하는 List 자료구조를 쓰고 있습니다. 아래 보면 rPush를 사용하는 approvalKey 와 clientId 입니다.

@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
......
    if (!authentication.isClientOnly()) {
	conn.rPush(approvalKey, serializedAccessToken);
    }
    conn.rPush(clientId, serializedAccessToken);
......
}

그런데 저 approvalKey와 clientId는 OAuth2Authentication -> OAuth2Request -> getClientId() 함수를 통해서 만들어집니다. 그런데 여기서 흔히들 하는 실수가 getClientId를 동일한 값으로 셋팅하는 것입니다. 그렇게 되면, 단순히 key가 있는지 확인할 때 말고 해당 List에 위와 같이 데이터가 들어가는 경우는 엄청나게 많은 아이템을 가진 List 자료구조가 생길 가능성이 높습니다.

그런데 Redis의 List 자료구조는 앞이나 뒤로 넣고, 앞이나 뒤에서 빼는 것은 빠르지만 O(1), 그 안의 데이터를 찾거나 모두 가져오면 결국 선형 탐색이 일어납니다. O(N), 그러면 그 안에 백만개 천만개가 들어있다고 가정하면 엄청난 속도 저하를 가져오게 됩니다. 그리고 그 안에 다른 명령을 하나도 처리할 수 가 없게됩니다.

아래 findTokensByClientIdAndUserName 와 findTokensByClientId 두 개의 함수는 대표적으로 모든 아이템을 가져오도록 하고 있습니다. 안쓰는걸로 하셔야 합니다.

	@Override
	public Collection findTokensByClientIdAndUserName(String clientId, String userName) {
		byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(approvalKey, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

	@Override
	public Collection findTokensByClientId(String clientId) {
		byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
		List byteList = null;
		RedisConnection conn = getConnection();
		try {
			byteList = conn.lRange(key, 0, -1);
		} finally {
			conn.close();
		}
		if (byteList == null || byteList.size() == 0) {
			return Collections. emptySet();
		}
		List accessTokens = new ArrayList(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections. unmodifiableCollection(accessTokens);
	}

위의 함수들은 안쓴다고 하더라도, 아래와 같이 removeAccessToken 함수는 자주 불리는데 여기서도 lRem을 통한 선형 탐색이 발생합니다.(lRem이 문제…)

	public void removeAccessToken(String tokenValue) {
		byte[] accessKey = serializeKey(ACCESS + tokenValue);
		byte[] authKey = serializeKey(AUTH + tokenValue);
		byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
		RedisConnection conn = getConnection();
		try {
			conn.openPipeline();
			conn.get(accessKey);
			conn.get(authKey);
			conn.del(accessKey);
			conn.del(accessToRefreshKey);
			// Don't remove the refresh token - it's up to the caller to do that
			conn.del(authKey);
			List results = conn.closePipeline();
			byte[] access = (byte[]) results.get(0);
			byte[] auth = (byte[]) results.get(1);

			OAuth2Authentication authentication = deserializeAuthentication(auth);
			if (authentication != null) {
				String key = authenticationKeyGenerator.extractKey(authentication);
				byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
				byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
				byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
				conn.openPipeline();
				conn.del(authToAccessKey);
				conn.lRem(unameKey, 1, access);
				conn.lRem(clientId, 1, access);
				conn.del(serialize(ACCESS + key));
				conn.closePipeline();
			}
		} finally {
			conn.close();
		}
	}

당연히 그러면 이건 사용자 실수가 더 큰거 아니냐 라고 하실 수 있습니다. 그런데 꼭 사용자의 실수가 아니더라도 특정 사용자가 비정상적으로 이런 동작을 하면 비슷한 이슈가 발생할 수 있습니다.

그러면 어떻게 해야하는가? 해당 로직들에서 list 를 사용하는 부분을 좀 더 속도가 빠르거나 하는 것들로 바뀌고 모든 데이터를 가져오는 부분은 사용하지 못하게 하는 것이 방법입니다.

어떤 분들은 이미 이런걸 겪으셔서 내부적으로 해당 모듈을 수정해서 쓰시는 분도 계셨습니다. 그러나 이런부분 때문에 관련 부분을 어느정도는 직접 구현해서 쓰시는 거나, 이런 부분을 수정해서 쓰셔야 할듯 합니다. 결론적으로 spring-security-oauth 의 RedisTokenStore를 쓰는 부분은 서비스 부하가 늘어날 때 장애를 일으킬 확률이 높습니다.

그 외에도 refreshToken의 경우는 보통 expire 기간이 훨씬 긴데, 이것들은 전부 메모리에 저장하는 부분들 또한 이슈가 생길 수 있습니다.(돈 많은면, 메모리 빵빵한 장비 쓰시면, 이 부분은 신경 안쓰셔도…)

보통 Spring이라는 이름을 붙이면 굉장히 안정적인데, 소스를 보고, 문제가 생길만한 부분에 대한 가정을 너무 약하게 하고 넘어간 부분이 문제인듯합니다.(하지만 해당 코드는 벌써 3년 전에 만들어진 코드라는 거…)

쉬시면서 Spring 코드 한번 읽어보시는 것도 좋을듯합니다.(전 읽어본 적 없습니다. ㅋㅋㅋ), 해당 글을 작성하는데 도움을 주신 우아한 형제들의 엯촋 이수홍 선생님께 감사드립니다.

[입 개발] 신묘한 Python locals() 의 세계

오늘도 약을 팔러온 입개발 CharSyam 입니다. 오늘은 지인 분께서, Python에서 locals() 함수를 쓰면 local 변수를 참조할 수 있는데, 특정 현상은 이해가 안된다고 얘기를 하셔서, 한번 왜 그럴까에 꽃혀서 찾아본 내용을 정리할려고 합니다. 참고로, Python 쓰시는데 이런게 있구나 빼놓고는 아마 하등의 도움을 못 받으실 내용이니, 조용히 뒤로 가기 버튼을 눌리시고, 생산적인 페이스북을 하시는게 더 도움이 되실꺼라고 미리 경고드립니다.

문제의 시작의 발단은 아래의 코드입니다.

def test(x):
    locals()['v'] = x + 10
    print(locals()['x'])
    return locals()['v']

print(test(100))

locals() 함수는 현재의 local 변수를 dict type으로 던져주는 내장 함수입니다.
그리고 사실 Python2, 3 문서에 보면 locals() 의 값은 고칠 수 없다라고 정의가 되어 있습니다.
https://docs.python.org/3.3/library/functions.html#locals
스크린샷 2018-05-04 오전 12.31.42

그런데 위에서 보면 locals()[‘x’]의 경우는 로컬 변수 x의 값을 가져오고, locals()[‘v’]를 하면 v가 추가가 됩니다. 즉 해당 변수안에서 새로운 값을 이 locals()에 추가하는 것은 된다는 것입니다. 그렇다고 해서 그냥 v로 쓸 수 있지는 않고 locals()[‘v’] 이런식으로만 사용이 가능합니다.

일단 locals()[‘v’] = 100 이라는 코드 자체는 locals()의 결과가 dict type이므로 전혀 문제가 없는 코드 입니다. 특별히 읽기 전용 속성 같은게 있는 것도 아니구요. 그리고 다시 locals()[‘v’]를 사용했을 때 해당 값을 가져올 수 있다는 것은, locals()가 같은 객체를 던져준다는 것입니다. id(locals()) 해보면 항상 같은 주소값을 던져줍니다.

즉 우리는 다음과 같은 가설을 세울 수 있습니다. 가정1) locals()에 값이 저장이 된다.
그럼 insert 가 되니 update 도 되지 않을까요? 당연히 아래와 같은 코드는 update도 잘됩니다. locals()의 결과는 dict type 의 객체일 테니…

def test(x):
    locals()['v'] = x + 10
    locals()['v'] = 1
    return locals()['v']

print(test(100))

그런데 다음과 같은 코드가 출동하며 어떨까요?

def test(x):
    locals()['x'] = 10
    return locals()['x']

print(test(100))

우리는 이미 답을 알고 있습니다. 처음 문서에 수정이 안된다고 했으니 당연히 결과는 100이 나오게 됩니다. 뭔가 이상하지 않나요? locals()의 결과는 그냥 dict type이고, 그래서 새로운 값을 추가하는 것도 분명히 되는데, 업데이트는 안됩니다. 뭔가 locals()는 특별하게 만든 기능이라, dict type 자체에서 set을 막고 있는 것일까요?

일단은 먼저 Python 2.7.14 기준으로 설명합니다. Objects/dictobject.c 의 PyDict_SetItem 함수를 보면 특별히 특정 속성일 때 쓰기를 막는다 이런코드는 보이지 않습니다. dict_set_item_by_hash_or_entry 안으로 들어가봐도 큰 차이는 없습니다.

int
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
{
    register long hash;

    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    assert(key);
    assert(value);
    if (PyString_CheckExact(key)) {
        hash = ((PyStringObject *)key)->ob_shash;
        if (hash == -1)
            hash = PyObject_Hash(key);
    }
    else {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }
    return dict_set_item_by_hash_or_entry(op, key, hash, NULL, value);
}

그렇다면 dict type 자체의 이슈가 아니라 locals() 의 이슈가 아닐까라는 생각이 들게 됩니다. 제가 위에서 locals()는 내장함수라고 말씀드렸습니다. 그래서 Python/bltinmodule.c 를 살펴보면 builtin_locals를 찾을 수 가 있습니다. builtin_methods 테이블에 locals과 builtin_locals와 연결되어 있는 것을 찾을 수 있습니다.

static PyObject *
builtin_locals(PyObject *self)
{
    PyObject *d;
    d = PyEval_GetLocals();
    Py_XINCREF(d);
    return d;
}

그냥 PyEval_GetLocals() 만 호출합니다. Python/ceval.c 에 있습니다. 다시 따라가 봅시다.

PyObject *
PyEval_GetLocals(void)
{
    PyFrameObject *current_frame = PyEval_GetFrame();
    if (current_frame == NULL)
        return NULL;
    PyFrame_FastToLocals(current_frame);
    return current_frame->f_locals;
}

뭔가 이름이 있어 보이는 PyFrame_FastToLocals 함수가 보입니다. current_frame 은 뭔지는 잘 모르겠지만, locals니, 특정 scope에서의 코드 정보(함수안이냐, 글로벌이냐?)를 가져오는 걸로 예측이 됩니다.

다시 PyFrame_FastToLocals 를 따라가 봅시다. Objects/frameobject.c 에 존재합니다.

void
PyFrame_FastToLocals(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();
        if (locals == NULL) {
            PyErr_Clear(); /* Can't report it 😦 */
            return;
        }
    }
    co = f->f_code;
    map = co->co_varnames;
    if (!PyTuple_Check(map))
        return;

    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;

    if (co->co_nlocals)
        map_to_dict(map, j, locals, fast, 0);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    fprintf(stderr, "co_nlocals: %d, ncells : %d, nfreevars : %d\n", co->co_nlocals, ncells, nfreevars);
    if (ncells || nfreevars) {
        fprintf(stderr, "map_to_dict1()\n");
        map_to_dict(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1);
        /* If the namespace is unoptimized, then one of the
           following cases applies:
           1. It does not contain free variables, because it
              uses import * or is a top-level namespace.
           2. It is a class namespace.
           We don't want to accidentally copy free variables
           into the locals dict used by the class.
        */
        if (co->co_flags & CO_OPTIMIZED) {
            fprintf(stderr, "map_to_dict2()\n");
            map_to_dict(co->co_freevars, nfreevars,
                        locals, fast + co->co_nlocals + ncells, 1);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

위의 코드를 보면 f->f_locals 를 locals에 대입하고 없으면 dict type을 생성하는 것을 알 수 있습니다. 우리의 locals()의 결과는 dict type이고 이름까지 비슷하니 이넘이구나 하실껍니다. 그런데 왜 update는 안되는 것일까요?

그 의문은 다음 블로그에서… 는 뻥이고… co->co_nlocals 라는 변수에 있습니다. co는 codeobject 의 약어로 보이고, codeobject 는 해당 함수 관련 정보(global 변수 정보, local 변수 정보)를 가지고 있는 것으로 보입니다.(대충 봐서) 그 중에서 co_nlocals 는 local 변수의 개수를 가지고 있습니다. 즉 local 변수가 한개라도 있으면 map_to_dict 이 실행됩니다. 저는 처음에는 map_to_dict 에서 뭔가 locals 를 만들어 주는 줄 알았습니다. 그런데 map_to_dict 함수를 보면 map 에 있는 key를 dict에다가 PyObject_SetItem 를 통해서 덮어씌워버립니다. 그리고 저 map은 코드가 실행될때 넘어온 파라매터나 로컬 변수의 값들이 저장되어 있는 상황입니다.

static void
map_to_dict(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
            int deref)
{
    Py_ssize_t j;
    assert(PyTuple_Check(map));
    assert(PyDict_Check(dict));
    assert(PyTuple_Size(map) >= nmap);
    for (j = nmap; --j >= 0; ) {
        PyObject *key = PyTuple_GET_ITEM(map, j);
        PyObject *value = values[j];

        assert(PyString_Check(key));
        if (deref) {
            assert(PyCell_Check(value));
            value = PyCell_GET(value);
        }
        if (value == NULL) {
            if (PyObject_DelItem(dict, key) != 0)
                PyErr_Clear();
        }
        else {
            if (PyObject_SetItem(dict, key, value) != 0)
                PyErr_Clear();
        }
    }
}

헉, 하고, 이해를 하신 분들이 이미 계실꺼 같지만, 넵 그렇습니다. locals()를 호출할 때 아까 x의 값이 안바뀐 이유는 실제로 바뀌어도 locals()를 다시 호출할 때 map_to_dict 함수를 통해서 원래의 값으로 덮어씌워지기 때문입니다. 그래서 다른값들은 같은 locals를 사용하므로 추가나 변경, 삭제도 가능하지만, 기존에 존재하는 값들은 다시 원래의 값으로 덮어씌워지기 때문입니다.

def test(x):
    x = 15
    locals()['x'] = 1
    return locals()['x']

print(test(100))

그래서 local 변수면 동일하게 동작하므로 파라매터가 아니고 그냥 내부에서 미리 선언된 local 변수라도 이렇게 값이 바뀌지 않습니다.

def test():
    x = 15
    locals()['x'] = 1
    return locals()['x']

print(test())

우와 Python 의 locals()는 참 신묘합니다. 그런데… 문제는 여기서 끝이 아닙니다.(아니 도대체 쓸데 없는 이야기를 얼마나 더 할려고?) 그냥 여기까지 이해하고 넘어가려고 하는데… PyFrame_FastToLocals 함수 밑과 위에 map_to_dict 말고 dict_to_map 과 PyFrame_LocalsToFast 라는 함수가 있는게 아니겠습니까?

먼저 dict_to_map 함수입니다.

static void
dict_to_map(PyObject *map, Py_ssize_t nmap, PyObject *dict, PyObject **values,
            int deref, int clear)
{
    Py_ssize_t j;
    assert(PyTuple_Check(map));
    assert(PyDict_Check(dict));
    assert(PyTuple_Size(map) >= nmap);
    for (j = nmap; --j >= 0; ) {
        PyObject *key = PyTuple_GET_ITEM(map, j);
        PyObject *value = PyObject_GetItem(dict, key);
        assert(PyString_Check(key));
        /* We only care about NULLs if clear is true. */

        if (value == NULL) {
            PyErr_Clear();
            if (!clear)
                continue;
        }
        if (deref) {
            assert(PyCell_Check(values[j]));
            if (PyCell_GET(values[j]) != value) {
                if (PyCell_Set(values[j], value) f_locals into fast locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    co = f->f_code;
    map = co->co_varnames;
    if (locals == NULL)
        return;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals)
        dict_to_map(co->co_varnames, j, locals, fast, 0, clear);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        dict_to_map(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1, clear);
        /* Same test as in PyFrame_FastToLocals() above. */
        if (co->co_flags & CO_OPTIMIZED) {
            dict_to_map(co->co_freevars, nfreevars,
                locals, fast + co->co_nlocals + ncells, 1,
                clear);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

다음은 PyFrame_LocalsToFast 함수입니다.

void
PyFrame_LocalsToFast(PyFrameObject *f, int clear)
{
    /* Merge f->f_locals into fast locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    co = f->f_code;
    map = co->co_varnames;
    if (locals == NULL)
        return;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    if (co->co_nlocals)
        dict_to_map(co->co_varnames, j, locals, fast, 0, clear);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        dict_to_map(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1, clear);
        /* Same test as in PyFrame_FastToLocals() above. */
        if (co->co_flags & CO_OPTIMIZED) {
            dict_to_map(co->co_freevars, nfreevars,
                locals, fast + co->co_nlocals + ncells, 1,
                clear);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

PyFrame_FastToLocals 가 원래 locals() 를 호출할 때 실제로 수행되는 함수였다면 PyFrame_LocalsToFast 는 뭔가 역으로 값을 바꿀 수 있는 함수가 아닐까라는 생각을 해봤습니다. 해당 함수를 역으로 살짝 추적해보니(악, 여기서 그만뒀어야 하는데!!!) Python/ceval.c 파일안에 exec_statement 라는 함수에서 이걸 호출해줍니다.

설마설마 하면서 다음과 같은 예제를 만들어봤습니다.

def a(x):
    exec("locals()['x'] = 100")
    print(x)
    return locals()['x']

print(a(10))

python 으로 실행을 시키니 그냥 10만 나옵니다. 안되나? 라고 생각했는데… 제가 보던 소스는 Python 2.7.14이고, 제가 실행시킨 python은 3.x(이래서 머리가 무식하면 손발이 고생합니다.), 다시 python2 버전으로 돌려보시면 짜잔, 값이 100이 나옵니다. 즉 우리는 로컬 변수 x를 덮어씌운겁니다. –-; 그런데 Python3를 살펴보면 이 exec_statement 라는 함수가 사라지고 실제로 PyFrame_LocalsToFast를 호출하기 힘든 형태로 바뀌었습니다. –-;(망할… 즉 python3 에서는 동작하지 않고 python2에서만 실행이 됩니다.)

심지어 이렇게 하면 이런 이상한짓도 가능합니다. 아까는 locals()는 그냥 dict_type이라 뭔가 추가해도 다시 dict에서 꺼내와야 했는데… 아래의 예제를 사용하면 local 변수의 생성도 가능합니다. -_-(그런데 이렇게 생성할 필요가…, 참고로 Python3에서는 실행조차 안됩니다.)

def a(x):
    exec("locals()['x'] = 100")
    exec("locals()['y'] = 10")
    print(x)
    print(y)
    return locals()['x']

print(a(10))

어쩌다 보니, 소스를 파다보니 이런 내용을 알게 되었는데, 사실 사용하실 때는 1의 도움도 안되는 뻘 글을 읽어주셔서 감사합니다.

[입 개발] 전문가는 계속 공부하는 사람이다. – 김창준님의 개발자 실력 평가 어떻게 할 것인가 후기

안녕하세요. 입개발 CharSyam입니다. 둘째가 100일을 넘어서 저녁 약속 없는 신데렐라 시간을 하다가, 김창준님이 “개발자 실력 평가 어떻게 할 것인가?” 에 대해서 강의하신다고 해서 마님에게 애교를 부리면 허락을 받고 세미나를 들으러 왔습니다.

평소 입개발자는 입개발의 단계를 올리기 위해서 부단히 노력해야 하며, 혀로 키보드 치기, 입에 발린 소리하기등을 연습해야 하는데, 개발자 실력 평가에서 어떻게 하면 잘 빠져나갈 수 있을 것인가에 대한 힌트를 얻기 위해서 겨우겨우 참여하게 되었습니다.

웬지 창천항로님이 어마어마한 후기 를 이런식으로 남겨주실 것 같아서, 저는 느낌만…(참고로 저 링크는 창천항로님이 다른 세미나를 듣고 쓰신 후기… 대박!!! – 벌써 오늘 후기를)

일단 요약부터 하자면, 개발자 실력이라고 적었지만, 전문가를 판별하는 방법은 질문(소통)과 공부를 하는 사람이라는 것이 핵심이었습니다. 먼저 코딩 테스트의 비효율성, 코딩 테스트로 테스트를 하면 코딩 테스트만 잘 푸는 사람이지, 회사의 업무를 잘 할 사람일 가능성과는 별 개의 일이라는 이야기… 전문가를 구분하는 테스트를 할 때, 기존의 테스트의 잘못된 가정으로 인해서(요새 통계학 쪽에서 말이 많은 P-Value 처럼), 지금까지는 비용이나 표본의 이슈등으로, 짧은 시간에 풀 수 있는 문제와, 혼자서 풀 수 있는 문제가 많았는데, 긴 시간을 들여야 하는 문제나, 협업해야 하는 문제의 경우, 정말 전문가는 다른게 문제를 인지하고 해결한다는 것이었습니다.

또한 전문가는 컨텍스트를 이해하고 적용할려고 하는 반면에, 초보자는 단순히 문제를 풀려고 하는데, 제출되는 문제들의 경우는 컨텍스트가 제거되고 단순히 어떤 결과만을 바라는 문제들이라, 이걸 풀었다고 해서, 정말 일을 잘하는 지는 알 수 없다라는 얘기가 나왔습니다.(아, 제가 이해하고, 기억하는게 맞는건지… 애매하네요.)

그럼 일단 전문가는 어떻게 알 수 있는가? 삼각측량 처럼, 다양한 평가(동료평가, 상사평가, 버그생성율, 코드 리뷰, 디자인 리뷰) 등을 거쳐서 점수가 골고루 높은 사람은 전문가일 가능성이 높은데, 뽑을 때는 이런 평가를 해서 뽑을 수는 없습니다.

그렇다면, 우리회사에 적합한 사람은 어떻게 뽑을 것인가? 실제로 할 일을 비슷하게 만들어서, 이런 일을 해보도록 시키는것, 다만, 이를 위해 회사에서 실제로 잘하는 개발자, 평범한 개발자 그룹을 만들어서 비슷한 시험을 보게해서 잘하는 개발자들은 어떤 특징을 가져야 하는지를 찾아야 한다고 합니다. 그리고 이를 평가할 때, 누구나 비슷한 기준이 나오도록 기준을 정하는게 중요한데, 단순히 pass, fail이 아니라, 점수로 표현을 해야 한다고 하네요.
그리고 이 채점 기준은, 잘하는 사람들과 평범한 사람들의 그룹에서 나오는 평균적인 행동의 차이(예를 들어, 잘하는 그룹은 평균적으로 질문을 5회 이상한다. 등의 기준을 찾아내야 한다고 합니다.)

이걸 들으면서 생각난게 피보탈랩스의 입사 시험 방식입니다.(전 본적은 없고 듣기만…) 해당 팀의 업무를 모두 하루나 길면 이틀 단위로 나누고, 실제로 입사자와 해당 태스크를 직접 구현하고, 가능하면 배포까지 하는 것이 면접이라고 합니다. 팀의 새로운 툴에 대한 이해도나 커뮤니케이션 능력, 적응력을 다 볼 수 있는 테스트라고 하네요. 다만 다른 회사 분들과 얘기를 해보면, 소스 코드의 유출등이 가능하고, 시간을 너무 들여야 해서 어렵울 것 같다고 하시던데, 오픈소스 회사는 이런게 또 가능할 듯 합니다.

회사 내에 해당 분야의 전문가가 있을 때는 기술력 검증이 쉽겠지만, 그렇지 않을 경우는 어떻게 할 것인가라는 질문이 있었는데, 전문가이고 잘할수록 더 열심히 공부하는 경향이 있다고 하네요.

이제 여러분도 이런걸 명심하시고 대비하시면 좋은 입개발러가 되실 수 있습니다.(엉?)