제작 조건 검증 및 제작 실행 구조
목차
1. 시스템 요구 사항
제작 슬롯과 선택 상태를 정리한 이후, 제작 시스템에서 가장 중요한 단계는 제작 버튼을 눌렀을 때 실제로 아이템을 만들어도 되는지 판단하고, 그 결과를 안정적으로 반영하는 구조였다.
제작은 단순히 아이템을 하나 추가하는 행위가 아니라, 재료 소모와 재화 차감이 동시에 이루어지는 트랜잭션에 가깝다.
이 과정에서 조건 검증과 실행 로직이 섞이면, 일부 재화만 차감되거나 제작 실패 후에도 아이템이 추가되는 등 치명적인 오류가 발생할 수 있다.
초기 구현에서는 버튼 클릭 시점에 재료 수량 비교와 제작 실행이 한 함수 안에서 동시에 이루어졌고, 이로 인해 어디까지가 검증이고, 어디서부터가 실제 변경인가가 코드상으로 명확하지 않았다.
특히 UI 텍스트를 그대로 신뢰해 제작을 실행하는 구조는, 표시와 실제 상태가 어긋날 경우 위험 요소가 될 수 있었다.
그래서 제작 시스템에서는 조건 검증과 실제 제작 처리를 명확히 분리하고, 제작 버튼은 항상 하나의 진입점만을 통해 동작하도록 구조를 재정리할 필요가 있었다.
2. 설계 목표
- 제작 조건 검증과 제작 실행을 명확히 분리할 것
- 제작 버튼 단일 진입점만을 사용하도록 할 것
- 재료·재화 차감과 아이템 지급을 하나의 흐름으로 묶을 것
- 실패 시 시스템 상태가 변경되지 않도록 할 것
3. 흐름도

이 구조에서 중요한 점은, 제작 실행이 조건 검증을 통과한 경우에만 발생한다는 것이다.
조건 검증 단계에서는 시스템 상태를 절대 변경하지 않고, 오직 제작이 가능한가만 판단한다.
실제 데이터 변경은 제작 실행 단계에서만 이루어진다.
4. 구현
4.1. 제작 시도 단일 진입점
private void TryCraft(Item item, int index)
{
int haveItems = int.Parse(haveItemTxt.text);
int needItems = int.Parse(needItemTxt.text);
int haveCoins = int.Parse(haveCoinTxt.text);
int needCoins = int.Parse(needCoinTxt.text);
if (haveItems >= needItems && haveCoins >= needCoins)
CraftItem(item, index, needItems, needCoins);
else
StartCoroutine(ShowTemporaryMsg(cantCraftMsg, 0.5f));
}
TryCraft 함수는 제작 버튼이 눌렸을 때 항상 가장 먼저 호출되는 단일 진입점이다.
이 함수의 책임은 제작을 실행하는 것이 아니라, 지금 이 제작 시도가 가능한 상태인지 판단하는 것에만 있다.
제작 시스템에서 가장 중요한 설계 원칙 중 하나는, 조건 검증과 실제 데이터 변경을 반드시 분리하는 것이다.
조건 판단과 재료 차감, 아이템 지급이 하나의 함수 안에서 섞이기 시작하면, 중간 단계에서 실패했을 때 일부 데이터만 변경되는 불완전한 상태가 발생하기 쉽다.
TryCraft는 이런 상황을 구조적으로 차단하기 위해, 오직 비교 연산과 분기만 담당하도록 설계되었다.
함수가 호출되면 가장 먼저 제작에 필요한 네 가지 수치를 정수형으로 추출한다.
haveItemTxt, needItemTxt, haveCoinTxt, needCoinTxt는 모두 UI 텍스트 컴포넌트이기 때문에, 내부 값은 문자열 형태로 존재한다.
제작 가능 여부를 판단하기 위해서는 수치 비교가 필요하므로, 이 문자열 값들을 int.Parse를 사용해 정수로 변환한다.
int.Parse는 문자열을 정수형으로 변환하는 C#의 표준 API다.
이 방식의 장점은 구현이 간단하고 코드 의도가 직관적이라는 점이다.
반면, 숫자가 아닌 문자열이 들어올 경우 예외가 발생할 수 있다는 단점이 있다.
그러나 이 제작 시스템에서는 UI 텍스트가 사용자의 직접 입력이 아니라, 항상 코드에 의해 설정되는 값이기 때문에 숫자가 아닌 값이 들어올 가능성은 구조적으로 차단되어 있다.
이 전제 덕분에 TryParse와 같은 방어적 파싱 대신 int.Parse를 사용해 코드 가독성과 의도를 우선했다.
이렇게 정수로 변환된 네 개의 값은 곧바로 제작 가능 여부를 판단하는 비교 연산에 사용된다.
haveItems와 haveCoins는 플레이어가 현재 보유하고 있는 자원이며, needItems와 needCoins는 선택된 아이템을 제작하는 데 필요한 최소 요구 수치다.
haveItems >= needItems && haveCoins >= needCoins
if 문에서 사용된 조건식은 재료 수량과 재화 수량이 모두 충분한가를 동시에 검사하는 의미를 가진다.
두 조건을 AND 연산으로 묶은 이유는, 제작이 성공하려면 두 조건이 모두 충족되어야 하기 때문이다.
하나라도 부족한 경우에는 제작이 이루어지지 않으며, 데이터 변경도 발생하지 않는다.
조건을 만족하는 경우에만 CraftItem 함수가 호출된다.
이 시점에서 제작 성공은 논리적으로 확정되며, 이후 단계에서는 재료 차감과 아이템 지급이 안전하게 수행된다.
반대로 조건을 만족하지 못한 경우에는, 제작 실패 메시지를 잠시 표시하는 코루틴만 실행되고 시스템 상태는 전혀 변경되지 않는다.
이 구조를 통해 TryCraft는 제작 시도의 검증 전용 관문 역할을 수행하며,
실제 게임 데이터 변경은 오직 검증을 통과한 경우에만 다음 단계로 위임된다.
그 결과, 제작 실패 시 일부 데이터만 변경되는 불완전한 상태가 구조적으로 발생할 수 없게 된다.
이 함수에서는 현재 보유 재료 수량과 재화 수량, 그리고 제작에 필요한 재료 수량과 비용을 각각 UI 텍스트에서 읽어온다.
이 방식이 가능한 이유는 이전 게시글에서 설계한 선택 상태 기반 UI 동기화 구조 덕분이다.
제작 패널의 UI는 selectedIndex를 기준으로 항상 최신 상태를 반영하고 있으며, UI가 별도의 상태를 저장하지 않고 데이터의 표현 계층으로만 동작한다는 전제가 성립한다.
따라서 이 단계에서 UI 값을 참조하더라도 데이터 불일치가 발생하지 않는다.
조건 비교 결과, 보유 재료와 재화가 모두 충분한 경우에만 CraftItem 함수로 처리가 위임된다.
여기서 UI 텍스트를 기반으로 수량을 읽는 구조를 유지한 이유는, 제작 패널의 UI가 항상 선택 상태 기준으로 실시간 갱신되고 있기 때문이다.
다만 이 구조는 UI가 단일 선택 상태(selectedIndex)를 기준으로 항상 데이터와 동기화되고 있다는 전제 하에서만 안전하다.
반대로 하나라도 부족한 경우에는 제작 실패 메시지를 표시하고, 시스템 상태는 전혀 변경하지 않는다.
이 분기 구조를 통해 제작 실패 시 데이터 변경이 0으로 보장되는 구조를 만들었다.
4.2. 제작 실행 로직 분리
private void CraftItem(Item item, int index, int requiredItem, int requiredCoin)
{
materialItems[index].count -= requiredItem;
itemManager.coin -= requiredCoin;
itemManager.AddItem(item.itemCode, 1);
UpdateUIAfterCraft(index);
}
CraftItem 함수는 조건 검증을 이미 통과한 이후에만 호출되는 제작 실행 전용 함수다.
이 함수 안에는 조건 판단이나 분기 로직이 전혀 존재하지 않으며, 제작 성공이 확정된 상태에서 실제 데이터 변경만을 순차적으로 수행한다.
먼저 선택된 인덱스를 기준으로 재료 아이템의 보유 수량을 차감하고, 이어서 ItemManager를 통해 재화를 차감한다.
여기서 index를 사용하는 이유는 이전 단계에서 선택 상태를 단일 인덱스로 관리하고 있기 때문이다.
CraftItem은 어떤 아이템이 선택되었는지를 다시 판단하지 않으며, 전달받은 index가 곧 제작 대상이라는 전제를 그대로 신뢰한다.
이때, 이미 TryCraft 단계에서 충분한 수량이 보장되었기 때문에 이 시점에서는 실패 가능성을 고려하지 않는다.
의도적으로 예외 처리나 추가 검증을 넣지 않고, 단순한 연산 흐름을 유지한 이유는 제작 성공 이후의 상태를 최대한 예측 가능하게 만들기 위함이다.
아이템 지급은 ItemManager.AddItem()을 통해 처리한다.
이 방식은 제작 시스템이 인벤토리 내부 구조를 직접 다루지 않고, 아이템 매니저에 아이템을 추가하라는 요청만 전달하도록 만든다.
이로 인해 제작 시스템과 인벤토리 시스템 간 결합도가 낮아지고, 이후 인벤토리 구조가 변경되더라도 제작 로직은 영향을 받지 않는다.
이 함수의 마지막에서는 UI를 직접 갱신하지 않고, UpdateUIAfterCraft 함수로 처리를 위임한다.
이는 데이터 변경과 UI 반영을 분리하기 위한 의도적인 설계 선택이다.
CraftItem은 오직 게임 상태를 변경하는 책임만 가지며, 화면에 무엇을 어떻게 보여줄지는 다음 단계에서 처리된다.
4.3. 제작 결과 UI 반영
private void UpdateUIAfterCraft(int index)
{
haveItemTxt.text = materialItems[index].count.ToString();
haveCoinTxt.text = itemManager.coin.ToString();
}
UpdateUIAfterCraft 함수는 제작이 성공적으로 완료된 이후, 이미 변경된 데이터 상태를 UI에 반영하는 역할을 담당한다.
이 단계에서는 어떤 계산이나 조건 판단도 수행하지 않으며, 단순히 현재 데이터 값을 화면에 표시하는 데만 집중한다.
중요한 점은 이 함수가 상태를 만들어내지 않는다는 것이다.
재료 차감과 재화 차감은 이미 CraftItem 단계에서 끝났고, UI는 그 결과를 그대로 보여주기만 한다.
이 구조를 통해 제작 성공 이후 UI가 잘못된 값을 표시할 여지가 줄어들며, 데이터와 화면 상태 사이의 불일치 가능성이 구조적으로 차단된다.
또한 제작 직후 즉시 UI를 갱신함으로써, 플레이어는 제작 버튼을 누른 결과를 지연 없이 확인할 수 있다.
이 즉각적인 피드백은 제작 시스템의 신뢰도를 높이는 중요한 요소다.
4.4. 실패 피드백 처리와 코루틴 사용
private IEnumerator ShowTemporaryMsg(GameObject obj, float duration)
{
obj.SetActive(true);
yield return new WaitForSeconds(duration);
obj.SetActive(false);
}
제작 조건을 만족하지 못한 경우에는, 시스템 상태를 변경하지 않고 시각적인 피드백만 제공한다.
ShowTemporaryMsg 코루틴은 지정된 메시지 오브젝트를 일정 시간 동안만 활성화한 뒤 자동으로 비활성화하는 역할을 한다.
이 처리에서 Coroutine을 사용한 이유는, 프레임 흐름을 막지 않으면서 시간 기반 동작을 구현할 수 있기 때문이다.
Invoke나 Update 기반 타이머를 사용하는 방식에 비해, 메시지를 잠깐 보여주고 사라지게 한다는 의도가 코드 구조 자체로 명확하게 드러난다는 장점이 있다.
중요한 점은 제작 실패 시 이 코루틴 외에는 어떤 로직도 실행되지 않는다는 것이다.
재료 차감, 재화 차감, UI 갱신 등 제작 성공과 관련된 흐름은 전혀 발생하지 않으며, 시스템 상태는 완전히 이전 상태로 유지된다.
이를 통해 제작 실패는 실패했다는 정보만 전달하고, 게임 상태에는 아무런 흔적도 남기지 않는 구조가 완성된다.
5. 개발 의도
이 게시글에서의 핵심은 제작을 하나의 안전한 트랜잭션처럼 다루는 구조를 만드는 것이었다.
제작 가능 여부 판단과 실제 제작 실행을 분리함으로써, 실패 시에는 아무것도 변하지 않고 성공 시에는 모든 변경이 일관되게 적용되도록 설계했다.
제작 버튼은 단일 진입점만을 가지며, 제작 실행은 항상 검증 이후에만 이루어진다.
이 구조 덕분에 이후 제작 결과 연출, 제작 수량 확장, 제작 실패 확률 같은 요소를 추가하더라도, 시스템의 중심 흐름은 흔들리지 않는다.
'선택 상태 → 조건 검증 → 제작 실행' 이라는 단계가 명확히 분리되어 있기 때문이다.
