Araina’s Blog

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

Unity Learn 번역/Pathway: Junior Programmer

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

Araina 2022. 2. 17. 23:01

출처

 

Encapsulation in object-oriented programming - Unity Learn

In this tutorial, you’ll learn about the second pillar in object-oriented programming: encapsulation. Explain how encapsulation is used to write code that can only be used as intended by the programmer Control access to data within a class by applying en

learn.unity.com


 

 

1. 서언

추상화처럼 캡슐화 또한 여러분의 코드와 해당 코드를 사용하는 다른 코드 사이의 근본적인 복잡성과 독립 수준을 유지하는 데 중점을 둡니다. 이러한 유사성 때문에, OOP 관행을 따르는 몇몇 개발자들이 추상화와 캡슐화를 캡슐화의 헤더 아래에 하나의 기둥으로 취급하는 것을 종종 보실 수 있으실 겁니다. 그러나, 여기서는 중요하다고 생각하는 부분을 구별하고 있습니다: 추상화는 다른 프로그래머들에게 더 쉽게 사용할 수 있도록 코드를 요약하는 것이지만, 캡슐화는 캡슐 안에 갇힌 것처럼 값과 데이터들을 보호하는 것이므로, 어느 데이터에 접근할 수 있고, 어느 데이터에 접근할 수 없는 지를 제어할 수 있습니다.

 


 

2. 캡슐화란?

캡슐화의 핵심 요소는 코드 안전성입니다. - 이는 즉, 코드가 의도한 대로만 사용되고, 여러분이 조작 중인 값과 데이터가 손상되지 않도록 보장하는 프로세스입니다. 캡슐화된 코드는 다른 프로그래머가 쉽게 변수의 값을 변경하거나 객체의 속성을 변경할 수 없습니다. 다른 스크립트가 여러분의 코드에 접근할 수 있는 모든 방법들을 설명하는 것은 불가능하므로, 작성한 코드를 캡슐화하여 의도한 대로만 동작할 수 있도록 만드는 것이 더 좋습니다.

Create with Code, Unit 1 - Player Control 튜토리얼로 돌아가서 캡슐화를 사용했던 상황을 살펴보겠습니다. 아래의 코드는 차량이 도로를 따라 이동할 때 사용된 변수들입니다.

public float speed = 20.0f;
public float turnSpeed = 45.0f;
public float horizontalInput;
public float forwardInput;

해당 코드를 처음 작성하셨을 때, 모든 변수의 접근 제한자를 public으로 설정하여 Play 모드가 실행되는 동안 Inspector 창을 통해 변수 값들이 어떻게 업데이트되는 지를 확인할 수 있었습니다. 당시에는 프로그래밍을 처음 하는 입장이었으니, 이러한 설정이 여러분의 변수 값 변화가 게임 플레이를 어떻게 변화시키는 지를 쉽게 이해하는 데 도움이 되었을 것입니다. 변수의 접근 제한자를 public으로 설정하면 speed와 turnSpeed 변숫값을 실시간으로 테스트해봄으로써, 자동차의 움직임을 미세하게 조정할 수도 있었습니다.

최종 수치를 설정한 후에는, 변수를 "고정"하는 의미에서 변수의 접근 제어자를 private로 설정했습니다.

// all variables now private instead of public
private float speed = 20.0f;
private float turnSpeed = 45.0f;
private float horizontalInput;
private float forwardInput;

더 이상 다른 사람들이 쉽게(아마 실수로) 여러분이 설정해둔 입력 값을 변경하지 않기를 원했습니다. 이는 간단하지만 여러분이 캡슐화를 경험해본 첫 사례입니다. 

변수 및 메서드의 접근성은 캡슐화의 주요 고려 사항입니다. 일반적인 규칙으로, 변수의 접근 제어자는 가급적 private로 유지하고, 메서드는 외부에서 호출할 것이 명확한 경우에만 public으로 설정합니다. 그렇지 않을 경우, 아무나 접근할 수 있을 테니까요.

혼자서 작업하는 작은 프로젝트를 진행하실 때는 캡슐화가 그다지 중요하지 않게 느껴질 수도 있습니다. 사실 모든 메서드와 변수들을 public으로 설정하는 게 편리해 보일 수도 있죠. 하지만, 만약 다른 사람들과 함께 작업을 하게 되거나, 다른 사람들이 여러분의 코드를 추후에 작업하게 될 때, 실제로 사용해서는 안 되는 변수와 메서드를 건드리면 예상치 못한 오류가 쉽게 발생할 수 있습니다.

 


 

3. private 변수와 Inspector 가시성

변수의 접근 제어자를 public으로 설정하면, Inspector 창에 항목이 노출됩니다. 하지만 이렇게 설정하면 애플리케이션 상의 다른 아무 메서드에서도 변수 값을 수정할 수 있게 됩니다. 다행히도 다른 코드에 변수를 노출시키지 않고도 직렬화(serialized)라고 알려진 기능을 사용하면 Inspector 창에 변수 값을 노출시킬 수 있습니다. 이렇게 하면, Inspector 창에서 변수 값을 자유롭게 수정할 수 있으면서도 변수를 안전하게 보호할 수 있습니다. 변수의 접근 제어자(public, private)는 실제로 직렬화를 제어하지 않지만, public 변수는 기본적으로 직렬화가 적용됩니다. 아래 코드처럼 [SerializeField] 태그를 추가하면 Inspector 창에 여러분의 private 변수를 간단하게 노출시킬 수 있습니다.

public class Rabbit : MonoBehaviour 
{
    [SerializeField] // exposes private variables in the inspector
    private Color furColor;
}

 


 

4. 프로젝트에서 취약한 public 변수 찾기

값의 오용을 막기 위해 모든 변수를 private로 만드는 것은 쉬울 것입니다. 하지만 정말로 public이 필요한 변수들은 어떻게 해야 할까요? 이 변수들은 어떻게 보호할 수 있을까요? 예제 프로젝트를 통해 실습해봅시다.

MainManager.cs에서 아래와 같이 public 변수를 추가합니다:

public static MainManager Instance;

이 변수는 private로 만들 수 없습니다. 해당 변수를 참조해야 하는 다른 스크립트들에서 에러가 발생할 것이기 때문입니다. 하지만 이 변수를 public으로 남겨둔다는 것은 잠재적인 오용에 취약하다는 것을 의미합니다. 해당 문제를 증명하기 위해 약간의 장난을 처보죠:

1. 실험을 위해, MenuUIHandler.cs 안에서 Start() 메서드 끝부분에 MainManager 인스턴스를 null로 설정하는 아래의 코드를 추가해주세요.

MainManager.Instance = null;

2. 색상 선택 버튼을 눌러서 애플리케이션이 제대로 동작하는지 테스트해보세요. 이제 Console 창에 Null Reference Exception 에러 문구가 떴을 것입니다.

변수의 접근 제어자를 public으로 계속 두면서도 이러한 오용을 막기 위한 방법이 필요합니다.

 


 

5. Getter, Setter로 속성 생성하기

근본적으로 우리가 하고 싶은 작업은 변수를 "읽기 전용"으로 만들어서 다른 클래스가 변수의 값을 가져올 수는 있지만, 값을 설정할 수는 없게 만드는 것입니다. 이를 위해서는 어떻게 변수를 사용할 수 있는지와 언제 변수를 사용할 수 있는지를 제어하는 C#의 get과 set 메서드를 사용해야 합니다.

1. MainManager.cs에서 MainaManager 인스턴스 변수 끝부분에 다음과 같이 접근자를 추가해주세요:

public static MainManager Instance { get; } // add getter to the end of the line

get 또는 set 접근자를 변수에 추가하는 즉시, 해당 변수는 속성(Property)이 됩니다. 속성은 특수 메서드를 통해 내부 데이터 접근을 제공하는 특별한 종류의 변수입니다.

2. set 접근자가 없으면, 해당 속성은 엄격하게 읽기 전용으로 고정됩니다. 즉, 해당 변수의 값은 어디에서도 설정할 수 없다는 것입니다. 그러므로 이전에 MenuUIHandler.cs에 추가한 장난스러운 코드에서 에러가 발생합니다. 좋아요. 이로써 다른 클래스에서 해당 변수를 재설정할 수 없도록 만들었으므로, 이제 해당 코드를 삭제해도 됩니다.

// MainManager.Instance = null; - delete or comment out this line of code

3. 하지만, 아직 문제점이 남아 있습니다: 속성이 엄격하게 읽기 전용으로 고정되어 있어서 MainManager 클래스에서도 변수 값을 설정할 수가 없습니다. 현재 IDE를 보시면 MainaManaver.cs 안의 Instance = this; 코드 부분에서 오류가 발생하고 있습니다. 이를 수정하기 위해서는 private setter를 속성으로 추가해주어야 합니다. 그럼 이제 에러가 해결될 것입니다:

public static MainManager Instance { get; private set; } // add private setter

위 코드를 사용하면 이제 속성의 값을 자체 클래스 내에서 설정할 수 있습니다. 하지만 외부에 있는 다른 클래스에서는 값을 읽을 수만 있습니다. 이는 오직 자체 클래스에서만 값을 수정할 수 있도록 만들어서 외부 세계로부터의 오용과 손상으로부터 안전하도록 캡슐화된 코드입니다!

 


 

6. setter 타당성 검증의 필요성 파악하기

위에서 여러분이 추가하신 getter와 setter는 캡슐화의 가장 기본적인 형식입니다. 이는 단순히 값을 가져오거나 설정하는 역할을 합니다. 특별한 것 없이 정말 이게 끝입니다. 이 단순한 구현을 자동으로 구현된 속성(auto-implemented property)이라고 부릅니다.

{ get; private set; }

하지만 값을 설정할 수 있는 곳을 제한하는 것 외에도, 보다 사용자 정의된 수동 방식으로 값을 설정할 수 있는 방법을 제한할 수도 있습니다. 예를 들어, 애플리케이션에서 다른 클래스가 날짜를 설정할 수 있도록 만든다고 가정할 때, 여러분은 월을 1에서 12 사이의 숫자만 사용할 수 있도록 제한할 수 있습니다. 또는 음수를 사용할 수 없도록 제한할 수도 있습니다.

여러분의 창고 프로젝트에서는 이를 정확히 써먹을 수 있는 곳이 있습니다: ProductionSpeed 변수는 절대로 음수 값이 들어가서는 안 되는 변수죠.

1. ResourcePile.cs로 가셔서, public ProductionSpeed 변수를 찾아주세요.

public float ProductionSpeed = 0.5f;

ProductivityUnit.cs에서 상수를 곱해주었던 public 변수도 함께 찾아주세요.

m_CurrentPile.ProductionSpeed *= ProductivityMultiplier;

이 코드에는 우리, 미래의 자기 자신, 또는 다른 어떤 느긋한 개발자가 변수에 음수 값을 넣어버려서 프로젝트에 문제가 발생할 수 있는 위험을 막아줄 수 있는 방법이 아무것도 없습니다. 

2. 실험을 목적으로 ProductionSpeed 변수에 값을 곱하는 것 대신, 값을 빼도록 코드를 의도적으로 바꿔주세요.

m_CurrentPile.ProductionSpeed -= ProductivityMultiplier; // replace '*=' with '-='

이제 애플리케이션을 테스트해보면 생산력 유닛이 음수 값 결과를 만들어낼 것입니다.

우리에게 필요한 것은 변수 값이 음수로 설정될 수 없도록 만들어주는 커스텀 setter입니다.

 


 

7. backing 필드로 속성 생성하기

getter 또는 setter 내에서 유효성을 검증 또는 계산하려면, 속성에 대한 backing 필드가 필요합니다: backing 필드는 public 속성에 의해 노출된 데이터를 저장하는 private 필드(변수)입니다.

이제 맥락을 살펴봅시다.

1. ResourcePile.cs에서 ProductionSpeed와 이름이 동일한 private 멤버 변수(backing 필드)를 추가하되, 새로 생성한 private 필드 안에 값을 대신 저장해주세요.

private float m_ProductionSpeed = 0.5f; // add new private backing field
public float ProductionSpeed; // remove value from public property

2. backing 필드가 선언되면, 이제 수동으로 누군가가 public 속성에 접근할 때 backing 필드를 검색하거나 할당하는 get과 set 함수를 추가할 수 있습니다. 

private float m_ProductionSpeed = 0.5f;
public float ProductionSpeed // delete semicolon
{
    get { return m_ProductionSpeed; } // getter returns backing field
    set { m_ProductionSpeed = value; } // setter uses backing field
}

이는 단순한 get, set 메서드가 아니기 때문에, 이전처럼 자동으로 구현된 속성과 함께 사용했던 "{get; set;}" 축약형을 쓸 수 없습니다.

그 대신, 수동으로 작업해야 합니다. 누군가가 ProductionSpeed 속성의 값을 가져오면, backing 필드의 값이 반환될 것입니다. 누군가가 ProductionSpeed 속성의 값을 설정하면, backing 필드가 입력된 값으로 설정됩니다.

3. 이제 데이터가 public 필드 대신 backing 필드에 저장되므로, 속성에 대한 참조를 private 필드로 변경해야 합니다. ResourcePile.cs에서 ProductionSpeed의 두 참조를 m_ProductonSpeed로 변경해주세요.

...
m_CurrentProduction += m_ProductionSpeed * Time.deltaTime; // swap in m_ version
...
return $"Producing at the speed of {m_ProductionSpeed}/s"; // swap in m_ version

이제 누군가가 public 속성의 값을 가져오거나 값을 설정하더라도, getter와 setter를 통해 클래스 내부에서 캡슐화된 backing 필드에 접근합니다. 그러나, 아직은 실질적인 setter 유효성 검사를 구현하지 못했으므로, 음수 값이 여전히 사용될 수 있습니다! 이제 이 문제를 고쳐보도록 하겠습니다.

 


 

8. 음수 값 차단으로 setter 타당성 검증 구현하기

마지막으로 모든 구성이 완료되면, 이제 set 메서드 내부에 유효성 검사 기능을 구현하여 ProductionSpeed 변수 값을 항상 양수 값이 되도록 보장할 수 있습니다.

1. set 메서드 내에서 if-else 조건문을 추가하여 값이 음수일 경우에는 경고 메시지를 출력하고, 값이 양수일 경우에는 변수에 해당 값을 저장하도록 만들어주세요.

set
{
    if (value < 0.0f)
    {
            
            Debug.LogError("You can't set a negative production speed!");
    }
    else
    {
            m_ProductionSpeed = value; // original setter now in if/else statement
    }
}

2. 애플리케이션을 다시 테스트해주시고 ProductionSpeed 변수 값이 음수일 경우 어떤 일이 발생하는지 확인해주세요. 아래 사진과 같이 Console 창에 에러 메시지가 정상적으로 출력되어야 합니다.

3. setter 유효성 검사가 제대로 구현되었고 테스트까지 완료되었다면, 이제 빼기 연산자 대신 곱셈 연산자를 사용하여 정상적으로 동작하도록 ProductivityUnit을 반환할 수 있습니다.

m_CurrentPile.ProductionSpeed *= ProductivityMultiplier; // revert '-=' to '*='

 


 

9. 정리

캡슐화는 값에 접근하고 변경하는 방식을 제어하여 코드를 보호하므로, 여러분의 코드가 의도된 대로만 동작하도록 만들어줍니다. 추상화처럼, 캡슐화도 코드의 기본적인 복잡성과 코드에 접근할 수도 있는 프로그래머들 사이에 분리된 계층을 배치해줍니다. 

프로젝트 내에서 여러분은 자동으로 구현된 속성을 사용하여 외부 클래스들이 변수를 읽기 전용으로 사용할 수 있도록 만들었습니다. 그런 다음, 음수 값이 설정되는 것을 막기 위해 backing 필드가 있는 속성에 대한 getter와 setter를 작성하였습니다. 이제 두 데이터 조각들 모두 캡슐화되어 장난이나 오용으로부터 안전하다는 것을 알게 되셨으니, 밤에 두 발 뻗고 푹 주무실 수 있으실 겁니다.

 


 


수고하셨습니다!


Comments