728x90
가상 면접 사례로 배우는 대규모 시스템 설계 기초의 내용을 정리했습니다.

 

1. 처리율 제한 장치 (Rate Limiter)

클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하기 위한 장치

 

예를 들어 특정 기간 내에 전송되는 클라이언트의 HTTP 요청 횟수를 제한

  • 트위터는 3시간동안 300개의 트윗을 올릴 수 있도록 제한
  • 구글 독스 API는 사용자당 분당 300회의 read 요청만 허용

 

1.1. 목적

  • DoS 공격에 의한 자원 고갈 방지
  • 비용 절감
  • 서버 과부하 예방

 

2. 개략적 설계

처리율 제한 장치를 서버에 둘 수도, 클라이언트에 둘 수도 있음

  • 하지만 클라이언트에 위치한 경우 위/변조가 쉽게 위변조 가능하고, 모든 클라이언트를 통제하는 데 어려움이 있기 때문에 서버에 위치하는 것을 권장

서버에 위치시키더라도 API 서버에 위치시킬 것인지, 앞단의 미들웨어로 둘 것인지 고민할 수 있음

  • 개별 API 서버가 관리하는 것보다는 앞단의 컴포넌트에 구현(e.g. API Gateway)하면 관리 용이
  • 다만 정답은 없음. 현재 기술 스택에 따라 적절하게 선택
    • 프로그래밍 언어, 캐시 등을 고려하여 현재 사용하는 언어가 서버 측 구현을 지원하기 충분한지
    • 적용하는 처리율 제한 알고리즘에 따라
    • monolithic service vs micro service
    • 개발 공수

 

3. 처리율 제한 알고리즘

처리율 제한을 실현하는 알고리즘은 여러 가지이고, 각각 장단점이 있기 때문에 상황에 알맞는 적절한 알고리즘 선택

 

널리 알려진 알고리즘은 아래와 같음

  • 토큰 버킷 (token bucket)
  • 누출 버킷 (leaky bucket)
  • 고정 윈도 카운터 (fixed window counter)
  • 이동 윈도 로그 (sliding window log)
  • 이동 윈도 카운터 (sliding window counter)

 

 

3.1. 토큰 버킷 알고리즘

토큰의 수를 이용해서 통해 요청 수를 제어
  • 버킷 크기와 토큰 공급률을 통해 요청을 제한
    • 버킷 크기: 버킷에 담을 수 있는 토큰의 최대 개수 = 동시에 처리할 수 있는 최대 요청 수
    • 토큰 공급률: 초당 몇 개의 토큰이 버킷에 공급되는가
  • 통상적으로 API 엔드포인트 별로 별도의 버킷 존재
    • 사용자별로 제어하는 경우, 사용자 별로 제한하고자 하는 엔드포인트의 수만큼 버킷 생성
    • 시스템의 처리율로 제어하는 경우, 모든 요청이 하나의 버킷을 공유

처리율 제한에 폭넓게 이용되고 있고, 간단한 편이라 기업들이 보편적으로 사용

  • 아마존, 스트라이프가 API 요청을 스로톨링하기 위해 활용

 

3.1.1. 동작 원리

  • 토큰 버킷은 지정된 용량을 갖는 컨테이너, 버킷에는 사전 설정된 양의 토큰이 주기적으로 채워짐
    • 토큰이 가득 찬 버킷에는 더 이상 토큰이 추가되지 않음
    • 버킷이 가득 차면 추가되는 토큰은 버려짐
  • 각 요청은 처리될 때마다 하나의 토큰을 사용
    • 요청이 오면 버킷에 토큰이 있는지 확인하고, 토큰 하나를 꺼낸 후 요청을 시스템에 전달
    • 토큰이 없는 경우 해당 요청은 버림

3.1.2. 장단점

장점:

  • 구현이 쉬움
  • 메모리 사용 측면에서 효율적
  • 짧은 시간에 집중되는 트래픽 처리 가능

단점:

  • 처리율 제한을 위해 버킷 크기와 토큰 공급률이라는 두 개의 인자를 가지고 있는데, 이 값을 적절하게 튜닝하는 것이 까다로움

 

 

3.2. 누출 버킷 알고리즘

토큰 버킷 알고리즘과 비슷하지만 요청 처리율이 고정, 보통 FIFO 큐로 구현
  • 버킷 크기와 처리율을 통해 요청을 제한
    • 버킷 크기: 큐 사이즈
    • 처리율: 단위 시간 별로 몇 개의 요청을 처리할지(dequeue)할지 지정하는 값, 보통 초 단위로 표현
  • 쇼피파이(전자상거래 기업)에서 활용

 

3.2.1. 동작 원리

  • 요청이 도착하면 큐가 가득 찼는지 확인
    • 빈 자리가 있는 경우 큐에 요청 추가
    • 큐가 가득 차 있는 경우 새 요청은 버려짐
  • 지정된 시간마다 큐에서 요청을 꺼내어 처리

3.2.2. 장단점

장점:

  • 큐의 크기 제한되어 있어 메모리 사용 측면에서 효율적
  • 고정된 처리율을 갖고 있기 때문에 안정적 출력(stable outflow rate)이 필요한 경우 적합

단점:

  • 단시간에 많은 트래픽이 몰리는 경우 큐에 오래된 요청이 적재, 해당 요청을 제때 처리 못하면 최신 요청들은 계속 버려짐
  • 토큰 버킷과 마찬가지로 요청을 제어하기 위한 인자(버킷 크기, 처리율)을 적합한 값으로 튜닝하는데 어려움 존재

 

 

3.3. 고정 윈도 카운터 알고리즘

윈도우 별로 구간을 나눠서 구간 별 요청 수를 제한

 

3.3.1. 동작 원리

  • 타임라인을 고정된 간격의 윈도(window)로 나누고, 각 윈도마다 카운터(counter)를 붙임
  • 요청이 올 때마다 카운터의 값을 1씩 증가
  • 카운터의 값이 사전에 설정된 임계치(threshold)에 도달하면 새로운 요청은 새로운 윈도가 열릴 때까지 버려짐

3.3.2. 장단점

장점:

  • 메모리 효율이 좋음
  • 이해하기 쉬움
  • 윈도가 닫히는 시점에 카운터를 초기화하기 때문에 특정한 트래픽 패턴을 처리하기 용이

단점:

  • 윈도 경계 부근에 일시적으로 많은 트래픽이 몰리는 경우, 기대했던 시스템의 처리 한도보다 더 많은 요청을 처리해야할 수 있음

 

 

3.4. 이동 윈도 로깅 알고리즘

고정 윈도의 경우 경계 부근에 과도한 트래픽이 몰리는 경우 제어하기 어려움

 

3.4.1. 동작 원리

  • 요청의 타임스탬프를 추적
    • 타임스탬프는 redis의 sorted set에 보관
  • 새 요청이 오면 만료된 타임스탬프는 제거
    • 만료된 타임스탬프는 그 값이 현재 윈도의 시작 시점보다 오래된 타임스탬프를 의미
  • 새 요청의 타임스탬프를 로그에 추가
  • 로그의 크기가 허용치보다 같거나 작으면 요청을 시스템에 전달
    • 큰 경우 요청은 거부됨 (로그에는 남지만 요청은 거부)

3.4.2. 장단점

장점:

  • 처리율 제한 메커니즘이 정교함, 어느 순간의 윈도라도 허용되는 요청의 개수는 시스템의 처리율 한도를 넘지 않음

단점:

  • 다량의 메모리를 사용, 거부된 요청의 타임스탬프도 보관

 

 

3.5. 이동 윈도 카운터 알고리즘

고정 윈도 카운터와 이동 윈도 로깅을 결합한 알고리즘

 

3.5.1. 동작 원리

  • 현재 단위 시간의 요청 수 + 직전 단위 시간의 요청수 X 이동 윈도와 직전 단위 시간이 겹치는 비율을 계산
    • 위 그림에서 단위 시간이 1분, 분당 7개의 요청을 설정했다면 3(현재 1분간 요청 수) + 5(직전 1분간 요청 수) X 70%(이동 윈도와 직전 1분간 겹치는 비율) = 6.5개
    • 이 값을 반올림 또는 내림해서 사용, 만약 내림하면 값은 6
  • 이 값을 단위 시간동안 설정한 처리량과 비교

3.5.2. 장단점

장점:

  • 이전 시간대의 평균 처리율에 따라 현재 윈도의 상태를 계산하기 때문에 짧은 시간에 몰리는 트래픽에 대응이 유연함
  • 메모리 효율이 좋음

단점:

  • 직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하기 때문에 다소 느슨함
    • 근데 이 단점은 크게 심각하지 않음
    • cloudflare에서 실시했던 40억개의 요청 가운데 시스템의 실제 상태와 맞지 않게 허용하거나 버려진 요청은 0.003%에 불과

 

 

 

4. 처리율 한도 초과 트래픽 처리

어떤 요청이 한도 제한에 걸리면 API는 HTTP 429 코드(too many request)을 클라이언트에게 전송

 

4.1. 처리율 제한 장치가 사용하는 HTTP 헤더

  • X-Ratelimit-Remaining : 윈도 내에 남은 처리 가능 요청의 수
  • X-Ratelimit-Limit : 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수
  • X-Ratelimit-Retry-After : 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림

 

 

5. 개략적인 아키텍처

 

728x90
728x90

Volume

PV / PVC

  • PV(PersistentVolume) : 데이터를 저장할 볼륨. 볼륨을 생성하고 이를 클러스터에 등록한 것
    • 볼륨 그 자체
  • PVC(PersistentVolumeClaim) : 필요한 저장 공간·RW모드 등 요청사항을 기술한 명세로서 PV에 전달하는 요청. PV와 바인딩을 하는 목적으로 사용
    • 사용자가 PV에 하는 요청 (e.g. 사용하고 싶은 용량은 얼마인지, 읽기/쓰기는 어떤 모드로 설정하고 싶은지 )

개념

영속성을 보장할 수 없는 파드에 데이터를 저장할 경우 언제든 데이터가 사라질 가능성 존재

따라서 파드의 생명주기와 무관하게 저장이 유지되는 데이터 저장소가 필요한데 이런 요구사항을 만족하기 위한 개념이 PV(PersistentVolume)와 PVC(PersistentVolumeClaim)

image

image

쿠버네티스 볼륨을 파드에 직접 할당하는 방식이 아니라 중간에 PVC를 두어 파드와 파드가 사용할 스토리지를 분리

  • 이런 구조는 파드 각각의 상황에 맞게 다양한 스토리지를 사용할 수 있게 함

클라우드 서비스를 사용할 때는 본인이 사용하는 클라우드 서비스에서 제공해주는 볼륨 서비스를 사용할 수도 있고, 직접 구축한 스토리지를 사용할 수도 있음

  • 이렇게 다양한 스토리지를 PV로 사용할 수 있지만 파드에 직접 연결하는 것이 아니라 PVC를 거쳐서 사용하므로 파드는 어떤 스토리지를 사용하는지 신경 쓰지 않아도 됨

라이프사이클

프로비저닝 -> 바인딩 -> 사용 -> 반환

image


프로비저닝 (Provisioning): PV를 만드는 단계


프로비저닝 방법에는 두 가지 존재

  • 정적(static): PV를 미리 만들어 두고 사용하는 방법
  • 동적(dynamic): 요청이 있을 때 마다 PV를 만드는 방법

정적(static) 프로비저닝

  • 미리 적정 용량의 PV를 만들어 두고 사용자의 요청이 있으면 미리 만들어둔 PV를 할당
  • 사용할 수 있는 스토리지 용량에 제한이 있을 때 유용
  • 사용하도록 미리 만들어 둔 PV의 용량이 100GB라면 150GB를 사용하려는 요청들은 실패합니다. 1TB 스토리지를 사용하더라도 미리 만들어 둔 PV 용량이 150GB 이상인 것이 없으면 요청이 실패

동적(dynamic) 프로비저닝

  • 사용자가 PVC를 거쳐서 PV를 요청했을 때 생성해서 제공
  • 사용자가 원하는 용량만큼을 생성해서 사용 가능

바인딩 (Binding): 프로비저닝으로 만든 PV를 PVC와 연결하는 단계


PVC에서 원하는 스토리지의 용량과 접근방법을 명시해서 요청하면 거기에 맞는 PV가 할당

  • PVC에서 원하는 PV가 없다면 요청은 실패
  • PVC는 원하는 PV가 생길 때까지 대기하다가 바인딩

PV와 PVC의 매핑은 1대1 관계


사용 (Using): PVC는 파드에 설정되고 파드는 PVC를 볼륨으로 인식해서 사용


할당된 PVC는 파드를 유지하는 동안 계속 사용하며 시스템에서 임의로 삭제할 수 없음

  • Storage Object in Use Protection (사용 중인 스토리지 오브젝트 보호)

반환 (Reclaiming): 사용이 끝난 PVC는 삭제되고 PVC를 사용하던 PV를 초기화(reclaim)하는 과정


초기화 정책은 3가지 존재

  • Retain: PV를 그대로 보존
  • Delete: PV를 삭제하고 연결된 외부 스토리지 쪽의 볼륨도 삭제
  • Recycle: PV의 데이터들을 삭제하고 다시 새로운 PVC에서 PV를 사용

생성

PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv
spec:
  capacity:
    storage: 100Mi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/pv/log"
  persistentVolumeReclaimPolicy: Retain

capacity : 볼륨 크기

accessModes : 볼륨 RW 모드

  • ReadWriteOnce : 하나의 노드에서만 RW 가능
  • ReadOnlyMany : 여러 노드에서 R 가능
  • ReadWriteMany : 여러 노드에서 RW 가능

persistentVolumeReclaimPolicy : PV 릴리즈(사용 종료) 시 볼륨에 저장된 데이터 삭제 옵션

  • Retain : PVC가 삭제되어도 PV(볼륨)의 데이터를 보존. 하지만 해당 PV를 다른 PVC가 사용하지 못하고, 재사용하기 위해서는 수동으로 PV를 반환해야 함
  • Delete : PVC가 삭제되면 PV(볼륨)의 데이터를 비롯해 PV(볼륨) 자체를 삭제
  • Recycle : PVC가 삭제되면 PV(볼륨)의 데이터만 삭제하고 볼륨 자체는 보존하여 곧바로 다른 PVC에 사용 가능. 하지만 현재는 deprecated되어 사용을 권하지 않음

PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sample-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 50Mi
  storageClassName: ''

accessModes : 사용하고자 하는 PV의 accessModes와 동일한 옵션을 사용해야 bound 가능

requests : 사용을 원하는 볼륨의 요구조건을 명시

  • storage : 사용하고자 하는 최소한의 크기로서 명시한 용량보다 큰 PV도 상관 없음

storageClassName: ' ' : 미리 생성한 PV들 (정적 프로비저닝 한) 안에서 가능한 PV를 바인딩하겠다는 뜻


확인

목록 및 상태 확인:

  • pv: kubectl get persistentvolume
  • pvc: kubectl get persistentvolumeclaims

세부 정보:

  • pv, pvc: kubectl describe persistentvolume [PV-이름] | [PVC-이름]

마운트

볼륨 마운트

apiVersion: v1
kind: Pod
metadata:
  name: webapp
spec:
  containers:
  - name: webapp
    image: nginx
    volumeMounts:
    - name: log-vol
      mountPath: "/log"
  volumes:
  - name: log-vol
    hostPath:
      path: "/var/log/webapp"

PVC 마운트

apiVersion: v1
kind: Pod
metadata:
  name: webapp
spec:
  containers:
  - name: webapp
    image: nginx
    volumeMounts:
    - name: log-vol
      mountPath: "/log"
  volumes:
  - name: log-vol
    persistentVolumeClaim:
      claimName: sample-pvc

Reference

728x90
728x90

들어가기 앞서

  • 2023년 RIDI 개인정보 유출 사고 관련해서 CDN 서버 캐시 설정 오류 문제 발생
  • CDN 설정의 경우 여러 서비스 및 부서에서 활용하다보니 의도치 않게 오설정될 수 있는 가능성 존재
  • 결국 내부적으로 이런 오설정 문제를 방지할 필요가 있고, 이에 대한으로 API 서버에 Cache-Control: private 헤더를 추가하여 문제를 방지할 수 있음

 

Cache-Control

Cache-Control Header

The Cache-Control HTTP header field holds directives (instructions) — in both requests and responses — that control caching in browsers and shared caches (e.g. Proxies, CDNs).

  • Cache-Control은 브라우저 및 공유 캐시에 캐싱 동작을 지시하는 HTTP 헤더

 

Browser Cache

  • 브라우저 캐싱은 웹 브라우저에서 웹 사이트 리소스를 저장하여 서버에서 다시 가져오는 것을 방지
  • 예를 들어 웹 사이트의 배경 이미지를 캐시에 로컬로 저장하면 사용자가 해당 페이지를 두 번째로 방문할 때 이미지가 사용자의 로컬 파일에서 로드되므로 페이지가 훨씬 빠르게 로드

Shared cache

  • 서버와 클라이언트 사이에 존재하는 캐시 (e.g. Proxy, CDN)
  • 단일 응답을 저장한 후 여러 사용자에게 재사용 가능 (It stores a single response and reuses it with multiple users)
    • so developers should avoid storing personalized contents to be cached in the shared cache.

 

Cache directives

Request Response
max-age max-age
max-stale -
min-fresh -
- s-maxage
no-cache no-cache
no-store no-store
no-transform no-transform
only-if-cached -
- must-revalidate
- proxy-revalidate
- must-understand
- private
- public
- immutable
- stale-while-revalidate
stale-if-error stale-if-error

 

no-store / no-cache / private

no-store

The no-store response directive indicates that any caches of any kind (private or shared) should not store this response.

Cache-Control: no-store
  • 아무것도 캐싱하지 않음

 

no-cache

The no-cache response directive indicates that the response can be stored in caches, but the response must be validated with the origin server before each reuse, even when the cache is disconnected from the origin server.

Cache-Control: no-cache
  • 응답을 캐시에 저장할 수는 있지만, 오리전 서버와 연결이 끊어졌다면 재사용 전에 오리진 서버를 통해 확인(유효성 검사)를 진행해야 함

 

must-revalidate

The must-revalidate response directive indicates that the response can be stored in caches and can be reused while fresh. If the response becomes stale, it must be validated with the origin server before reuse.

  • 만료된 캐시만 서버에 확인하고, 만료되지 않은 캐시는 캐시에서 바로 사용 (no-cache 로직이 다름)

 

private

The private response directive indicates that the response can be stored only in a private cache (e.g. local caches in browsers).

Cache-Control: private
  • 브라우저와 같은 특정 사용자 환경에 한해 저장 가능

 

public

The public response directive indicates that the response can be stored in a shared cache. Responses for requests with Authorization header fields must not be stored in a shared cache; however, the public directive will cause such responses to be stored in a shared cache.

  • 공유 캐시(Proxy or CDN)에 저장 가능

 

cf-cache-status

  • cf-cache-status는 Cloudflare에서 제공하는 헤더로, 캐시 상태를 확인할 수 있음
  • cf-cache-status: DYNAMIC: 기본적으로 캐시를 사용하지 않음

 

Reference

728x90
728x90

https://product.kyobobook.co.kr/detail/S000001766328

 

 

 

요즘 애플리케이션은 데이터 중심(data- intensive)적

  • 계산 중심(compute-intensive)적이지 않음
  • CPU 성능보다 데이터의 양이나 복잡도, 변화 속도에 영향을 더 많이 받음

시스템 설계 시 엔지니어들은 데이터 저장소 및 엔진을 구상하지 않음

  • 데이터베이스가 데이터 저장 작업을 위해 매우 효율적인 도구이기 때문
  • 다만 시스템 별로 요구사항이 다르기 때문에 다양한 데이터베이스의 특성 및 적용 방법을 고민할 필요가 있음

 

데이터 시스템에 대한 생각

일반적으로 데이터베이스. 큐, 캐시 등을 매우 다른 범주에 속하는 도구로 생각

데이터가 보관되는 시간, 크기 / 데이터를 접근하는 방법 / 지연 시간

 

 

그러면 이 모든 것을 데이터 시스템이라는 포괄적 용어로 묶어야 할까?

  1. 데이터 저장과 처리를 위한 여러 도구들이 최근에 만들어지며 경계가 모호하다
  • 각 도구들이 다양한 유스 케이스에 최적화됐기 때문에 전통적인 분류에 들어맞지 않음
  • 데이터 베이스처럼 지속성을 보장하는 기술(e.g. kafka)등도 등장하고 있기에 경계가 흐려짐
  1. 단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시킬 수 있는 광범위한 요구사항
  • 간단한 예를 생각해보면 데이터베이스와 캐시를 같이 사용
  • 검색 엔진(서버)를 구성하기 위해 전문 저장(데이터베이스), 색인, 색인을 위한 이벤트 전송(큐)

 

최근의 요구사항을 충족하기 위해서는 하나의 기술만을 사용하는 것이 아니라 여러 기술들을 조합해서 사용하게 되고, 이러한 복합 데이터 시스템을 구축할 필요가 있음

 

 

데이터 시스템이나 서비스를 설계할 때 아래 세 가지 관심사를 중요시 해야 함

  1. Reliability(신뢰성)
  • 결함이 발생했을 때 지속적으로 올바르게 동작(원하는 성능 수준에서 정확한 기능을 수행)
  1. Scalability(확장성)
  • 데이터의 양, 트래픽이 증가할 때 이를 처리할 수 있어야 함
  1. Maintainability(유지보수성)
  • 시스템을 사용하는 모든 사용자가 생산적으로 작업할 수 있어야 함

 

Reliability(신뢰성)

올바르게 동작한다(기대한 대로 동작한다), 무언가 잘못되더라도 지속적으로 올바르게 동작한다.

 

결함(fault)을 예측하고 대처할 수 있는 시스템을 의미함

  • 내결함성(fault-tolerant) 또는 탄력성(resilient)을 지녔다고 표현

 

cf. 신뢰성 vs 가용성

  • 신뢰성: 서비스가 일정 기간동안 얼마나 정상적으로 동작하는지
  • 가용성: 서비스가 얼마나 사용 가능한 상태로 유지되는지 (장애로 인한 중단 시간 최소화)

 

 

결함과 장애(failure)는 동일하지 않음

  • 결함: 정의 또는 사양에서 벗어난 시스템의 한 구성 요소로 정의
  • 장애: 정상적으로 서비스가 제공되지 못 하고 시스템 전체가 중단된 것

결함 확률을 0으로 줄이는 것은 불가능하기에, 결함으로 인해 장애가 발생하지 않도로 설계하는 것이 중요함

 

카오스 엔지니어링처럼 고의적으로 결함을 일으켜 탐지하는 방법도 내결함성을 높이기 위한 좋은 접근 방법

 

 

하드웨어 결함

시스템 장애의 원인을 생각할 때 가장 먼저 생각할 수 있는 결함

  • 화재, 정전으로 인한 장애
  • 물리 장비의 고장

하드웨어 결함으로 인한 시스템 장애를 어떻게 대비해야 할까?

  • 하드웨어 구성 요소에 중복(redundancy)을 추가 (이중화)

 

 

소프트웨어 오류

하드웨어 결함은 무작위적이고 독립적임

  • 어떤 장비에 장애가 발생했을 때 다른 장비에 직접적으로 장애가 전파되지 않음

반면 소프트웨어 오류(시스템 내 체계적 오류)는 예상하기 어렵고, 노드 간 상관관계 때문에 오히려 시스템 오류를 더욱 유발하는 경향이 있음

  • 잘못된 특정 입력이 있을 때 모든 애플리케이션 서버 인스턴스가 죽는 버그
  • 특정 프로세스가 공유 자원(CPU, 메모리, 디스크, 대역폭)을 과도하게 사용
  • 시스템 속도가 느려져 잘못된 응답을 반환하는 서비스
  • 단일 장애 지점(SPOF)

 

소프트웨어 오류로 인한 시스템 장애를 어떻게 대비해야 할까?

  • 시스템 내 상호작용에 대해 고민
  • 지속적인 테스트
  • 프로세스 격리
  • 지속적인 모니터링

 

인적 오류

인적 오류로 인한 시스템 장애를 어떻게 대비해야 할까?

  • 오류의 가능성을 최소하는 방향으로 시스템 설계
    • 잘 설계된 추상화
    • 인터페이스 설계
  • 사람의 실수로 장애가 발생할 수 있는 부분을 분리
    • 샌드박스 제공
  • 지속적인 테스트
    • 단위 테스트부터 통합 테스트까지 모든 수준에 대한 테스트
  • 복구의 용이성
    • 롤백 또는 롤아웃을 쉽고 빠르게
  • 명확한 모니터링 기준 및 대책

 

신뢰성의 중요성

  • 신뢰성은 원자력 발전소나 항공 관제 등 직접적인 안전과 연관된 시스템에만 적용되는 것은 아님
  • 일상적인 애플리케이션에서도 동일하게 안정적으로 작동해야 하며, 이는 서비스의 비용 및 명성에 영향을 줄 수 있음
  • 또한 적은 수의 사용자 및 중요하지 않은 애플리케이션에도 사용자에 대한 책임도 있음

 

 

Scalability(확장성)

증가한 부하에 대처하는 시스템 능력

확장성에 대한 이분법적인 생각이 아닌 시스템이 특정 방식으로 커졌을 때 대처하는 선택, 추가 부하를 다루기 위해 계산 자원을 어떻게 투입하지에 대한 고민이 중요

 

부하 기술하기

시스템의 부하 요소를 간결하게 기술하는 것은 중요함

  • 부하 매개변수(부하의 요인)이 정의되어야 부하가 증가했을 때에 대한 논의를 진행할 수 있음

부하 매개변수는 몇 개의 숫자로 나타낼 수 있는데, 가장 적합한 변수 선택은 시스템 설계에 따라 달라질 수 있음

  • RPS, 데이터베이스의 읽기/쓰기 비율, 활성 사용자 수, cache hits, ...

 

트위터 사례

트윗 작성과 타임라인 조회에 대한 부하

  • 트윗 작성 시 홈 타임라인에서 조회 가능

이를 어떻게 효율적으로 대처해야 하는가

  1. 트윗은 하나, 개별 사용자의 타임라인 조회 시 팔로워 테이블을 모두 조인하여 조회
  2. 트윗을 개별 팔로워의 레코드에 모두 추가, 사용자는 본인의 트윗만 조회

1번 방식의 경우 타임라인 조회에 대한 부하가 매우 큼, 2번 방식의 경우 쓰기 작업을 위한 코스트는 커지지만 타임라인 조회를 매우 수월하게 진행할 수 있음

  • 평균적으로 트윗 게시량(쓰기)보다 타임라인 조회(읽기) 요청량이 수백 백 많기 때문에 2번이 효율적

다만 2번 방식의 경우 팔로워가 적을 때는 문제가 없지만, 팔로워의 수가 극도로 많은 경우 쓰기 작업에 대한 코스트가 매우 커질 수 있음

  • 결국 이 두 가지 접근 방식을 혼합(e.g. 팔로워가 적은 경우 2번 방식, 팔로워가 많은 경우 1번 방식)해서 사용하는 형태를 채택

 

성능 기술하기

아래 질문에 대해서 답하기 위해서는 모두 성능 수치가 필요함

  • 부하가 증가했을 때 시스템 자원(리소스 스펙)을 변경하지 않으면 시스템 성능은 어떻게 영향을 받을까?
  • 부하가 증가했을 때 성능을 유지하기 위해서는 리소스를 어떻게 증가시켜야 할까?

시스템 별로 성능의 기준 및 우선순위는 다름

  • 하둡 같은 일괄 처리 시스템은 처리량이 중요함
  • 일반적인 온라인 서비스는 서비스의 응답 시간이 중요함

 

cf. 응답 시간(response time) vs 지연 시간(latency time)

응답 시간은 클라이언트 관점에서의 시간으로 실제 서비스 시간 외에 네트워크 지연 등 모든 지연을 포함, 지연 시간은 실제 요청이 처리되길 기다리는 시간으로, 서비스를 기다리는 휴지 상태 시간을 의미함

 

 

응답 시간을 확인하기 위해서는 아래의 지표를 활용할 수 있음

  • 평균 응답 시간
  • p50, p95, p99

 

부하 대응 접근 방식

  • 비공유 아키텍처
    • scale up(vertical) vs scale out(horizontal)
  • 탄력성
    • 자동으로 자원을 추가(e.g. HPA) vs 수동으로 확장

 

Maintainability(유지보수성)

소프트웨어 비용의 대부분은 초기 개발이 아닌 지속적인 유지보수에 발생

유지보수 비용을 최소하하기 위해 아래 3가지 원칙을 고려해야 함

  1. 운용성(Operability): 운영자(Operator)가 시스템을 원활하게 운영할 수 있게 쉽게 구현
  2. 단순성(Simplicity): 시스템 복잡도를 최대한 제거해서 새로운 엔지니어가 투입됐을 때 시스템을 이해하기 쉽게 구현
  3. 발전성(Evolvability): 엔지니어가 추후 시스템을 쉽게 변경할 수 있게 구현

 

운용성

운영을 쉽고 빠르게 수행

 

운영 중 일부 측면은 자동화할 수 있고, 자동화해야 함

  • 다만 자동화를 처음 설정하고 제대로 동작하는지 확인하는 일도 사람의 몫

좋은 운영성이란 동일하게 반복되는 작업을 쉽고 빠르게 수행할 수 있게 만들어, 더 가치있는 일에 노력을 집중할 수 있게하는 것

 

단순성

시스템이 복잡해지면 작업 진행 속도가 저하됨

 

시스템이 복잡해지면 모든 사람의 작업 진행 속도가 느려지고, 이는 유지보수 비용의 증가로 이어짐

 

복잡한 시스템은 예산과 일정을 초과시키고, 버그가 유발할 가능성이 증가됨

 

복잡도를 제거하기 위한 좋은 도구는 추상화

  • 좋은 추상화는 외관을 깔끔(세부 구현을 숨겨 복잡함을 제거)하게 하고, 재사용성을 증가시킴

 

발전성

새롭고 변경되는 요구사항에 대해 유연하게 대처할 수 있어야 함

 

시스템 요구사항은 계속 바뀌기에, 이러한 변경 사항을 민첩하게 대처할 수 있어야 함

 

 

Appendix

분산 데이터베이스에서 CAP 이론

  • Consistency(일관성): 사용자가 분산 데이터베이스 상의 어떤 노드와 통신하는지 상관없이 같은 데이터를 조회할 수 있는 것
  • Availability(가용성): 모든 요청이 응답을 받을 수 있어야 한다는 것을 의미
  • Partition Tolerance(분할 허용성): 시스템 내 분할이 생겼을 때 시스템이 여전히 작동하는 것을 의미

CAP를 모두 충족할 수 있는 시스템은 없음. 하지만 분산 환경에서는 Partition Tolerance는 필수적임

728x90
728x90

ArgoCD

  • 쿠버네티스 환경에서 GitOps 기반의 CD(Continuous Deployment) 도구 (GitOps CD tools for k8s)
  • 대상 환경에서 원하는 상태의 배포 자동화를 지원 (Automate the deployment of the desired state in target env)
  • 쿠버네티스에서 지원하는 다양한 리소스 형태를 지원 (k8s menifests can be specified in serveral ways)
    • kubstomize, helm, yaml, ...

 

ArgoCD 구성 요소

ArgoCD는 크게 3가지 컴포넌트(서버)로 구성

  • API server, Repository Server, Application Controller

 

API server

  • 사용자(or 관리자) UI 또는 CLI를 통해 접근
  • 다른 서비스에서 gRPC 또는 REST API를 통해 접근
  • Git webhook event를 통해 접근
  • 철학적으로 ArgoCD는 외부의 identity provider에게 권한을 위임해서 인증/인가 구현
  • ArgoCD는 RBAC을 통해 권한을 관리

Repository Server

  • Git repository에 대한 캐싱
    • Git에 있는 일련의 현상을 k8s 워크로드에 싱크해주는 게 ArgoCD의 목적
  • helm, yaml 파일들을 k8s manifest로 변경

Application Controller

  • k8s controller
  • reconciliation(지속적으로 비교해서 current state을 desired state로)을 담당

 

 

ArgoCD Project & Application

Project는 Application을 그룹으로 관리하는 개념으로, Appliction은 생성할 때 Project를 선택해야 하고 Project는 0개 이상 Application을 가질 수 있음

 

image

  • Project: 여러 application의 묶음 (k8s의 namespace)
  • Application: k8s의 workload에 맵핑

 

 

ArgoCD 도입과 배포 프로세스

ArgoCD 도입 시 아래와 같은 형태로 배포 프로세스를 가져갈 수 있음

 

ArgoCD 배포 프로세스

  1. 사용자는 기능(피처) 개발을 진행 후 해당 코드를 Source Repository에 푸시
  2. 구축된 CI 파이프라인을 통해 해당 소스의 도커 이미지 빌드 및 이미지 레지스트리에 등록
  3. CI 프로세스에서 생성된 이미지에 대해 트리거를 통해 ArgoCD와 Sync 되어 있는 GitOps Repository에 원하는 버전을 적용(푸시)
  4. Webhook 또는 ArgoCD polling을 통해 GitOps Repository에 선언된 리소스와 쿠버네티스 클러스터 상의 리소스를 확인 및 비교
  5. GitOps Repository에 선언된 리소스와 배포된 리소스의 상태가 다른 경우 GitOps Repository에 선언된 리소스를 자동화 적용

 

 

 

Reference

 

GitHub - argoproj/argo-cd: Declarative Continuous Deployment for Kubernetes

Declarative Continuous Deployment for Kubernetes. Contribute to argoproj/argo-cd development by creating an account on GitHub.

github.com

 

728x90
728x90

 

CI (Continuous Integration)

  • 빌드/테스트 자동화 과정
  • CI를 성공적으로 구현할 경우 애플리케이션에 대한 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합되므로 여러 명의 개발자가 동시에 애플리케이션 개발과 관련된 코드 작업을 할 경우 서로 충돌할 수 있는 문제를 해결
  • 커밋할 때마다 빌드와 일련의 자동 테스트가 이루어져 동작을 확인하고 변경으로 인해 문제가 생기는 부분이 없도록 보장
  • Compile, Build, Unit test, Integration Test, Static analysis

CI common practices

  • Maintain a code repository
    • Application should be buildable from a fresh checkout without requiring additional dependencies
  • Automate the build & Keep the build fast
  • Everyone commits to the baseline every day & Every commit (to baseline) should be built
  • Every bug-fix commit should come with a test case
  • Test in a clone of the production environment(Staging)

 

CD (Continuous Deployment)

  • 배포 자동화 과정
  • 지속적인 서비스 제공(Continuous Delivery) 또는 지속적인 배포(Continuous Deployment)를 의미
  • 코드 변경이 파이프라인의 이전 단계를 모두 성공적으로 통과하면 수동 개입 없이 해당 변경 사항이 프로덕션에 자동으로 배포
  • 간단한 코드 변경이 정기적으로 마스터에 커밋되고, 자동화된 빌드 및 테스트 프로세스를 거치며 다양한 사전 프로덕션 환경으로 승격되며, 문제가 발견되지 않으면 최종적으로 배포

 

CI/CD 종류

  • Jenkins
  • CircleCI
  • TravisCI
  • Github Actions
  • Argo

 

CI/CD 적용 전과후 비교

CI/CD를 적용하기 전

  1. 개발자들이 개발하여 코드를 수정
  2. 각자의 feature 브랜치에 코드를 push (but, 어느 한 부분에서 에러가 발생한 경우 개발자들은 순간순간 확인이 어려움)
  3. 각자의 코드를 git에 올리고 통합(Intergration)
  4. 에러가 발생했지만 어느 부분에서 에러가 났는지 모르므로 다시 어디부분에 에러가 있는지 디버깅하고 코드를 수정
  5. (1) ~ (4)의 과정을 반복
  6. 많은 시간을 할애하여 에러가 해결되었으면 배포. 하지만 배포과정 또한, 개발자가 직접 배포과정을 거치므로 많은 시간이 소요됨

 

CI/CD를 적용 후의 과정

  1. 개발자들이 개발하여 feature 브랜치에 코드를 push
  2. git push를 통해 Trigger되어 CI서버에서 알아서 Build, Test, Lint를 실행하고 결과를 전송
  3. 개발자들은 결과를 전송받고 에러가 난 부분이 있다면 에러부분을 수정하고 코드를 master 브랜치에 merge
  4. master 브랜치에 코드를 merge하고 Build, Test가 정상적으로 수행이 되었다면 CI 서버에서 알아서 Deploy 과정을 수행

 

 

Reference

 
728x90

'CI CD' 카테고리의 다른 글

[CI/CD] ArgoCD 란? - ArgoCD 개념과 구조  (0) 2024.07.28
728x90

본 글에서는 아래 내용을 포함하고 있습니다.

• 암호화의 종류 및 방식을 정리합니다.
    • 단방향 vs 양방향
    • 대칭키 vs 공개키


• 단방향 암호화 방식 중 하나인 SHA-256, 대칭키 방식 중 하나인 AES-256 GCM 알고리즘을 코틀린으로 구현합니다.

암호화

1. 용어 설명

암호학을 이용하여 보호해야 할 메시지를 평문(plaintext)이라고 하며, 평문을 암호학적 방법으로 변환한 것을 암호문(cipherText)이라고 한다.

이때 평문을 암호문으로 변환하는 과정을 암호화(encryption)라고 하며, 암호문을 다시 평문으로 변환하는 과정을 복호화(decryption)라고 한다.

 

2. 암호화 종류

암호화 방식은 복호화 가능 여부에 따라 단방향/양방향 암호화로 나누고, 양방향 암호화 내에서 암호화/복호화를 위해 사용되는 키에 따라 비밀키(대칭키) 암호화와 공개키(비대칭키) 암호화로 분류할 수 있다.

 

 

3. 단방향 암호화

암호화된 데이터를 복호화할 수 없는 암호화 방식

 

단방향 암호화는 대표적으로 신원 증명과 인증 과정에서 사용된다.

 

예를 들어 비밀번호를 ‘wiki123’이라고 지정했을 때, 이를 암호화하여 ‘A3pnqq49.Hw’라는 아무런 유사성 없는 암호문을 만들어 낸다.

 

단방향 암호화의 특징은 역으로 변환하는 것이 불가능하고, 어떤 방법을 쓰더라도 암호문을 원래의 평문으로 되돌릴 수 없다.

 

3.1. 단방향 암호화의 종류

 

4. 양방향 암호화

암호화된 데이터를 복호화할 수 있는 암호화 방식

 

4.1. 대칭키 암호화

암호화와 복호화에 같은 암호키(대칭키)를 사용한다.

 

암호문을 생성(암호화)할 때 사용하는 키와 암호문으로부터 평문을 복원(복호화)할 때 사용하는 키가 동일한 암호 시스템이다.

 

암호 시스템의 안전성은 키의 길이, 키의 안전한 관리에 상대적으로 의존성이 높다.

 

대칭키 암호 시스템은 알고리즘이 상대적으로 단순한 장점이 있지만, 사용자 간 키를 공유해야 하고, 이를 각각 관리해야 하기에 키 관리에 어려움이 많다.

 

4.1.1. 암호화 및 복호화 과정

 

4.1.2. 대칭키 암호 시스템의 종류

대칭키 암호 시스템의 안전성은 키의 길이와 매우 관련이 크다. 일반적으로 키의 길이가 길수록 안전성은 높다. 그러나 키의 길이를 무한정 길게 하면 그에 따르는 관리의 어려움이 커진다.

 

4.2. 공개키(비대칭키) 암호화

대칭키 암호 시스템의 가장 큰 약점은 키관리의 어려움에 있다. 한 사용자가 관리해야 할 키의 수가 너무 많아지기 때문이다.

이러한 약점을 보완하기 위해 나타난 암호 시스템이 공개키 암호 시스템이다. 공개키 암호 시스템에서 각 사용자는 두 개의 키를 부여 받는다.

그 하나는 공개되고(공개키, public key), 다른 하나는 사용자에 의해 비밀리에 관리 되어야 한다.(비밀키, private key) 공개키 암호 시스템에서 각 사용자는 자신의 비밀키만 관리하면 되므로 키 관리의 어려움을 줄일 수 있다.

 

4.2.1. 암호화 및 복호화 과정

 

 

4.2.2. 공개키 암호 시스템의 종류

 

5. 요약

암호화 방식 복호화 여부 암호화/복호화 키 특징 종류
단방향 암호화 X - 복호화 불가 MD5, SHA-2, ...
대칭키 암호화 O 동일 빠르지만 키 관리 어려움 AES, DES, ...
공개키 암호화 O 분리 느리지만 키 관리 용이함 RSA, 타원 곡선 암호, ...

 

 

5.1. SSL/HTTPS?

대칭키와 공개키 암호화 방식을 적절히 혼합해서 사용하면 SSL 탄생의 시초가 된다

  1. A가 B의 공개키로 암호화 통신에 사용할 대칭키를 암호화하고 B에게 보냄
  2. B는 암호문을 받고, 자신의 비밀키로 복호화함
  3. B는 복호화 성공 여부에 따라 handshake 과정을 진행
  4. 성공 시 서로에게 공유된 대칭키를 이용하여 통신

handshake 과정에서 공개키 방식을 사용하고, 이후 데이터 전송은 대칭키 방식을 사용

 

6. 구현

단방향 암호화 방법의 한 종류인 SHA-256과 양방향 암호화 방법의 한 종류인 AES를 각각 구현

 

SHA-256

object Sha256Utils {
    private const val algorithm = "SHA-256"
    private const val digits = "0123456789ABCDEF"

    fun encrypt(plainText: String): String {
        val hash: ByteArray
        try {
            val md = MessageDigest.getInstance(algorithm)
            md.update(plainText.toByteArray())
            hash = md.digest()
        } catch (e: CloneNotSupportedException) {
            throw DigestException("couldn't make digest of partial content");
        }

        return bytesToHex(hash)
    }

    private fun bytesToHex(byteArray: ByteArray): String {
        val hexChars = CharArray(byteArray.size * 2)
        for (i in byteArray.indices) {
            val v = byteArray[i].toInt() and 0xff
            hexChars[i * 2] = digits[v shr 4]
            hexChars[i * 2 + 1] = digits[v and 0xf]
        }
        return String(hexChars)
    }
}
class Sha256UtilsTest {
    @Test
    fun `SHA-256 알고리즘을 이용하여 암호화한다`() {
        // given
        val plainText = "12345678"

        // when
        val encryptedText = Sha256Utils.encrypt(plainText)

        // then
        assert(plainText != encryptedText)
        assert(encryptedText == Sha256Utils.encrypt(plainText))
    }
}

 

AES-256 (GCM)

AES 알고리즘은 키의 길이에 따라 AES-128, AES-192, AES-256으로 나뉜다.

  • AES-128 : 16bytes
  • AES-192 : 24bytes
  • AES-256 : 32bytes

또한 AES 알고리즘은 BCM(Block Cipher Mode)에 따라 암호화 방식이 다르다.

  • ECB (Electronic Code Block)
    • 가장 기본적인 암호화 방식
    • 평문을 블록 단위로 나누어 암호화
    • 항상 같은 결과가 나오기 때문에 역추적 가능
  • CBC (Cipher Block Chaining)
    • ECB의 단점을 보완한 방식으로 암호화 키에 IV(Initial Vector)를 추가해서 항상 같은 결과가 나오는 문제를 해결
    • 이전 블록의 암호문을 다음 블록의 평문과 XOR 연산을 수행
    • 병렬 처리 불가능
  • GCM (Galois/Counter Mode)
    • CBC의 단점을 보완한 방식으로 병렬 처리가 가능
    • 암호문에 hash 값을 추가하여 무결성을 보장

예제는 GCM(Galois/Counter Mode) 방식으로 구현

object Aes256Utils {
    enum class Algorithm(val transformation: String) {
        GCM("AES/GCM/NoPadding"),
    }

    private const val KEY_SPEC_ALGORITHM = "AES"
    private const val IV_HASH_ALGORITHM = "SHA-1"
    private const val IV_LENGTH_BYTE = 12
    private const val KEY_LENGTH_BIT = 256
    private const val GCM_SPEC_TAG_LEN = 128

    private fun secretKeySpec(key: String): SecretKeySpec {
        /**
         * AES-256 알고리즘을 사용하기 위해 key를 32바이트로 설정한다
         */
        require(
            key.isNotEmpty() && key.length == KEY_LENGTH_BIT / 8
        ) { "Key must be ${KEY_LENGTH_BIT / 8} characters long, but was ${key.length} characters long." }
        val keyBytes = key.toByteArray()
        return SecretKeySpec(keyBytes, 0, KEY_LENGTH_BIT / 8, KEY_SPEC_ALGORITHM)

    }

    private fun iv(plainText: String): ByteArray {
        val ivHash = MessageDigest.getInstance(IV_HASH_ALGORITHM).digest(plainText.toByteArray())
        return Arrays.copyOf(ivHash, IV_LENGTH_BYTE)
    }

    fun encrypt(plainText: String, key: String, algorithm: Algorithm = Algorithm.GCM): String {
        /**
         * 대칭키를 생성한다
         */
        val secretKey = secretKeySpec(key)

        /**
         * 블록 암호화에서 사용되는 IV(Initialization Vector)를 생성한다
         */
        val iv = iv(plainText)

        /**
         * 암호화를 위한 파라미터를 생성한다
         * @param: tLen: 암/복호화에 사용될 인증 태그 길이 (128, 120, 112, 104, 96)
         * @param: iv: 초기화 벡터
         */
        val parameterSpec = GCMParameterSpec(GCM_SPEC_TAG_LEN, iv)

        /**
         * 암호화를 위한 Cipher 객체를 생성한다
         */
        val cipher = Cipher.getInstance(algorithm.transformation)
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec)

        val cipherText = cipher.doFinal(plainText.toByteArray())

        return Base64.getEncoder().encodeToString(iv) +
                Base64.getEncoder().encodeToString(cipherText)
    }

    fun decrypt(input: String, password: String, algorithm: Algorithm = Algorithm.GCM): String {
        /**
         * 대칭키를 생성한다
         */
        val secretKey = secretKeySpec(password)

        /**
         * 암호화된 문자열을 디코딩한다
         */
        val ivAndCipherText = Base64.getDecoder().decode(input)

        /**
         * IV와 암호화된 문자열을 분리한다
         */
        val iv = ivAndCipherText.sliceArray(0 until IV_LENGTH_BYTE)
        val cipherText = ivAndCipherText.sliceArray(IV_LENGTH_BYTE until ivAndCipherText.size)

        /**
         * 복호화를 위한 파라미터를 생성한다
         * @param: tLen: 암/복호화에 사용될 인증 태그 길이 (128, 120, 112, 104, 96)
         * @param: iv: 초기화 벡터
         */
        val parameterSpec = GCMParameterSpec(GCM_SPEC_TAG_LEN, iv)

        /**
         * 복호화를 위한 Cipher 객체를 생성한다
         */
        val cipher = Cipher.getInstance(algorithm.transformation)
        cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec)

        val plainText = cipher.doFinal(cipherText)

        return String(plainText)
    }
}
class Aes256UtilsTest {
    @Test
    fun `AES-256 GCM 알고리즘을 사용하여 암호화 및 복호화를 진행한다`() {
        // given
        val key = "12345678901234567890123456789012"
        val plainText = "12345678"

        // when
        val encryptedText = Aes256Utils.encrypt(plainText, key)
        val decryptedText = Aes256Utils.decrypt(encryptedText, key)

        // then
        assert(plainText != encryptedText)
        assert(plainText == decryptedText)
    }
}

728x90
728x90
📌 들어가기 앞서

본 글은 향로 선생님의 4. Spring Batch 가이드 - Spring Batch Job Flow 의 내용을 바탕으로 작성되었습니다.
내용을 효과적으로 읽기 위해서는 위 글을 우선적으로 읽어보시는 것을 권장드립니다.


본 글은 아래의 내용을 포함하고 있습니다.
• Spring Batch Job Flow와 흐름 제어


원본 글에 대한 인용 및 요약은 음영 처리된 형태로 표시됩니다.
글의 작성 방향은 이 글을 참고해주시면 감사하겠습니다.

관련 시리즈

1. Spring Batch 5 가이드 - 배치 어플리케이션이란?
2. Spring Batch 5 가이드 - Batch Job 실행해보기
3. Spring Batch 5 가이드 - 메타테이블엿보기
4. Spring Batch 5 가이드 - Spring Batch Job Flow
5. Spring Batch 5 가이드 - Spring Batch Scope & Job Parameter
6. Spring Batch 5 가이드 - Chunk 지향 처리
7. Spring Batch 5 가이드 - ItemReader
8. Spring Batch 5 가이드 - ItemWriter
9. Spring Batch 5 가이드 - ItemProcessor 

 

앞서 Spring Batch의 Job을 구성하는데는 Step이 있다고 말씀드렸습니다.

Step은 실제 Batch 작업을 수행하는 역할을 합니다. 이전에 작성한 코드를 보시면 Job은 코드가 거의 없죠?
실제로 Batch 비지니스 로직을 처리하는 (ex: log.info()) 기능은 Step에 구현되어 있습니다.

이처럼 Step에서는 Batch로 실제 처리하고자 하는 기능과 설정을 모두 포함하는 장소라고 생각하시면 됩니다.

Batch 처리 내용을 담다보니, Job 내부의 Step들간에 순서 혹은 처리 흐름을 제어할 필요가 있는데요.
이번엔 여러 Step들을 어떻게 관리할지에 대해서 알아보겠습니다.

 

4.1. next

next()는 순차적으로 Step들 연결시킬때 사용됩니다.
step1 -> step2 -> stpe3 순으로 하나씩 실행시킬때 next()는 좋은 방법입니다.
@Configuration
class SimpleNextJobConfiguration(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {
    private val log = logger()

    @Bean
    fun stepNextJob(): Job {
        return JobBuilder("stepNextJob", jobRepository)
            .start(step1())
            .next(step2())
            .next(step3())
            .build()
    }

    @Bean
    fun step1(): Step {
        return StepBuilder("step1", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step1")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun step2(): Step {
        return StepBuilder("step2", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step2")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun step3(): Step {
        return StepBuilder("step3", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step3")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }
}

실행 결과

 

4.1.1. 지정한 Batch Job 만 실행

배치 코드 내에 2개 이상의 Job이 정의되어 있는 경우 아래와 같은 에러가 발생합니다.

Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs

Job name must be specified in case of multiple jobs

 

이러한 경우 application.yml 파일 내에 아래와 같이 실행할 job name을 지정할 수 있게 하고,
Program argument 혹은 Environment vairables로 jobName을 지정해주시면 됩니다.

# application.yml

spring.batch.job.name: ${jobName:NONE}
Spring Boot 3.x.x 로 버전이 올라가면서 기존에 사용했던 spring.batch.job.names 프로퍼티는 spring.batch.job.name 으로 대체되었습니다.

관련 이슈: Remove support for running multiple Spring Batch jobs

 

 

4.2. 조건 별 흐름 제어

Next가 순차적으로 Step의 순서를 제어한다는 것을 알게 됐습니다.

여기서 중요한 것은, 앞의 Step에서 오류가 나면 나머지 뒤에 있는 Step 들은 실행되지 못한다는 것입니다.

하지만 상황에 따라 정상일때는 Step B로, 오류가 났을때는 Step C로 수행해야할때가 있습니다.

이 경우 FlowBuilder에서 제공하는 아래 메서드를 통해 Job의 흐름 및 순서를 제어할 수 있습니다.

  • .on()
    • 캐치할 ExitStatus 지정
    • * 일 경우 모든 ExitStatus가 지정
  • to()
    • 다음으로 이동할 Step 지정
  • from()
    • 일종의 이벤트 리스너 역할
    • 상태값을 보고 일치하는 상태라면 to()에 포함된 step을 호출
    • step1의 이벤트 캐치가 FAILED로 되있는 상태에서 추가로 이벤트 캐치하려면 from을 써야만 함
  • end()
    • end는 FlowBuilder를 반환하는 end와 FlowBuilder를 종료하는 end 2개가 있음
    • on("*")뒤에 있는 end는 FlowBuilder를 반환하는 end
    • build() 앞에 있는 end는 FlowBuilder를 종료하는 end
    • FlowBuilder를 반환하는 end 사용시 계속해서 from을 이어갈 수 있음

 

아래의 시나리오를 가정하여 예제를 작성해보겠습니다.

  • step1 실패 시나리오: step1 -> step3
  • step1 성공 시나리오: step1 -> step2 -> step3
@Configuration
class StepNextConditionalJobConfiguration(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {
    private val log = logger()

    @Bean
    fun stepNextConditionalJob(): Job {
        return JobBuilder("stepNextConditionalJob", jobRepository)
            .start(step1())
                .on("FAILED") // FAILED 일 경우
                .to(step3()) // step3으로 이동한다.
                .on("*") // step3의 결과 관계 없이
                .end() // step3으로 이동하면 Flow가 종료한다.
            .from(step1()) // step1로부터
                .on("*") // FAILED 외에 모든 경우
                .to(step2()) // step2로 이동한다.
                .next(step3()) // step2가 정상 종료되면 step3으로 이동한다.
                .on("*") // step3의 결과 관계 없이
                .end() // step3으로 이동하면 Flow가 종료한다.
            .end() // Job 종료
            .build()
    }

    @Bean
    fun step1(): Step {
        return StepBuilder("step1", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step1")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun step2(): Step {
        return StepBuilder("step2", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step2")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun step3(): Step {
        return StepBuilder("step3", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> This is Step3")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }
}

 

4.2.1. BatchStatus vs. ExitStatus

조건 별 흐름제어를 설정할 때 FlowBuilder에서 제공하는 on() 메서드는 BatchStatus가 아닌 ExitStatus를 기준으로 제어합니다.

@Configuration
class StepNextConditionalJobConfiguration(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {
    @Bean
    fun stepNextConditionalJob(): Job {
        return JobBuilder("stepNextConditionalJob", jobRepository)
            .start(step1())
                .on("FAILED") // ExitStExitStatus.FAILED 를 의미
                .to(step3())
            ...
    }
    ...
 }

 

BatchStatus는 Job 또는 Step 의 실행 결과를 Spring에서 기록할 때 사용하는 Enum입니다. BatchStatus로 사용 되는 값은 아래와 같습니다.

BatchStatus

 

반면 흐름 제어에서 참조하는 ExitStatus는 Step의 실행 후 상태를 얘기합니다. ExitStatus는 Enum class가 아닙니다.

ExitStatus

 

Spring Batch는 기본적으로 ExitStatus의 exitCode는 Step의 BatchStatus와 같도록 설정이 되어 있습니다.

 

만약 흐름 제어를 위해 별도의 커스텀 ExitStatus 가 필요한 경우 아래와 같이 별도의 exitCode를 반환할 수 있게 설정해주시면 됩니다.

class SkipCheckingListener : StepExecutionListener {
        override fun afterStep(stepExecution: StepExecution): ExitStatus? {
            val exitCode = stepExecution.exitStatus.exitCode
            return if (exitCode != ExitStatus.FAILED.exitCode &&
                stepExecution.skipCount > 0
            ) {
                ExitStatus("COMPLETED WITH SKIPS") // 커스텀 exitCode 반환
            } else {
                null
            }
        }
    }

 

4.3 decide

자 위에서 (4.2)에서 Step의 결과에 따라 서로 다른 Step으로 이동하는 방법을 알아보았습니다.

이번에는 다른 방식의 분기 처리를 알아 보겠습니다. 위에서 진행했던 방식은 2가지 문제가 있습니다.

1. Step이 담당하는 역할이 2개 이상 존재
    - 실제 해당 Step이 처리해야할 로직 외에도 분기처리를 시키기 위해 ExitStatus 조작이 필요합니다.

2. 다양한 분기 로직 처리의 어려움
    - ExitStatus를 커스텀하게 고치기 위해선 Listener를 생성하고 Job Flow에 등록하는 등 번거로움이 존재합니다.

명확하게 Step들간의 Flow 분기만 담당하면서 다양한 분기처리가 가능한 타입이 있으면 편하겠죠?

그래서 Spring Batch에서는 Step들의 Flow속에서 분기만 담당하는 타입이 있습니다. JobExecutionDecider 라고 하며, 이를 사용한 샘플 코드를 한번 만들어보겠습니다.

 

@Configuration
class DeciderJobConfiguration(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {
    private val log = logger()

    @Bean
    fun deciderJob(): Job {
        return JobBuilder("deciderJob", jobRepository)
            .start(deciderStartStep())
            .next(decider()) // 홀수 | 짝수 구분
            .from(decider()) // decider의 상태가
            .on("ODD") // ODD라면
            .to(oddStep()) // oddStep로 간다.
            .from(decider()) // decider의 상태가
            .on("EVEN") // ODD라면
            .to(evenStep()) // evenStep로 간다.
            .end() // builder 종료
            .build()
    }

    @Bean
    fun deciderStartStep(): Step {
        return StepBuilder("deciderStartStep", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> Start Step")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun evenStep(): Step {
        return StepBuilder("evenStep", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> 짝수입니다")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun oddStep(): Step {
        return StepBuilder("oddStep", jobRepository)
            .tasklet(
                { _, _ ->
                    log.info(">>>>> 홀수입니다")
                    RepeatStatus.FINISHED
                },
                transactionManager,
            )
            .build()
    }

    @Bean
    fun decider(): JobExecutionDecider {
        return OddDecider()
    }

    class OddDecider : JobExecutionDecider {
        private val log = logger()

        override fun decide(jobExecution: JobExecution, stepExecution: StepExecution?): FlowExecutionStatus {
            val pivot = Random().nextInt(50) + 1
            log.info("랜덤숫자: {}", pivot)

            return if (pivot % 2 == 0) {
                FlowExecutionStatus("EVEN")
            } else {
                FlowExecutionStatus("ODD")
            }
        }
    }
}

실행 결과

 

분기 로직에 대한 모든 일은 OddDecider가 전담하고 있습니다.
아무리 복잡한 분기로직이 필요하더라도 Step과는 명확히 역할과 책임이 분리된채 진행할 수 있습니다.

728x90
728x90
📌 들어가기 앞서

본 글은 향로 선생님의 3. Spring Batch 가이드 - 메타테이블엿보기 의 내용을 바탕으로 작성되었습니다.
내용을 효과적으로 읽기 위해서는 위 글을 우선적으로 읽어보시는 것을 권장드립니다.


본 글은 아래의 내용을 포함하고 있습니다.
• Spring Batch 메타데이터 스키마 설명


원본 글에 대한 인용 및 요약은 음영 처리된 형태로 표시됩니다.
글의 작성 방향은 이 글을 참고해주시면 감사하겠습니다.

관련 시리즈

1. Spring Batch 5 가이드 - 배치 어플리케이션이란?
2. Spring Batch 5 가이드 - Batch Job 실행해보기
3. Spring Batch 5 가이드 - 메타테이블엿보기
4. Spring Batch 5 가이드 - Spring Batch Job Flow
5. Spring Batch 5 가이드 - Spring Batch Scope & Job Parameter
6. Spring Batch 5 가이드 - Chunk 지향 처리
7. Spring Batch 5 가이드 - ItemReader
8. Spring Batch 5 가이드 - ItemWriter
9. Spring Batch 5 가이드 - ItemProcessor 

 

지난 글(2. Spring Batch 5 가이드 - Batch Job 실행해보기)의 말미에 Spring Batch의 메타데이터 스키마에 대해 간략히 소개를 했습니다.

 

이번 글에서는 각 메타 데이터의 역할이 무엇인지, 어떤 정보를 담고 있는지 정리해보겠습니다.

https://docs.spring.io/spring-batch/reference/schema-appendix.html

TL;DR

Spring Batch meta-data schema : https://docs.spring.io/spring-batch/reference/schema-appendix.html
  • BATCH_JOB_INSTANCE
    • 배치 Job의 생성 정보를 담는 테이블
  • BATCH_JOB_EXECUTION
    • 배치 Job의 개별 실행 정보(성공, 실패 등)를 담는 테이블
    • Job Instance(BATCH_JOB_INSTANCE) 하나에 여러 개의 Job Execution(BATCH_JOB_EXECUTION)이 생성
  • BATCH_JOB_EXECUTION_PARAM
    • 배치 Job에서 사용되는 파라미터 값들을 담는 테이블
    • 과거에는 타입 별로 파라미터를 담을 수 있는 컬럼이 존재했지만, 현재는 파라미터의 타입과 값을 지정하여 정보를 저장
  • BATCH_JOB_EXECUTION_CONTEXT
    • 배치 Job 실행 중에 사용된 정보에 대한 Context를 저장하기 위한 테이블
    • Job Execution 하나당 하나씩의 Job Execution Context를 갖고, 해당 Context 정보를 통해 동일한 Job Scope 내에서 데이터를 공유 할 수 있음
    • Context 데이터를 통해 오류가 발생한 이후 중단된 부분부터 시작할 수 있음
  • BATCH_STEP_EXECUTION
    • 배치 Step의 개별 실행 정보를 담는 테이블
  • BATCH_STEP_EXECUTIOIN_CONTEXT
    • 배치 Step 실행 중에 사용된 정보에 대한 Context를 저장하기 위한 테이블

 

 

3.1. BATCH_JOB_INSTANCE

3.1.1. BATCH_JOB_INSTANCE 테이블

BATCH_JOB_INSTANCE 테이블은 Job Parameter에 따라 생성되는 테이블입니다.

이 Job Parameter가 생소할텐데요. 간단하게 말씀드리면, Spring Batch가 실행될때 외부에서 받을 수 있는 파라미터입니다.

예를 들어, 특정 날짜를 Job Parameter로 넘기면 Spring Batch에서는 해당 날짜 데이터로 조회/가공/입력 등의 작업을 할 수 있습니다.

같은 Batch Job 이라도 Job Parameter가 다르면 Batch_JOB_INSTANCE에는 기록되며, Job Parameter가 같다면 기록되지 않습니다.

 

BATCH_JOB_INSTANCE 테이블은 배치 Job의 생성 정보를 담는 테이블입니다.

CREATE TABLE BATCH_JOB_INSTANCE  (
  JOB_INSTANCE_ID	BIGINT  PRIMARY KEY,
  VERSION		BIGINT,
  JOB_NAME		VARCHAR(100) NOT NULL,
  JOB_KEY		VARCHAR(32) NOT NULL
);
컬럼명 설명
JOB_INSTANCE_ID Job 고유 ID
VERSION 버전 정보 (update 될 때마다 1씩 증가)
JOB_NAME Job 빌드 시 생성하는 이름
JOB_KEY 동일한 Job의 개별 인스턴스를 구분하기 위한 고유 값

동일한 JOB_NAEM을 갖는 Job Instance는 서로 다른 Job Parameters에 의해 고유한 JOB_KEY를 갖음

 

 

3.2. BATCH_JOB_EXECUTION

3.2.1. BATCH_JOB_EXECUTION 테이블

BATCH_JOB_EXECUTION 테이블은 배치 Job의 개별 실행 정보를 담는 테이블입니다.

JOB_EXECUTION와 JOB_INSTANCE는 부모-자식 관계입니다.
JOB_EXECUTION은 자신의 부모 JOB_INSTACNE가 성공/실패했던 모든 내역을 갖고 있습니다. 
CREATE TABLE BATCH_JOB_EXECUTION  (
  JOB_EXECUTION_ID	BIGINT  PRIMARY KEY ,
  VERSION		BIGINT,
  JOB_INSTANCE_ID	BIGINT NOT NULL,
  CREATE_TIME		TIMESTAMP NOT NULL,
  START_TIME		TIMESTAMP DEFAULT NULL,
  END_TIME		TIMESTAMP DEFAULT NULL,
  STATUS		VARCHAR(10),
  EXIT_CODE		VARCHAR(20),
  EXIT_MESSAGE		VARCHAR(2500),
  LAST_UPDATED		TIMESTAMP,
  constraint JOB_INSTANCE_EXECUTION_FK foreign key (JOB_INSTANCE_ID)
  references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ;
컬럼명 설명
JOB_EXECUTION_ID Job 실행의 고유 ID
VERSION 버전 정보 (update 될 때마다 1씩 증가)
JOB_INSTANCE_ID Job 고유 ID
CREATE_TIME Job 실행 생성 일시
START_TIME Job 실행 생성 일시
END_TIME Job 실행 종료 일시
STATUS 실행 상태 (BatchStatus)
EXIT_CODE 종료 코드
EXIT_MESSAGE Job 수행 종료(실패) 시 메세지
LAST_UPDATED 최종 업데이트 일시

 

 

3.2.2. BATCH_JOB_EXECUTION_PARAM 테이블

BATCH_JOB_EXECUTION_PARAM 테이블은 배치 Job에서 사용되는 파라미터 값들을 담는 테이블입니다.

BATCH_JOB_EXECUTION_PARAM 3.x.x vs 5.1.2

이전에는 String, Date, Long, Double 네가지 타입의 값만 사용이 가능했고, 해당 컬럼에 값을 저장했다면,
현재는 파라미터의 타입과 값을 지정하여 정보를 저장합니다. 

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS  (
  JOB_EXECUTION_ID	BIGINT NOT NULL ,
  PARAMETER_NAME	VARCHAR(100) NOT NULL ,
  PARAMETER_TYPE	VARCHAR(100) NOT NULL ,
  PARAMETER_VALUE	VARCHAR(2500) ,
  IDENTIFYING		CHAR(1) NOT NULL ,
  constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
  references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);
컬럼명 설명
JOB_EXECUTION_ID Job 실행의 고유 ID
PARAMETER_NAME 파라미터명
PARAETER_TYPE 파라미터 타입
PARAMETER_VALUE 파라미터 값
IDENTIFYING Job Parameter가 Job Instance 생성 시 관여했는지 여부를 나타내는 플래그

 

 

3.2.3. BATCH_JOB_EXECUTION_CONTEXT 테이블

BATCH_JOB_EXECUTION_CONTEXT 테이블은 배치 Job 실행 중에 사용된 정보에 대한 Context를 저장하기 위한 테이블입니다.

  • Job Execution 하나당 하나씩의 Job Execution Context를 갖고, 해당 Context 정보를 통해 동일한 Job Scope 내에서 데이터를 공유 할 수 있습니다.
  • Context 데이터를 통해 오류가 발생한 이후 중단된 부분부터 시작할 수 있습니다.
CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT  (
  JOB_EXECUTION_ID	BIGINT PRIMARY KEY,
  SHORT_CONTEXT		VARCHAR(2500) NOT NULL,
  SERIALIZED_CONTEXT	CLOB,
  constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
  references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;
컬럼명 설명
JOB_EXECUTION_ID Job 실행의 고유 ID
SHORT_CONTEXT String 형태의 Context
SERIALIZED_CONTEXT Serializied 된 전체 Context

 

 

3.3. BATCH_STEP_EXECUTION

3.3.1. BATCH_STEP_EXECUTION 테이블

BATCH_STEP_EXECUTION 테이블은 배치 Step의 개별 실행 정보를 담는 테이블입니다.

CREATE TABLE BATCH_STEP_EXECUTION  (
  STEP_EXECUTION_ID	BIGINT NOT NULL PRIMARY KEY,
  VERSION		BIGINT NOT NULL,
  STEP_NAME		VARCHAR(100) NOT NULL,
  JOB_EXECUTION_ID	BIGINT NOT NULL,
  CREATE_TIME		TIMESTAMP NOT NULL,
  START_TIME		TIMESTAMP DEFAULT NULL,
  END_TIME		TIMESTAMP DEFAULT NULL,
  STATUS		VARCHAR(10),
  COMMIT_COUNT		BIGINT,
  READ_COUNT		BIGINT,
  FILTER_COUNT		BIGINT,
  WRITE_COUNT		BIGINT,
  READ_SKIP_COUNT	BIGINT,
  WRITE_SKIP_COUNT	BIGINT,
  PROCESS_SKIP_COUNT	BIGINT,
  ROLLBACK_COUNT	BIGINT,
  EXIT_CODE		VARCHAR(20),
  EXIT_MESSAGE		VARCHAR(2500),
  LAST_UPDATED		TIMESTAMP,
  constraint JOB_EXECUTION_STEP_FK foreign key (JOB_EXECUTION_ID)
  references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
);
컬럼명 설명
STEP_EXECUTION_ID Step 실행의 고유 ID
VERSION 버전 정보 (update 될 때마다 1씩 증가)
STEP_NAME Step 이름
JOB_EXECUTION_ID Job 실행의 고유 ID
CREATE_TIME Step 실행 생성 일시
START_TIME Step 실행 생성 일시
END_TIME Step 실행 종료 일시
STATUS 실행 상태 (BatchStatus)
COMMIT_COUNT 트랜잭션당 커밋 수
READ_COUNT 조회한 item 수
FILTER_COUNT 필터링된 item 수
WRITE_COUNT 저장된 item 수
READ_SKIP_COUNT 조회 skip한 item 수
WRITE_SKIP_COUNT 저장 skip한 item 수
PROCESS_SKIP_COUNT 가공(Process) skip된 item 수
ROLLBACK_COUNT 롤백 발생 횟수
EXIT_CODE 종료 코드
EXIT_MESSAGE Step 수행 종료(실패) 시 메세지
LAST_UPDATED 최종 업데이트 일시

 

 

3.3.2. BATCH_STEP_EXECUTION_CONTEXT 테이블

BATCH_STEP_EXECUTIOIN_CONTEXT 테이블은 배치 Step 실행 중에 사용된 정보에 대한 Context를 저장하기 위한 테이블입니다.

  • BATCH_JOB_EXECUTION_CONTEXT의 Step 버전이라고 생각하시면 됩니다.
CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT  (
  STEP_EXECUTION_ID	BIGINT PRIMARY KEY,
  SHORT_CONTEXT		VARCHAR(2500) NOT NULL,
  SERIALIZED_CONTEXT	CLOB,
  constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
  references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
);
컬럼명 설명
STEP_EXECUTION_ID Step 실행의 고유 ID
SHORT_CONTEXT String 형태의 Context
SERIALIZED_CONTEXT Serializied 된 전체 Context

 

 

3.4. JOB, JOB_INSTANCE, JOB_EXECUTION

위의 메타데이터 스키마와 Spring Batch에서 프로세스를 연관지어서 정리해보면 아래와 같습니다.

  1. Spring Batch를 통해 Job을 정의(e.g. SimpleJob)한다.
  2. Job 실행 시 JobParameter를 통해 특정 일시 또는 조건을 설정한다. (= JobParameter에 따른 JobInstance 생성)
  3. 특정 조건에 의해 생성된 JobInstance가 실행될 때마다 개별 실행에 따른 JobExecution 정보가 저장된다.
728x90
728x90
📌 들어가기 앞서

본 글은 향로 선생님의 2. Spring Batch 가이드 - Batch Job 실행해보기 의 내용을 바탕으로 작성되었습니다.
내용을 효과적으로 읽기 위해서는 위 글을 우선적으로 읽어보시는 것을 권장드립니다.


본 글은 아래의 내용을 포함하고 있습니다.
• Spring Batch 프로젝트 생성하기
• Spring Batch 기본 용어
• 간단한 Job과 Step을 구성하여 배치 실행하기
• MySQL과 함께 실행하기
• Spring Batch 5에서 추가로 변경된 사항


원본 글에 대한 인용 및 요약은 음영 처리된 형태로 표시됩니다.
글의 작성 방향은 이 글을 참고해주시면 감사하겠습니다.

관련 시리즈

1. Spring Batch 5 가이드 - 배치 어플리케이션이란?
2. Spring Batch 5 가이드 - Batch Job 실행해보기
3. Spring Batch 5 가이드 - 메타테이블엿보기
4. Spring Batch 5 가이드 - Spring Batch Job Flow
5. Spring Batch 5 가이드 - Spring Batch Scope & Job Parameter
6. Spring Batch 5 가이드 - Chunk 지향 처리
7. Spring Batch 5 가이드 - ItemReader
8. Spring Batch 5 가이드 - ItemWriter
9. Spring Batch 5 가이드 - ItemProcessor 

작업한 코드는 Github에 작성되어 있습니다.
필요한 경우 참고하시면 됩니다.

 

2.1. Spring Batch 프로젝트 생성하기

기존 향로님이 진행하셨던 개발 환경은 아래와 같습니다.

  • IntelliJ IDEA 2018.2
  • Spring Boot 2.0.4
  • Java 8
  • Gradle

 

본 시리즈에서는 아래의 개발 환경으로 예제 코드가 작성됩니다.

  • Spring Boot 3.3.0 (Spring Batch 5.1.2)
  • Kotlin JVM 1.9.24
  • JDK 17
  • Gradle (+ Kotlin DSL)

 

아래 파일은 위 환경에 따른 Gradle 파일입니다.

plugins {
	id("org.springframework.boot") version "3.3.0"
	id("io.spring.dependency-management") version "1.1.5"
	kotlin("jvm") version "1.9.24"
	kotlin("plugin.spring") version "1.9.24"
}

group = "com.ruthetum"
version = "0.0.1-SNAPSHOT"

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-batch")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	runtimeOnly("com.h2database:h2")
	runtimeOnly("com.mysql:mysql-connector-j")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testImplementation("org.springframework.batch:spring-batch-test")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
	compilerOptions {
		freeCompilerArgs.addAll("-Xjsr305=strict")
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}

 

 

Spring Batch 기본 용어

간단한 Job을 선언하는 방법을 확인하기 전에 기본적인 Spring Batch의 용어들에 대해 간단하게 설명드리겠습니다.

  • JoLauncher : Job을 실행시키는 컴포넌트
  • Job : 배치 작업
  • JobRepository : Job 실행과 Job, Step을 저장
  • Step : 배치 작업의 단계
  • ItemReader, ItemProcesser, ItemWriter : 데이터를 읽고 처리하고 쓰는 구성

 

Job은 1개 이상의 Step으로 구성되어 배치 작업을 수행합니다.

 

후에 설명을 드리겠지만 Step 같은 경우 Chunk 기반의 작업을 수행하는 경우와 Tasklet 기반의 작업을 수행하는 경우로 구분됩니다.

  • 일반적으로 Chunk 기반 스텝을 많이 사용합니다.
  • Tasklet 스탭은 하나의 트랜잭션 내에서 작동하고, 단순한 처리를 할 때 사용하게 됩니다.

 

 

2-2. Simple Job 생성하기

기본 용어를 이해한 상태에서 간단한 Job을 구성해보도록 하겠습니다.

먼저 Java와 Spring Batch 4 기준으로 간단한 Job을 구성하게 되면 아래와 같은 형태로 작성되어집니다.

@Slf4j // log 사용을 위한 lombok 어노테이션
@RequiredArgsConstructor // 생성자 DI를 위한 lombok 어노테이션
@Configuration
public class SimpleJobConfiguration {
    private final JobBuilderFactory jobBuilderFactory; // 생성자 DI 받음
    private final StepBuilderFactory stepBuilderFactory; // 생성자 DI 받음

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob")
                .start(simpleStep1())
                .build();
    }

    @Bean
    public Step simpleStep1() {
        return stepBuilderFactory.get("simpleStep1")
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is Step1");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

 

각 컴포넌트 및 라인에서 의미하는 바는 아래와 같습니다.

@Configuration
    - Spring Batch의 모든 Job은 @Configuration으로 등록해서 사용합니다.

jobBuilderFactory.get("simpleJob")
    - simpleJob 이란 이름의 Batch Job을 생성합니다.
    - job의 이름은 별도로 지정하지 않고, 이렇게 Builder를 통해 지정합니다.

stepBuilderFactory.get("simpleStep1")
    - simpleStep1 이란 이름의 Batch Step을 생성합니다.
    - jobBuilderFactory.get("simpleJob")와 마찬가지로 Builder를 통해 이름을 지정합니다.

tasklet((contribution, chunkContext))
    - Step 안에서 수행될 기능들을 명시합니다.
    - Tasklet은 Step안에서 단일로 수행될 커스텀한 기능들을 선언할때 사용합니다.
    - 여기서는 Batch가 수행되면 log.info(">>>>> This is Step1") 가 출력되도록 합니다.

 

기본적으로 Spring 및 Spring Batch에서 활용되는 어노테이션 및 주요 컴포넌트는 대부분 동일합니다.

다만 Spring Batch의 버전이 올라가면서 몇몇 어노테이션 지원 및 Spring Batch에서 배치를 실행하기 위해 정의하는 Job에 대한 설정 방법에 일부 수정이 있습니다.

 

[Batch 4 vs. Batch 5] JobBuilderFactory, StepBuilderFactory deprecated

먼저 Job과 Step을 선언하는 경우 JobBuilderFactory, StepBuilderFactory는 Batch 5를 기준으로 지원하지 않게 되었습니다.

  • 이에 따라 더이상 Builder Factory를 사용하지 않고 JobBuilder, StepBuilder를 사용합니다.

 

[Batch 4 vs. Batch 5] JobRepository, TransactionManager 

Spring Batch 5에서는 내부의 필요한 객체를 명시적으로 표시해주는 형태로 변경되었습니다.

  • 이에 따라 기존에는 생략했던 JobRepository, TransactionManager를 선언하여 Job을 구성하게 됩니다.

 

변경점을 고려하여 위에서 작성했던 예제 코드를 Kotlin과 Srping Batch 5 기준으로 작성하면 아래와 같이 변경됩니다.

@Configuration
class SimpleJobConfiguration(
    private val jobRepository: JobRepository,
    private val transactionManager: PlatformTransactionManager,
) {
    private val log = logger()

    @Bean
    fun simpleJob(): Job {
        return JobBuilder("simpleJob", jobRepository)
            .start(simpleStep1())
            .build()
    }

    @Bean
    fun simpleStep1(): Step {
        return StepBuilder("simpleStep1", jobRepository)
            .tasklet(simpleTasklet(), transactionManager)
            .build()
    }

    fun simpleTasklet(): Tasklet {
        return Tasklet { _, _ ->
            log.info(">>>>> This is Step1");
            RepeatStatus.FINISHED
        }
    }
}

 

실행 결과는 아래와 같습니다.

 

정상적으로 Job과 Step이 실행됨을 확인할 수 있습니다.

 

 

2-3. MySQL 환경에서 Spring Batch 실행해보기

이전 과정에서 굉장히 간단하게 Spring Batch가 수행되었습니다.
Spring Batch는 어플리케이션 코드만 작성하면 되는구나! 라고 생각하실수 있으실텐데요.

실제로는 그렇지 않습니다. Spring Batch에선 메타 데이터 테이블들이 필요합니다.

Spring Batch의 메타 데이터는 다음과 같은 내용들을 담고 있습니다.
- 이전에 실행한 Job이 어떤 것들이 있는지
- 최근 실패한 Batch Parameter가 어떤것들이 있고, 성공한 Job은 어떤것들이 있는지
- 다시 실행한다면 어디서 부터 시작하면 될지어떤 Job에 어떤 Step들이 있는지
- Step들 중 성공한 Step과 실패한 Step들은 어떤것들이 있는지

등등 Batch 어플리케이션을 운영하기 위한 메타데이터가 여러 테이블에 나눠져 있습니다.

 

아래 테이블은 

 

https://docs.spring.io/spring-batch/reference/schema-appendix.html

 

Spring Batch의 버전이 올라감에 따라 메타 데이터 스키마에도 작은 변경이 있었고, 해당 내용에 대해서는 다음 글에서 설명드리겠습니다.

테이블의 정의나 관계는 크게 변하지 않았고, Job Parameter 정보를 담는 BATCH_JOB_EXECUTION_PARAMS의 테이블 정의가 일부 변경되었습니다.

 

이 테이블들이 있어야만 Spring Batch가 정상 작동합니다.

기본적으로 H2 DB를 사용할 경우엔 해당 테이블을 Boot가 실행될때 자동으로 생성해주지만, MySQL이나 Oracle과 같은 DB를 사용할때는 개발자가 직접 생성해야만 합니다.

그럼 이 테이블들의 스키마가 궁금하실텐데요.
이미 Spring Batch에 해당 스키마가 존재하고 있고, 이를 그대로 복사해서 create table 하면 됩니다.

 

각 데이터베이스 별로 Native Query를 확인하고 싶으신 경우 Spring Batch Github을 통해 확인하실 수 있습니다.

e.g. MySQL Metadata table schema

 

 

2.4. Spring Batch 5에서 추가로 변경된 사항

본 내용은 원본 글에는 포함되어 있지 않은 Spring Batch 5를 적용할 때  Batch 4와 다른 부분에 대해서 내용을 정리합니다.

 

[Batch 4 vs. Batch 5] @EnableBatchProcessing 

Spring Boot 3.0부터는 @EnableBatchProcessing 사용을 권장하지 않습니다.

  • 정확히는 필수적으로 사용해야 하는 어노테이션이 아니게 되었습니다.
  • @EnableBatchProcessing 어노테이션의 경우 Batch와 관련된 기본 Bean들을 자동으로 등록해주는 기능을 담당합니다.
  • 그래서 버전 4까지는 위 어노테이션을 사용하여 기본 Bean들을 등록했지만, 버전5부터는 해당 어노테이션을 사용하지 않아도 Bean 등록이 진행됩니다.
 
728x90

+ Recent posts