새롭게 정립한 '설계 주도형 사고(SOP)'에 따라, 시뮬레이션 문제(이기적 유전자: 반복되는 죄수의 딜레마)를 풀기 위해 5단계 모듈화 작업을 진행했습니다.
함수의 역할을 매칭(match) -> 게임 진행(play_game) -> 행동 결정(action) -> 인구 업데이트(update)로 쪼개는 것까지는 완벽했다고 자부했습니다. 하지만 이 뼈대에 살을 붙이려던 찰나, 그대로 코딩을 진행했다면 무조건 런타임 에러를 뱉어냈을 3가지 치명적인 설계 오류를 발견했습니다.
오늘은 스켈레톤 코드 단계에서 인터페이스(파라미터/반환값)를 설계할 때 놓치기 쉬운 포인트들을 회고해 봅니다.
[초기 설계]
def play_game((e1, e2), G, E):
# 매칭된 두 개체가 G번 게임을 하고 에너지를 업데이트한다.
[무엇이 문제였나?]
함수를 독립적으로 쪼개는 데만 집중한 나머지, 해당 함수가 목적을 달성하기 위해 '외부에서 무엇을 받아와야 하는지'를 놓쳤습니다. play_game이 두 개체의 게임 결과를 계산해 에너지를 업데이트하려면, 협력/배신에 따른 보상표인 scores (m, n, k, l) 배열이 반드시 필요합니다. 하지만 파라미터로 넘겨주지 않아, 함수 내부에서 점수를 계산할 방법이 없는 '스코프 단절' 상태가 되었습니다.
[초기 설계]
문제에서 주어진 '초기 에너지' 변수명은 E입니다. 그런데 저는 모든 개체의 '현재 에너지 상태를 담은 2차원 배열'의 이름도 무의식적으로 E라고 짓고 파라미터로 넘겼습니다.
[무엇이 문제였나?]
이름이 겹치면 논리적 혼란이 옵니다. 나중에 복제 조건에서 기존 에너지를 반으로 나눌 때(E / 2), 정수 값 하나를 나눠야 할 것을 배열 전체를 나누려고 시도하다가 TypeError가 발생할 뻔했습니다.
상태를 관리하는 배열은 e_list나 energies처럼 단일 변수(E)와 명확히 구분되는 이름을 써야 함을 깨달았습니다.
[초기 설계]
def match(index):
# 1차원 index 배열을 셔플해서 두 개씩 pop한 뒤 반환
return e1, e2
def action_by_entity(e, op_last_move):
# e 개체의 종족에 따라 행동을 리턴
[무엇이 문제였나?]
가장 뼈아픈 실수였습니다. match 함수가 던져주는 e1, e2는 그저 1차원 배열(0 ~ N-1)을 섞어서 뽑은 '단순 숫자(인덱스)'일 뿐입니다.
그런데 이 숫자만 보고 action_by_entity 함수가 이 개체가 신사인지, 배신자인지, 복수자인지 어떻게 알 수 있을까요? 종족(Species)을 알아야 행동 패턴을 결정할 수 있는데, '번호표'만 쥐여주고 '너의 본성을 드러내라'고 명령한 꼴이 되었습니다.
단순히 인덱스만 넘길 것이 아니라, 현재의 인구수 S, B, R 정보를 기준선으로 삼아 이 인덱스가 어느 종족 그룹에 속하는지 판별하는 '매핑(Mapping) 로직'이 play_game 내부에 반드시 포함되어야 함을 확인했습니다.
함수를 작게 쪼개는 것(Divide)만큼 중요한 것은, 쪼개진 함수들이 매끄럽게 데이터를 주고받을 수 있도록 연결 고리(Interface)를 꼼꼼히 설계하는 것임을 배웠습니다.
[개선된 구조]
def play_game(idx1, idx2, populations, G, scores, e_list):
# 1. populations 정보를 바탕으로 idx1, idx2의 종족(species) 판별
# 2. scores를 받아와 게임 결과 계산
# 3. 명확히 분리된 e_list에 에너지 결과 즉시 업데이트
이제 뼈대가 단단해졌으니, 자신 있게 내부 로직 구현(Bottom-Up)으로 넘어갈 수 있게 되었습니다.