Mock 이란?

정의

소프트웨어 테스트에서 실제 객체를 대신하여 사용하는 '가짜 객체'를 의미

 

사용하는 이유

  • 테스트 환경을 구현하기 힘든 경우
  • 테스트 대상이 외부 시스템과의 의존성이 높아 직접 테스트하기 힘든 경우 
  • 테스트하기 힘든 상황이 기대한 대로 상호작용하는지 검증하기 위한 경우 
  • 테스트 실행에 많은 시간이 걸리는 경우

사용되는 경우

  • 데이터베이스 연동 테스트
  • 외부 API 호출 테스트
  • 네트워크 통신 테스트
  • 파일 시스템 접근 테스트

사용하지 말아야하는 경우

  • 순수 함수 : reduce, map 등등 은 입력이 같으면 항상 같은 결과를 반환
  • 간단한 상태 관리 로직 
  • 단순한 의존성 또는 데이터 구조를 사용하는 경우
  • 실제 동작 검증이 필요한 경우
  • 과도한 mocking 으로 인해 테스트 유지보수가 어려운 경우
  • Mock 객체의 설정이 실제 환경과 크게 다를 경우
  • 상태 검증 기반이 더 적합한 경우

종류

  • 스텁(stub) : 특정한 입력에 대해 미리 정해진 결과를 반환하도록 하드 코딩된 객체
  • 페이크 객체 (fake object) : 실제로 동작하는 것처럼 보이지만, 프로덕션 환경에서 사용하기에는 단순화된 구현을 가진 객체
  • 스파이 (spy) : 실제 객체를 부분적으로 목킹하여, 호출된 메서드나 파라미터 등을 기록하고 검증할 수 있는 객체
  • Mock 객체 (mock object) : 실제 객체의 행위를 흉내내어 행위 기반 테스트를 위해 사용. 호출 여부, 호출 횟수, 전달된 파라미터 등을 검증가능

이점

  • 외부 의존성(API, 데이터베이스 등) 없이 테스트 가능
  • 테스트 실행 속도 향상
  • 특정 상황(에러, 타임아웃 등)을 쉽게 시뮬레이션
  • 함수 호출 여부, 인자, 횟수 등을 검증 가능

Mock 객체 사용 여부 판단 기준

  1. 이 코드가 외부 리소스에 의존하는가?
    • API 호출
    • 데이터베이스 접근
    • 파일 시스템 사용 : 파일 읽기/쓰기 가 필요한 경우
  2. 테스트 실행이 느리거나 비용이 드는가?
    • 무거운 연산
    • 네트워크 지연
    • API 호출 비용
  3. 테스트 결과가 비결정적인가?
    • 랜덤 값 사용 : 무작위 값에 따라 결과가 달라지는 경우
    • 시간에 의존적인 로직 : 현재 시간이나 날짜에 따라 동작이 변하는 경우
    • 동시성 이슈 : 멀티스레딩 등으로 인해 겨로가가 일정하지 않은 경우 
  4. 테스트 셋업이 복잡한가?
    • 많은 의존성 필요 : 여러 모듈이나 서비스에 의존하는 경우
    • 복잡한 상태 설정 필요

 

(단순한 예시) 테스트를 위해서 만들어진 user 객체 ?

▶ user 는 테스트를 위해서 임의로 만들어진 단순한 데이터 객체이지 행위는 없다. 그러므로 Mock 객체라고 할 수 없다. 

분명 인덱스를 걸었음에도 풀 테이블 스캔으로 데이터를 조회하는 경우가 있다.

경우 1 : 넓은 범위의 데이터를 조회하는 경우

 

옵티마이저가 넓은 범위의 데이터를 조회할 때는 인덱스를 활용하는 것이 비효율적이라고 판단한다. 왜냐면, 굳이 인덱스를 거쳤다가 각 원래 테이블의 데이터를 일일이 하나씩 찾아내는 것보다, 바로 원래 테이블에 접근해서 모든 데이터를 통째로 가져와서 정렬하는게 효율적이라고 판단하기 때문이다.

 

(예시)

SELECT * FROM users 
ORDER BY name DESC;

  ▶ 이런 경우 인덱스를 활용하지 않고 풀 테이블 스캔으로 데이터를 찾을 때 훨씬 효율적이라고 판단

넓은 범위의 데이터를 조회하는 경우, 인덱스를 사용해서 조회하는 것보다 풀 테이블 스캔을 이용하는 것이 효과적.

 

* 최적화를 위하여 넓은 범위의 데이터를 조회하는지 잘 파악이 안된다면, 인덱스를 적용한 후에 실행 계획 조회해 보자.

그림에 보이듯이 인덱스를 했음에도 불구하고 trype이 ALL 인 것을 볼 수 있다. 

  → 풀 테이블 스캔으로 데이터를 찾을 때 훨씬 효율적

CREATE INDEX idx_name ON users (name);

EXPLAIN SELECT * FROM users 
ORDER BY name DESC;

 

 

경우 2 : 인덱스 컬럼을 가공(함수 적용, 산술 연산, 문자열 조작 등 )을 한 경우

 

id, name, salary, created_at 콜롬들을 가진 users 테이블에 100만 건의 랜덤 데이터 삽입

 

인덱스 생성

CREATE INDEX idx_name ON users (name);
CREATE INDEX idx_salary ON users (salary);

 

실행 계획 조회해보기

# User000000으로 시작하는 이름을 가진 유저 조회
EXPLAIN SELECT * FROM users
WHERE SUBSTRING(name, 1, 10) = 'User000000';

# 2달치 급여(salary)가 1000 이하인 유저 조회
EXPLAIN SELECT * FROM users
WHERE salary * 2 < 1000
ORDER BY salary;

 

첨부된 결과 사진에 있는 타입에 보이듯이 ALL 이다. 즉, 풀테이블 스캔을 한 것을 볼 수 있다.

 

인덱스 컬럼을 가공해서 사용하지 않게 SQL문 수정하기

# User000000으로 시작하는 이름을 가진 유저 조회
EXPLAIN SELECT * FROM users
WHERE name LIKE 'User000000%'; # % 와일드 카드: 0개 이상의 어떤 문자든지 매칭될 수 있음

# 2달치 급여(salary)가 1000 이하인 유저 조회
EXPLAIN SELECT * FROM users
WHERE salary < 1000 / 2
ORDER BY salary;

위 두 결과 그림에서 type 을 보면 인덱스가 제대로 활용 된 것을 볼 수 있다.

 

인덱스를 활용하기 위해서는 인덱스 컬럼 자체를 최대한 가공하지 않아야 한다.

 

최근 3일 이내에 가입한 유저 조회하기

데이터 조회

SELECT * FROM users
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

 

인덱스를 추가하기전

약 0.35초
tyep = ALL

인덱스 추가 후

CREATE INDEX idx_created_at ON users (created_at);

 

약 0.056초
type = range

 

created_at 에 인덱스를 추가함으로써 검색 타입이 ALL 에서 range 로 바뀌고 소요시간이 약 0.35 초 에서 0.056 초로 감소

 

Sales 부서이면서 최근 3일 이내에 가입한 유저 조회하기

데이터 생성

DROP TABLE IF EXISTS users; 

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100),
    department VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

100만건 데이터 생성 후 데이터 조회해서 성능 측정

SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY)

0.24초

 

실행계획 확인

EXPLAIN SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

#세부내용
EXPLAIN ANALYZE SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

type=ALL -> 모든 테이블을 검색

 

-> Filter: ((users.department = 'Sales') and (users.created_at >= <cache>((now() - interval 3 day))))  (cost=93877 rows=33224) (actual time=3.34..243 rows=115 loops=1)
    -> Table scan on users  (cost=93877 rows=996810) (actual time=0.148..191 rows=1e+6 loops=1)

 

성능개선 할 수 있는 방법들

1. created_at 컬럼을 기준으로 인덱스 추가

2. department 컬럼을 기준으로  인덱스 추가

3. department 와 created_at 두 컬럼  인덱스 추가

4. 멀티 컬럼 인덱스 = (created_at, department) ? (department, created_at) ?

 

< created_at 컬럼을 기준으로 인덱스 추가 > 

CREATE INDEX idx_created_at ON users (created_at);

#성능평가
SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획
EXPLAIN SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획 세부 내용
EXPLAIN ANALYZE SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

0.24초 에서 0.033 초로 소요시간 단축

type = range , 모든 테이블을 검색하지 않고 idx_created_at 인덱스 레인지 스캔을 함 데이터 접근 개수(rows)도 줄어들었다.

 

< department 컬럼을 기준으로 인덱스 추가 > 

ALTER TABLE users DROP INDEX idx_created_at; # 기존 created_at 인덱스 삭제
CREATE INDEX idx_department ON users (department);

# 성능 측정
SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획
EXPLAIN SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획 세부 내용
EXPLAIN ANALYZE SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

 

약 0.13초 소요 created_at 컬럼을 기준으로 인덱스 추가 한 경우가 소요시간이 훨씬 짧음

ref 타입, = 비고유 인덱스로 조회를 했다 rows = 191,314 는 인덱스를 하기전 보다 좋아 졌지만 created_at 컬럼을 기준으로 인덱스 추가  한 경우보다 많은 데이터에 접근

 

 

<department 와 created_at 두 컬럼  인덱스 추가>

# CREATE INDEX idx_department ON users (department); # 위에서 이미 추가함
CREATE INDEX idx_created_at ON users (created_at); # created_at 인덱스 추가

# 성능 측정
SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획
EXPLAIN SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

# 실행 계획 세부 내용
EXPLAIN ANALYZE SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

 

소요 시간은 idx_created_at 과 비슷

idx_created_at 과 똑같이 range 타입으로 일정 범위만 검색

 

-> Filter: (users.department = 'Sales')  (cost=498 rows=212) (actual time=0.0929..11.6 rows=114 loops=1)
    -> Index range scan on users using idx_created_at over ('2024-12-27 16:39:14' <= created_at), with index condition: (users.created_at >= <cache>((now() - interval 3 day)))  (cost=498 rows=1106) (actual time=0.062..11.3 rows=1106 loops=1)

디테일을 확인해 보면 idx_created_at 만 사용

 

결론적으로 idx_department 을 사용하지 않았다는 것은 department 컬럼에 한 인덱스가 필요가 없고 판단하여 idx_department는 의미 성능평가에서도 비슷하므로 created_at 컬럼에만 인덱스를 하는 것이 가장 효율적인 최적화 방법으로 볼수 있다.

 

[이것만은 꼭 기억해두자!] 

데이터 액세스(rows)를 크게 줄일 수 있는 컬럼은 중복 정도가 낮은 컬럼이다.

따라서 중복 정도가 낮은 컬럼을 골라서 인덱스를 생성해라.

 

 

< 멀티 컬럼 인덱스 >

공통 : 성능테스트

SELECT * FROM users
WHERE department = 'Sales'
AND created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY);

 

 

순서 (created_at, department) 

ALTER TABLE users DROP INDEX idx_created_at;
ALTER TABLE users DROP INDEX idx_department;
CREATE INDEX idx_created_at_department ON users (created_at, department);

성능 테스트 결과 :  소요시간 약 0.0035 초

 

순서 (department, created_at)

ALTER TABLE users DROP INDEX idx_created_at_department;
CREATE INDEX idx_department_created_at ON users (department, created_at);

 

성능 테스트 결과 : 소요시간 약 0.0033초

 

 

멀티 컬럼 인덱스를 사용했지만 created_at 만 인덱스를 설정했을 때의 성능과 크게 차이가 나지 않는다.

 

 ’단일 컬럼에 설정하는 일반 인덱스’를 설정했을 때와 ‘멀티 컬럼 인덱스를 설정했을 때’의 성능 차이가 별로 나지 않는다면 ?

▶ 멀티 컬럼 인덱스를 사용하지 말고 일반 인덱스를 활용하자.

 

비교한 인덱스 방식의 성능 테스트 비교 표

인덱스 방식 성능 테스트 (소요시간 (s)) rows
인덱스 없음 0.24 115
created_at 0.0033 114
department 0.14 114
created_at 와 department 0.0032 114
멀티 (created_at, department) 0.0035 109
멀티 (department, created_at) 0.0033 109

 

 

참고 강의

(인프런) 비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전

 

실제 페이스북, 인스타그램의 서비스를 보더라도 한 번에 모든 게시글의 데이터를 불러오지 않는다. 

→ 페이지네이션을 이용하여 일부 데이터만 조회

 

예를 들어 100만 건의 데이터가 있을 때 하나의 데이터를 찾기 위해 모든 데이터를 불러오면 성능에 영향을 끼친다.

조회하는 데이터의 개수가 성능에 많은 영향

 

조회하는데 걸리는 시간 측정 (100만 유저가 있는 데이터베이스)

 

전부조회

SELECT * FROM users;

21 초 소요

 

제한된 조회 10000 건

SELECT * FROM users LIMIT 10000;

약 0.23초

 

제한된 조회 100 건

약 0.023 초 소요

 

조회하는 데이터의 개수가 많을 수록 소요시간이 많아진다.

 

 

시스템 성능 저하를 방지하기위해 해야할 것

1. 데이터를 조회할 때 한 번에 너무 많은 데이터를 조회하는 건 아닌 지 체크

2. LIMITWHERE 문 등을 활용해서 한 번에 조회하는 데이터의 수를 줄이는 방법을 고려

 

 

참고 강의

(인프런) 비전공자도 이해할 수 있는 MySQL 성능 최적화 입문/실전

 

헬스 체크를 지원하는 도커 이미지 빌드하기

도커는 컨테이너를 시작할 때마다 애플리케이션의 기본적인 상태를 확인한다.

  • 컨테이너를 실행하면 내부에서 실행되는 프로세스(앱 실행파일 | 닷넷 런타임 | 셸 스크립트 같은 특정한 프로세스)가 있는데 도커가 확인하는 것은 이 프로세스의 실행 사태다.
  • 만약 이 프로세스가 종료되면 컨테이너도 종료상태가 된다.

만약 웹 애플케이션이  '500 Internal Server Error' 응답 받고 중지되어도 컨테이너는 정상이라고 판단한다.

 

웹 앱이 중지되어도 컨테이너가 정상적으로 작동하는 예시

500 상태코드를 받은 웹애플리케이션 예시
컨테이너가 정상적으로 작동하고 있는 예시

컨테이너의 진입점 프로세스가 실행 중 상태이므로 도커는 애플리케이션도 정상상이라고 판단하여 STATUS 가 Up 인것을 확인할 수 있다.

 

Dokcerfile 에서 HEALTHCHECK 인스트럭션을 이용하면 인스트럭션에 정의된 정보를 이용해 동작 중인 애플리케이션의 상태가 정상인지 확인할 수 있다.

  • HEALTHCHECK 동작방식
    • 인스트럭션에 도커 컨테이너안에서 실행하는 명령을 지정
    • 이 명령이 반환하는 상태코드를 보고 애플리케이션의 상태를 파악
    • 도커는 일정간격으로 이 명령을 실행
      • 상태 코드가 연속으로 일정 횟수 이상 실패로 나오면 해당 컨테이너를 이상 상태로 간주

 

Dockerfile 스크립트의 HEALTHCHECK 인스트럭션 예시

FROM diamol/dotnet-aspnet

ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health

WORKDIR /app
COPY --from=builder /out/ .
  • ENTRYPOINT 에서 "dotnet" 명령을 실행 → 앱 상태를 확인하기 위해 모니터링하는 프로세스도 "dotnet"
  • HEALTHCHECK CMD curl
    • --fail  = 정상이면 0 이외에는 다른 숫자 반환하도록 하는 옵션
    • http://localhost/health = 버그가 발동했는지 확인하기 위한 또 다른 API 엔트포인트 (500:버그, 200:정상)

< HEALTHCHECK가 적용된 도커파일 사용해보기>

diamol/ch08/exercises/numbers 폴더로 가서 이미지 빌드

docker image build -t diamol/ch08-numbers-api:v2 -f ./numbers-api/Dockerfile.v2 .

 

실행시킨 앱의 건강상태 확인

docker run -d -p 8081:80 diamol/ch08-numbers-api:v2

docker container ls

STATUS : healthy

버그 생성

curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng
curl http://localhost:8081/rng

 

컨테이너 건강상태 체크 (90초 정도 시간이 지나고 나서 해야 상태가 변함, 3번 건강상태를 체크해야 하기 때문)

docker container ls

에러가 발생하여 건강상태가 unhealthy 로 변함

  • 3 번 연속 건상상태를 확인한 결과 실패하였기 때문에 컨테이너 상태가 이상(unhealthy)로 출력
  • 컨테이너 상태가 이상이여도 종료되지는 않음

최근 컨테이너의 상태를 출력하여 더 자세히 확인해 보자

docker container inspect $(docker container ls --last 1 --format '{{.ID}}')

건강상태는 unhealthy 인데 앱은 runnging 상태인 것을 알 수 있다.

 

왜 이상 상태에 있는 컨테이너를 재시작하거나 다른 컨테이너로 교체하지 않은 것일까?

→ 도커가 컨테이너 재시작/교체 작업을 안전하게 처리할 수 없기 때문

 

클러스터는 항상  컨테이너를 추가로 실행할 여력이 있기 때문에 이상상태를 보이는 컨테이너는 두고, 대체컨테이너를 실행해 앱을 중단 시간없이 상태를 회복가능

디펜던시 체크가 적용된 켄테이너 실행하기

여러 컨테이너에 나뉘어 실행되는 분산 애플리케이션은 이상이 생긴 컨테이너를 교체할 때 처음 앱을 실행할 때처럼 컨테이너 간 의존관계를 고려하지 않기때문에 문제를 격는다.

 

도커가 동작하는 서버가 한 대뿐이라면 웹컨테이너를 실행하기전에 API 컨테이너가 실행되도록 가능하다.

하지만, 클러스터 환경의 컨테이너 플랫폼이라면 컨테이너의 순서까지 통제할수가 없어서 API 가 사용 가능한 상태가 되기 전에 웹 앱이 실행되는 일이 있을 수도 있다.

 

(예시) 컨테이너 상태는 정상인데 핵심 의존 관계를 만족하지 않아 앱이 정상적으로 동작하니 않는 상황

방식 : 실행 중인 모든 컨테이너를 제거해 동작 중인API 컨테이너가 없게한 후 앱 컨테이너를 실행한 다음 웹 브라우저에서 앱에 접근한다. 

docker container rm -f $(docker container ls -aq)
docker container run -d -p 8082:80 diamol/ch08-numbers-web
docker container ls

 

컨테이너 실행 상태 : 컨테이너 실행 중, 앱도 이상이 없음
서버 프로세스가 실행 중이고 컨테이너도 정상이지만 API를 사용할 수 없는 상태여서 웹 앱도 제대로 동작하지 않음

 

이런 경우를 방지하기 위해

의존 관계를 만족하는지 점검하는 디펜던시 체크 기능도 도커 이미지에 추가할 수 있다.

 

디펜던시 체크

  • 앱 실행전에 필요한 요구사항을 체크
  • 명시된 모든 요구사항이  확인되면 디펜던시 체크가 성공하고 앱이 실행
  • 인스트럭션으로 구현 된것은 아니고 앱 실행 명령에 로직을 추가하는 방법으로 구현

디펜던시 체크가 구현된 예시 (CMD 인스트럭션에 구현)

FROM diamol/dotnet-aspnet

ENV RngApi:Url=http://numbers-api/rng

CMD curl --fail http://numbers-api/rng && \
    dotnet Numbers.Web.dll

   먼저 API가 사용가능한 상태라면 curl 명령이 성공하고 이어지는 닷넷 코어 앱 실행 명령을 실행

 

디펜던시 체크가 설정된 컨테이너 실행

docker container run -d -p 8083:80 diamol/ch08-numbers-web:v2

 

도커 컨테이너 상태 확인 ▶ 실행 중인 API 가 없어 v2 는 종료된 것을 확인 가능

 

애플리케이션 체크를 위한 커스텀 유틸리티 만들기

curl은 웹 애플리케이션이나 API 를 테스트하는데 매우 유용한 도구다.

  • 실무에서 개발하는 앱을 테스트하는 목적으로는 사용하지 않는다.
  • 이유 = 사용하지 않을 도구를 추가하면 
    • 이미지 크기증가
    • 외부 공격에 노출될 여지 증가

▶  앱 체크에는 앱과 같은 언어로 구현된 별도의 커스텀 유틸리티를 사용

 

앱과 같은 언어로 구현된 커스텀 유틸리티의 장점

  • 이미지에 추가적인 소프트웨어를 포함 시킬 필요가 없다.
  • 재시도 횟수나 분기등 셸 스크립트로는 표현하기 까다로운 복잡한 체크 로직을 적용할 수 있다. 특히 리눅스와 윈도 양쪽에서 사용할 크로스 플랫폼 이미지라면 더욱 유용
  • 앱과 같은 설정을 사용해 대상 URL을 여러 곳에 반복 정의하거나 수정에서 누락시키는 일을 방지할 수 있다.
  •  컨테이너 실행 전에 확인이 필요한 모든 사항을 검증할 수 있다. (앱과 같은 라이브러리 환경에서 데이터베이스 접속이나 인증서 파일의 존재 유무등)
  • 다양한 상황에서 동작이 가능하다

 

닷넷 코어로 구현한 간단한 HTTP 테스트 유틸리티를 사용해 API 이미지의 헬스 체크와 웹 이미지의 디펜던시 체크 앱 빌드 과정

 마지막 단계 코드

FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "Numbers.Api.dll"]
HEALTHCHECK CMD ["dotnet", "Utilities.HttpCheck.dll", "-u", "http://localhost/health"]

WOKDIR /app
COPY --from=http-check-builder /out/ .
COPY --from=builder /out/ .

HEALTHCHECK 에서 curl 대신 닷넷 코어로 구현된 테스트 유틸리티를 사용

 

<실습> 모든 컨테이너 삭제후 무작위 숫자 API를 v3 버전의 컨테이너로 실행한다. 이번에는 헬스 체크 간격을 조금 줄인다. 컨테이너의 상태가 정상인지 확인하고 API를 몇번 호출해 상태가 이상으로 바뀌는지 확인하라.

# 기존 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
      -a  = 모든 컨테이너(중지된 컨테이너 포함)를 조회합니다.
      -q = 컨테이너 ID만 출력
      -aq = 모든 컨테이너의 ID를 출력 
      $(...) = 명령어 치환으로  ( )에서 얻은 결과를 가져온다. 

# API를 v3 버전의 이미지로 실행한다.
docker container run -d -p 8080:80 --health-interval 5s diamol/ch08-numbers-api:v3

# 5초 정도 기다린 후 컨테이너 목록을 확인한다.
docker container ls

# API 를 네번 호출 - 세번은 성공, 마지막은 실패
curl http://localhost:8080/rng

# 앱에 버그가 발생했다. 15초 기다린 후 상태가 이상으로 바뀌는지 확인한다.
docker container ls

 

커맨드 실행 출력 내용

4번 째 요청 부터 버그가 발생 → HTTP 테스트 유틸리리티가 세 번 연속 실패를 반환 → unhealthy 상태로 변환

 

<실습> 웹 앱 버전 v3를 실행하라. API 가 없으므로 컨테이너가 바로 종료된다.

docker container run -d -p 8081:80 diamol/ch08-numbers-web:v3
docker container ls --all

 

diamol/ch08-numbers-web:v3 : 웹 앱 컨테이너의 디펜던스 체크가 실패해 컨테이너가 종료

diamol/ch08-numbers-api:v3   : api 컨테이너가 실행 중이지만 컨테이너 이름이 "number-api" 로 지정되지 않아서 웹 앱이 API                                                                컨테이너를 발견하지 못한다.

 

도커 컴포즈에 헬스 체크와 디펜던시 체크 정의하기

디펜던시 체크에 실패했을 때 실행하던 컨테이너를 종료해야 하는 이유

▶  단일 서버에서 앱을 실행 중이라면 이상이 생긴 컨테이너를 새 컨테이너로 교체하면 더 심각한 장애를 일으킬 수 있기 때문

 하지만 종료된 컨테이너를 재시작하거나 이미지에 정의 되지 않은 헬스 체크를 추가할 수는 있다.

 

도커 컴포즈 파일에서 헬스 체크 옵션 설정하기

number-api:
	image: diamol/ch08-numbers-api:v3
    ports:
    	- "8087:80"
    healthCheck:
    	interval: 5s
        timeout: 1s
        retries: 2
        start_period: 5s
    networks:
    	- app-net

 

이미지에 헬스 체크가 정의 되어 있지 않은 경우 컴포즈 파일에서 정의하는 법

numbers-web:
    image: diamol/ch08-numbers-web:v3
    restart: on-failure
    environment:
      - RngApi__Url=http://numbers-api/rng
    ports:
      - "8088:80"
    healthcheck:
      test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
      interval: 5s
      timeout: 1s
      retries: 2
      start_period: 10s
    networks:
      - app-net

 

<실습> 지금 있는 컨테이너를 모두 삭제하고 도커 컴포즈를 이용해 무작위 숫자 애플리케이션을 실행하라. 애플리케이션이 제대로 실행됐는지 알아보기 위해 실행 후 컨테이너 목록을 확인하라

 

# 컴포즈 파일이 있는 디렉터리로 이동
 
# 현재 컨테이너를 모두 삭제
docker container rm -f $(docker container ls -aq)
 
# 애플리케이션 실행
docker-compose ls
 
# 5초를 기다린 다음 컨테이너 목록을 확인
docker container ls
 
# 웹 애플리케이션 로그도 확인
docker container logs numbers-numbers-web-1

 

도커 컴포즈 파일에 depends_on 설정을 사용해 직접 디펜던시 체크를 하도록 하지 않는 이유

▶ 도커 컴포즈가 디펜던시 체크를 할 수 있는 범위가 단일 서버로 제한되기 떄문

헬스 체크와 디펜던시 체크로 복원력있는 애플리케이션을 만들 수 있는 이유

물리 서버가 한 대뿐인 환경이라면 도커 컴포즈에 웹 컨테이너보다 API 컨테이너를 먼저 실행시키라고 지시할 수 있다.

 

10 대의 물리 서버와 20개의 API 컨테이너와 50 여개 웹앱을 실행해야 한다면 어떻게 이 애플리케이션의 시작 절차를 설계해야할까?

---> API 컨테이너를 먼저 실행시킨 후 웹앱을 실행하도록 설계를 한 경우, 마지막 1 개의 API 가 실행이 늦어져 5분이나 걸리면 웹앱은 그 동안 하나도  실행되지 않아서 애플리케이션이 동작 중이라고 할 수가 없다.

  ▶ 여기서 문제는 API 컨테이너가 부족하더라도 웹 앱 컨테이너를 실행하는데는 문제가 없다.

 

!! 디펜던시 체크와 헬스 체크를 도입하면 보이는 효과

  • 플랫폼이 실행 순서를 보장하게 할 필요가 없다.
  • 가능한 빨리 컨테이너를 실행하면 된다.
  • 일부 컨테이너가 의존관계를 만족하지 못한 상태라면 재실행되거나 다른 컨테이너로 교체된다.

운영 환경의 클러스터에서 이러나느 컨테이너의 생애주기

*애플리케이션의 자기수복 :  일시적인 오류를 플래폼이 해소해 주는 것 

      앱에 메모리 누수를 일으키는 까다로운 버그가 있더라도 플랫폼에서 해당 컨테이너를 메모리를 잃지 않은 새 컨테이너로 대체 - 버그를 수정하지 않았지만 애플리케이션은 계속 동작할 수 있다. 

 

헬스 체크와 디펜던시 체크에 주의 할 점

  • 헬스 체크는
    • 주기적으로 실행되므로 시스템에 부하를 주는 내용이어서는 안 된다.
    • 자원을 너무 많이 소모하지 않으면서 앱이 실질적으로 동작 중인지 검증할 수 있는 핵심적인 부분을 테스트해야 한다.
  • 디펜던시 체크는
    • 앱 시작 시에 한 번만 실행되므로 테스트 대상이 빠짐없이 정확하도록 주의해야 한다.
    • 누락된 의존 관계가 있으면 이 문제를 플랫폼이 해결하지 못하면 앱에도 문제가 발생한다.

연습문제 : 메모리 누수 대처

조건

  • 애플리케이션 시작 시 충분한 메모리가 있는지 확인하고, 메모리가 부족한 경우 컨테이너를 종료한다
  • 애플리케이션 실행 중 4 초 간격으로 최대치를 초과해 메모리를 사용하는지 확인한다. 최대치를 초과했다면 해당 컨테이너의 상태를 이상으로 판정해야 한다.
  • 테스트 로직은 memory-check.js 스크립트에 이미 작성돼 있다. Dockerfile 스크립트에서 테스트 스크립트를 그대로 사용하면 된다.
  • 테스트 스크립트와 Dockerfile 슼크립트는 ch08/lab 디렉터리에 있다.

Dokerfile 스크립트

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD ["node", "memory-hog.js"]

WORKDIR /app
COPY src/ .

 

 

Dockerfile solution

더보기

FROM diamol/node

ENV MAX_ALLOCATION_MB=4096 \
    LOOP_ALLOCATION_MB=512 \
    LOOP_INTERVAL_MS=2000

CMD node memory-hog.js && \

          node memory-check.js

 

HEALTHCHECK --interval=5S \

  CMD node memory-check.js

WORKDIR /app
COPY src/ .

 

문제 풀면서 틀린 부분

 

CMD ["node", "memory-hog.js", "memory-check.js"] 

  • 형태: JSON 배열 방식
  • 실행: node memory-hog.js memory-check.js 로 실행
  • 동작: node 명령어에 두 파일이 인자로 전달
  • 결과: node는 첫 번째 파일(memory-hog.js)만 실행하고, 두 번째 인자(memory-check.js)는 실행되지 않고 단순히 인자로 전달
  • 의도: 만약 memory-hog.js가 다른 스크립트를 받아서 실행하도록 설계되었다면 정상 동작하지만, 그렇지 않다면 memory-check.js는 무시됨.

 

+ Recent posts