1. 쿠버네티스란?

  • 컨테이너화 된 워크로드와 서비스를 관리하기 위한 플랫폼. 

2. 쿠버네티스가 유용하게 된 히스토리

  • 물리 배포 시대에는 리소스 할당이 어려웠음. 하나의 물리 서버에 여러 개의 애플리케이션이 돌아갈 경우 한 애플리케이션이 모든 리소스를 할당해 가져갈 수도 있었고 이를 격리하기에는 물리 격리가 필요했음.
  • 이를 해결하기 위해 VM을 도입함. VM간의 격리를 두고 폐기 가능하도록 했다.

3. 컨테이너와 VM의 차이는?

  • VM이 좀 더 격리 수준이 높음. 컨테이너는 운영체제를 공유한다. 그렇기에 VM보다 좀 더 가볍다.
  • 배포 시점이 아닌 빌드해서 배포할 수 있고 빠르고 효율적으로 롤백할 수 있다.

4. 쿠버네티스가 그래서 왜 필요한가?

  • 컨테이너화 된 워크로드를 확장성있게, 중단 없이 운영하기 위한 많은 기능들을 제공하기 때문임.

5. 어떤 기능?

  • 자동 복구 : 실패한 컨테이너를 다시 시작
  • 롤아웃, 롤백
  • 스토리지 오케스트레이션
  • 서비스 디스커버리, 로드밸런싱
  • 스크릿과 구성 관리

6. 쿠버네티스의 구성 요소

  • 노드 : 컨테이너화 된 애플리케이션을 실행하는 것을 노드. 워커 머신의 집합.
  • 파드 : 노드는 애플리케이션의 구성요소인 파드를 호스팅함.
  • 컨트롤 플래인 : 워커 노드와 클러스터 내 파드를 관리한다. 

6-1. 컨트롤 플레인

  • 클러스터에 관한 전반적인 결정(스케줄링 등)을 수행. 클러스터 이벤트를 감지하고 반응(ex. Replicas 맞추기)
  • 어디서든 동작 가능하지만, 보통 사용자 컨테이너와 분리하여 동작시킨다.(간결성을 위하여)

6-1.-1. 컨트롤 플레인의 컴포넌트들

  • Kube-apiserver
  • Etcd : 모든 클러스터 데이터를 담는 쿠버네티스 뒷단의 저장소. 
  • Kube-scehduler : 노드가 배정되지 않은 파드를 감지, 실행할 노드를 선택하는 컨트롤 플래인 컴포넌트
    • 노드 컨트롤러, 컨트롤러, 엔드포인트 컨트롤러, 서비스 어카운트 & 토큰 컨트롤러
  • Kube-controller-manager : 컨트롤러 프로세스를 실행하는 컴포넌트.
  • Cloud-controller-manager
    • 노드 컨트롤러, 라우트 컨트롤러, 서비스 컨트롤러의 컴포넌트를 가지고 있음.

6-2. 노드 컴포넌트

  • 노드컴포넌트는 동작중인 파드를 유지시키고, 런타임 환경을 제공하며 모든 노드상에서 동작한다.

6-2-1. 노드의 컴포넌트들

  • Kubelet: 파드에서 컨테이너가 확실하게 동작하도록 관리한다.
  • Kube-proxy : 네트워크 프록시로, 쿠버네티스 서비스 개념의 구현부.
  • 컨테이너 런타임: 컨테이너 런타임은 컨테이너 실행을 담당하는 소프트웨어

6-3. 애드온

  • 리소스를 이용하여 클러스터 기능을 구현. 클러스터 단위 기능을 제공하기에 kube-system 네임스페이스에 속한다.
  • DNS, 웹 UI, 컨테이너 리소스 모니터링, 클러스터-레벨 로깅

7. 쿠버네티스 오브젝트 (핵심 개념)

  • 쿠버네티스 오브젝트는 시스템에서 영속성을 가지는 오브젝트. 클러스터의 상태를 나타내기 위해 이 오브젝트를 이용한다.
  • 상태?
    • 어떤 애플리케이션이 동작중이고 어느 노드에서 동작 중인지
    • 애플리케이션이 이용할 수 있는 리소스
    • 재구동 정책, 업그레이드 등 동작 정책
  • 결국 쿠버네티스 오브젝트는 의도를 가진 레코드라고 할 수 있다. 
  • 생성 방식?
    • 오브젝트를 생성하기 위해서는 컨트롤플레인의 쿠버네티스 API를 사용해야 함. 주로 Kubectl cli를 통해 api를 대신 호출한다.

8. 오브젝트 명세(Spec)와 상태(Status)

  • 거의 모든 쿠버네티스 오브젝트는 spec과 status를 가진다. 
  • spec을 통해 의도하는 상태를 기술하여 쿠버네티스에 전달하면 해당 오브젝트의 status라는 개념이 생기고 이는 쿠버네티스 시스템에 의해 제공된다.
  • 의도된 spec과 현재 status를 일치시키기 위해 능동적으로 관리한다.

9. 오브젝트 기술 방법

  • 쿠버네티스 API를 활용하기 위해서는 JSON 형식으로 정보를 포함시켜 줘야 한다. (대부분은 kubectl을 활용하여 yaml로 기술하여 제공한다, 이를 kubectl이 JSON형태로 변환하여 전달해 준다.)
  • 요구되는 필드
    • apiVersion : 오브젝트를 생성하기 위해 사용되고 있는 쿠버네티스 API 버전이 어떤 것인지
    • Kind: 어떤 종류의 오브젝트를 생성하려고 하는지
    • Metadata: 이름 문자열, UID, 선택적인 namespace를 포함하여 오브젝트를 유일하게 구분 지어줄 데이터
    • spec: 오브젝트에 대해 어떤 상태를 의도하는지

'Developer > Architecture, Cloud' 카테고리의 다른 글

Kafka 학습  (1) 2022.12.11

 토픽이란?

 하나의 데이터 스트림이라 볼 수 있음. 어떤 형태의 데이터도 가능하다.(json, binary, text 등). 토픽은 파티션으로 나뉜다. 각 토픽 파티션에는 오프셋이 증가하면서 데이터가 쌓인다. 데이터는 일정시간만 유지된다.(기본은 1주일). 파티션내에서만 순서가 보장된다. 오프셋은 데이터가 삭제된다하더라도 변경되지 않는다.

 프로듀서가 메시지와 메시지의 키를 정의해서 토픽으로 메시지를 보낸다. 키가 없을 경우에는 라운드로빈으로 보낸다. 키가 존재하면 해싱해서 보낸다. 동일한 키는 항상 동일한 파티션으로 가게 된다. 

 메시지의 기본 구성 요소는 헤더, 키, 메시지, 압축방식, 파티션과 오프셋, 타임스탬프 로 구성된다. 메시지 직렬화를 통해 키와 밸류값을 바이너리 값으로 변경해준다. 

 카프카 파티셔너의 기본 해싱 알고리즘은 murmur2 algorithm이다. 파티션의 수가 바뀌면 바뀐다.

컨슈머

카프카의 컨슈머는 기본적으로 pull model이다. 카프카의 deserializer가 키와 밸류값을 역직렬화한다. 이 프로듀서에서의 직렬화와 컨슈머에서의 역직렬화의 타입을 토픽내에서는 변경하면 안된다.

 모든 컨슈머는 컨슈머그룹으로써 데이터를 읽는다. 컨슈머가 파티션보다 많을 경우 파티션 개수만큼만 동작한다. 당연히 여러 컨슈머그룹이 하나의 토픽을 읽을 수 있다.

 카프카에서는 __consumer_offsets라는 토픽에 컨슈머그룹이 어떤 offset까지 읽었는지 저장하고 있다. 오프셋을 커밋치면 다음것을 읽도록 한다. 이 커밋전략은 기본적으로는 자동인데 3개의 선택지가 존재한다. 

1. At leat Once(usually preferred)
- 메시지가 처리된 후에 커밋, 오류가 날 경우 다시 읽도록 함.
- 이 경우 메시지를 다시 처리해도 동일한 결과가 나오도록 해야 함( idempotent )

2. At most once
- 메시지를 받자마자 커밋을 친다.
- 오류가 날 경우 메시지 손실이 날 수 있다.

3. Exactly Once
- 카프카 스트림즈 API를 활용하는 것처럼 완벽히 한번만 실행되도록 한다. - 아직 정확히 이해가 안된다.

autocommit을 Enable할 경우 Interval이 지난 후에 poll을 할 경우 commit을 하는 구조. 다음 Poll까지는 이번 배치의 Processing을 끝내줘야함. 

offset을 가지고 있는 주기는 설정 가능하다. 2.0미만버전은 하루 리셋이 기본 그 이상은 7일이다.

컨슈머 설정 값

- heartbeat.interval.ms 몇초에 한번씩 보낼지
- session.timeout.ms : heartbeat가 몇초동안 오지 않으면 해당 컨슈머를 죽였다고 판정하고 리밸런싱할지
- Max.poll.interval.ms: 두 폴 사이의 Interval의 최대시간을 얼마로 기다려줄지, 이 시간이 지나면 컨슈머 죽었다고 판단
- max.poll.records : 한번에 몇개나 받을지
- fetch.min.bytes: 최소 몇 바이트까지 기다릴건지 fetch.max.wait.ms가 도달하면 어쨌든 가져온다.
- max.partition.fetch.bytes: 서버가 돌려주는 파티션별 maximum byte. fetch.max.bytes로 제한된다.

 

카프카 브로커

- ID로 정해지며 어떤 브로커로 연결되든지 모든 클러스터에 연결된다. 
- Topic-A 3 파티션, Topic B 2 Partition이 있다고 가정하자.
- 카프카 브로커들은 bootstrap server로 불리기도 한다. 카프카 클라이언트가 하나의 브로커(부트스트랩 서버)에 연결하고 메타데이터를 요청하면 모든 브로커들의 리스트를 제공한다. 

토픽 레플리케이션

- 두개 이상의 레플리케이션 팩터를 사용한다. 주로 2~3. 브로커가 오류가 생길경우 데이터를 서빙할 수 있게끔.
- 파티션의 리더 : 하나의 브로커가 하나의 파티션에 리더이다. 프로듀서는 파티션의 리더에게만 데이터를 서빙한다. 마찬가지로 컨슈머도 파티션의 리더에게서 데이터를 읽어간다. 하지만 2.4버전 이후부터는 컨슈머는 ISR중 하나에서 읽을 수 있도록 됨. 
- 파티션 리더가 데이터를 받으면 다른 브로커가 그 데이터를 복사해간다. multiple ISR(In-Sync-Replica)

Producer Acknowledgements & Topic Durability

- acks = 0 : 프로듀서가 보내고 끝 (metric 모으는등의 곳에 쓰이면 쓰루풋에 좋음)
- acks = 1 : 프로듀서가 파티션 리더가 데이터를 썼는지 기다림 (리턴값을 기다림)
- acks = all : 프로듀서가 모든 레플리카에 다 동기화 됐는지 기다림 ( acks = -1 )
- Topic Durability : replica가 3이면 2개의 브로커 로스까지는 버틸 수 있음
- 딥하게 들어가면 min.insync.replicas 값이 있는데, 이건 최소 레플리카의 개수를 뜻한다. 최소 3개의 레플리카를 원한다면 이 값을 2로 해야한다. 흔한 설정값은 RF=3일 경우 min.insync.replicas를 2로 두는 것이다. 하나의 브로커가 down되어도 가능하도록.

 

 

Zookeeper

- 2.x 버전은 주키퍼가 필수, 3.x 버전은 kafka raft(kraft)를 사용(주키퍼 없이도 사용가능), 4.x 버전은 주키퍼 없이 사용
- zookeeper는 브로커들을 매니징하고, 파티션 리더를 뽑는 도움을 줬(었)다.
- 주키퍼 서버는 홀수로 동작하도록 되어있음.
- 4.0버전 전에 production-ready가 되려면 주키퍼를 사용해야함. 운영이 아니라면 주키퍼 없이 동작은 가능함.
- 카프카 클라이언트입장에서는 이젠 주키퍼에 연결할 일이 없음. 
- 주키퍼는 카프카 클라이언트에서 연결됐을 경우 안전하지 않다.( Not secure )
- 주키퍼는 10만 파티션이상일 경우 스케일링 이슈가 있었음
- 주키퍼를 제거함으로써 더 쉽게 모니터링할 수 있고 하나의 시큐리티만 신경쓰면 되고( 클라이언트가 주키퍼를 연결하지 않으므로 ). 시작과 컨트롤러 셧다운 리커버리 타임이 짧다.

Sticky Partitioner

- key값이 존재하지 않고 produce를 빠르게 하고 있을 경우에 퍼포먼스 증가를 위하여 같은 파티션내에 넣어준다. 

리밸런싱 과정

- 컨슈머그룹에 새로운 컨슈머가 들어오거나 나갔을 경우 어떤 파티션을 어떤 컨슈머가 컨슘할것인지 다시 결정하는 과정. 기본적으로는 Eager Rebalance로 Stop-the-world 이벤트가 발생하고 모든 파티션을 다시 재 지정해준다.
- Cooperative Rebalance는 비교적 최근 리밸런싱 과정인데 모두 멈추고 다시 하는것이 아니라 일부만 재 배정해주기 위해 멈춘다.

AutoCommit Interval

- 자바로 따지면 auto.commit.interval.ms = 5000일 경우 5초에 한번씩 커밋을 치는 것이라 보면 된다.
 - 동작 과정은 한번의 poll에 3초씩 걸릴 경우, 1poll, 2poll 후에 6초가 지나게 되는데 이 후에 동작하는 것이라 보면 된다. (5초라고 해서 6초 이전에 갑자기 동작하는 것이 아니다) 어떤 poll 후에 동작할지?를 정하면 된다.


Producer Retries

- kafka 2.0 이하일경우 retry 0 이 default, 이상일경우에는 2147483647이라는 높은 값이 설정된다. 하지만, delivery.timeout.ms 라는 설정값보다 오래 retry 하지 않는다.
-  retry.backoff.ms 설정값은 다음 리트라이를 몇 ms 이후에 할 것인지를 결정한다. 
- 이 리트라이 설정은 key ordering에 의존할 경우 문제가 될 수 있는데 - (producer는 정상적으로 kafka에 데이터를 보냈으나, ack을 받는 도중에 네트워크 에러가 생겼다. 프로듀서는 다시 리트라이를 하게 돼서 중복되는 메시지를 보내게 된다.) 의 경우에 그렇다.
- 이런걸 방지할 수 있는게 Idemptoent Producer이다. Duplicate 메시지를 발견하고 두번 보내지 않도록 해준다. (3.0부터 default임) 
  

Producer Message Compression

- snappy, ztsd 등의 압축을 적용하여 kafka에 보낼 수 있다.
- 장점 : 작아서 높은 throughput, kafka 용량 줄어듦
- 단점 : compress, decompress에 cpu 사이클이 들어감
- linger.ms batch.size를 조절하여 큰 배치를 진행하고 높은 처리량을 갖도록 한다.
- 브로커레벨에서의 compression도 가능하다.
- 프로듀서가 압축을해서 보낼경우 컨슈머에서 압축을 풀어줘야한다.(당연히)

 

 

Linger.ms & batch.size 등 프로듀서의 배치에 대해

  • 기본적으로 카프카 프로듀서는 레코드를 asap 보내는데, 많은 메시지가 한번에 나갈 경우 배치 프로세스를 통해 보낸다.
  • linger.ms 설정값은 보내기전에 얼마나 기다릴 것인지 결정한다. 이 값을 줄 경우 ex(5ms)를 줄경우 많은 메시지를 배치하게 된다.
  • batch.size 정하고 어떤 메시지가 이상일경우 바로 보낸다. 값은 최대치를 정하는 거다. 
  • 프로듀서가 높은 스루풋을 가질 경우 브로커가 속도를 못 따라가면 레코드는 메모리 버퍼안에 들어간다. 이 경우 async로 보내지 않고 블록을 하게 되는데 그 블록하는 시간이 max.block.ms이다.

 

 

 

 

아직 작성중 (배경 설명)

데이터가 한번에 들어오는 것이 아니라 업데이트 로직을 사용해야 한다.

한번에 모든 데이터가 들어오는 것이 아니라 비동기적으로 각각의 데이터가 모아지고 있습니다. 아래 토픽들을 바라보고 토픽 메시지에서 얻을 수 있는 값들을 모아서 파싱중입니다.

Primary Key를 TraceID로 두고 Document를 계속 업데이트 하면 되곘다라고 생각하고 진행을 했는데 데이터가 워낙 많이 들어와서 데이터 몇몇개가 누락되고 있습니다.

성능상 매번 ES에 쿼리를 날릴 수 없다.

1초에 수십~수백건의 http request를 날릴 수 없음. 이건 문제점이라기 보단 어려웠던 점. → 배치성으로 Update Query들을 모아서 날리는 것으로 해결하고 있음.

Elastic Search에는 Lock을 지원하지 않아서 여러 Pod를 올려 성능을 내기가 힘들었다.

하나의 Document를 계속 업데이트하는 방식이기 때문에 동시에 Update가 일어나면 나중의 update만 반영되게 됩니다. → 버전 컨트롤 확인

하나의 tid에 5~10개의 Event-Message가 발생될 것.

1초에 1000개가 올라온다면 1초에 5천개~1만개의 Consume에서 Query까지 가능해야 함. update로 하기 어려움

하나의 Document를 완성 시키는 것이 아니라 쿼리시에 여러개의 Document를 Join해서 사용하는 방식은 어떨까?

 

빠른 결론

  • goroutine, 자바 Thread, OS Thread 모두 동시성(Concurrent) 처리를 구현한다.
  • 이들의 차이를 세가지 요소 (메모리 소비 / 설치와 철거 비용 / Context Switching 비용)의 관점에서 볼 수 있다.
  • 고루틴은 메모리와 소비, 설치와 철거 비용, 컨텍스트 스위칭 비용이 자바 스레드에 비해 저렴하다.
  • Thread는 유저 레벨 스레드 / 커널 레벨 스레드로 볼 수 있다.
  • 자바의 Thread는 유저 레벨 스레드이지만 커널 스레드로 1:1 매핑되는 방식으로 동작한다.

내용

동시성 프로그래밍을 다루기 위해서 Go는 고루틴, 자바는 쓰레드를 활용합니다. 고루틴의 개념을 빠르게 이해하기 위해서는 가벼운 스레드라고 이해할 수 있습니다.

관리

고루틴은 기본적으로 Go 런타임이 자체 관리합니다. Go 런타임 상에서 관리되는 작업 단위인 여러 Goroutine은 OS 쓰레드와 1:1 매핑되지 않고, 훨씬 적은 OS 쓰레드를 사용합니다.

하지만 자바의 스레드는 OS의 커널 스레드와 1:1 매핑됩니다.(초기의 JVM에서는 JVM이 직접 스케줄링 해 줬었지만, 현재는 OS의 스케줄링 정책을 따릅니다).

User Level 스레드 : Kernel Level 스레드

  1. N:1방식 (여러 user-level 스레드가 하나의 OS 스레드 위에서 돌아갑니다.)
  • 컨텍스트 스위칭이 빠르지만, 멀티코어를 활용할 수 없습니다.
  1. 1:1 자바가 사용하는 방식으로 1개의 스레드는 1개의 OS 스레드와 일치합니다.
  • 멀티코어를 활용할 수 있지만, 컨텍스트 스위치 속도가 느립니다.

3. M:N 여러개의 OS스레드 위에 여러개의 흐름을 갖습니다.

  • 구현이 어렵지만, 컨텍스트 스위치 속도도 빠르고 멀티코어도 활용할 수 있습니다.

결국 Go는 M:N 모델을 택하고, 구현의 어려움을 언어 차원에서 구현하여 제공함으로 해결했습니다.

메모리

고루틴의 생성에는 2kb의 스택공간만 필요로 합니다.

반면 쓰레드는 1Mb(500 * 2Kb)로 시작합니다.

웹 서버를 만든다고 할 때, 요청당 1개의 스레드를 만들게 된다면 스레드의 경우 OutOfMemoryError를 만나게 되거나, 메모리 공간을 넘어 가상 메모리에 페이징 되면서 성능 저하를 겪습니다.

설치와 철거

쓰레드는 OS로 부터 리소스를 요청하고 작업이 끝나면 리소스를 돌려주기 때문에 설치, 철거 비용이 큽니다. 이 문제의 해결방법 중 하나로 쓰레드 풀을 사용합니다.

고루틴은 런타임에서 설치(생성), 철거(파괴)작업 비용이 작습니다. 그렇기 때문에 Go는 고루틴들의 관리를 따로 지원하지 않습니다.

Context Switching 비용

고루틴은 교체가 일어날 때 3개의 레지스터(PC, SP, DX)만 저장,복원됩니다.

하지만 쓰레드는 16개의 범용 레지스터, 16개의 XMM 레지스터, 16개의 AVX 레지스터 등을 저장,복원해야 하기에 비용이 많이 듭니다.

  • 고루틴의 Context 스위칭은 다음의 경우 이루어집니다.

(unbuffered 채널에 접근할 때, 시스템 I/O가 발생할 때, 메모리가 할당되었을 때, time.Sleep()코드가 실행될 때, runtime.Gosched()가 실행될때)

 

출처 및 참조

https://blog.nindalf.com/posts/how-goroutines-work/

https://medium.com/@unmeshvjoshi/how-java-thread-maps-to-os-thread-e280a9fb2e06

https://brownbears.tistory.com/313

http://tleyden.github.io/blog/2014/10/30/goroutines-vs-threads/

'Developer > Langauge' 카테고리의 다른 글

GC (Garbage Collection)에 대해.  (0) 2020.11.04

 

어디가서 Node.js '개발자'라고 말 하지는 못 하지만 그래도 간단한 프로젝트에서는 언제나 Node.js를 사용해왔다. Node.js 사용자 정도 될 것 같은데, 사용자 입장에서 불편했던 점과 Nestjs + Typescript를 공부하면서 느꼈던 좋은 점을 적어보려 한다. 

 

Node.js + js를 활용하면서 불편했던 점

- 요약: 대부분 높은 자유도에서 오는 피로감.

1. 좋은 프로젝트 구조를 생각해야 한다.
이건 높은 자유도에 따라오는 동전 뒷면같은 부분인데, 프로젝트를 시작시에 어떤 구조로 이 프로젝트 구조를 잡을지 생각해야하고 (시간도 없고 귀찮아지면) 결국 원래 썼던 구조를 따라가게..되는 단점아닌 단점이 있다. 팀원들과 함께 개발을 진행하게 되면 이 구조를 결정한 이유, 어떻게 잡힌 구조인지 설명해야 한다. 

사실 아래처럼 이미 어느정도 자리잡은(?) 좋은 구조가 존재하긴 한다. 

https://velog.io/@hopsprings2/%EA%B2%AC%EA%B3%A0%ED%95%9C-node.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0

 

견고한 node.js 프로젝트 설계하기

본 글은 Sam Quinn의 “Bulletproof node.js project architecture” 글을 번역한 것입니다. [Bulletproof node.js project architecture 🛡️ Express.js is great frameworks f

velog.io

 

2. 많은 에러에 대비해야 한다.
간단한 API를 만든다고 생각하면 유저가 보내는 Body나 쿼리에서 최악의 케이스를 생각하고 null값, '', 기대와 다른 타입의 값들을 대비하고 막아주는 처리를 해줘야 하고, 런타임시에 특정 타입에서만 작동하는 메소드를 사용하는 경우 ( ex. .length 등) 에러가 나는 경우도 대비해야 하고.. 많은 것들이 습관화 되면 빠른 개발을 할 수 있지만 더 높은 자유도 때문에 더 신경이 곤두서는 느낌도 든다.

 

Typescript + Nestjs를 배워보면서 좋다고 느낀 점.

- 정해진 틀에서 오는 안정감, 규칙을 지키면 알아서 해주는 많은 기능.

1. 좋은 프로젝트 구조를 알아서 제공해 준다.
도메인 혹은 비즈니스 로직별로 묶어주는 모듈, 모듈내에 있는 API제공 Controller, 로직을 담은 Service, 이렇게 Nestjs에서 제공하는 아키텍처를 따라 개발하면 모듈별로 직관적으로 정리할 수 있다. 서로 다른 모듈끼리 결합시켜서 하나의 큰 로직을 만들 수 있다. E2E나 Unit 테스트를 같이 구성하도록 구조를 함께 잡아주는 것은 덤. 물론 이 구조는 특별한 것이 아니라 견고한 Node.js 프로젝트를 만들기 위한 구조일 수 있지만, 그래도 여러 구조중에 이렇게 써라! 정해주는 느낌이라 더 편하다고 할까..

 

2. 많은 에러를 대비해준다.
유효성 검증에 대한 것들을 DTO와 Validation Pipe등의 기능을 활용하면 정해진 Type에 대한 검증, 빈 값에 대한 검증, 자동 타입 변경등의 많은 역할을 대신해준다. Node.js에서도 여러 패키지를 활용하면 가능할 것 같지만 Nestjs는 이걸 굉장히 자연스럽고 또 쉽게 제공해준다는 느낌을 받았다. 

기타) Status 나 응답 구조를 잡아주는 것도 Node.js의 사용자로서 편하다고 느꼈다. 

 

 

* 입문시 좋은 강의

https://nomadcoders.co/nestjs-fundamentals/lectures/1940

* 읽으면 좋을 참조 글

https://medium.com/daangn/typescript%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%9C%EB%B0%9C-73877a741dbc

GraphQL의 장점은?

Overfetching, UnderFetching의 문제를 해결할 수 있음.

하나의 endpoint에서 모든 요청을 처리

 

GraphQL은 명세, 형식일 뿐. GraphQL을 구현할 솔루션이 필요하다.

ex. GraphQL.js / GraphQL Yoga 등. 

대표적인 솔루션 - Apollo (서버/프론트) 모두 제공 및 간편하고 쉬운 설정과 다양한 기능.

 

GraphQL의 개념을 공부하면서 처음에 헷갈렸던 부분이 GraphQL이 무엇인지 알겠는데 어떻게 구현하는거지? 라는 생각이었다. 쉽게 생각해보면 하나의 endpoint를 만들어두고 해당 endpoint에서 모든 분기를 공통적으로 처리할 수 있도록 해야하는데, 생각보다 복잡한 구현이 될 것 같았다.

 

역시 GraphQL을 구현해주는 솔루션이 있었는데 Apollo가 대표적인 솔루션 중에 하나이다.

Node.js Apollo서버 예시 코드를 보면서 이해해보자.

* 참고: GraphQL은 5개의 기본 타입이 있는데 ID, String, Int, Float, Boolean이 있다. 열거 타입 enum / 객체타입 type / union타입 : 한 배열안에 다른 타입을 넣고 싶을 때

//db가 있다고 생각해본다.
const seedDB = require('./someseedDB')

//npm install로 apollo server를 설치한다.
const { Apollo, gql } = require('apollo-server')

//일종의 스키마
const schema = gql`
	//Item들 가져오기, Item 단수 하나 가져오기에 해당하는 쿼리 정의
	type Query{
    	getItems : [Item]
        getItem : Item
    }
    //Item 생성, 삭제, 업데이트
    //일반적으로 graphQL에서는 생성, 삭제와 업데이트에 해당하는 것은 Mutation으로 정의한다.
    type Mutation {
    	insertItem(
        	id: Int
            name: String
        	stocks: Int
            price: Int
        ): Item
    	//id에해당하는 Item을 삭제하고 삭제된 item을 반환하는 resolver를 구현한다.
        deleteItem(id: Int): item
        editItem(
        	id: Int
            name: String
            stocks: Int
            price: Int
        ): Item
    }
    type Item {
    	id: Int
        name : String
        stocks: Int
        price : Int
    }
`
//쿼리를 해결해줄 핸들러로 생각하면 이해가 잘 된다.
const resolver = {
	Query: {
    	getItems: () => seedDB.items,
        getItem: (parent, args, context, info) => {},
        editItem: (parent, args, context, info) => {
        	//수정에 관련된 db로직이 들어가면 된다.
        },
        insertItem: (parent, args, context, info) => {
        	//args에 item이 들어올테니 args를 활용해 db에 추가 로직이 들어간다.
        },
        deleteItem: (parent, args, context, info) => {
        	//args를 통해 query나 mutation의 argument를 가져올 수 있다.
        	/*
            	args.id를 활용해 DB에서 삭제로직이 들어갈 것이다.
        	*/
        },
    }
}

//Schema와 resolver를 함께 넣으며 서버를 생성한다.
const server = new ApolloServer({schema, resolver})
server.listen().then(()=>console.log("running at ~~"))

근데 여기까지만 보면 사실 resolver가 그동안 했던 핸들링을 모두 해주는거 아닌가 생각이 든다. 우선 이런형태다.

 

GraphQL을 흔히 REST API와 비교하곤 하는데, 그럼 REST처럼 CRUD 각각 graphQL로 어떤 쿼리를 날려야 하는지 확인 해보고, REST에서 해결하기 힘든 상황을 통해 graphQL의 장점을 확인해보자.

REST getItems에 해당하는 쿼리.

//REST에서 /api/items 로 item전부를 가져오는 쿼리는 graphQL에서는 이렇게 만든다.
query{
  getitems {
          id
          name
          price
          stock
  }
}

 

REST API에서 getItem (단수) 에 해당하는 쿼리.

//id가 1에 해당하는 item을 주세요.
query {
	item(id: 1) {
    	id
        name
        price
        stock
    }
}

 

REST API에서 createItem (addItem)에 해당하는 쿼리(graphQL에서는 Mutation)

mutation {
	//추가할 item의 명세를 인자로 적어 전달한다. 이는 args에 들어갈 것이다.
    //추가하면 resolver를 통해 생성된 item을 받아올 것이기 때문에 뒤에 item을 받아온다.
	insertItem (
    	id: 23939
        name: "nike shoes"
        price: 99900
        stocks: 291
    ) {
		id
        name
        price
        stocks
	}
}

 

REST API에서 updateItem에 해당하는 쿼리(mutation) 위에서 생성한 nike shoes의 stocks를 변경해준다.

mutation {
	//수정할 item의 명세를 인자로 적어 전달한다. 이는 args에 들어갈 것이다.
    //수정하면 resolver를 통해 생성된 item을 받아올 것이기 때문에 뒤에 item을 받아온다.
	editItem (
    	id: 23939
        name: "nike shoes"
        price: 99900
        stocks: 120
    ) {
		id
        name
        price
        stocks
	}
}

 

REST API에서 deleteItem에 해당하는 쿼리

mutation {
	//삭제할 Item의 id를 적어서 보내준다. 삭제한 Item을 리턴값을 받아온다.
	deleteItem (
    	id: 23939
    ) {
	id
        name
        price
        stocks
	}
}

 

여기까지만 보면 graphQL의 장점이 무엇인지 정확히 이해하기 힘들 것이다. 이제 다음 상황을 생각해보자.

 

1. Overfetching 상황

위의 예시에서는 Item이 오직 네개의 필드만을 가지는 간단한 데이터였다. 근데 만일 Item이 엄청나게 많은 필드를 가진 데이터라면 어떤 불편함이 있을까?

type Item struct{
    Name
    Stock
    Stars
    like
    dislike
    Price
    isTrend
    isHot
    wholike
    ...(등등 200개의 필드가 더 있다고 가정해보자)
    created_at
    updated_at
    deleted_at

}

만일 여기서 Item들이 현재 트렌드이고 인기상품인지 알기 위해 (isTrend와 isHot)필드만 알고 싶다고 가정하면, REST에서는 getItems를 통해 전체 필드를 다 가져온 후 확인해야 한다. 결국 필요하지 않은 필드 200여개를 함께 가져오게 되는 것이다. 이게 바로 overfetching이다.

 

그럼 graphQL에서는 어떻게 하면될까?

//이렇게 원하는 필드만 가져옴으로써 overfetching을 해결할 수 있다.
query{
  getitems {
    isHot
    isTrend
  }
}

이렇게 원하는 필드만 가져오면 된다.

 

2. Underfetching 상황

또 다른 예시를 생각해보자. 만일 Item 자원만 필요한 것이 아니라, 구매자들 (Buyer) 자원도 함께 필요하다고 생각해보자. 그럼 REST API를 활용한다면 이런 형태로 활용할 것이다. (Pseudo Code)

//의사 코드로 표현하면 다음과 같을 것이다.

function getItems() {
	return axios.get('/api/items');
}

function getBuyers() {
	return axios.get('/api/buyers');
}

axios.all([getItems(), getBuyers()]).then(....)



이렇게 두 번에 걸쳐서 데이터를 가져오게 된다. 한 번으로는 원하는 값들을 전부 가져오지 못한다 (= undefetching). 이게 underfetching 상황이다.

 

그럼 GraphQL에서는 이를 어떻게 할까? 한번에 쿼리로 원하는값만 딱 가져올 수 있다.

query{
    //심지어 원하는것만 가져올 수 있다.
    getItems{
        id
        isHot
        isTrend
    }
    getBuyers{
        id
        name
        gender
    }
}

 

이정도만 봐도 GraphQL이 어떤 장점이 있는지 캐치하기 쉬울 것이다. 더 다룰 내용이 남았는데 다음에 정리해야 겠다.

 

*다음에 정리할 것

또한 GraphQL은 이름에서부터 알 수 있듯이 여러 자원들간의 연결을 쉽게 해주는 장점이 있다.

이렇게 GraphQL의 장점을 알 수 있는데 이건 기능적인 측면이고, 생산적인 측면에서도 장점을 갖는다.

생산성

기존의 REST방식이라면 다른 API가 필요할 때 백엔드 개발자에게 이를 요청해야 한다. 하지만 GraphQL은 이를 좀 더 유연하게 대처할 수 있도록 해준다. 이는 같은 자원을 요구할 때 서로 다른 플랫폼에서는 다른 필드들만 필요할 수 있는데 이와 같은 상황도 쉽게 대처할 수 있게 해준다.

 

* graphQL한번에 이해하기 쉬운 두개의 영상

참조 1: www.youtube.com/watch?v=1p-s99REAus  

참조 2: www.youtube.com/watch?v=9BIXcXHsj0A

+ Recent posts