Araina’s Blog

Apply object-oriented principles: Abstraction in object-oriented programming 본문

Unity Learn 번역/Pathway: Junior Programmer

Apply object-oriented principles: Abstraction in object-oriented programming

Araina 2022. 1. 27. 20:28

출처

 

Abstraction in object-oriented programming - Unity Learn

In this tutorial, you’ll learn about the first pillar of object-oriented programming: Abstraction. By the end of this tutorial, you will be able to: Explain how abstraction is used to expose only necessary script components Expose only the important deta

learn.unity.com


 

 

1. 서언

이제 여러분은 기존의 코드를 사용해 작업하고 외부 기능(리펙토링(Refactoring)이라 불리는 프로세스)을 변경하지 않고 코드를 개선할 준비가 되었습니다. 리펙토링은 개발 과정에서 중요한 단계로, 이전에 작성된 지저분한 코드를 정리하고, 향후 프로젝트 개발 프로세스의 반복되는 과정에서 여러분의 코드가 사용될 방식을 재검토할 수 있는 단계입니다. 다시 말해, 개발 단계가 "지금은 잘 동작하는" 코드를 작성하는 거라면, 리펙토링 단게는 이를 "앞으로도 잘 작동할" 코드로 수정하는 것입니다.

지난 미션에서 객체 지향 프로그래밍의 개념에 대해 소개해드렸고, 주니어 프로그래머 패스웨이 전반에 걸쳐서 여러분은 모범 사레들 몇 가지를 구현해보셨습니다. 이제 여러분은 핵심 기둥들(추상화, 상속, 다향성 및 캡슐화)에 대해 깊이 있게 검토하고, 패러다임을 가이드 삼아 추상화를 시작으로 여러분의 코드를 효과적으로 리펙토링 해보겠습니다.

이번 마지막 미션을 수행하시면서, 이전 미션에서 구축하신 인벤토리 프로젝트를 계속 개선해나가실 것입니다.

 


 

2. 추상화란?

OOP(객체 지향 프로그래밍)의 1번째 기둥은 여러분의 코드를 깔끔하고, 프로그래머가 자신이든 다른 사람이든지 간에 사용하기 쉬운 코드로 유지하는 것입니다. 추상화는 다른 프로그래머가 보게 될 스크립트에서 복잡한 코드를 제거하고, 다른 프로그래머가 실질적으로 필요로 하는 기능들만 보여주는 것입니다. 여러분이 코드의 세부 사항들을 "추상화"하면, 중복되는 코드를 줄일 수 있고, 가장 유용한 기능들에 쉽게 접근할 수 있도록 만들 수 있습니다. 여러분은 이미 이 기둥에 대해 꽤나 익숙하실 겁니다. 이번 패스웨이를 통해 추상화의 이점을 자주 접하셨기 때문입니다. 손으로 직접 모든 코드를 작성하는 것 대신 작업을 수행하기 위한 메서드를 호출하실 때마다 여러분은 추상화의 이점을 누리고 있는 겁니다!

이전에 여러분이 추상화를 사용해본 사례를 하나 살펴보겠습니다. Create with Code Unit 4 - Gamplay Mechanics 튜토리얼에서 웨이브마다 적이 스폰되는 기능을 구현하셨습니다.

코드를 처음 작성하실 때, Start 메서드에 아래와 같이 코드를 기입하셨습니다.

private void Start()
{
    for (int i = 0; i < 3; i++)
    {
        Instantiate(enemyPrefab, GenerateSpawnPosition(),   
        enemyPrefab.transform.rotation);
    }
}

Start 메서드는 게임이 로드될 때 1번만 호출되므로, 다른 웨이브에서 적이 스폰되도록 만들고 싶으시다면, 위의 코드를 복사해서 스크립트 내에 원하는 위치에 붙여 넣으셔야 합니다. 동일한 코드를 계속해서 다시 작성(하거나 복사/붙여 넣기)하는 것은 비효율적이며 오류를 일으키기 쉽습니다. 또한 모든 스크립트를 검색하고 모든 레퍼런스를 다시 작성해야 하므로 추후에 리펙토링을 하기가 어려워집니다.

그 대신 여러분은 적 스폰 기능을 분리하여 독립적인 메서드로 만들어준 뒤, Start 함수에서 이 메서드를 호출하였습니다:

void SpawnEnemyWave() // create new higher-level method 
{
    for (int i = 0; i < 3; i++)
    {
        Instantiate(enemyPrefab, GenerateSpawnPosition(), 
        enemyPrefab.transform.rotation);
    }
}

private void Start()
{
    SpawnEnemyWave(); // call higher-level method in Start()
}

SpawnEnemyWave라는 이름으로 더 "추상화"된 더 높은 수준의 함수를 생성했습니다. 이렇게 해서 스크립트 상의 어디에서나 코드 1줄 만으로 적을 스폰할 수 있도록 만들었습니다. 그리고 추후에 이를 리펙토링 해야 하는 경우에는 한 곳에서만 리펙토링을 진행하면 됩니다. 또한, 여러분과 여러분의 동료 프로그래머 분들은 더 이상 어떻게 적들이 스폰되는지 세부 사항에 대해 걱정할 필요가 없습니다. 중요한 것은 여러분이 SpawnEnemyWave()를 호출하셨다면, 적들이 스폰된다는 더 추상적인 개념입니다.

추상화에 대한 경험은 여러분이 직접 만든 메서드에 한정된 것이 아닙니다. 사실 여러분은 유니티에 내장된 메서드를 호출하실 때마다 추상화의 이점을 누리고 있습니다:

rb.AddForce(Vector3.up * Random.Range(12, 16), ForceMode.Impulse);

RigidBody를 사용해 힘을 가하려면 물리 시스템과 상호작용하는 수많은 수학 계산이 필요합니다. 하지만 유니티는 "추상화"된 단순한 고차원 기능(AddForce)을 제공하며, 여러분은 단순하게 AddForce 메서드를 호출하고 여러분이 필요로 하는 매개변수들을 제공하기만 하면 됩니다. 프로젝트에서 RigidBody로 힘을 가하기 위해 유니티의 물리학까지 이해할 필요가 없는 것이죠!

 


 

3. 추상화와 스크립트 리펙토링

코드 리펙토링의 중요한 부분은 다른 프로그래머들이 코드와 상호작용하는 방식을 바꾸지 않고 기능을 개선하는 것임을 명심하세요. 추상화는 여기서 중요한 역할을 맡고 있습니다! 메서드 호출과 출력이 변경되지 않는 한, 메서드의 구성요소들을 다른 프로그래머 모르게 수정할 수 있습니다. 이는 즉, 여러분의 프로젝트 내의 다른 부분에서 에러가 발생할 위험 없이 여러분의 코드를 안전하게 리펙토링 할 수 있다는 것을 의미합니다.

아래의 간단한 예제 코드를 살펴봅시다:

int TripleResult (int inputNumber)
{
    int outputNumber = inputNumber + inputNumber + inputNumber;
    return outputNumber; 
}

위의 메서드에서 호출 스크립트는 매개변수로 정수형 값을 전달받으며, 그 값을 3배 곱한 값을 반환합니다. 반환 값을 얻는 접근법이 조금 비효율적이므로, 아래 코드와 같이 리펙토링 할 수 있습니다.

int TripleResult (int inputNumber)
{
    int outputNumber = inputNumber * 3;
    return outputNumber;
}

메서드의 내용이 변경되었음에도, 메서드를 호출하는 방법은 이전과 동일하며, 반환하는 값도 동일합니다. 그러므로 프로젝트 상에서 해당 메서드를 호출했던 코드들도 전부 문제없이 작동할 것입니다.

 


 

4. 프로젝트에서 추상화 구현하기

이제 방금 학습한 추상화 원칙을 인벤토리 프로젝트의 UserControl 스크립트를 리펙토링 하면서 직접 구현해보겠습니다. 지게차를 선택하고, 원하는 위치로 이동시키는 모든 기능들은 Update 메서드 안에 있습니다. 하지만 만약에, 다른 요소가 이러한 지게차 선택 및 이동 제어 기능을 사용할 수 있도록 만들고 싶다면 어떻게 해야 할까요? 이를 가능하게 만들기 위해 2가지 새로운 메서드를 사용하여 추상화해보겠습니다.

1. 필요한 경우, 이전 미션에서 사용하셨던 인벤토리 프로젝트를 실행합니다.

2. Scripts 폴더에서 UserControl 스크립트를 찾은 뒤 비주얼 스튜디오로 열어주세요.

3. Update 메서드 위에 반환이 없는 public 메서드, HandleSelection과 HandleAction 2개를 선언해주세요.

public void HandleSelection ()
{

}

public void HandleAction ()
{

}

HandleSelection 메서드는 사용자가 애플리케이션 내에 있는 지게차 중 하나를 마우스 좌 클릭했을 때 발생하는 모든 이벤트들을 관리할 것입니다. 마우스 좌측 버튼 클릭을 검사하는 (Input.GetMouseButtonDown(0)) 부분의 코드는 여전히 Update 메서드 내에서 동작해야 합니다만, 다른 나머지 코드들은 전부 새로 선언한 HandleSelection 메서드로 옮길 것입니다.

 

4. Update 메서드 내에서 if(Input.GetMouseButtonDown()) 조건문 안에 있는 코드들을 선택하고 HandleSelection 메서드 안으로 옮겨주세요.

public void HandleSelection()
{
    // start of code cut from GetMouseButtonDown(0) check
    var ray = GameCamera.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit))
    {
        // the collider could be children of the unit, so we make sure to check in the parent
        var unit = hit.collider.GetComponentInParent<Unit>();
        m_Selected = unit;


        // check if the hit object have a IUIInfoContent to display in the UI
        // if there is none, this will be null, so this will hid the panel if it was displayed
        var uiInfo = hit.collider.GetComponentInParent<UIMainScene.IUIInfoContent>();
        UIMainScene.Instance.SetNewInfoContent(uiInfo);
    }
    // end of code cut from GetMouseButtonDown(0) check
}

private void Update()
{
    // ...

    if (Input.GetMouseButtonDown(0))
    {
        // code cut from here  
    }
    
    // ... 
}

5. if(Input.GetmouseButtonDown()) 조건문 안에서 HandleSelection();으로 메서드를 호출해주세요.

if (Input.GetMouseButtonDown(0))
{
    HandleSelection(); // method now called from here   
}

이제 HandleAction() 메서드도 위 과정을 반복해 수정해줍시다.

6. Update 메서드 안에 else if (m_Selected != null && Input.GetMouseButtonDown(1)) 조건문을 찾으신 뒤, 안에 있는 코드들을 HandleAction() 메서드 안으로 옮겨주세요.

public void HandleAction()
{
    // start of code cut from GetMouseButtonDown(1) check
    var ray = GameCamera.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit))
    {
        var building = hit.collider.GetComponentInParent<Building>();

        if (building != null)
        {
            m_Selected.GoTo(building);
        }
        else
        {
            m_Selected.GoTo(hit.point);
        }
    }
    // end of code cut from GetMouseButtonDown(1) check
}


private void Update()
{
    // ...

    else if (m_Selected != null && Input.GetMouseButtonDown(1))
    {
        // code cut from here 
    }

    // ...
}

7. else if (m_Selected != null && Input.GetMouseButtonDown(1)) 조건문 안에서 HandleAction() 메서드를 호출해주세요.

else if (m_Selected != null && Input.GetMouseButtonDown(1))
{
    HandleAction(); // method now called from here  
}

8. 스크립트를 저장하시고, 유니티로 돌아와 테스트 플레이를 진행해주세요.

스크립트의 기능 자체는 변하지 않았으므로, 지게차를 마우스 좌 클릭하고 자원 더미에 우클릭하시면, 이전과 동일하게 지게차가 자원을 운반해야 합니다. 이전과 달라진 점은 여러분 또는 다른 프로그래머 분이 추후에 해당 기능이 필요하다면, 애플리케이션 내의 어디에서나 자유롭게 사용할 수 있게 되었다는 것입니다.

 


 

5. 정리

추상화는 여러분의 코드를 단순하게 만들고 깔끔하게 유지하여 프로그래머들이 더 쉽게 사용할 수 있도록 만들어줍니다. 추상화의 핵심 신조는 다른 프로그래머들에게는 불필요하고 복잡한 요소들을 숨기고, 코드가 의도한 대로 동작하도록 만드는 데 필요한 요소들만 보여주는 것입니다. 이는 복잡한 내부 작업들을 추상적이고 재사용할 수 있는 코드 조각으로 대체하는 것을 의미합니다. 이 원칙을 따르면 추후에 리펙토링 과정도 단순화할 수 있습니다. 이제 어떻게 인벤토리 프로젝트에 추상화를 적용하는지에 대해 배우셨으니, 여러분의 애플리케이션에도 추상화를 어떻게 적용할 수 있을지에 대해 고민해보시거나, 직접 시도해보세요!

 


 


수고하셨습니다!


Comments