연금술 공방 관리 시스템 SRP 설계
재료·레시피·창고·주문의 책임을 가르기
한 클래스에 다 몰아넣으면 기능 하나 고칠 때마다 전체를 다시 읽어야 한다. 단일 책임 원칙으로 경계를 먼저 긋고, 그 위에 vector·map으로 창고와 레시피를 얹은 구현.
연금술 공방 관리 시스템을 만들면서 가장 먼저 정한 건 기능이 아니라 경계였다. 재료·레시피·창고·주문을 한 클래스에 몰아넣으면 기능 하나를 고칠 때마다 전체를 다시 읽어야 하기 때문이다. 이 글에서는 단일 책임 원칙(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 = 3은 static 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)을 상수로 분리하면 정책 변경이 한 줄 수정으로 끝난다.