외주 레거시로부터의 여정 4편

간만에 쓴다. 원래 4월 말에 적은 이후에 갑자기 너무 바빠져서 기억 저편에 사라져 있다가 갑자기 생각나서 이제서야 적는다. 원래 4월달에 적은 글인데, 이제야 퍼블리싱한다 : >

DB/Model

휴… 한숨이 먼저 나온다. 외주 코드가 엉망이라서가 아니라 이 안에는 굉장한 쿼리가 숨어있어서 반은 존경, 반은 어휴 이걸 어떻게 라는 한숨이 나왔던 것이었다. 1편에서 말한대로 외주 코드에서는 Model을 따로 정의했고 MySQL Driver로 Raw Query를 사용했다. 여기까지는 좋은데 테이블이 지나치게 많은 경향이 있었다.

예를 들어 컨테이너 종류를 따로 테이블로 만들었는데, 사실 20피트 드라이 컨테이너가 없어질 일도 없고 새로운 규격의 컨테이너가 나올 가능성은 굉장히 희박하다. 따라서 컨테이너 정보는 필요한 테이블에서 enum 으로 관리해서 넣어도 무방했다. 또한 파일정보 관련된 테이블만 10여개에 달했는데 기업별 로고 파일 정보를 저장한 테이블, 소개자료 정보를 저장한 테이블 등등. 테이블 컬럼도 거의 동일하기 때문에 하나로 통합하고 플래그로 구분하면 될 일이었다. 뭐 그 당시에는 다 계획된 일이 있어서 만들었기 때문에 절대 비난한 일은 아니지만 지금 보면 좀 아쉽긴 하다.

DB는 Sequelize ORM을 도입했다. 다만 모든 쿼리를 Sequelize Query로 짜진 못했는데 아래와 같은 무지막지한 쿼리 때문이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SELECT SQL_CALC_FOUND_ROWS DISTINCT
u1.id, f1.id AS column1, u1.company, isCertifiedBL, isBelieve, column4, column5, column6, column7, column8,
avgStars, reviewCount, fl1.src as logo,
(SELECT JSON_ARRAYAGG(JSON_OBJECT('name', c1.name)) AS SAMPLE8
FROM SAMPLE1 fj1
INNER JOIN SAMPLE8 c1 on fj1.cityId = c1.id
WHERE f1.id = fj1.column1) SAMPLE8,

(SELECT JSON_ARRAYAGG(JSON_OBJECT('name', si1.name)) AS services
FROM Services s1
INNER JOIN SAMPLE2 si1 on s1.column2 = si1.id
WHERE s1.userId = f1.userId) services,

(SELECT JSON_ARRAYAGG(JSON_OBJECT('name', fs1.name)) AS ffff
FROM SAMPLE3 fss1
INNER JOIN SAMPLE4 fs1 on fss1.column3 = fs1.id
WHERE fss1.column1 = f1.id) ffff, ffair1.price, ffair1.expiredAt
FROM Forwarders f1
INNER JOIN SAMPLE9 u1 on f1.userId = u1.id
LEFT JOIN SAMPLE10 s1 on s1.userId = u1.id
LEFT JOIN SAMPLE1 fj1 on f1.id = fj1.column1
LEFT JOIN SAMPLE8 c1 on fj1.cityId = c1.id
LEFT JOIN SAMPLE2 si1 on s1.column2 = si1.id
LEFT JOIN SAMPLE3 fss1 on fss1.column1 = f1.id
LEFT JOIN SAMPLE4 fs1 on fss1.column3 = fs1.id
INNER JOIN SAMPLE5 ff1 on f1.id = ff1.column1 AND ff1.column11 = blahblah
LEFT JOIN SAMPLE6 fl1 on u1.id = fl1.userId
INNER JOIN
(
SELECT ff2.column12, ff2.colume24 AS price, expiredAt
FROM SAMPLE5LCL fflcl1
WHERE column22 = 'blahblah' AND column23 = 'bbbb'
AND DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 9 HOUR), '%Y-%m-%d') < expiredAt
)
fflcl1 on ff1.id = fflcl1.column24

중요 내용은 다 텍스트 대체했고 귀찮아서 뒤에 10줄은 다 적지 않아서 유효한 SQL문은 아니다. 다만 이런식의 쿼리를 만나니 정말 정신이 혼미했다… 여태까지 테이블 10개씩 조인하는 쿼리는 처음이었다. 자연의 위대함 앞에 선 인간의 심정이 이러할까 싶다. 이렇게 Join이 많은 쿼리는 그냥 그대로 Raw Query형태로 사용하기로 했고, 단순 findOne, findAll, Create, Update 정도만 Sequelize 를 사용하기로 결정했다. 이렇게 조인이 많으면 시퀄라이즈에서 쿼리 구성하는데 백 만년이 걸릴것 같았다. 중요한건 쿼리랑 그 타입 정보가 있는 결과값이지 그 형태가 중요한건 아니니까

TypeORM 도입도 생각했는데, 당시 백엔드 개발자가 나 혼자뿐이고 사용 경험이 없어서 그냥 익숙한 걸로 사용했다.

그 후

거의 90개의 테이블을 외주로부터 인수인계를 받았는데 받았는데 비즈니스 모델이 계속 변경됨에 따라 금방 100개가 넘어갔다. 솔직히 하는 일에 비해서 테이블 수가 너무 많았던지라, 얼마 전(11월 말)에 한 번 대대적인 리팩토링을 해서 현재는 70개선으로 유지 중이며 앞으로 20개를 살생부에 올려놓은 상황이다.

외주 레거시로부터의 여정 3편 - API 개편

백엔드 개편

API

외주 개발사가 개발한 API는 그야말로 순수한 Express에 자체 미들웨어로 중무장한 상태였다. 겉으로 포장만 안했지 일종의 외주사 전용 프레임워크라고 해도 무방했다. 그런데 말입니다.

유닛 테스트가 없다

테스트가 없으니 물론 커버리지도 0%였다. 테스트가 없는 여파는 내가 합류한 3월부터 슬슬 찾아오기 시작했다. 초반에는 비즈니스 검증 때문에 기능이 수시로 변한다. 대표가 외주에게 몇 번 변경을 요청했는데 여지없이 잔버그가 우수수 떨어지기 시작했다. 고민의 시간이 찾아왔다. 여기다가 유닛테스트를 붙일 것인가. 근데 코드를 보니 컨트롤러에 비즈니스 로직이 잔뜩 있어서 supertest로 API테스트를 붙여야만 테스트 커버리지를 올릴 수 있었다. 게다가 타입이 없는 동적언어인 자바스크립트 특성 상 커버리지를 90%정도까지는 올려야 안정적으로 돌아갈 수 있었다.

게다가 비즈니스 로직이 생각보다 복잡했다. 테이블 70개가 괜히 생긴게 아니었다. 컨테이너 종류만도 17가지 종류나 되었고 국가 간의 무역거래조건인 인코텀즈만 해도 10종류가 넘었다. 게다가 컨테이너, 인코텀즈 외에도 수 없이 많은 요소가 있었고 각 요소마다 처리하는 로직도 다 달랐기 때문에 발을 살짝만 잘못 디뎌도 순식간에 골로가는 아주 좋은(?) 조건이었다.

타입스크립트 도입

여기서의 결심은 타입스크립트 도입이었다. 그렇다고 전면 재개발은 아니었다. 나는 넷스케이프의 전철을 밟을 생각이 전혀 없었다. 다행히 개발된 코드가 ES6라서 코드 복붙 70% 정도로 API를 구성할 수 있을 것 같았고, 실제로도 그러했다. 타입스크립트 도입 시 NestJS를 비롯한 각종 타입스크립트 프레임워크를 좀 물색했는데, 최종적으로 Typescript-rest를 도입하기로 하였다. 가장 큰 이유는 나에게 주어진 시간이 많지가 않았기 때문에 바로 실전 투입할 수 있는 프레임워크여야 했다. Typescript-rest는 전 회사에서도 써서 러닝커브가 없었고 무엇보다 NestJS처럼 특정 구조를 강제하지도 않았다. NestJS가 틀에 박힌 노드판 스프링이라면 Typescript-rest는 그나마 Express에 가까운 친구였다.

소스코드를 옮기는건 힘들었지만 꽤나 재미있는 작업이었다. 보통 아래와 같이 작업을 했다.

  1. Typescript-rest에서 API 컨트롤러를 만들고 여기에 기존 소스코드를 그대로 붙인다.
  2. 물론 타입이 없기 때문에 그대로 붙지는 않는다. 한 번 붙일때마다 VSCode 화면은 공산혁명이라도 일어난듯이 온통 빨간줄이 쫙쫙 그어지곤 했다. 대부분은 타입 문제였고 일부만 코드 문제였다. 타입이 없으면 일단 any로 메꾸고 number, string, boolean 같은 Primitive 타입만 대체하는 방향으로 컨트롤러 내 API를 구성했다.
  3. API 만들때 당연히 모델도 필요했다. API 만들때마다 모델을 몇 개씩 만들기도 했다.
  4. 컨트롤러에서 비즈니스 로직을 떼어낸다. Service를 만들고 컨트롤러에 DI(Dependency Injection)했다.
  5. 최종적으로 컨트롤러에서는 Request, Response, Validation에 대한 책임만 지고, 비즈니스 로직은 서비스에서 담당하게 된다.
  6. 서비스를 상대로 유닛 테스트를 만든다.

이렇게 해서 한 달만에 거의 대부분의 소스코드를 옮겼고 커버리지도 70% 이상을 달성했다. 여기에는 우리 인턴님의 지대하신 공헌이 있었다. : >

그 외 Pub/Sub 모델 도입, Notification, Scheduling 등의 주제는 별도의 포스트를 통해서 소개하도록 하겠다.

외주 레거시로부터의 여정 2편 - 개발팀 구성

팀 꾸리기

소프트웨어 개발은 혼자 할 수 없다. 더욱이 8~9개월 가까이 외주에서 진행된 프로젝트를 이어 받아야 하는 입장에서는 더더욱 그렇다. 이런 초창기 개발팀일수록 2인자 테크 리더의 역할이 굉장히 크다. 2인자는 나의 오른팔로서 내가 모자란 부분을 채워주고 때에 따라서는 적어도 2~3명의 개발자를 매니징 할 수 있을 인재여야 한다. 경험상 보통 3~4년차 개발자부터 이 역할을 맡을 수 있다.

나는 연차나 학력으로 개발자를 평가하지 않는다. 1년 차 고졸 개발자라도 굉장히 실력이 좋은 경우도 드물지만 존재하며, 20년차 개발자라도 과거의 영광에 사로잡혀 구닥다리 방식대로만 일을 하는 사람도 있다. 다만 짬밥(?)을 무시하지 못하는게 보통 3~4년차 개발자는 평균적으로 어느 정도의 기술적인 기반을 다져놨기 때문에 내가 원하는 2인자 - 테크 리더일 가능성이 굉장히 높았다.

문제는 이 3~4년차 개발자들이 시장에서 가장 잘팔리고, 가장 자만심이 높을때라 이렇게 듣보잡(분하지만 지금은 그렇다…) 스타트업에 지원할 가능성이 굉장히 낮다. 이 분들은 주로 한 회사에서 주니어 개발자로 경력을 쌓고 그 다음 회사로 배민이나 토스, 네이버, 카카오같은 테크 대기업에 지원하고 있으리라. 혹시나 하는 마음에 개발자 구합니다 - 라고 로켓펀치에 올렸으나 결과는 대실패였다. 외국인 개발자 한 분만이 지원했을 뿐 아무도 지원하지 않았다. 게다가 개발자들에게 생소한 “물류” 분야의 스타트업이라 더더욱 힘들었다. 예를 들어 킥보드 스타트업이다 한다면 개발자들도 대충 뭔지는 안다. 도심을 활보하는 킥보드를 타본 적은 없을지언정 뭔지는 알고 있다. 실버 케어 스타트업이다 한다면 개발자들도 대충 뭔지는 안다. 다들 할아버지 할머니가 있거나 있었거나 없어도 뉴스 등을 통해서 간접적으로 접했을테니까. 근데 택배도 아니고 수출입물류입니다 - 인코텀즈 아세요? 20피트 드라이는요? 그럼 급정색하고 “안녕히계세요” 이러고 집에 갈거다.

여기서 내가 취할 수 있는 선택지는 단 2개였다.

  1. 오른팔이 될 수 있는 3~4년차 개발자를 계속 찾는다.
  2. 주니어를 뽑아서 멱살잡아 끌고가서라도 중니어로 만든다.

1번이 이루어지면 확실하게 개발팀에 큰 힘이 된다. 다만 “언제”라는 측면에서 불확실성이 매우 컸다.
2번 - 주니어 채용은 확실히 쉽다. 다만 주니어 특성상 똥인지 된장인지는 겪어봐야 알고 소프트웨어 개발자로서의 성공 의지에 따라서 발전 속도의 차이가 너무 심하다. 그리고 멱살잡고 끌고가는 사람도 보통 힘든게 아니다.

결국 나는 2번을 택하기로 했다. 모든 비즈니스에서는 ‘시간’이 매우 중요하다. 전설 속의 유니콘과 같이 언제 올지 모르는 테크 리더급 개발자 채용을 기다리다가 개발을 그르칠 수 있었다. 개인적으로 불확실한 최선보다는 확실한 차선을 선호한다. 게다가 Node.js 백엔드, AWS 인프라 정도는 나 혼자 할 수 있고 리액트도 다시 공부하면 되긴 된다. 근데 이놈의 리액트는 잠깐 손놨더니 최신 코드는 죄다 React Hooks 일세?

내가 상대적으로 모자란 부분인 프론트엔드 (특히 스타일링)쪽을 강화시켜줄 개발자가 절실했지만, 어쩔 수 없이 지금 현재 주니어 프론트엔드 개발자와 백엔드 인턴으로 팀을 만들었다. 그리고 이 어린 동료들은 입사하자마자 불행히도 벌써부터 공부거리를 받고 4월 말까지 검사를 받아야 한다.

인턴에게는 인터넷 기본(HTTP, Cookie, Session)에 대한 발표를 주니어 프론트엔드 개발자에게는 DNS(호스팅, DNS, 도메인네임, google.com을 쳤을 때 뒷단에서 벌어지는 흐름)관련 내용을 공부해오라고 했다. 참고로 앞으로 공부할 커리큘럼도 짜놨다. HTML, CSS, Javascript(쪽은 좀 더 세분화), Git 심화(PR, Merge, Rebase, Cherrypick), 보안, 주석, CSS 레이아웃(Flex, 반응형, Styled Component), 테스팅, Database, OS, 자료구조, 인증, CI/CD, 개발/설계, 아키텍쳐 패턴, 도커 그 외 Devops 관련된 모든 사항이다.

외주 레거시로부터의 여정 1편 - 기존 레가시 파악

시작

2020년 1월 중순, 서울시 광화문의 어느 술집에서 남자 셋이서 술을 마시고 있었다. 셋은 기분이 좋은지 상 위의 안주는 거들떠보지도 않은 체, 술만 마시고 있었다. 두 사람은 좀 더 나이들어 보이는 어떤 남자에게 연거푸 고맙다는 인사와 함께 계속 잔을 부딪혔다. 이렇게 좀 더 나이많은 남자는 셀러노트 CTO가 되었다. 그리고 그 남자는 3월 중순에 본격적으로 판도라의 상자를 여는데…

심심해서 도입부를 소설 형태로 적어보았다. 아내가 봤으면 무슨 똥글이냐고 욕했겠지만 마침 몇 시간동안 자리를 비운지라 거침없이 글을 쓴다 : >

암튼 그 판도라의 상자를 여는 남자는 바로 나, 자신이다. 첫 임무는 외주사가 개발하고 있는 셀러노트의 물류 플랫폼 서비스 쉽다를 자체개발로 돌리는 일이었다. 외주는 다행히 PHP로 개발하지 않고 node.js 로 개발을 했다. 아마 PHP로 했었으면 CTO 자리를 정중히 거절했을거다. PHP자체가 나쁜건 아니다. 아니 아직까지 PHP 5.x로 개발했다면 그건 나쁘다고 단언할 수 있다. 허나 PHP도 7부터는 속도도 어느정도 빨라지고 상당히 괜찮은 물건이 된건 알고 있으나, $투성이인 코드가 싫다는 말도 안되는 핑계로 PHP를 거부하고 있고 앞으로도 사용하지 않을 예정이다.

4월 13일부로 입사한지 1달이 지났다. 지난 1달 간은 레거시를 바탕으로 좀 더 나은 시스템으로 만드는 과정의 연속이었다. 이를 회고 겸 경험 공유 차원에서 시리즈로 연재할 예정이다.

진짜 시작

입사는 3월이지만 소스 코드와 AWS는 입사 전인 2월 중순부터 살펴볼 수 있었다. 외주 개발물에 대해 파악한 내용을 개인적인 감상/비평은 최대한 자제하고 그냥 사실만 적도록 하겠다.

기 개발 기간

외주 개발자들이 2019년 7월부터 2~3명의 인원이 붙어서 작업했다. 단순 M/M만 따져봐도 2월 당시엔 최소 14M/M가 된다.

백엔드

서버 코드는 천만 다행으로 ES6+를 사용하고 있었고 node.js/Express 상에서 동작했다. 코드를 보니 미들웨어가 상당히 많았는데 이는 다년간 node.js 프로젝트를 진행했고 외주 회사에서 어느정도 프레임워크로 갖춰놓았다는 얘기가 된다. ESLint를 적용하진 않았고 유닛테스트가 없다. ㅠㅠ

인증

인증은 JWT였다. OIDC나 OAuth0 썼을까 살짝 기대했는데 그건 아니고 생짜 인증키로 토큰을 만들고 내려주는 형태였다.

Database

데이터베이스는 MySQL 8.0이었다. ORM을 쓰지 않고 생짜 MySQL Driver를 사용했다. 근데 테이블이 70개가 넘었다.

프론트엔드

React.js 다. ES6+를 사용했고 Redux로 전역 상태관리를 했다. 일부 컴포넌트는 Styled Component를 사용했으나 전체 스타일은 7000줄에 달하는 거대한 CSS가 담당하고 있었다. 스토리북으로 뭔가 테스트를 도입하려고 한것 같은데 테스트가 3개밖에 없다. 하다가 중단한듯 하다. 어드민 페이지는 별도의 프로젝트로 있었는데 Material UI로 만들어져 있었다.

인프라

AWS를 사용했다. dev/prod 로 각각 EC2를 띄워서 사용했는데 이상하게 EC2가 4대나 되었다. 잘 살펴보니 Frontend Serve용 dev/prod 2대, Backend용 dev/prod 2대 이렇게 4대였다. DB는 하나의 EC2에 넣지 않고 AWS RDS를 사용하고 있었다. 오토스케일링 설정은 하지 않았고 Elastic IP를 받아서 Route53에서 A레코드에 해당 IP를 매핑하는 식으로 웹 서비스를 구성했다. 주로 사용하는 서비스는 사실 이게 다였다. 프론트엔드 웹서버는 serve 를 사용하고 있었고 API 서버는 nginx를 사용하고 있었다.

배포

프론트엔드/백엔드 모두 배포는 CI/CD는 없었고 로컬 컴퓨터에서 스크립트로 배포했다. 단계는 아래와 같다

  1. npm run build로 빌드
  2. docker build 도커 이미지를 만들고 태깅한다.
  3. ECR에 올린다
  4. EC2에 SSH로 접속해서 Docker Pull을 땡긴다
  5. scp를 사용해서 docker-compose파일을 EC2로 복사한다.
  6. docker-compose 실행
  7. 기존 이미지를 지운다

물론 대부분 다시 손을 봐야된다. 허나 이 정도만 되도 굉장히 감사한 편이다.
지인의 요청으로 기존에 만들어놓은 외주 프로젝트를 본 일이 여러 번 있었는데 PHP 5.x로 코드이그나이터도 안쓰고 짠 소스 + 무식하게 큰 EC2 한 대에 웹서버 + 디비까지 같이 넣어놔서 오토스케일링도 못하게 만든게 제일 최악이었다. 거기에 비하면 이 정도는 사실 감사하다고 인사하고 써야할 것 같다.

자, 이를 어떻게 바꿀지는 3편부터…

EKS 503 탈출기

EKS를 처음 구축하고 나를 반겨준 화면은 단연 503 Service Unavailable 이었다. ㅠㅠ 문제의 원인은 Pod, Service, Ingress 혹은 이미지 자체에 있을 수 있다.

문제점을 찾기 위해서 아래와 같은 순서대로 확인했다.

Pod가 정상 동작 중인지 확인

먼저 pod가 동작 중인지 확인한다.

1
2
3
4
5
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
shipda-api-v2-7b45cdf6df-7hfsk 1/1 Running 0 2d12h
shipda-api-v2-7b45cdf6df-b8hzq 1/1 Running 0 2d12h
shipda-api-v2-7b45cdf6df-v24vh 1/1 Running 0 2d12h

뭐 다들 정상 동작중이니 이미지를 확인해본다.

이미지 검사

Pod에 올라간 도커 이미지가 진짜로 제대로 도는지 확인해본다. 다만 이는 Health Check를 잘 만들어놨다면 좀 쉽게 체크할 수 있다.

1
kubectl port-forward pod/shipda-api-v2-7b45cdf6df-7hfsk 8080:8080

해당 Pod의 8080포트를 localhost 8080 포트에 바인딩시킨다. 그 후 로컬호스트의 8080포트에 대고 API를 테스트해본다.

여기까지 문제가 없으면 Service 혹은 Ingress가 문제다.

서비스

원인은

  1. 인그레스가 서비스를 못찾거나
  2. 서비스가 Deployment 를 못찾거나

이 둘 중 하나로 좁혀졌다. 하지만 어디서 문제인지 도통 알 수가 없었다. 그래서 옆에 곰돌이를 앉혀놓고 다시 한 번 서비스를 확인해본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ kubectl describe service -n shipda
Name: shipda-api-v2
Namespace: shipda
Labels: <none>
Annotations: kubectl.kubernetes.io/
Selector: run=shipda-api-v2
Type: NodePort
IP: 10.100.75.73
Port: <unset> 80/TCP
TargetPort: 8080/TCP
NodePort: <unset> 30888/TCP
Endpoints: 172.31.1.205:8080,172.31.21.125:8080,172.31.39.55:8080
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>

deployment는

1
2
3
4
5
6
7
8
9
apiVersion: apps/v1
kind: Deployment
metadata:
name: shipda-api-v2
labels:
app: shipda-api-v2
namespace: shipda
spec:
...

곰돌이에게 deployment의 metadata를 설명하다가 갑자기 뜨얽!
Deployment에서는 label을 app: shipda-api-v2 로 해놨는데 Service Selector는 run: shipda-api-v2 로 해놓았던 것이었다!

따라서

잉그레스 -> 서비스 -> 디플로이먼트

이렇게 흘러가야할 트래픽이 서비스에서 디플로이먼트로 흘러가지 못해서 503에러가 발생했다. 왜 selector를 run으로 한지 이유는 모르겠지만…

같은 VPC인데도 EKS에서 RDS 연결 안될 경우

AWS의 Kubernetes 서비스인 EKS를 구성할때 겪었던 일이다.
EKS와 RDS를 같은 VPC 안에 두었음에도 불구하고 EKS에서 RDS로 연결이 되지 않고 계속해서 타임아웃에러만 발생했다.

이 경우 먼저 Pod에서 RDS 주소를 인지하는지 먼저 확인했다.
먼저 Pod 이름 먼저 확인

1
$ kubectl get all -n shipda

파드 이름이 pod/shipda-api-v2-79d868d65-2kh4t 이라 한다면 pod에다 nslookup 명령어를 실행시킨다.

1
2
3
4
5
6
7
8
9
10
11
$ kubectl exec -it pod/shipda-api-v2-79d868d65-2kh4t -n shipda nslookup <blahblah>.rds.amazonaws.com
Server: 10.100.0.10
Address: 10.100.0.10:53

Non-authoritative answer:
<blahblah>.rds.amazonaws.com canonical name = <blahblah>.ap-northeast-2.compute.amazonaws.com
Name: <blahblah>.ap-northeast-2.compute.amazonaws.com
Address: 111.111.111.111

Non-authoritative answer:
<blahblah>.rds.amazonaws.com canonical name = <blahblah>.ap-northeast-2.compute.amazonaws.com

DNS 를 리졸빙 하는걸로 봐서는 DNS는 문제 없는것 같다. 그러면 실제로 접속이 되는지 핑을 날려본다.

1
$ kubectl exec -it pod/shipda-api-v2-79d868d65-2kh4t -n shipda ping <blahblah>.rds.amazonaws.com

핑이 가지 않았다. 해결 방법을 한참 찾았다. 같은 VPC라서 문제 없을 줄 알았는데 뒤통수 맞은 기분이었다. 사실 프로덕션으로 GCP와 Azure는 썼었는데 AWS는 처음이라 좀 더 헤맸을수도 있다. 사실 요즘 클라우드가 다들 엇비슷해서 있을거 (거의) 있어서 적응에는 큰 문제는 없지만 아마존 같은 경우엔 VPC, Security Group, Subnet, RoutingTable 등을 좀 더 세밀하게 지정해야 하기 때문에 헤맸을 수도 있다.

결국 해결책은 -

RDS의 시큐리티 그룹에 EKS의 시큐리티 그룹을 넣어주면 해결되는 일이었다.

뭔가 굉장히 허탈하다. 참고로 다른 VPC라면 VPC 피어링을 사용하면 된다.

에이팀벤쳐스 (개발팀)에 대한 오해

잡 플래닛에 올라온 에이팀벤쳐스에 대한 안좋은 글이 있다.

  1. 열정페이 관련

누가 올렸는지는 모르지만 적어도 개발팀에는 이런 문화가 없다고 자신있게 말할 수 있다. 내가 알기론 다른 팀도 크게 다르지는 않다.

현재 개발팀은 스크럼 방식으로 일하고 있고, 주말근무 안 한지는 1년이 넘었고 야근한 기억도 가물가물하다. 도리어 시니어 개발자로서 늦은 면접에 참여하고 나오면 주니어 개발자들이 몽땅 퇴근해서 배신감이 들 정도니까. 열정페이 이딴거 하나도 없고 2019년 3월에 개발팀이 새로 꾸려진 이후로는 들어온 사람이 훨씬 많다.

  1. 임원

또 하나의 단점으로 지적된 임원의 수는 솔직히 잘 모르겠다. 같은 공간에서 일하는 사내 이사 2명이 있긴한데, 실제로 등록된 임원이 몇이나 되는지는 잘 모르겠다. 아마 사외이사가 아닐까? 나 조차 모를 정도면 뭐 그닥 영향력이 없다는 반증이 되겠다. 그리고 사내 이사 2명도 굉장히 훌륭하신 분들이라 존경과 감사를 담아 일을 했던 기억이 난다.

  1. 무조건 열심히 해라, 보상은 나중에

처음 들어보는 얘기다. 언급할 가치가 없다.

코딜리티(Codility) 첫 경험 - 데모 문제 1번

개인적으로 코딩테스트에 대한 공포감(?)이 조금 있다. 사실 코딩테스트에 대해서는 논쟁이 많은 편이다. 개인적으로 코딩테스트는 그냥 최소한의 자격 요건을 보는거지 이 결과로 당락을 결정하는건 아니라고 보는 편이다. 운동하는 사람들끼리 통용되는 말인 노가다에서 쓰는 근육과 헬스에서 쓰는 근육이 다르다이 프로그래밍 세계에서도 그대로 통용된다. 실무에서 쓰는 근육과 코딩테스트에서 쓰는 근육이 다르다 - 즉 코딩테스트 잘본다고 해서 실무를 잘하는게 아니다. 프로그래머라면 물론 코딩도 잘해야 하지만 직급에 맞는 시야와 상호간 커뮤니케이션 능력, 품질에 대한 고려 등등 코딩 외적인 능력이 있어야 훌륭한 개발자가 될 수 있다는게 내 생각이다.

아 근육 얘기 나오니까 하는 얘긴데 코딩테스트 본지 오래된 사람 갑자기 불러다가 코딩테스트 시키면 아무리 천하의 구글러라도 원하는 점수를 못얻을거다. 쓰는 근육이 다르니까.

암튼 말이 길어졌는데 코딩테스트에 대한 공포감을 해결하고자 코딜리티라는걸 이번에 처음 접해보고 한 번 도전해보았다. 코딜리티에는 데모 문제가 있는데 첫 번째 문제인 바이너리 갭(Binary Gap)을 한 번 풀어보았다.

이 문제는 함수를 짜는데 이진수 1과 1 사이에 있는 최대 0의 갯수를 구하는 문제다. 예를 들어 529는 이진수로 1000010001 이며, 함수는 4를 리턴한다. 9는 이진수로 1001이며 함수를 2를 리턴한다. 다만 32는 1000000 이라 갭이 없으므로 0을 리턴하게 된다.

처음에는 뭔가 이진수로 변환한 스트링을 순회하면서 Candidate를 배열에 추가하고 등등등 생각하다가 생각하다가…
갑자기 번뜩이는 split() 함수! 이걸 사용하면 굉장히 쉽게 문제를 해결 -ㅅ-;;;
뭔가 개꼼수 같지만 동작은 잘한다. 테스트케이스 몽땅 통과. 아래 코드는 3분만에 적었다.

알고리즘을 신성시하고 뭔가 대단한걸 기대한 사람들은 날더러 비정통파, 넌 프로그래머도 아냐라고 욕할지도 몰라.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function solution(N) {
// write your code in JavaScript (Node.js 8.9.4)
if (N === undefined || N < 0) {
throw new Error('invalid input');
}

// 10진수를 2진수로
const bin = N.toString(2);

// 1을 delimeter로 쪼갠다.
const sp = bin.split('1');
// 이진수 마지막이 0이면 배열의 마지막을 뺀다.
if (bin.endsWith('0')) {
sp.splice(sp.length - 1, 1);
}

const lens = sp.map((v) => {
return v.length;
});

return Math.max(...lens);
}

물론 이건 1번 문제니까 쉽겠지.

타입스크립트 전면 도입기

회사 개발팀에 사정이 있어서 지난 3월 말부터 개발팀을 이끌게 되었다. 첫 번째 임무는 기존 백엔드의 C#, CQRS, Event-Sourcing, DDD를 걷어내는 일이었다. 좋은 디자인 패턴과 개발방법론과 언어긴 하지만 현 개발팀 아니 한국에서는 좀 쉽지 않은 결정이다. 이유는 많지만 한 마디로 얘기하자면 현 개발팀에 안맞는 옷이었다.

2020.12 첨언
이렇게 적고나니 DDD가 한국 실정과 안맞는게 아니냐고 하는 오해가 있어서 추가적으로 적자면 C#이 한국에서는 쉽지 않은 결정이고, DDD(CQRS, Event-Sourcing과 세트)는 개발팀 몸에 맞지 않는 옷이었다. 제일 큰 이유는 C#, 정확히는 .Net Core 개발자를 못구해서였다.

DDD는 좋은 아키텍쳐지만 도메인 전문가의 유무, 프로젝트의 성격, 개발팀의 실력에 따라서 그 결과가 불투명하다. 다만 도메인이 복잡할 경우나 대규모 개발팀 운영에는 DDD가 강력한 아키텍쳐다.

변화가 생긴 개발팀은 Javascript 기반 프론트 1명, 풀스택 2명이었고 C#/Java Entry Level 1명 그리고 나까지 5명이었다. 마음 먹은대로 하자면 백엔드는 Python 혹은 Go로 하고 싶었지만 작은 회사일수록 테크 스택을 통일해야 한다는게 나의 지론이기 때문에 백엔드는 Express를 도입하기로 했다.

Express 도입과 함께 한 가지 도입한 테크 스택은 Typescript다. 현재 웹 프론트엔드인 vue.js와 백오피스 프론트엔드인 react.js는 이미 Typescript였다. 이 참에 Express도 Typescript를 도입하기로 했고 최종적으로 Typescript Rest을 선택했다.

프론트엔드와 백엔드를 모두 Typescript로 통일하고 그 장점을 극대화 하기 위해서 양쪽 사이의 API에서 사용하는 인터페이스나 Enum을 모아놓은 프로젝트를 만들었다. 예를 들어 Item 정보를 만드는 POST API가 있다고 가정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { POST, Path } from 'typescript-rest';

interface ItemBody {
name: string;
quantity: number;
}

@Path('/item')
export class ItemController {
/**
* 아이템을 만든다.
*/
@Path('')
@POST
public postItem(body: ItemBody){
// .....
}
}

그리고 프론트엔드에서 axios.js로 해당 API를 쓴다면 아래와 같겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ItemBody {
name: string;
quantity: number;
}

//// 생략
class APIs {
public async doSend(item: ItemBody) {
try {
await axios.post(`${BASE_URL}/item`, item);
} catch (e) {
// ....
}
}
}
//// 후략

두 프로젝트에서 공통으로 쓰는 인터페이스인 ItemBody를 별도의 프로젝트로 분리, Private NPM 저장소를 만들고 이걸 import를 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { POST, Path } from 'typescript-rest';
import { ItemBody } from '@mycompany/interface';

/* 이 부분을 없앨 수 있다.
interface ItemBody {
name: string;
quantity: number;
}*/

@Path('/item')
export class ItemController {
/**
* 아이템을 만든다.
*/
@Path('')
@POST
public postItem(body: ItemBody){
// .....
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ItemBody } from '@mycompany/interface';
/* 아래 코드를 없앨 수 있다.
interface ItemBody {
name: string;
quantity: number;
}*/

//// 생략
class APIs {
public async doSend(item: ItemBody) {
try {
await axios.post(`${BASE_URL}/item`, item);
} catch (e) {
// ....
}
}
}
//// 후략

여기서는 프로퍼티가 단순히 2개인 인터페이스만 대상으로 했지만, 프로퍼티가 많아질 수록, 그 인터페이스가 많아질 수록 강력한 효과를 발휘했다. 프론트엔드에서 힘들게 Swagger를 보고 인터페이스를 정의할 필요가 없었고,만들다가 생기는 휴먼 에러도 방지할 수 있었다. 단순히 npm을 업데이트함으로서 API와 싱크를 맞추기도 쉬웠다. 무엇보다 각각의 프로젝트에서 각각 관리하던 인터페이스 코드를 통합관리 할 수 있어서 굉장히 생산성이 높아졌다.

필요성을 느껴서 만들었기 때문에 다른 프로젝트에서는 이렇게 쓰는지는 잘 모르겠지만 프론트엔드와 백엔드 모두 타입스크립트를 쓴다면 한 번 도입해보면 좋겠다.

P.S 두 달 반만에 성공적으로 C#에서 자바스크립트로 이전했다.

현재 개발자 채용 시장에 대한 작은 생각

최근 회사에서 개발자를 뽑고 있다. 2년차 이상 웹 개발자를 뽑기 시작한지 1달이 다 되어가는데 생각보다 이력서가 많이 모이지 않았다. 현재 회사에서는 Node.js, Express, React.js, Mongodb + Mongoose, Typescript, Azure 을 사용하고 있는데 이 개발 스택에 해당하는 개발자가 없는지, 아니면 다른 곳에서 너무 많이 뽑아서 내가 다니는 회사까지 온기가 오기 않는건지 궁금해서 로켓펀치와 윈티드 구인 공고를 훑어봤다.

아래는 나의 주관적인 생각이다. 객관적인 데이터는 없지만 시비 걸지는 말아주시길 : )

백엔드 언어

  1. 백엔드 언어는 Java 35%, Python 20%, PHP 20%, Node.js 10%, C# & Ruby & Go & 기타 15%
  2. 자바가 제일 많다. 토스, 배민, 네이버 등 크고 유명한 곳에서 Spring을 사용하고 있기 때문일거다.
  3. 생각보다 PHP가 많은데 보통은 Laravel 개발자를 뽑는 경우가 많았다. 근데 Laravel이 Full Web Framework라서 편하긴 하지만 이걸로 API 서버를 만들면 성능이 안날텐데… 왜들 쓰는지 모르겠다. Show me the money라면 어느 정도 부하를 처리할 수 있겠지 ㅎㅎ
  4. 파이썬은 Flask는 별로 없고 Django가 많다. 가끔씩 Sanic 쓰는 곳도 있더라. 다만 파이썬은 백엔드 분야 외 데이터 분야 등 다양하게 쓰여서 공고가 많을 지도.
  5. 노드는 생각보다 별로 없다. 좀 의외다. 주로 쓰는 곳은 작은 스타트업.
  6. C#, Ruby, Go, 기타 합해서 15% 정도다. Ruby, C#, Go 언어 순으로 공고가 많았다.
  7. Rust를 메인 백엔드로 쓰는 곳은 아직 본 적이 없다. Go는 간간히 눈에 띄지만 거의 없다.
  8. 한국에서 C#은 쉽지 않겠다.
  9. Ruby는 과거보다 위상이 많이 떨어졌고, 지금도 떨어지는 듯 하다.
  10. 기타로는 Scala가 있다. 데이터 분석 분야와 뱅크샐러드에서 많이 쓰는 듯.

프론트엔드 언어

React 60%, Vue.js 25%, Angular + jQuery 15%

클라우드

AWS 천하통일. 간간히 GCP가 있고 희귀하게 Azure가 있다. 스타트업 중에서는 Azure를 쓰는 곳을 거의 못봤다. 앗! 이것 때문에 지원을 안하는 겐가!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×