TIL

[멋쟁이사자처럼 부트캠프 TIL회고] 45일차 : 옵저버 패턴 + 리팩토링

Cadi 2025. 1. 5. 14:10

 

옵저버 패턴 마무리

 

 

오늘 새벽까지 진행한 코드는 데이터를 전달해주는 StateMachine이 있는 코드였다. 

그러나, StateMachine은 상태를 저장하고 관리하는 역할만을 하게 하는 방향으로 코드를 짤 수 있다.

(그리고 이 방향이 조금 더 기능을 작게 분해하는 과정이다.)

각 State들이 관찰자로 참여해 직접 CapsuleController에 들어오는 데이터를 관찰하고, 

그 데이터를 기반으로 행동하게 할 것이다.

 

 

그렇게 하기 위해서는 다음과 같은 과정을 거쳐야 한다. 

  1. 옵저버들을 관리할 새 Dictionary 만들기
  2. CapsuleController (subject)에 state들을 옵저버로 등록할 수 있는 함수 생성.
  3. Ontrigger와 OnReadValue에서 예외처리 변경하기
  4. 각각의 State들에서 2번에서 만든 함수로 스스로를 옵저버로 등록, 삭제하기

완성된 코드를 보며 해석해보자면 

 

public class CapsuleController : Singleton<CapsuleController>
{
 private Dictionary<string, GameObject> capsules = new Dictionary<string, GameObject>();

 //만일 관찰자가 하나라면 (캡슐이 하나라면) 다른 방식으로 구현해도 괜찮지만, 지금은
 //관찰자가 6명이기 때문에  (캡슐이 두 개고, 상태도  3개) 사전 형식으로 구성
 public Dictionary<GameObject, IReceiveInput> inputs = new();
 private GameObject currentCapsule;


 IEnumerator Start()
 {
  yield return new WaitForSecondsRealtime(0.5f);
  ChangeCapsule("Number_1");
 }

 public void AddObserver(string key, GameObject obj)
 {
  capsules.Add(key, obj);
 }

 public void AddInputObserver(GameObject obj, IReceiveInput input)
 {
  inputs[obj] = input;
  //inputs.Add(obj, input); 이 방식은 추가하는 것인데 우리는 덮어씌워야 함
  // 왜 ? input이 달라질 때마다 관찰자가 바뀌어야 하니까 ! 안그럼 input 즉 state가 달라질 때
  // 같은 키값을 사용해서 오류가 발생할 가능성이 있음
 }

 private bool ChangeCapsule(string key)
 {
  if (capsules.TryGetValue(key, out var capsule))
  {
   currentCapsule = capsule;
   return true;
  }

  return false;
 }



 // 자 자 이 밑에 두개는 ,이제 key 값과 bool 값을 받아와서 stateMachine에 전달
 //근데 내가 지금 하고 싶은건 ? state 그 자체에다가 notify 하고 싶은 것

 public void OnTriggered(string key, bool triggered)
 {

  // if(triggered && ChangeCapsule(key)) return;
  // if (!currentCapsule) return;
  // if (currentCapsule.TryGetComponent<CStateMachine>(out var stateMachine))
  // {
  //  stateMachine.OnTriggered(key, triggered);
  // }

  if (triggered && ChangeCapsule(key)) return; 
  if (!currentCapsule) return;
  if (ContainsState())
  {
     inputs[currentCapsule].OnTriggered(key, triggered);
  }
 }

 public void OnReadValue(string key, Vector2 value)
 {
  //  if (!currentCapsule) return;
  //  if (currentCapsule.TryGetComponent<CStateMachine>(out var stateMachine))
  //  {
  //   stateMachine.OnReadValue(key, value);
  //  }
  // }
  if (!currentCapsule) return;
  if (ContainsState())
  {
   inputs[currentCapsule].OnReadValue(key, value);
  }
 }

 private bool ContainsState()
  {
   return inputs.ContainsKey(currentCapsule) && 
          inputs[currentCapsule] != null;
  }
 }

 

1. 옵저버들을 관리할 새 Dictionary 만들기.

 

 왜 사전으로 만드나 ? 처음에 캡슐들을 관리할 때에는 tag창에서 넣어주었던 tag( string)으로 키 값을, 갖고 있는 객체를 value 값으로 넣어주었다. 그리고 ChangeCapsule에서 string으로 Key값 (tag , 이름) 을 넣어주면 그 key 값을 소유하고 있는 vlaue 값인 capsule을 현재 캡슐로 지정했다.

이 지정한 currentCapsule을 기반으로 currnetCapsule의 stateMachine을 찾아 데이터를 보내주었다.

 

다만 이제는 옵저버의 수가 늘어난 것 뿐만 아니라 , 2개의 객체도 옵저버이고, 또 각각 3개의 상태라는 옵저버가 존재한다.

그래서 상태에 직접적으로 데이터를 전달하기 위해 상태를 옵저버로 등록해줄 함수가 필요하다. 

상태는 3개이지만 똑같은 상태가 두 캡슐에 모두 존재한다.

따라서 캡슐에 따라 다른 상태를 관리해야한다. 

그렇기 때문에  Dictionary로 GameObject와 IReceiveInput을 등록해주어 변화를 관리한다.

 

주의할 점은 같은 키 값에 다른 데이터가 들어갈 수 있다는 것이다. 우리는 지금 객체 2개에서 상태가 3개씩 있기에

key 값인 GameObject를 상태 3개가 공유한다. 따라서 모든 옵저버를 추가하고 변환시켜 주는 것이 아닌

계속해서 state가 변할 때마다 옵저버를 추가하고 삭제하는 방식을 택해야 한다. 

 

2. CapsuleController (subject)에 state들을 옵저버로 등록할 수 있는 함수 생성

 

 위에서 말했듯 , 덮어쓰는 방식을 채택해야 한다. 

public void AddInputObserver(GameObject obj, IReceiveInput input)
 {
  inputs[obj] = input;
  //inputs.Add(obj, input); 이 방식은 추가하는 것인데 우리는 덮어씌워야 함
  // 왜 ? input이 달라질 때마다 관찰자가 바뀌어야 하니까 ! 안그럼 input 즉 state가 달라질 때
  // 같은 키값을 사용해서 오류가 발생할 가능성이 있음
 }

 

3, Ontrigger와 OnReadValue에서 예외처리 변경하기

 

옵저버가 변경되었으니, 데이터를 전달해줄 notifying 함수도 변화해야 한다. 

원래 했던 예외처리는 두 가지 였다. 

  • currentCapsule이 null일때 
  • currentCapsule이 변경되었을 때

위의 두 가지 경우가 아니면 InputManager에서 계속해서 받아오고 있는 데이터를 OnTriggered/OnReadValue에

전해주었다. 

 

이제는 inputs이 currentCasule을 Key값으로 들고 있고, 그 Key값에 매칭되는 Value값이 null이 아닌지 확인해야 한다.

 private bool ContainsState()
  {
   return inputs.ContainsKey(currentCapsule) && 
          inputs[currentCapsule] != null;
  }

 

 

4. 각각의 State들에서 2번에서 만든 함수로 스스로를 옵저버로 등록, 삭제하기

 

public void Enter()
{
   // BlackBoard.meshRenderer.material.color = Color.green;
   BlackBoard.color = Color.green;
   BlackBoard.textMesh.color = Color.green;
   BlackBoard.textMesh.text = "Walk";
   CapsuleController.Instance.AddInputObserver(FSM.gameObject, this);


}
public void Exit()
{
    jumpInputTriggered = false;
    moveInput = Vector2.zero;
    CapsuleController.Instance.AddInputObserver(FSM.gameObject, null);

}

 

 

사실 전통적인 옵저버 패턴이라기 보다는 조금은 변형된 느낌이다. 

 

 

 

 

 

 

코딩테스트 : 진료 순서 정하기

외과의사 머쓱이는 응급실에 온 환자의 응급도를 기준으로 진료 순서를 정하려고 합니다. 정수 배열 emergency가 매개변수로 주어질 때 응급도가 높은 순서대로 진료 순서를 정한 배열을 return하도록 solution 함수를 완성해주세요.

 

 

using System;

public class Solution
{

    public int[] solution(int[] emergency)
    {
        int[] answer = new int[] { };
        int index = 0;

        foreach (int i in emergency)
        {
            int counter = 0;
            for (int j = 0; j < emergency.Length; j++)
            {
                if (i >= emergency[j])
                {
                    counter++;
                }
            }
            answer[index] = counter;
            index++;
        }
        
        return answer;
    }
}

왜 안될까 ? >> i 를 순서대로 꺼내오는 것이 아니라 i 는 큰 숫자이기 때문에...

그럼 순서대로 꺼내와보자 !

using System;

public class Solution
{
    public int[] solution(int[] emergency)
    {
        int[] answer = new int[] { };
        int index = 0;

        for (int i = 0; i < emergency.Length; i++)
        {
            int counter = 0;
            for (int j = 0; j < emergency.Length; j++)
            {
                if (emergency[i] >= emergency[j])
                {
                    counter++;
                }
            }

            answer[index] = counter;
            index++;
        }

        return answer;
    }
}

 

똑같이 System.IndexOutOfRangeException: Index was outside the bounds of the array. 문제 발생

 

 

using System;

public class Solution
{
    public int[] solution(int[] emergency)
    {
        int[] answer = new int[emergency.Length]; { };
        int index = 0;
        for (int i = 0; i < emergency.Length; i++)
        {
            int counter = emergency.Length+1;
            for (int j = 0; j < emergency.Length; j++)
            {
                if (emergency[i] >= emergency[j])
                {
                    counter--;
                }
            }
            answer[index] = counter;
            index++;
        }
        return answer;
    }
}

emergency.Length로 처음에 길이를 정해주는 것으로 해결 ~ 

--로 바꾼 이유는 로직이 반대였기 때문 !

 

린큐로 하는 방법도 있다. 

 

나중에 린큐를 사용해서도 풀어볼 것 .

 

코딩테스트 : 모스 부호(1)

머쓱이는 친구에게 모스부호를 이용한 편지를 받았습니다. 그냥은 읽을 수 없어 이를 해독하는 프로그램을 만들려고 합니다. 문자열 letter가 매개변수로 주어질 때, letter를 영어 소문자로 바꾼 문자열을 return 하도록 solution 함수를 완성해보세요.
모스부호는 다음과 같습니다.

morse = { 
    '.-':'a','-...':'b','-.-.':'c','-..':'d','.':'e','..-.':'f',
    '--.':'g','....':'h','..':'i','.---':'j','-.-':'k','.-..':'l',
    '--':'m','-.':'n','---':'o','.--.':'p','--.-':'q','.-.':'r',
    '...':'s','-':'t','..-':'u','...-':'v','.--':'w','-..-':'x',
    '-.--':'y','--..':'z'
}

 

이것만 보고는 감이 안와서 GPT님한테 짤라달라했다. 

var morse = new Dictionary<string, char>
{
    { ".-", 'a' }, { "-...", 'b' }, { "-.-.", 'c' }, { "-..", 'd' }, { ".", 'e' },
    { "..-.", 'f' }, { "--.", 'g' }, { "....", 'h' }, { "..", 'i' }, { ".---", 'j' },
    { "-.-", 'k' }, { ".-..", 'l' }, { "--", 'm' }, { "-.", 'n' }, { "---", 'o' },
    { ".--.", 'p' }, { "--.-", 'q' }, { ".-.", 'r' }, { "...", 's' }, { "-", 't' },
    { "..-", 'u' }, { "...-", 'v' }, { ".--", 'w' }, { "-..-", 'x' }, { "-.--", 'y' },
    { "--..", 'z' }
};
using System;
using System.Collections.Generic;

public class Solution
{
    public string solution(string letter)
    {
        string answer = "";
        var morse = new Dictionary<string, char>
        {
            { ".-", 'a' }, { "-...", 'b' }, { "-.-.", 'c' }, { "-..", 'd' }, { ".", 'e' },
            { "..-.", 'f' }, { "--.", 'g' }, { "....", 'h' }, { "..", 'i' }, { ".---", 'j' },
            { "-.-", 'k' }, { ".-..", 'l' }, { "--", 'm' }, { "-.", 'n' }, { "---", 'o' },
            { ".--.", 'p' }, { "--.-", 'q' }, { ".-.", 'r' }, { "...", 's' }, { "-", 't' },
            { "..-", 'u' }, { "...-", 'v' }, { ".--", 'w' }, { "-..-", 'x' }, { "-.--", 'y' },
            { "--..", 'z' }
        };
        var answer1 = letter.Split(' ');
        for (int i = 0; i < answer1.Length; i++)
        {
            answer += morse[answer1[i]];
            
        }
        return answer;
    }
}

 

다만 밑에

{".-","-...","-.-.","-..",".","..-.","--.","....","..",".---",
"-.-",".-..","--","-.","---",".--.","--.-",".-.",
"...","-","..-","...-",".--","-..-","-.--","--.."}

배열이 따로 있어서 다른 방식 (abcd~를 배열로 만드는 방식) 으로도 풀 수 있을것 같지만 패스 ! 

노가다라서 패스 !

 

 

코딩테스트 : 구슬을 나누는 경우의 수

 

머쓱이는 구슬을 친구들에게 나누어주려고 합니다. 구슬은 모두 다르게 생겼습니다. 머쓱이가 갖고 있는 구슬의 개수 balls와 친구들에게 나누어 줄 구슬 개수 share이 매개변수로 주어질 때, balls개의 구슬 중 share개의 구슬을 고르는 가능한 모든 경우의 수를 return 하는 solution 함수를 완성해주세요.

공식이 나와 있어 공식대로 풀어봤다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class programers : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        solution(3, 2);
    }
    
    public int solution(int balls, int share)
    {
        int answer = 1;

        for (int i = 1; i <= balls; i++)
        {
            answer *= i;
        }
        Debug.Log(answer);
        for (int i = 1; i <= share; i++)
        {
            answer *= 1 / i;
            Debug.Log($"안에 있음 : {answer}");
        }
        for (int i = 1; i <= balls - share; i++)
        {
            answer *= 1 / i;
        }
        return answer;
    }
}

 

 

왜... 자꾸 0이 나와서 디버그를 찍어 봤는데

첫 번째 안에 들어갔을때 까지는 6이 잘 나오나 두번째 들어갔을 시점에서 0이 나와버린다. 

 

알고 보니 i가 int형이라  1/i가 항상 0으로 들어갔던 것,

using System;

public class Solution
{
    public int solution(int balls, int share)
    {
        float answer1 = 1;

        for (int i = 1; i <= balls; i++)
        {
            answer1 *= i;
        }

        for (float i = 1; i <= share; i++)
        {
            answer1 *= 1 / i;
        }

        for (float i = 1; i <= balls - share; i++)
        {
            answer1 *= 1 / i;
        }

       int answer = (int)(answer1);  
       return answer;
    }
}

다음과 같이 바꿔 주었는데 오류가 뜬다. 

 

문제의 이유는 또 int, float 값의 변환인 것 같다. 한 번에 곱하고, 나눠주는 방식이 문제가 있던 것. 

 

 

 

using System;

public class Solution {
    public int solution(int balls, int share)
    {
        long answer1 = 1L;
        int cnt = 1;
        while (cnt > share)
        {
            answer1 *= balls--;
            answer1 /= cnt++;
        }
        int answer = (int)answer1;
        return answer;
    }
}

 

다른 사람의 답변을 보고 바꿨다....

와 진짜 똑똑하다...

수학 공식을 조금만 더 생각해보고, 반복의 조건을 생각해보면 되었던 것.

 

노트에 적으면서 푸는 습관을 만들어봐야지. 

 

 

 

 

코딩테스트 : 중복된 문자 제거

문자열 my_string이 매개변수로 주어집니다. my_string에서 중복된 문자를 제거하고 하나의 문자만 남긴 문자열을 return하도록 solution 함수를 완성해주세요.

using System;
using System.Collections.Generic;

public class Solution {
    public string solution(string my_string)
    {
        List<char> my_chars = new List<char>();
        string answer = "";

        foreach (char letter in my_string)
        {
            if (!my_chars.Contains(letter))
            {
                answer += letter;
                my_chars.Add(letter);
            }
        }
        
        return answer;
    }
}

클리어 ~ 

 

TheRanger 리팩토링

 

하다가 버튼을 굳이 매니저로 관리해야 할까 ? 라는 생각이 들어서 외국 유튜브 영상들 보면서 다양한 매니저 클래스들을 

만드는 것을 봤다. 가장 이해가 잘 되었던 영상 하나를 소개하자면 

https://www.youtube.com/watch?v=rdX7nhH6jdM