[C#] C#으로 공부하는 객체지향 프로그래밍 2 - 객체지향의 4가지 특성 : 캡슐화




객체 지향은 4가지의 특성을 가지고 있다.

  1. 추상화 (Abstraction)
  2. 캡슐화 (Encapsulation)
  3. 상속 (Inheritance)
  4. 다형성 (Polymorphism)
이 중에서 캡슐화에 대해 먼저 알아보자.




캡슐화 (Encapsulation) 란?

말 그대로 캡슐로 싸서 내용물을 보호하고 확인할 수 없게 하는 것을 말한다. 감기약을 생각해보면, 캡슐은 가루가 흘러내리지 않고 몸 안에 제대로 스며들 수 있게 보호해주지만, 알약의 캡슐 안에 들어 있는 가루의 색이나 성분 등을 캡슐을 열기 전까진 알 수가 없다. 대신 우리는 캡슐을 열지 않아도 그 약이 감기를 낫게 해준다는 것을 알고 있다.
객체 지향에서의 캡슐화도 마찬가지다. 코드의 내용이 어떻게 이루어져있는지, 그 코드를 까보지 않고선 알 수 없지만, 굳이 알 필요 없이 그 코드가 어떤 역할을 하는 지가 중요한 것이다.

따라서 캡슐화는 객체 내 데이터에 대한 보안, 보호, 외부 접근 제한 등을 위한 것이며 이렇게 내부 데이터 접근을 제한하는 것을 정보 은닉화 (information hiding) 이라고 한다.

캡슐화를 사용하기 위해선 접근 지정자에 대해서 모르면 안될 것이다. 접근 지정자에 대해서도 알아보자.





접근 지정자

접근 지정은 타 객체가 해당 객체를 접근할 수 있는지 여부를 지정하는 것이다. C#의 접근 지정자에는 public, protected, private, internal, protected internal이 있다.


1. public
같은 클래스에 있든, 다른 클래스에 있든, 어느 패키지에 있든 간에 원하는 곳 어디든 제한 없이 사용 가능하게 해준다. 제한 없이 공개적으로 사용하게 하고 싶으면 사용하자. 유니티에서는 public을 사용할 경우 Inpector 창에서 public 변수를 코딩 없이 변경할 수 있다.


2. private
반대로 제한이 크게 걸린다. 오직 해당 클래스에서만 사용할 수 있다. 변경을 하면 위험할 경우에서 사용하도록 하자.


3. protected
2가지의 경우에서 허용한다.

  • 같은 패키지 내의 클래스끼리 접근이 가능하다.
  • 다른 패키지라도 상속받은 자식 클래스라면 접근이 가능하다.

4. internal
같은 프로젝트 내에서는 public과 같은 효과를 갖지만, 다른 프로젝트끼리에서는 private와 같은 효과를 갖게 된다. 즉, 같은 어셈블리 내에서 접근 가능하다.


5. protected internal
protected는 말 그대로 protected와 internal의 특징을 모두 가진 것인데, 가령 A라는 클래스가 있다면,

  • A 클래스
  • A 자식 클래스
  • 같은 어셈블리 안의 클래스
에서 접근이 가능하다.





그렇다면 캡슐화는 왜 사용할까?


사실 협업 프로젝트와 같은 경험이 부족한 본인은 캡슐화가 얼마나 중요한지 아직 크게 와닿진 않지만, 적어도 몇가지 경우에서는 꼭 필요하다고 느낀다.


1. 일관성이 생기게 된다.

가령 이런 코드가 있다.

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

public class Human : MonoBehaviour
{
    public string Hname;
    public int Hage;
    private int ID;

    public Human()
    {

    }

    public Human(string s, int i)
    {
        Hname = s;
        Hage = i;
    }
}

이름과 나이는 public으로 접근을 허용했지만 ID는 접근을 불허했다. 사실 이렇게 아주 짧은 코드는 한 눈에 보이기 때문에 큰 문제가 있지는 않다. 그런데 이런 경우도 생각해보자. (필자가 과거에 작성했던 코드의 일부이다.)


using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.Advertisements;
using System.Text.RegularExpressions;
using System.Collections;

public class sceneManager : MonoBehaviour
{

    // map
    public GameObject[ , ] minimap = new GameObject[30, 20];
    public GameObject[] Point = new GameObject[3];

    public bool[,] isUsed = new bool[30, 20];
    public bool[] DirectionKeyActivation = new bool[4];
    public bool[] isDirectionKey = new bool[4];
    public bool[] rotatebool = new bool[3];

    public string[] isSwitch = new string[3];
    public string[] StepName = new string[6];
    public Sprite[] StepSprite = new Sprite[10];

    public Sprite point1, point2, point3, point0;

    int[,] Direction = { { 0, 1 }, { 0, -1 }, { -1, 0 }, { 1, 0 } };

    public string stageName;
    public int stageLevel;
    public int dir, currPoint;
    static int RestartButtonClickCnt = 0;


    public bool isChanged = false;


    public int[,] objColor = new int[5, 3];
    public int[] StepSound = new int[3];

    public bool IsRestart;
    public bool IsUndo;
    //public bool IsLastClickedButton_Undo;
    public bool IsLastClickButton_Move;
    public Sprite VerticalBar, HorizontalBar, CrossBar;
    //public Stack<UndoItem> stack = new Stack<UndoItem>();

    private GameObject player, obj;
    private Vector3 playerpos, objpos;

    private static GameObject gameManager, soundManager;
    //public Sprite upSprite, DownSprite, LeftSprite, RightSprite, StepSprite;

    public List<GameObject> stepList = new List<GameObject>();

    .
    .
    .
    .
    .
    .
    .
}

다른 함수는 다 지웠고 변수만 가져왔는데, 어떤 변수는 Public을 쓰고, 어떤 변수는 private을 쓰고, 완전 난장판이다. 이렇게 될 경우 다른 클래스에서 해당 클래스의 변수에 접근을 하려고 했을 때, 이런 생각이 들 수가 있다.

'이건 public으로 접근할 수 있게 해놨으면서, 이건 왜 private으로 해놓은거지?'

차라리 아예 전부 접근을 허용하던가, 전부 하지 말던가, 개발자의 의도를 알 수 없게 된다. 그래서 그냥 전부 접근 제한을 걸어놓고 getter, setter 등으로 접근 허용을 하는게 좀 더 깔끔해보일 것이다.



2. 이상한 실수를 줄일 수 있다.

애당초 이런 실수가 나와선 안되지만, 사람이란게 실수를 할 수 밖에 없는 동물인지라, 종종 나오는 것 같다. 아래 코드를 보자.

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

public class Human : MonoBehaviour
{
    public string Hname;
    public int Hage;
    private int ID;

    public Human()
    {

    }

    public Human(string s, int i)
    {
        Hname = s;
        Hage = i;
    }
}


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

public class GameManager : MonoBehaviour
{
    private int ID;

    public void HumanInstantiate()
    {
        Vector2 humanVector = new Vector2(Random.Range(-3.0f, 3.0f), Random.Range(-9.0f, 9.0f));
        GameObject humanObject = Instantiate(Resources.Load("prefabs/human"), humanVector, Quaternion.identity) as GameObject;

        Human human = humanObject.GetComponent<Human>();
        human.Hname = "keykat"; human.Hage = 25;
        Debug.Log(human.Hname + ", " + human.Hage);

        ID = 10;
    }
}

2가지 클래스가 있는데, 둘 다 ID라는 변수를 가지고 있다. 만약 ID가 public일 경우에, 두 코드 모두 길이가 짧아서 헷갈릴 수가 없지만, 코드가 엄청 길어지게 된다면 슬슬 머릿속이 복잡해지면서 ID를 바꾸고자 할 때 아무 생각 없이 human.ID를 바꿔버릴 수 있다! 절대 이런 실수를 안 할 것 같지만, 이상하게 종종 하게 된다. 자신을 너무 믿지 말자. 따라서 변수는 접근을 제한하고, 해당 변수에 접근할 수 있는 방법을 따로 만드는게 좋을 것이다.



3. 수정 작업 등이 이루어졌을 때 작업량을 줄일 수 있게 된다.

만약 위의 코드에서 Hname이라는 변수를 Human_Name이라는 이름으로 바꾸고 싶다고 하자. GameManager 클래스도 human.Hname으로 접근하기 때문에 역시 human.Human_Name으로 바꿔줘야한다. 문제는 이렇게 수정해줘야하는게 100개 정도 있다고 생각해봐라. 다 바꿀 것인가? 그럴 필요 없이 SetName 혹은 GetName이라는 getter / setter 를 만들어서 해당 함수 내의 Hname만 바꿔준다면 상대적으로 쉽게 접근, 변경이 가능하게 된다.


그 밖에도 보안 등 여러가지 이유가 존재한다. 우리 선배들이 사용하는 데에는 이유가 있는 법이다. 그 분들의 기술을 잘 배워보자.










++ C# Property

C#에는 get, set에 대한 프로퍼티라는 게 존재한다. 이게 무엇이냐.

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

public class Human : MonoBehaviour
{
    private string Name;

    public void SetName(string s)
    {
        Name = s;
    }
    public string GetName()
    {
        return Name;
    }
}

Name에 대한 getter / setter를 구현했다. 작성하는게 조금 귀찮지만 그럭저럭 만족한다. 그런데 private 변수가 수십개일 때, 모두 getter / setter를 저렇게 구현할 것인가? 물론 그 수십개가 전부 getter / setter가 필요하진 않을 수 있겠지만, 어쨌든 귀찮은 작업인 것은 분명하다.
이 과정을 좀 더 간편하게 만들어 주는게 프로퍼티다.

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

public class Human : MonoBehaviour
{
    private string Name;
    public string hName
    {
        set { Name = value; }
        get { return Name; }
    }
}

코드가 좀 더 간결해진 것이 보인다. Name이란 변수에 접근하기 위한 hName을 선언한 후, 그 아래에 코드와 같이 set, get을 선언할 수 있다. 이 때 value는 말 그대로 받아온 값이다. 받아온 값을 Name에 적용해준다는 것이다. 위와 같은 get, set은 상황과 용도에 따라 본인이 원하는만큼 수정할 수 있다. 가령


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

public class Human : MonoBehaviour
{
    private string Name;
    public string hName
    {
        set { if(value == "keykat") Name = value; }
        get { return Name; }
    }
}

이런 식으로 조건도 달아줄 수 있다.

그런데 사람이란 게 언제나 좀 더 편한 걸 추구하는 동물이다. 이보다 더 간단한 방법이 있는데, 자동 완성 기능이다.


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

public class Human : MonoBehaviour
{
    public string Name { get; set; }
}

Name 변수를 public으로 지정한 후 get, set을 옆에 적어 읽기, 쓰기를 결정할 수 있다.
혹시 여기서 의문이 생길 수 있다.

'위의 경우 타 클래스에서 Name 변수를 자유롭게 수정하고 가져올 수 있는데, 그렇다면 그냥 public 변수를 썼을 때와 뭐가 다른 것인가?'

당연히 다르다. public 변수는 읽기 / 쓰기 전용이고, 위의 get / set을 사용한 Name 변수는 읽기 / 쓰기 허용 여부를 결정할 수 있다. 궁금하면 get이나 set을 하나 빼고 다른 클래스에서 Name 변수에 변경 및 읽기 접근을 시도해보자. 당연하지만 우리가 싫어하는 붉은 밑줄을 볼 수 있을 것이다.


댓글