오늘 학습 키워드

MVP 패턴

오늘 학습 한 내용을 나만의 언어로 정리하기

계산기 만들기

UICalculator가 메인으로 있고 Buttons에서 OnClickButton 만들어서 버튼을 누르면 표시되도록 UICalculator로 데이터 전달 그리고 equal (=) 누르면 UICalculator가 ItemHistory를 생성하도록 만듬

// UICalculator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class UICalculator : MonoBehaviour
{
    public static UICalculator instance;
 
    public Text Result;
    public Text TotalResult;
    public RectTransform HistoryParent;
    public GameObject itemHistoryPref;
 
    public float first;
    public float second;
    private float answer;
    private bool isFirstFull;
    private bool isSecondFull;
    private bool isAnswerFull;
 
    public string temp = "";
    public string oper = "";
 
 
    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(this);
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        Result.text = "";
        TotalResult.text = "";
    }
 
    // Update is called once per frame
    void Update()
    {
        
    }
 
    public void InputNumber(string str)
    {
        temp += str;
        ChangeText();
    }
 
    public void InputOther(string str)
    {
        switch(str)
        {
            case "+":
            case "-":
            case "X":
            case "/":
            case "%":
                if(isAnswerFull)
                {
                    first = answer;
                    isFirstFull = true;
                    isSecondFull = false;
                    isAnswerFull = false;
                }
                else if(!isFirstFull)
                {
                    first = float.Parse(temp);
                    temp = "";
                    isFirstFull = true;
                }
                else
                {
                    second = float.Parse(temp);
                    temp = "";
                }
                oper = str;
                break;
            case ".":
                temp += ".";
                break;
            case "+/-":
                if (temp == "")
                {
                    temp = "-";
                }
                else if (temp[0] == '-')
                {
                    temp = temp.Substring(1);
                }
                else
                {
                    temp = "-" + temp;
                }
                break;
            case "<-":
            case "CE":
                if(temp.Length == 0) break;
                temp = temp.Substring(0, temp.Length - 1);
                break;
            case "C":
                first = 0.0f;
                second = 0.0f;
                answer = 0.0f;
                TotalResult.text = "";
                isFirstFull = false;
                isSecondFull = false;
                isAnswerFull = false;
                break;
 
            case "=":
                OnClickEqual();
                break;
        }
        ChangeText();
    }
    public void OnClickEqual()
    {
        second = float.Parse(temp);
        temp = "";
        isSecondFull = true;
        switch (oper)
        {
            case "+":
                answer = first + second;
                break;
            case "-":
                answer = first - second;
                break;
            case "X":
                answer = first * second;
                break;
            case "/":
                if(second == 0)
                {
                    answer = 0;
                    break;
                }
                answer = first / second;
                break;
            case "%":
                answer = first % second;
                break;
        }
 
        if(oper == "/" && second == 0)
        {
            TotalResult.text = "0으로 나눌 수 없습니다.";
        }
        else
        {
            TotalResult.text = answer.ToString("N4");
        }
 
        isAnswerFull = true;
 
        GameObject newHistory = Instantiate(itemHistoryPref, HistoryParent);
        newHistory.GetComponent<ItemHistory>().SetStrings(Result.text, TotalResult.text);
    }
 
    public void ChangeText()
    {
        if (!isFirstFull)
        {
            Result.text = temp;
        }
        else if (isSecondFull)
        {
            Result.text = first + " " + oper + " " + second;
        }
        else
        {
            Result.text = first + " " + oper + " " + temp;
        }
    }
}
 
// Buttons.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class Buttons : MonoBehaviour
{
    public Text text;
 
 
    public void OnClickButton()
    {
        if (isNumber(text.text))
        {
            UICalculator.instance.InputNumber(text.text);
        } else
        {
            UICalculator.instance.InputOther(text.text);
        }
    }
 
    bool isNumber(string str)
    {
        if (str[0] - '0' >= 0 && str[0] - '0' < 10) return true;
        else return false;
    }
}
 
// ItemHistory.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class ItemHistory : MonoBehaviour
{
    public Text result;
    public Text totalResult;
 
    public void SetStrings(string result, string totalResult)
    {
        this.result.text = result;
        this.totalResult.text = totalResult;
    }
}
 

코드를 깔끔하게 리팩토링

어제 만든 코드는 진짜 작동만 하도록 만든 코드라서, MVP 패턴이 전혀 사용되지 않았음. 그래서 리팩토링을 하고자 함

코드 수정은 직접 해보고 싶어서, 챗지피티에게 리팩토링 힌트만 부탁했음.

🎯 목표: 기존의 UICalculator를 MVP 패턴으로 분리하기


1. Model 분리 힌트

  • 계산 로직(+, -, *, /, %)을 담당하는 순수 클래스 하나를 만들자.

  • 이 클래스는 first, second, oper를 받고, 결과를 반환하기만 해야 해.

  • 즉, 상태 저장보다는 입력 → 출력만 담당.

📌 힌트 키워드: CalculatorModel, Calculate(first, second, operator) 함수 만들기


2. View 역할 분리 힌트

  • UICalculator는 View가 될 거야. UI(Text, Button 등)와 직접 연결돼 있지?

  • 이제 이 클래스는 사용자 입력이나 버튼 처리를 Presenter에게 전달만 해야 해.

  • View는 InputField, Text, Button만 알고 있어야 해.

📌 힌트 키워드:

  • ICalculatorView 인터페이스 만들기

  • DisplayResult(string) 같은 함수 포함시키기


3. Presenter 만들기 힌트

  • 사용자 입력 처리는 이제 Presenter가 담당해야 해.

  • 버튼에서 받은 입력을 Presenter가 받고,

    • 숫자면 숫자 처리,

    • 연산자면 연산 처리,

    • ’=’ 이면 모델을 호출해서 계산,

    • 결과를 View에 넘겨주는 역할을 해야 해.

📌 힌트 키워드:

  • CalculatorPresenter, OnNumberInput(string), OnOperatorInput(string) 함수

4. Buttons.cs 수정 힌트

  • 현재는 UICalculator에 직접 접근하고 있는데,

  • 버튼은 Presenter에 직접 접근하게 바꾸는 게 좋아.

  • View에서 Presenter를 참조할 수 있도록 BindPresenter() 같은 메서드를 만들어두면 돼.

📌 힌트 키워드:

  • BindPresenter(CalculatorPresenter presenter)

  • ButtonsUICalculatorCalculatorPresenter


5. ItemHistory와 MVP 관계 힌트

  • ItemHistory는 사실상 UI 전용이니까 View 영역에 속해.

  • 다만 히스토리 추가 시점을 Presenter에서 제어하도록 해보자.

  • 즉, Presenter가 계산이 끝나면 View에게 “히스토리 하나 생성해줘”라고 명령하는 식.

📌 힌트 키워드:

  • View 인터페이스에 AddHistory(string exp, string result) 추가

🧠 정리된 역할별 분리

역할하는 일
Model숫자 계산만. 내부 상태 없음.
View (UICalculator)사용자 UI 표시. 버튼 연결. Presenter에 이벤트 위임.
Presenter숫자 입력 처리, 연산 제어, 계산 요청, 결과 전달
ButtonsView를 통해 Presenter에게 전달
ItemHistoryView에 속함. Presenter 지시에 따라 UI에 생성됨

문자열에서 특정 글자 찾기

  • IndexOf : 문자열의 인덱스 번호 출력 (존재하지 않으면 -1)

  • LastIndexOf : 같은 문자열이 여러번 있을때 가장 마지막에 출력된 문자열의 인덱스 번호 출력 (존재하지 않으면 -1)

  • Contains : 해당 문자열이 존재하는지 확인 (true,false 반환)

출처

그냥 싱글톤으로 엮으면 되는거 아냐..?

이 고민은 Buttons UiCalculator 를 연결하는데에 있어서 시작된다… 아무리 생각해도 그냥 UICalculator를 싱글톤으로 만드는게 낫지 않나..? 그래서 우리의 챗지피티에게 물어본 결과…

🔥 정답부터 말하자면:

단기적으로 편하긴 하지만,
MVP 아키텍처 철학에는 어긋나고,
유지보수/확장성/테스트성 측면에서 추천되지 않아.

이유는 버튼이 뷰에 너무 의존하게 된다고 함. 그리고 MVP 패턴의 흐름이 깨져버림. presenter 중심의 코드가 view 중심으로 변경됨.

Csharp의 var와 C++의 auto?

뭔가 비슷해보여서, 챗지피티의 도움을 빌려서 정리해봄

비교 항목C# varC++ auto
역할타입 추론타입 추론
foreach에서 사용OO
값 복사 vs 참조값 타입이면 복사, 수정 불가auto = 복사, auto& = 참조
수정 가능 여부❌ 불가능 (readonly)auto& 사용하면 수정 가능
사용 위치지역 변수만거의 어디든 가능

몰랐는데, C#은 foreach var에서 대부분은 읽기 전용임. 만약에 반복하는 데이터가 클래스의 경우에는 내부 데이터를 변경할 수 있다고 함.

// 내부 데이터 변경 가능한 경우
public class Person
{
    public string Name;
    public int Age;
}
 
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 20 },
    new Person { Name = "Bob", Age = 25 }
};
 
foreach (var p in people)
{
    p.Age += 1;         // ✅ 가능! 내부 값 수정
    p = new Person();   // ❌ 불가능! p 변수 자체는 readonly
}

변수 자체인 p를 바꿀 수는 없지만, p 안에 있는 내부 값은 바꿀 수 있다!

MVP 패턴으로 변경해본 결과

// CalculatorPresenter.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
 
public class CalculatorPresenter
{
    public CalculatorPresenter(ICalculatorView view, CalculatorModel model)
    {
        this.view = view;
        this.model = model;
 
        view.BindPresenter(this);
    }
 
    private CalculatorModel model;
    private ICalculatorView view;
 
    public float first;
    public float second;
    private float answer;
    private bool isFirstFull;
    private bool isSecondFull;
    private bool isAnswerFull;
 
    public string temp = "";
    public string oper = "";
 
    public void Debuging(string str)
    {
        Debug.Log(str);
    }
 
    public void OnNumberInput(string str)
    {
        temp += str;
        Debuging("Temp : "+temp);
        ChangeText();
    }
 
    public void OnOperatorInput(string str)
    {
        switch (str)
        {
            case "+":
            case "-":
            case "X":
            case "/":
            case "%":
                if (isAnswerFull)
                {
                    first = answer;
                    isFirstFull = true;
                    isSecondFull = false;
                    isAnswerFull = false;
                }
                else if (!isFirstFull)
                {
                    first = float.Parse(temp);
                    temp = "";
                    isFirstFull = true;
                }
                oper = str;
                break;
            case ".":
                int idx = temp.IndexOf('.');
                if(idx < 0)
                {
                    temp += "."; 
 
                }
                break;
            case "+/-":
                if (temp == "")
                    {
                        temp = "-";
                    }
                else if (temp[0] == '-')
                    {
                        temp = temp.Substring(1);
                    }
                else
                    {
                        temp = "-" + temp;
                    }
                    break;
                case "<-":
                case "CE":
                    if (temp.Length == 0) break;
                    temp = temp.Substring(0, temp.Length - 1);
                    break;
                case "C":
                    first = 0.0f;
                    second = 0.0f;
                    answer = 0.0f;
                    view.DisplayTotalResult("");
                    isFirstFull = false;
                    isSecondFull = false;
                    isAnswerFull = false;
                    break;
 
                case "=":
                    OnClickEqual();
                    break;
                }
        ChangeText();
    }
    public void OnClickEqual()
    {
        second = float.Parse(temp);
        temp = "";
        isSecondFull = true;
        
 
        if (oper == "/" && second == 0)
        {
            view.DisplayTotalResult("0으로 나눌 수 없습니다.");
        }
        else
        {
            answer = model.Calculate(first, second, oper);
            view.DisplayTotalResult(answer.ToString("N4"));
        }
 
        isAnswerFull = true;
        view.AddHistory();
    }
 
    public void ChangeText()
    {
        if (!isFirstFull)
        {
            view.DisplayResult(temp);
        }
        else if (isSecondFull)
        {
            view.DisplayResult(first + " " + oper + " " + second);
        }
        else
        {
            view.DisplayResult(first + " " + oper + " " + temp);
        }
    }
}
 
// ICalculatorView.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public interface ICalculatorView
{
    public void DisplayResult(string str);
    public void DisplayTotalResult(string str);
 
    public void AddHistory();
 
    public void BindPresenter(CalculatorPresenter presenter);
}
 
// UICalculator.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class UICalculator : MonoBehaviour, ICalculatorView
{
    public Text Result;
    public Text TotalResult;
    public RectTransform HistoryParent;
    public GameObject itemHistoryPref;
 
    public Buttons[] allButtons;
 
    private CalculatorPresenter presenter;
 
    // Start is called before the first frame update
    void Start()
    {
        Result.text = "";
        TotalResult.text = "";
 
        presenter = new CalculatorPresenter(this, new CalculatorModel());
        foreach(var btn in allButtons)
        {
            btn.BindPresenter(presenter);
        }
    }
 
    public void DisplayResult(string str)
    {
        Result.text = str;
    }
 
    public void DisplayTotalResult(string str)
    {
        TotalResult.text = str;
    }
 
    public void AddHistory()
    {
        GameObject newHistory = Instantiate(itemHistoryPref, HistoryParent);
        newHistory.GetComponent<ItemHistory>().SetStrings(Result.text, TotalResult.text);
    }
 
    public void BindPresenter(CalculatorPresenter presenter)
    {
        this.presenter = presenter;
    }
}
 
// CalculatorModel.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class CalculatorModel
{
    public float Calculate(float first, float second, string oper)
    {
        float result = 0.0f;
        switch (oper)
        {
            case "+":
                result = first + second; break;
            case "-":
                result = first - second; break;
            case "X":
                result = first * second; break;
            case "%":
                result = first % second; break;
            case "/":
                result = first / second; break;
        }
 
        return result;
    }
 
}
 
// Buttons.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class Buttons : MonoBehaviour
{
    public Text text;
 
    private CalculatorPresenter presenter;
 
 
    public void OnClickButton()
    {
        if (isNumber(text.text))
        {
            presenter.OnNumberInput(text.text);
        } else
        {
            presenter.OnOperatorInput(text.text);
        }
    }
 
    public void BindPresenter(CalculatorPresenter presenter)
    {
        this.presenter = presenter;
    }
 
    bool isNumber(string str)
    {
        if (str[0] - '0' >= 0 && str[0] - '0' < 10) return true;
        else return false;
    }
}
 

학습하며 겪었던 문제점 & 에러

문제 1

  • 문제&에러에 대한 정의

같은 조 분이 타입캐스팅이 안됐음

  • 내가 한 시도

const를 붙여줬음

  • 해결 방법

함수 안에서 변경하신게 아니고 클래스 안에서 자꾸 선언하시고 바꾸려고 하시니까 안되신거였음.

  • 새롭게 알게 된 점

깜빡했음. 함수 밖에서는 변수 써다가 뭐 할 수가 없음

  • 이 문제&에러를 다시 만나게 되었다면?

데이터는 함수 안에서 바꾸자.

문제 2

  • 문제&에러에 대한 정의

Presenter 가 new로 생성되지 않았음.

  • 내가 한 시도

검색을 함..

  • 해결 방법

모노비헤이비어(MonoBehaviour) 형태는 new 로 만들면 안되고, AddComponent로 만들어줘야 함. 그런데 Presenter는 UI에 붙이는 용도가 아니기 때문에, 일반 C# 클래스로 만들어주면 됨.

  • 새롭게 알게 된 점

Presenter는 일반 C# 클래스로 만들자.

  • 이 문제&에러를 다시 만나게 되었다면?

게임 내부 UI에 붙이는 클래스가 아니라면 MonoBehaviour를 뗀다.

문제 3

  • 문제&에러에 대한 정의

Presenter를 일반 C# 스크립트로 만들다보니까 디버깅이 너무 어려웠음

  • 내가 한 시도

검색을 함.. 22

  • 해결 방법

Debug.Log 출력 자체는 여전히 가능함.

  • 새롭게 알게 된 점

UI에 붙는 MonoBehaviour가 아니더라도 디버그 로그는 쓸 수 있다! 또한, View를 활용해서 인게임에 보이게 할 수도 있다.

  • 이 문제&에러를 다시 만나게 되었다면?

Debug.Log()를 적극 활용하자.

내일 학습 할 것은 무엇인지

이제 구조를 MVP 패턴으로 바꾸었으니, 이 외에 로직 부분을 더 깔끔하게 수정해보고자 함