- 1588-pizza : PIZZA 통합주문콜센터
- Table of contents
- 서비스 시나리오
- 분석/설계
- 구현:
- 운영
- application.yml
- (Order) StoreService.java
- (Order) StoreServiceFallbackFactory.java
- (Store) StoreController.java
- http POST http://104.42.177.6:8080/orders customerId=1 pizzaNm="하와이안피자" qty=1 regionNm="강남구"
- http POST http://104.42.177.6:8080/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="종로구"
- Self-healing (Liveness Probe)
- 끗~
기능적 요구사항
- 고객이 피자를 주문한다.
- 고객 주문이 완료되면 해당지역 체인점에 주문이 접수된다.
- 체인점에서 피자 조리가 완료되면 지배인(Master)이 "조리완료" 처리한다.
- 피자 조리가 완료되면 배달을 시작한다.
- 고객이 마이페이지를 통해 주문 상태를 확인할 수 있다.
- 고객이 주문을 취소할 수 있다.
- 관리자가 신규 체인점을 등록할 수 있다.
비기능적 요구사항
- 트랜잭션
- 주문 시 해당 지역의 체인점 중 "영업중"인 곳이 단 한 곳도 없다면 주문이력만 남기고 주문은 거절된다. (Sync 호출)
- 장애격리
- 고객센터/배달 기능이 수행되지 않더라도 주문은 365일 24시간 받을 수 있어야 한다 Async(event-driven), Eventual Consistency
- 체인점 시스템이 과중되면 주문을 잠시동안 받지 않고 재주문하도록 유도한다 Circuit breaker, fallback
- MSAEz 로 모델링한 이벤트스토밍 결과:
- http://msaez.io
- 고객이 피자를 주문한다.
- 고객 주문이 완료되면 해당지역 체인점에 주문이 접수된다.
- 체인점에서 피자 조리가 완료되면 지배인(Master)이 "조리완료" 처리한다.
- 피자 조리가 완료되면 배달을 시작한다.
- 고객이 마이페이지를 통해 주문 상태를 확인할 수 있다.
- 고객이 주문을 취소할 수 있다.
- 관리자가 신규 체인점을 등록할 수 있다.
비기능적 요구사항
- 트랜잭션
- 주문 시 해당 지역의 체인점 중 "영업중"인 곳이 단 한 곳도 없다면 주문이력만 남기고 주문은 거절된다. (Sync 호출)
- 장애격리
- 고객센터/배달 기능이 수행되지 않더라도 주문은 365일 24시간 받을 수 있어야 한다 Async(event-driven), Eventual Consistency
- 체인점 시스템이 과중되면 주문을 잠시동안 받지 않고 재주문하도록 유도한다 Circuit breaker, fallback
- Chris Richardson, MSA Patterns 참고하여 Inbound adaptor와 Outbound adaptor를 구분함
- 호출관계에서 PubSub 과 Req/Resp 를 구분함
- 서브 도메인과 바운디드 컨텍스트의 분리: 각 팀의 KPI 별로 아래와 같이 관심 구현 스토리를 나눠가짐
분석/설계 단계에서 도출된 헥사고날 아키텍처에 따라, 각 BC별로 대변되는 마이크로서비스들을 스프링부트로 구현하였다. 구현한 각 서비스를 로컬에서 실행하는 방법은 아래와 같다 (각자의 포트넘버는 8081 ~ 808n 이다)
cd gateway
mvn spring-boot:run
cd order
mvn spring-boot:run
... 이하 동일(생략) ...
- 각 서비스내에 도출된 핵심 Aggregate Root 객체를 Entity 로 선언하였다: (예시는 Order 마이크로서비스). 이때 가능한 현업에서 사용하는 언어 (유비쿼터스 랭귀지)를 그대로 사용하려고 노력하였다.
package pizza;
import javax.persistence.*;
import org.springframework.beans.BeanUtils;
import java.util.Date;
@Entity
@Table(name="Order_table")
public class Order {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long orderId;
private Long customerId;
private String pizzaNm;
private Integer qty;
private String status;
private String regionNm;
private Date orderDt;
@PrePersist
public void onPrePersist(){
//Following code causes dependency to external APIs
// Req/Res Calling
boolean bResult = false;
// mappings goes here
bResult = OrderApplication.applicationContext.getBean(pizza.external.StoreService.class).chkOpenYn(this.regionNm);
// 주문가능 (해당 regionNm에 Open된 Store가 있음)
if (bResult) {
this.status = "Ordered" ;
} else {
this.status = "NoStoreOpened" ;
}
this.orderDt = new Date();
}
@PostPersist
public void onPostPersist(){
if ("Ordered".equals(this.status)) {
System.out.println("#### PUB :: Ordered : orderId = " + this.orderId);
Ordered ordered = new Ordered();
BeanUtils.copyProperties(this, ordered);
ordered.publishAfterCommit();
} else if ("NoStoreOpened".equals(this.status)) {
System.out.println("#### PUB :: OrderRejected : orderId = " + this.orderId);
OrderRejected orderRejected = new OrderRejected();
BeanUtils.copyProperties(this, orderRejected);
orderRejected.publishAfterCommit();
}
}
@PostUpdate
public void onPostUpdate(){
if(this.status.equals("OrderCancelled"))
{
System.out.println("#### PUB :: OrderCancelled : orderId = " + this.orderId);
OrderCancelled orderCancelled = new OrderCancelled();
BeanUtils.copyProperties(this, orderCancelled);
orderCancelled.publishAfterCommit();
} else {
System.out.println("#### PUB :: StatusUpdated : status updated to " + this.status);
StatusUpdated statusUpdated = new StatusUpdated();
BeanUtils.copyProperties(this, statusUpdated);
statusUpdated.publishAfterCommit();
}
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getPizzaNm() {
return pizzaNm;
}
public void setPizzaNm(String pizzaNm) {
this.pizzaNm = pizzaNm;
}
public Integer getQty() {
return qty;
}
public void setQty(Integer qty) {
this.qty = qty;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getRegionNm() {
return regionNm;
}
public void setRegionNm(String regionNm) {
this.regionNm = regionNm;
}
public Date getOrderDt() {
return orderDt;
}
public void setOrderDt(Date orderDt) {
this.orderDt = orderDt;
}
}
- Entity Pattern 과 Repository Pattern 을 적용하여 JPA 를 통하여 다양한 데이터소스 유형 (MySQL or H2)에 대한 별도의 처리가 없도록 데이터 접근 어댑터를 자동 생성하기 위하여 Spring Data REST 의 RestRepository 를 적용하였다. (로컬 개발환경에서는 MySQL/H2를, 쿠버네티스에서는 SQLServer/H2를 각각 사용하였다)
package pizza;
import java.util.Optional;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(collectionResourceRel="orders", path="orders")
public interface OrderRepository extends PagingAndSortingRepository<Order, Long>{
Optional<Order> findByOrderId(Long orderId);
}
- 적용 후 REST API 의 테스트
# Store 서비스의 신규 체인점 등록
http POST http://localhost:8088/stores regionNm="강남구" openYN=true
# Order 서비스의 주문
http POST http://localhost:8088/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="강남구"
# 주문 상태 확인
http GET http://localhost:8088/myPages/1
- 고객이 피자를 주문한다.
- 고객 주문이 완료되면 해당지역 체인점에 주문이 접수된다.
- 체인점에서 피자 조리가 완료되면 체인점 지배인(Master)이 "조리완료" 처리한다.
- 피자 조리가 완료되면 배달을 시작한다.
- 고객이 마이페이지를 통해 주문 상태를 확인할 수 있다.
--> 정상적으로 조회됨 확인하였음
- 고객이 주문을 취소할 수 있다.
- 관리자가 신규 체인점을 등록할 수 있다.
- 트랜잭션
주문 시 해당 지역의 체인점 중 "영업중"인 곳이 단 한 곳도 없다면 주문이력만 남기고 주문은 거절된다. (Sync 호출)
- 장애격리 고객센터/배달 기능이 수행되지 않더라도 주문은 365일 24시간 받을 수 있어야 한다 Async(event-driven), Eventual Consistency
- 상점시스템이 과중되면 주문을 잠시동안 받지 않고 재접속하도록 유도한다 Circuit breaker, fallback
--> 뒤의 Hystrix를 통한 Circuit Break 구현에서 검증하도록 한다.
분석/설계 및 구현을 통해 이벤트를 Publish/Subscribe 하도록 구현하므로써, 다음 서비스가 트리거될 수 있도록 하였다.
[Publish]
[Subscribe]
또한, 아래와 같이 보상 이벤트를 준비하여 Rollback이 가능하도록 구현되었다.
SAGA 및 ROLLBACK의 동작은 앞 서 기능/비기능 검증부분에서 이미 검증완료하였다.
Materialized View 를 구현하여, 타 마이크로서비스의 데이터 원본에 접근없이(Composite 서비스나 조인SQL 등 없이) 도 내 서비스의 화면 구성과 잦은 조회가 가능하게 구현해 두었다.
본 프로젝트에서 View 역할은 CustomerCenter 서비스의 마이페이지가 수행한다.
CQRS를 구현하여 주문건에 대한 상태는 Order 마이크로서비스의 접근없이 CustomerCenter의 마이페이지를 통해 조회할 수 있도록 구현하였다.
- 주문(ordered) 실행 후 myPage 화면
- 주문취소(OrderCancelled) 후 myPage 화면
각 이벤트 건(메시지)이 어떤 Policy를 처리할 때 어떤건에 연결된 처리건인지를 구별하기 위한 Correlation-key를 제대로 연결하였는지를 검증하였다.
API GateWay를 통하여 마이크로 서비스들의 진입점을 통일할 수 있다. 다음과 같이 GateWay를 적용하여 모든 마이크로서비스들은 http://localhost:8088/{context}로 접근할 수 있다.
server:
port: 8088
---
spring:
profiles: default
cloud:
gateway:
routes:
- id: order
uri: http://localhost:8081
predicates:
- Path=/orders/**
- id: store
uri: http://localhost:8082
predicates:
- Path=/stores/**,/storeOrders/**
- id: delivery
uri: http://localhost:8083
predicates:
- Path=/deliveries/**
- id: customercenter
uri: http://localhost:8084
predicates:
- Path= /myPages/**
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins:
- "*"
allowedMethods:
- "*"
allowedHeaders:
- "*"
allowCredentials: true
---
spring:
profiles: docker
cloud:
gateway:
routes:
- id: order
uri: http://order:8080
predicates:
- Path=/orders/**
- id: store
uri: http://store:8080
predicates:
- Path=/stores/**/storeOrders/**
- id: delivery
uri: http://delivery:8080
predicates:
- Path=/deliveries/**
- id: customercenter
uri: http://customercenter:8080
predicates:
- Path= /myPages/**
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins:
- "*"
allowedMethods:
- "*"
allowedHeaders:
- "*"
allowCredentials: true
server:
port: 8080
각 마이크로서비스의 다양한 요구사항에 능동적으로 대처하고자 최적의 구현언어 및 DBMS를 선택할 수 있다. 1588-pizza에서는 다음과 같이 2가지 DBMS를 적용하였다.
- MySQL(쿠버네티스에서는 SQLServer) : Order, Store, Delivery
- H2 : CustomerCenter
# (Order, Store, Delivery) application.yml
spring:
profiles: default
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/1588-pizza?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
username: *****
password: *****
spring:
profiles: docker
datasource:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
url: jdbc:sqlserver://aramidhwan.database.windows.net:1433;database=1588-pizza;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;
username: ${SQLSERVER_USERNAME}
password: ${SQLSERVER_PASSWORD}
...
# (CustomerCenter) application.yml
spring:
profiles: default
h2:
console:
enabled: true
path: /h2-console
분석단계에서의 조건 중 하나로 주문(Order)->체인점(Store) 간의 호출은 동기식 일관성을 유지하는 트랜잭션으로 처리하기로 하였다. 호출 프로토콜은 RestController를 FeignClient 를 이용하여 호출하도록 한다.
- 체인점 "영업중" 상태 확인 서비스를 호출하기 위하여 Stub과 (FeignClient) 를 이용하여 Service 대행 인터페이스 (Proxy) 를 구현
# (Order) StoreService.java
package pizza.external;
@FeignClient(name="store", url="${api.url.book}")
public interface StoreService {
@RequestMapping(method= RequestMethod.GET, path="/stores/chkOpenYN")
public boolean chkOpenYN(@RequestParam("regionNm") String regionNm);
}
- 주문을 받은 직후 해당 지역의 체인점 "영업중" 확인을 요청하도록 처리
# StoreController.java
package pizza;
@RestController
public class StoreController {
@Autowired
StoreRepository storeRepository;
@RequestMapping(value = "/stores/chkOpenYN", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
public boolean chkOpenYN(@RequestParam("regionNm") String regionNm) throws Exception {
System.out.println("##### /store/chkOpenYn called #####");
boolean status = false;
List<Store> storeList = storeRepository.findByRegionNmAndOpenYN(regionNm, Boolean.valueOf(true));
// 주문이 들어온 regionNm에 Open된 Sotre가 한군데라도 있으면 true를 리턴
if (storeList.size() > 0) {
status = true ;
}
return status;
}
}
- 동기식 호출에서는 호출 시간에 따른 타임 커플링이 발생하며, 체인점 관리 시스템이 장애가 나면 주문도 못받는다는 것을 확인:
# 체인점 관리 (Store)) 서비스를 잠시 내려놓음 (ctrl+c)
#주문처리
http POST http://localhost:8088/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="강남구" #Fail
#체인점 관리 서비스 재기동
cd Store
mvn spring-boot:run
#주문처리
http POST http://localhost:8088/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="강남구" #Success
추후 운영단계에서는 Circuit Breaker를 이용하여 재고 관리 시스템에 장애가 발생하여도 주문 접수는 가능하도록 개선할 예정이다.
주문이 이루어진 후에 체인점/배달 시스템으로 이를 알려주는 행위는 동기식이 아니라 비 동기식으로 처리하여 체인점/배송 시스템의 처리를 위하여 주문이 블로킹 되지 않도록 처리한다.
- 이를 위하여 주문이력에 기록을 남긴 후에 곧바로 주문이 완료되었다는 도메인 이벤트를 카프카로 송출한다(Publish)
package pizza;
@Entity
@Table(name="Order_table")
public class Order {
...
@PostPersist
public void onPostPersist(){
if ("Ordered".equals(this.status)) {
System.out.println("#### PUB :: Ordered : orderId = " + this.orderId);
Ordered ordered = new Ordered();
BeanUtils.copyProperties(this, ordered);
ordered.publishAfterCommit();
} else if ("NoStoreOpened".equals(this.status)) {
System.out.println("#### PUB :: OrderRejected : orderId = " + this.orderId);
OrderRejected orderRejected = new OrderRejected();
BeanUtils.copyProperties(this, orderRejected);
orderRejected.publishAfterCommit();
}
}
}
- 배달 서비스에서는 주문 및 조리완료 이벤트에 대해서 이를 수신하여 자신의 정책을 처리하도록 PolicyHandler 를 구현한다:
package pizza;
...
@Service
public class PolicyHandler{
@Autowired
DeliveryRepository deliveryRepository;
@StreamListener(KafkaProcessor.INPUT)
public void wheneverCooked_DeliveryAccept(@Payload Cooked cooked){
if(!cooked.validate()) return;
System.out.println("\n\n##### listener DeliveryAccept : " + cooked.getOrderId());
// 배달접수
Delivery delivery = new Delivery();
BeanUtils.copyProperties(cooked, delivery);
delivery.setStatus("DeliveryStart");
deliveryRepository.save(delivery);
}
}
배달 시스템은 주문과 완전히 분리되어있으며, 이벤트 수신에 따라 처리되기 때문에, 배달 시스템이 유지보수로 인해 잠시 내려간 상태라도 주문을 받는데 문제가 없다:
# 배달관리 서비스 (Delivery) 를 잠시 내려놓음 (ctrl+c)
#주문처리
http POST http://localhost:8088/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="강남구" #Success
#주문상태 확인
http GET http://localhost:8088/orders/1 # 정상적으로 주문됨을 확인
#체인점에서 "조리완료" 처리
http PATCH http://localhost:8088/storeOrders/1 status=Cooked
#배송 서비스 기동
cd Delivery
mvn spring-boot:run
#주문상태 확인
http localhost:8088/orders/1 # 주문 상태가 "DeliveryStart"로 확인
- git에서 소스 가져오기
git clone https://github.com/aramidhwan/1588-pizza.git
- Build 하기
cd order
mvn package
cd ../store
mvn package
...이하 생략...
- Docker Image build/Push/
cd order
docker build -t myacr00.azurecr.io/order:latest .
docker push myacr00.azurecr.io/order:latest
cd ../store
docker build -t myacr00.azurecr.io/store:latest .
docker push myacr00.azurecr.io/store:latest
...이하 생략...
- yml파일 이용한 deploy (예시: 1588-pizza/order/kubernetes/deployment.yml 파일)
kubectl apply -f deployment.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: order
namespace: pizza
labels:
app: order
spec:
replicas: 1
selector:
matchLabels:
app: order
template:
metadata:
labels:
app: order
spec:
containers:
- name: order
image: myacr00.azurecr.io/order:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: '/actuator/health'
port: 8080
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 10
livenessProbe:
httpGet:
path: '/actuator/health'
port: 8080
initialDelaySeconds: 120
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 5
- deploy 완료
-
시스템별로 변경 가능성이 있는 설정들을 ConfigMap을 사용하여 관리
-
1588-pizza에서는 주문에서 체인점 영업상태 체크 호출 시 "호출 주소"를 ConfigMap 처리하기로 결정
-
Java 소스에 "호출 주소"를 변수(api.url.book) 처리 (/order/src/main/java/pizza/external/StoreService.java)
- application.yml 파일에서 api.url.book을 ConfigMap과 연결
- 클러스터에 ConfigMap 생성
kubectl create configmap resturl --from-literal=sotreUrl=http://Store:8080
- Deployment.yml 에 ConfigMap 적용
- DBMS 연결에 필요한 username 및 password는 민감한 정보이므로 Secret 처리하였다.
- deployment.yml에서 env로 설정하였다.
- 쿠버네티스에서는 base64 처리하여 다음과 같이 Secret object를 생성하였다.
- Spring FeignClient + Hystrix를 사용하여 구현함
시나리오는 주문(order)-->체인점(Store) 영업여부 체크 확인 시 1초를 넘어설 경우 Circuit Breaker 를 통하여 장애격리.
- Hystrix 를 설정: FeignClient 요청처리에서 처리시간이 1초가 넘어서면 CB가 동작하도록 (요청을 빠르게 실패처리, 차단) 설정 추가로, 테스트를 위해 1번만 timeout이 발생해도 CB가 발생하도록 설정
# application.yml
- 호출 서비스(주문)에서는 체인점(Store) API 호출에서 문제 발생 시 FallBack 구현
# (Order) StoreService.java
# (Order) StoreServiceFallbackFactory.java
- 피호출 서비스(체인점:Store)에서 테스트를 위해 주문지역이 "종로구"인 주문건에 대해 sleep 처리
# (Store) StoreController.java
- 서킷 브레이커 동작 확인:
주문지역이 "강남구" 인 경우 정상적으로 주문 처리 완료
# http POST http://104.42.177.6:8080/orders customerId=1 pizzaNm="하와이안피자" qty=1 regionNm="강남구"
주문지역이 "종로구" 인 경우 CB에 의한 timeout 발생 확인 (Order건은 NoStoreOpened 처리됨)
# http POST http://104.42.177.6:8080/orders customerId=1 pizzaNm="페퍼로니피자" qty=1 regionNm="종로구"
time 아웃이 연달아 2번 발생한 경우 CB가 OPEN되어 체인점(Store) 호출이 아예 차단된 것을 확인 (테스트를 위해 circuitBreaker.requestVolumeThreshold=1 로 설정)
일정시간 뒤에는 다시 주문이 정상적으로 수행되는 것을 알 수 있다.
- 시스템이 죽지 않고 지속적으로 CB 에 의하여 적절히 회로가 열림과 닫힘이 벌어지면서 Thread 자원 등을 보호하고 있음을 보여줌.
주문 서비스가 몰릴 경우를 대비하여 자동화된 확장 기능을 적용하였다.
- 주문서비스에 대한 replica 를 동적으로 늘려주도록 HPA 를 설정한다. 설정은 테스트를 위해 CPU 사용량이 50프로를 넘어서면 replica 를 3개까지 늘려준다:
hpa.yml
- deployment.yml에 resource 관련 설정을 추가해 준다.
- 100명이 60초 동안 주문을 넣어준다.
siege -c100 -t60S --content-type "application/json" 'http://10.0.223.154:8080/orders POST {"customerId":"1","pizzaNm":"페퍼로니피자","qty":"1","regionNm":"강남구"}'
- 오토스케일이 어떻게 되고 있는지 모니터링을 걸어둔다:
kubectl get deploy -l app=order -w
- 어느정도 시간이 흐른 후 스케일 아웃이 벌어지는 것을 확인할 수 있다.
- siege 의 로그를 보면 오토스케일 확장이 일어나며 주문을 100% 처리완료한 것을 알 수 있었다.
** SIEGE 4.0.4
** Preparing 100 concurrent users for battle.
The server is now under siege...
Lifting the server siege...
Transactions: 5077 hits
Availability: 100.00 %
Elapsed time: 59.58 secs
Data transferred: 1.58 MB
Response time: 1.16 secs
Transaction rate: 85.21 trans/sec
Throughput: 0.03 MB/sec
Concurrency: 98.86
Successful transactions: 5077
Failed transactions: 0
Longest transaction: 5.64
Shortest transaction: 0.00
- Zero-downtime deploy를 위해 deployment.yml에 readiness Probe를 설정함
- 먼저 store 이미지가 v1.0 임을 확인
- Zero-downtime deploy 확인을 위해 seige 로 1명이 지속적인 체인점 등록 작업을 수행함
siege -c1 -t180S --content-type "application/json" 'http://10.0.223.154:8080/stores POST {"regionNm": "강남구","openYN":"true"}'
새 버전으로 배포(이미지를 v2.0으로 변경)
kubectl set image deployment store store=myacr00.azurecr.io/store:v2.0
store 이미지가 변경되는 과정 (POD 상태변화)
kubectl get pod -l app=store -w
store 이미지가 v2.0으로 변경되었임을 확인
- seige 의 화면으로 넘어가서 Availability가 100% 인지 확인 (무정지 배포 성공)
** SIEGE 4.0.4
** Preparing 1 concurrent users for battle.
The server is now under siege...
Lifting the server siege...
Transactions: 51297 hits
Availability: 100.00 %
Elapsed time: 179.17 secs
Data transferred: 10.18 MB
Response time: 0.00 secs
Transaction rate: 286.30 trans/sec
Throughput: 0.06 MB/sec
Concurrency: 0.99
Successful transactions: 51297
Failed transactions: 1
Longest transaction: 0.42
Shortest transaction: 0.00
- Self-healing 확인을 위한 Liveness Probe 옵션 변경 (Port 변경)
1588-pizza/delivery/kubernetes/deployment.yml
- Delivery pod에 Liveness Probe 옵션 적용 확인
- Liveness 확인 실패에 따른 retry발생 확인
이상으로 12가지 체크포인트가 구현 및 검증 완료되었음 확인하였다.