앞 글에서 M/M/1을 사건 중심으로 직접 구현했다. 작동은 했지만, 미래사건 리스트와 사건 처리 규칙을 손으로 일일이 관리해야 했다. 이 글에서는 같은 모델을 SimPy로 다시 작성한다. SimPy는 6번 글의 프로세스 중심(process-interaction) 관점을 구현한 Python 라이브러리로, “entity의 생애를 시나리오처럼 적는” 방식을 가능하게 한다. 직접 구현과 나란히 두고, 추상화가 무엇을 덜어 주는지 본다.
SimPy의 핵심 개념
SimPy는 Python의 generator(generator)와 yield를 영리하게 활용한다.
Environment: 시뮬레이션 시계와 사건 처리를 담당하는 엔진. 우리가 직접 짰던 FEL과 루프가 여기 숨어 있다.- 프로세스(process):
yield를 쓰는 generator 함수로 entity의 생애를 기술한다.yield를 만나면 “여기서 시간을 보내거나 무언가를 기다린다”는 뜻이고, 조건이 충족되면 그 지점에서 다시 깨어나 이어 실행된다. env.timeout(d): 만큼 시간이 흐르길 기다린다(활동). 내부적으로는 ” 뒤의 사건”을 FEL에 등록하고 깨어나는 것이다.Resource: 용량이 제한된 자원(서버).request()로 점유를 요청하면, 자리가 없을 때 자동으로 큐에서 기다린다. 우리가 직접 짰던 큐 관리가 여기에 내장돼 있다.
즉 직접 구현에서 가장 번거로웠던 FEL 관리와 큐잉 로직이 통째로 사라진다.
전체 코드
import math
import random
import simpy
def exp_rv(rate, rng):
"""지수분포 난수 — 역변환법 (04번 글)."""
return -math.log(1.0 - rng.random()) / rate
def customer(env, server, mu, rng, sojourn_times):
"""손님 한 명의 생애: 도착 → 줄 서기 → 서비스 → 떠남."""
arrival_time = env.now
with server.request() as req: # 서버 점유 요청 (자리 없으면 자동 대기)
yield req # 내 차례가 올 때까지 기다림
yield env.timeout(exp_rv(mu, rng)) # 서비스 받는 동안 시간 경과
sojourn_times.append(env.now - arrival_time) # 떠나며 체류시간 기록
def source(env, server, lam, mu, rng, sojourn_times):
"""도착 과정: 간격마다 새 손님 프로세스를 만든다."""
while True:
yield env.timeout(exp_rv(lam, rng)) # 다음 도착까지 대기
env.process(customer(env, server, mu, rng, sojourn_times))
def simulate_mm1(lam, mu, end_time, seed=0):
rng = random.Random(seed)
env = simpy.Environment()
server = simpy.Resource(env, capacity=1) # 서버 1대 → M/M/1
sojourn_times = []
env.process(source(env, server, lam, mu, rng, sojourn_times))
env.run(until=end_time)
W = sum(sojourn_times) / len(sojourn_times)
return W
W = simulate_mm1(lam=0.8, mu=1.0, end_time=1_000_000, seed=42)
print(f"W ≈ {W:.3f} (이론값 5.0)")직접 구현과 비교
같은 M/M/1인데, 두 코드의 사고 단위가 다르다.
- 직접 구현은 사건(arrival/departure)을 중심으로, “이 사건이 일어나면 상태를 어떻게 바꿀까”를 기술했다. 서버가 비었는지 검사하고, 큐에 넣고 빼고, 다음 사건을 예약하는 일을 전부 손으로 했다.
- SimPy 코드는 손님 한 명의 일생을 중심으로 읽힌다.
customer함수를 위에서 아래로 읽으면 그대로 시나리오다 — 도착해서(arrival_time 기록) → 서버를 요청해 기다리고(yield req) → 서비스 시간을 보내고(yield timeout) → 떠나며 체류시간을 적는다. 큐도, FEL도 코드에 드러나지 않는다.
with server.request() as req 한 줄이 직접 구현의 “서버가 비었으면 서비스 시작, 아니면 큐에 추가”와 departure 때의 “큐에서 다음 손님 꺼내기”를 모두 대신한다. yield req에서 손님은 자동으로 줄을 서고, 앞 손님이 자원을 놓는(with 블록을 벗어나는) 순간 깨어난다. 이것이 프로세스 중심 관점이 주는 추상화의 이득이다.
결과 확인
이 시뮬레이션도 이론값 을 재현한다. 여기서는 체류시간만 모았지만, 평균 손님 수 이 궁금하면 따로 면적을 누적할 필요 없이 Little’s law로 을 바로 얻을 수 있다.
확장이 쉽다
SimPy의 진짜 가치는 복잡해질수록 드러난다. 직접 구현에서는 사건 종류가 늘 때마다 루프를 뜯어고쳐야 했지만, SimPy에서는 보통 프로세스 함수에 몇 줄을 더하면 된다.
- 서버 여러 대 (M/M/c):
capacity=1을capacity=c로 바꾸기만 하면 된다. 큐와 자원 분배는 SimPy가 알아서 한다. - 우선순위:
simpy.PriorityResource를 쓰면 된다. - 장비 고장: 서버를 점유했다가 일정 시간마다 풀어 주는 별도 프로세스를 추가한다.
- 여러 단계 공정:
customer함수 안에request → timeout → release를 단계마다 이어 쓰면, 손님이 공정을 차례로 거치는 흐름이 자연스럽게 표현된다.
이 밖에도 SimPy는 자원을 Resource(용량 제한 서버) 외에 Store(개별 객체를 담고 꺼내는 창고)와 Container(연속량 — 연료·재고 등)로 제공해, 다양한 시스템을 모델링할 수 있다.
정리
- SimPy는 프로세스 중심 DES 라이브러리로, generator와
yield로 entity의 생애를 시나리오처럼 기술한다. Environment(엔진)·프로세스(yield)·env.timeout(활동)·Resource(자원+큐)가 핵심이며, 직접 구현에서 손으로 짰던 FEL과 큐 관리가 사라진다.- 같은 M/M/1을 훨씬 짧고 읽기 쉽게 작성했고, 동일하게 이론값()을 재현했다.
capacity변경만으로 M/M/c가 되는 등 확장이 쉬운 것이 추상화의 가장 큰 이득이다.
여기까지가 DES 이론과 구현(3부)이다. 다음 4부에서는 한 발 물러나, 이렇게 만든 시뮬레이터의 결과를 어떻게 신뢰할 것인가 — 입력 분포를 데이터로 정하고(input modeling), 출력을 통계적으로 분석하고(output analysis), 모델을 검증하는(V&V) — 방법론으로 들어간다.