20171004

Cursor 이전, VSCode 확장 조합

Sublime Text 오래 썼는데 올해 중반부터 VSCode로 완전히 갈아탐. 쓰고 있는 확장 모음 공유 겸 정리.

팀에서도 하나 둘 넘어오고 있는데 보통 "뭐 깔아?" 라는 질문이 제일 많아서 dotfiles 대신 그냥 블로그에 남김.

기본 필수

  • EditorConfig for VS Code — 팀 공통 스타일 (탭/스페이스/개행) 강제
  • ESLint — 저장 시 자동 린트. editor.codeActionsOnSavesource.fixAll.eslint 걸어두면 저장할때 자동수정
  • Prettier — 코드 포맷터. eslint-prettier 연동해서 충돌 방지
  • GitLens — 한 줄씩 git blame 보여줌. 이제 없으면 일 못함

언어별

  • Python: ms-python 공식 확장. pylint + pytest 연동
  • Go: lukehoban/vscode-go. gopls 붙이면 hover / goto definition 거의 IDE 수준
  • PHP: PHP IntelliSense (felixfbecker). 레거시 심볼리 참조 잘 잡음
  • Docker: 공식 확장. Dockerfile hover + compose 스키마 자동완성
  • YAML: redhat.vscode-yaml. k8s 스키마 검증

추천

  • Remote - SSH (preview) — 원격 서버에서 바로 편집. 개발 컨테이너 올려놓고 로컬처럼 쓰는게 가능. 개인적으론 이거 써본게 편집기 교체 결정타
  • REST Client — .http 파일에 요청 적고 즉석 실행. Postman 따로 안 킴
  • Bracket Pair Colorizer — 괄호 매칭 색상. JS 중첩이 심하면 눈이 편함

키바인딩 커스텀: Sublime 습관이 남아서 Cmd+P / Cmd+Shift+P 는 그대로 유지. Cmd+D (멀티 커서 같은 단어 선택)도 Sublime 방식으로 교체. VSCode 공식 Sublime keymap 확장 깔면 자동 적용됨.

설정 동기화는 Settings Sync 확장으로 gist 백업. 요즘 공식 Settings Sync 기능 추가한다는 이야기가 있어서 곧 빠질 것 같다.

Sublime에 비해 느린게 좀 거슬렸는데 (Electron이라) 최근 업데이트들에서 많이 좋아짐. 확장 생태계가 커지는 속도가 말이 안됨. 당분간 이걸 주력으로.

20170914

PostgreSQL 10 Logical Replication

PostgreSQL 10 GA. 가장 기대했던 기능이 드디어 제대로 된 logical replication. 기존 9.x는 streaming replication (물리적 WAL) 뿐이라 테이블 단위 선택 복제가 안 됐음.

10부터는 publication / subscription 개념으로 테이블 단위로 구독 가능.

Publisher (source):

-- postgresql.conf
-- wal_level = logical
-- max_replication_slots = 4
-- max_wal_senders = 4

CREATE PUBLICATION my_pub FOR TABLE orders, order_items;

Subscriber (target):

-- 동일 스키마 먼저 CREATE TABLE
CREATE SUBSCRIPTION my_sub
  CONNECTION 'host=db-master.internal dbname=app user=repl password=...'
  PUBLICATION my_pub;

기본 동작: 구독 생성 시점에 initial data copy → 이후 변경사항 실시간 스트리밍.

쓰임새:

  • 특정 테이블만 분석 DB로 스트리밍 — OLTP 부하 없이 DW 구축 가능
  • 메이저 버전 업그레이드 시 다운타임 최소화. 기존 9.6 → 10 이관 시 physical replica는 버전 달라서 못 쓰는데 logical은 가능
  • 멀티 테넌트 DB에서 특정 테넌트만 뽑아서 별도 DB로

제약:

  • DDL은 복제 안됨. ALTER TABLE 하면 양쪽에 수동 적용해야 함
  • TRUNCATE 도 10.0에선 복제 안됨 (11에서 개선 예정이라 함)
  • sequence 값은 복제 안됨 — ID 충돌 주의. 구독 쪽에서 INSERT 금지 원칙
  • 테이블 PK 있어야 UPDATE/DELETE 복제됨. 없으면 replica identity full 설정 필요 (느림)

간단 테스트 결과: 수초 이내 delay로 동기화. WAL 레벨이라 네트워크 비용은 row 기반보다 큼.

예전 pglogical 플러그인으로 비슷한거 했었는데, 이제 코어 기능이라 운영 부담이 확 줌. DW 파이프라인 개편 예정이고, 여기에 쓸 생각. 10.0은 갓 나와서 아직 prod 전환은 보수적으로 검토 중.

20170821

Kubernetes Ingress 도입기

지난번 k8s 이관 이후 쌓여있던 숙제. ingress를 제대로 도입함. 그동안은 서비스마다 type: LoadBalancer 로 띄워서 비용이 새고 있었음.

선택은 nginx-ingress-controller. traefik도 고려했는데 사내 nginx 경험 많아서 디버깅 편한 쪽으로.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1
        backend:
          serviceName: api-v1
          servicePort: 80
      - path: /v2
        backend:
          serviceName: api-v2
          servicePort: 80

인증서는 cert-manager 도입. Let's Encrypt로 자동 발급/갱신. ingress에 annotation 하나 달고 Issuer 만들면 끝. 수동 인증서 발급하던거랑 비교하면 정말 편함.

metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"

뻘짓한 포인트:

1. proxy-body-size 기본 1m. 업로드 API에서 413 뜸. annotation으로 늘려야 함. 이거 찾는데 한 시간.

2. rewrite-target. path: /v1 로 매칭했을때 백엔드에는 / 만 전달하려면 rewrite 필요. 아니면 백엔드가 /v1/* 를 받게 되어 라우팅 꼬임.

3. health check. ingress-controller 자체가 죽지 않게 PDB (PodDisruptionBudget) 걸어둠. 노드 drain 때 전체 다운 안 되도록.

4. sticky session. 세션 고정이 필요한 서비스는 annotation:

nginx.ingress.kubernetes.io/affinity: cookie
nginx.ingress.kubernetes.io/session-cookie-name: sticky

이관 후 LoadBalancer 인스턴스 개수 7개 → 1개 (ingress용). 비용 2/3 절감. 배포 유연성도 올라감. 도메인/경로 기반 라우팅을 Ingress 리소스로 선언적으로 관리하니 리뷰/기록이 깔끔.

다음 숙제는 external-dns로 Route53 레코드까지 자동화. 점점 GitOps 방향으로 가는 중.

20170723

React 16 Fiber 출시 후 리팩토링

React 16 beta 찍먹. 커뮤니티에서 말 많던 Fiber가 드디어 기본 렌더러. 기존 코드가 얼마나 그대로 도는지 확인해봄.

결론: 대부분 그대로 동작. 몇 가지만 손보면 됨.

바뀌는 점 (주의할 것):

  • render()에서 배열 / 문자열 / 숫자 / null 직접 반환 가능. 예전엔 div로 감싸야 했던거
  • 에러 경계 (componentDidCatch) 도입. 트리 내 어디서 throw 나도 fallback UI로 대체 가능
  • Fragment — <React.Fragment>...</React.Fragment> 로 무의미 div 제거
  • Portal — 특정 DOM 노드 밖으로 렌더 (모달, 툴팁)
  • PropTypes, React.createClass 는 별도 패키지로 분리. 안 바꾸면 경고 뜸
  • setState에 null 반환하면 업데이트 스킵됨 (noop)

Fiber 자체는 "기존 stack 기반 reconciler를 교체" 한 거라 use-site에선 티 안 남. 내부적으로 렌더를 중단/재개 가능하게 쪼개놓은 구조. time slicing이 가능해지는 기반이지만 16에선 아직 기본 활성은 아님. 그건 앞으로 (async mode) 점진 도입.

리팩토링 체크리스트 (팀에 공유한 거):

1. PropTypes import 경로 확인 — import PropTypes from 'prop-types'; 로 바꿈
2. React.createClass 쓰는 오래된 파일 있나 검색 — ES6 class로 교체
3. render return 에 무의미 wrapper div 있으면 Fragment로
4. 앱 루트에 에러 경계 하나 붙여두기. 컴포넌트 하나 터져서 흰 화면 안 되도록

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    logger.error(error, info);
  }
  render() {
    if (this.state.hasError) return <FallbackUI/>;
    return this.props.children;
  }
}

번들 사이즈도 조금 줄음. 체감 rendering 성능은 일반 UI에선 아직 큰 차이 없는데, 대규모 트리 업데이트 상황에서 dropped frames가 줄어든 것이 측정됨. 나중에 async mode 켜지면 이게 더 극적으로 좋아질 것 같다.

16은 마이너 브레이킹만 잘 잡으면 안전하게 올릴 수 있는 버전. 회사 주력 프로덕트는 다음달 적용 예정.

20170408

Docker Swarm 운영 vs K8s 이관

6개월 전 docker swarm mode (1.12 부터 통합된 그거)로 올려놨던 서비스 몇개. 이제 k8s로 이전하는 중. 왜 바꿨는지 정리.

swarm 좋았던 점:

  • docker compose 익숙하면 러닝커브 제로 수준. docker stack deploy -c docker-compose.yml mystack
  • 기본 세팅이 빠름. docker swarm init 한 줄
  • built-in routing mesh — 어느 노드로 요청 오든 해당 서비스 컨테이너로 라우팅

swarm 한계:

  • ingress controller / 복잡한 라우팅 규칙 제한적
  • configmap은 있는데 secret rotation 같은 운영 기능이 덜 무르익음
  • helm 같은 패키지 매니저 생태계 없음. 직접 stack 파일 관리
  • 커뮤니티 확장이 k8s 대비 훨씬 적음. 모니터링/로깅 도구 연동이 번거로움
  • 대규모 클러스터 (수백 노드) 운영 사례가 많지 않음

k8s 쪽에서 지금 쓰는 것들:

  • deployment / service / ingress
  • configmap / secret
  • helm chart (prometheus, grafana 등 기본 스택)
  • nginx-ingress-controller
  • cert-manager (let's encrypt 자동 발급)

마이그 진행방식: 서비스별로 helm chart 만들고, stage에서 동시에 swarm/k8s에 띄워놓고 비교 → k8s 쪽으로 DNS 스위치 → swarm 측 철수. 한번에 넘기지 않고 점진적으로.

빡셌던 건 ingress 쪽. swarm의 routing mesh처럼 암묵적 분배가 되는게 아니라 명시적 ingress 리소스 만들고 rule 정의 해야됨. 처음엔 귀찮은데 익숙해지면 강력함.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web
          servicePort: 80

결론: 소규모 / 단순 케이스에서는 swarm이 여전히 간편하고 충분. 근데 조금만 요구사항 커져도 k8s로 가는게 장기적으로 덜 피곤. 커뮤니티 모멘텀이 k8s로 완전히 쏠린게 체감됨. 회사 표준도 k8s로 맞출 계획.

20170321

MongoDB 3.4 aggregation 파이프라인 튜닝

리포트 쿼리 하나가 aggregation 안에서 7초 걸려서 튜닝한 기록. MongoDB 3.4 기준.

컬렉션: events, 5천만 doc. 하루치 집계해서 이벤트 타입별 count를 내는 쿼리.

db.events.aggregate([
  { $match: { createdAt: { $gte: ISODate("2017-03-20"), $lt: ISODate("2017-03-21") } } },
  { $group: { _id: "$type", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
]);
// 7.2 초

Explain 보니 $match가 인덱스 안 탐. 인덱스는 {createdAt: 1, userId: 1} 이었는데 쿼리에서 userId 없어서 prefix만 탄다고 하더라. 실제로는 index만 스캔해도 빠를텐데 옵티마이저가 전체 읽고 있었음.

조치:

1. {createdAt: 1, type: 1} 인덱스 추가. type이 group key라 covering 가능.

2. 3.4부터 추가된 $project + $expr 기능 검토했는데 이 케이스엔 불필요. 그냥 인덱스로 해결.

3. $match를 맨 앞에 두는건 기본중 기본. 원 쿼리도 맞음.

db.events.aggregate([
  { $match: { createdAt: { $gte: ISODate("2017-03-20"), $lt: ISODate("2017-03-21") } } },
  { $group: { _id: "$type", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
], { hint: { createdAt: 1, type: 1 } });
// 0.9 초

인덱스 커버 + hint 명시. 대략 8배 향상.

추가 팁:

  • allowDiskUse: true 옵션. 메모리 100MB 넘는 aggregation은 이거 없으면 실패. 리포트 쿼리엔 필수
  • 3.4의 $facet: 한 번 순회로 여러 집계 결과를 배열로 받음. 대시보드 같은데 유용
  • $lookup (join)은 쓸만하긴 한데 크면 역시 느림. denormalize가 Mongo 철학에 맞음

MongoDB는 인덱스 설계가 실무 성능의 90%. RDBMS랑 똑같이 쿼리 패턴 보고 인덱스 짜야 함. 문서 DB라고 "아무거나 넣으면 된다"가 아님. 당연하지만 매번 상기해야 되는 포인트.

20170213

GraphQL 처음 도입해본 팀

내부 어드민 API 하나를 GraphQL로 재작성. Python쪽은 Graphene 2.0beta 써봤다. 팀이 처음 도입하는 상황이라 적어두기.

왜 도입했나: 모바일/웹에서 같은 엔드포인트 쓰는데 필드 요구가 달라서 REST 엔드포인트가 자꾸 늘어남. 캐시 문제랑 overfetch 이슈가 누적.

스키마 예시 (Graphene):

import graphene
from graphene_django import DjangoObjectType

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = ('id', 'email', 'name')

class Query(graphene.ObjectType):
    user = graphene.Field(UserType, id=graphene.Int(required=True))
    users = graphene.List(UserType)

    def resolve_user(self, info, id):
        return User.objects.get(pk=id)

    def resolve_users(self, info):
        return User.objects.all()

schema = graphene.Schema(query=Query)

클라이언트는 필요한 필드만 명시:

{ user(id: 1) { email name } }

좋았던 점:

  • 프론트에서 새 필드 필요할 때 백엔드 변경 없이 쿼리만 수정 → 진짜 편함
  • 스키마가 문서 역할. GraphiQL IDE가 자동 생성
  • 타입 명세가 강제되니까 팀원끼리 대화 혼란 줄어듦

걸린 점들 (조심):

1. N+1 쿼리. 리스트 쿼리에서 각 item의 related 객체 resolver 돌리면 ORM 호출이 행 수만큼 나감. DataLoader 같은 배치 로더 꼭 필요. 이거 안 쓰면 DB 폭주.

2. 권한 체크. REST는 URL 단위로 걸면 됐는데 GraphQL은 필드 단위로 걸어야 함. resolve_ 안에서 info.context.user 체크. 이거 빠뜨리면 사고남. 공통 데코레이터로 묶어서 관리.

3. 쿼리 복잡도 제한. 악의적인 중첩 쿼리 막아야 함. depth limit / cost analysis 플러그인 필요. { user { friends { friends { friends ... } } } } 같은거 방어.

4. HTTP 캐시. GET이 아니라 POST 위주라 브라우저/CDN 캐시 활용 어려움. persisted query 쓰거나 클라이언트(apollo 등)에 캐시 위임.

어드민은 괜찮은데 공개 API까지 갈지는 판단 보류. 트래픽 큰 엔드포인트는 REST가 성능/캐시 면에서 여전히 유리함. 하이브리드로 가는게 현실적인듯.

20170117

Async/Await on Node 7 — production memo

Node 7.x에서 --harmony 없이 async/await 쓸 수 있게 됨. 6.10 LTS에선 아직 flag 필요하긴 한데, 7.6부터는 기본 활성화. 프로덕션 API 하나 이걸로 작성하면서 느낀 것들.

promise chain 지옥을 벗어나니까 확실히 코드가 읽힘:

// before (promise chain)
function getOrder(id) {
  return db.Order.findById(id)
    .then(order => {
      return db.User.findById(order.userId)
        .then(user => ({ order, user }));
    })
    .then(({ order, user }) => {
      return api.tracking(order.shipmentId)
        .then(tracking => ({ order, user, tracking }));
    });
}

// after
async function getOrder(id) {
  const order = await db.Order.findById(id);
  const user = await db.User.findById(order.userId);
  const tracking = await api.tracking(order.shipmentId);
  return { order, user, tracking };
}

운영에서 배운 것들:

1. error handling은 try/catch. 안 감싸면 unhandledRejection 뜨고 메모리 새기 시작. express 핸들러는 래퍼 하나 만들어서 씀:

const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/orders/:id', asyncHandler(async (req, res) => {
  const result = await getOrder(req.params.id);
  res.json(result);
}));

2. 병렬화 놓치기 쉬움. 위 예시처럼 await를 줄줄이 쓰면 순차 실행이라 느림. 병렬 가능한 건 Promise.all로 묶어야 함:

const [user, tracking] = await Promise.all([
  db.User.findById(order.userId),
  api.tracking(order.shipmentId)
]);

3. 스택 트레이스. async 함수는 예전 callback/promise보다 스택이 잘 남음. 7.x에서 꽤 개선된 편. 그래도 async_hooks 같은 도구는 아직 실험적.

4. V8 버전. Node 7은 단명 (LTS 아님). 6 LTS 쓰려면 --harmony-async-await. 8이 4월 릴리즈 예정이고 곧 LTS 전환될테니 정식 production 이관은 8 나온 다음이 안전.

코드 리뷰 때 await 붙이고 Promise.all로 묶는 타이밍 훈련만 되면 생산성 상승이 체감됨. callback hell 시절에서 정말 많이 왔다.