포스트

연금술 공방 관리 시스템 SRP 설계

재료·레시피·창고·주문의 책임을 가르기

한 클래스에 다 몰아넣으면 기능 하나 고칠 때마다 전체를 다시 읽어야 한다. 단일 책임 원칙으로 경계를 먼저 긋고, 그 위에 vector·map으로 창고와 레시피를 얹은 구현.

연금술 공방 관리 시스템 SRP 설계

연금술 공방 관리 시스템을 만들면서 가장 먼저 정한 건 기능이 아니라 경계였다. 재료·레시피·창고·주문을 한 클래스에 몰아넣으면 기능 하나를 고칠 때마다 전체를 다시 읽어야 하기 때문이다. 이 글에서는 단일 책임 원칙(SRP)을 기준으로 클래스를 가른 설계와, 그 위에 vector·map으로 창고와 레시피를 얹은 구현을 이야기하려 한다.


구현 기능 정리

필수 기능

기능설명
레시피 추가물약 이름 + 재료 목록(여러 개)을 입력받아 등록
전체 레시피 출력등록된 모든 레시피와 재고 출력
이름으로 검색물약 이름으로 정확히 1개 검색
재료로 검색특정 재료가 포함된 모든 레시피 검색

도전 기능

기능설명
자동 초기 재고레시피 추가 시 재고 자동 3개 세팅
물약 지급 (이름)재고 1개 이상일 때만 지급, 재고 -1
물약 지급 (재료)해당 재료 포함 물약 중 재고 있는 것 모두 지급
공병 반환재고 +1, 최대 3개 초과 불가

클래스 설계 (책임 분리 — SRP)

SRP(단일 책임 원칙 — 클래스가 바뀌어야 할 이유는 하나여야 한다)를 기준으로 네 클래스로 나눴다.

1
2
3
4
PotionRecipe     → 데이터만 보관 (이름, 재료 목록)
RecipeManager    → 레시피 추가 / 이름 검색 / 재료 검색
StockManager     → 재고 초기화 / 지급 / 반환 / 조회
AlchemyWorkshop  → 위 세 클래스를 조합해 공방 기능 제공 (Facade)

AlchemyWorkshop은 Facade(여러 클래스를 묶어 하나의 진입 창구로 노출하는 패턴) 역할만 한다.


코드 작성 과정

먼저 기존 제공 코드를 분석했다. PotionRecipe는 이름 + 단일 재료 구조라 다중 재료(vector<string>)로 바꿔야 했고, AlchemyWorkshop에는 레시피 관리·출력에다 향후 재고 기능까지 뒤섞일 판이라 SRP 위반이 보였다. 기존 코드를 최대한 건드리지 않으려고, 고치는 대신 새 클래스(RecipeManager, StockManager)를 추가하는 방향을 골랐다.

필수 기능은 RecipeManager에 넣었다. FindRecipeByName()은 이름 일치(==) 비교로 찾고 없으면 nullptr를 반환한다. FindRecipesByIngredient()는 재료 목록을 순회해 결과를 vector로 돌려준다.

도전 기능은 StockManager가 맡는다. map<string, int>로 물약별 재고를 관리하고, 최대 재고 MAX_STOCK = 3static constexpr 상수로 빼 뒀다 — 재고 정책이 바뀌어도 한 곳만 고치면 된다. DispensePotion()은 재고가 있으면 감소 후 true, 없으면 false를 반환하고, ReturnPotion()은 재고가 MAX_STOCK 미만일 때만 증가시킨다.

마지막으로 중복 제거를 했다. cin.ignore + getline 패턴이 6곳에 반복돼 있어 ReadLine() 헬퍼 하나로 통일했고, 재료 목록 출력 중복 3곳도 PrintIngredients() 헬퍼로 모았다. 검색·지급·반환의 모든 진입점에는 빈 문자열 방어(empty() 체크)를 넣었다.


코드 유의점

설계 원칙

설계하면서 스스로 던진 질문은 두 가지였다. “각 클래스가 바뀌어야 할 이유가 하나인가?”(SRP) — RecipeManager는 레시피 사양이 바뀔 때만, StockManager는 재고 정책이 바뀔 때만 수정된다. “새 기능을 넣을 때 기존 코드를 건드리지 않는가?”(OCP, 개방-폐쇄 원칙) — MAX_STOCK 상수 분리 덕에 재고 정책 변경이 기존 로직 수정 없이 끝나고, 나중에 재고 정책이 다양해지면 IStockPolicy 인터페이스로 확장할 여지도 남겨 뒀다.

입력 검증

  • 숫자가 아닌 메뉴 입력 → cin.fail() 감지 후 복구
  • 빈 물약 이름 / 빈 재료 이름 → empty() 체크 후 안내 메시지
  • 재료 0개로 레시피 추가 시도 → 취소 메시지 출력

에러 처리 메시지 목록

상황출력 메시지
중복 레시피 추가이미 존재하는 레시피입니다
이름 검색 결과 없음이름의 레시피를 찾을 수 없습니다
재료 검색 결과 없음재료를 사용하는 레시피가 없습니다
재고 부족 지급재고가 부족합니다
MAX 초과 반환창고가 가득 찼습니다
없는 레시피 지급/반환레시피가 존재하지 않습니다
빈 입력이름을 입력해주세요

최종 코드

핵심 요약 — 기존 코드에 기능을 얹어야 할 때, 원본을 고치기보다 책임별 새 클래스를 추가하는 쪽이 SRP도 지키고 수정 범위도 줄인다는 것. 정책 값(MAX_STOCK)을 상수로 분리하면 정책 변경이 한 줄 수정으로 끝난다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.