C#

C# 객체지향 문법 [캡슐화]

devrabbit22 2025. 5. 13. 20:20

관련성이 있는 데이터와 그 데이터를 다루는 메서드를 객체 안에 구현하는 것이 일반적인 통념이고, 더 나아가서는 객체의 밖에서 알 필요가 없는 내부 멤버를 숨기기도 하는데, 이를 두고 캡슐화(encapsulation)라는 용어를 사용한다.

double pi = 3.14;
double GetAreaOfCircle(double radius)	//원의 넓이를 반환하는 함수
{
    return radius * radius * pi;
}

void Print(double value)
{
    Console.WriteLine(GetAreaOfCircle(value));
}
Print(10);	//출력 결과: 314

객체가 없어 파일로 구분해줄 수 있다.

ex) 모든 코드를 math.c라는 파일안에 넣어 둘 수 있다. 어찌 보면 파일로 구분한 것도 일종의 캡슐화라고 볼 수 있지만, 여기에는 다음과 같은 단점이 있다.

  • 변수 pi 파일 내의 모든 함수에서 접근할 수 있다. 다른 파일에서도 접근할 수 있게 되어 있다면 머든 파일의 함수에서 읽고 쓰는 것이 가능하게 된다.
  • 다른 파일에서 Print함수만 쓰도록 강제할 수 없다. 고의든 실수든 GetAreaOfCircle 함수를 써도 막을 방법이 없다.

이 같은 문제는 객체지향 프로그래밍에서 클래스를 통해 캡슐화하면 쉽게 해결된다.

class Circle
{
    double pi = 3.14;
    
    double GetArea(double radius)
    {
        return radius * radius * pi;
    }
    
    public void Print(double value)
    {
        Console.WriteLine(GetArea(value));
    }
}

멤버변수 pi와 멤버 메서드 GetArea에는 public 접근 제한자가 명시되어 있지 않으므로 외부에서 접근하려고 하면 컴파일 단계에서 오류가 발생한다.

Circle o = new Circle();
o.pi = 6.28;	//컴파일 오류 발생 : 접근 불가(inaccessible)
o.GetArea(10);	//컴파일 오류 발생 : 접근 불가(inaccessible)

o.Print(10);	//public 멤버이므로 호출 가능

캡슐화가 잘 된 클래스는 그것을 사용하는 입장에서도 편리하다는 장점이 있다. 

클래스의 이름 자체에서 이미 제공되는 기능을 대략 파악할 수 있고, 외부로 제공해야 할 기능에 대해서만 정확하게 public으로 노출한다.

함수가 블랙박스였던 것처럼 클래스 역시 객체의 역할을 추상화 한다.

접근 제한자

C#에서 접근 제한자(access modifier)와 관련된 예약어로는 private, protected, public, internal이 있고 이러한 접근 제한자가 적용되는 유형은 5가지 이다.

private 내부에서만 접근을 허용한다. 우리말로 '사설', '개인', '전용'이라고 상황에 따라 다르게 표현하기도 한다.
protected 내부에서의 접근과 함께 파생 클래스에서만 접근을 허용한다.
public 내부 및 파생 클래스에서의 접근뿐만 아니라 외부에서도 접근을 허용한다. 우리말로 보통  '공용'이라는 표현을 쓴다.
internal 동일한 어셈블리 내에서는 public에 준한 접근을 허용한다. 다른 어셈블리에서는 접근할 수 없다.
internal protected 동일 어셈블리 내에서 정의된 클래스이거나 다른 어셈블리라면 파생 클래스인 경우에 한해 접근을 허용한다.(protected internal로도 지정 가능), 즉 internal 또는 protected 조건이다.

- C# 7.2부터 private protected가 추가되 6개의 접근 제한자가 있다.

 

접근 제한자가 적용되는 곳은 클래스와 구조체, 인터페이스가 있고 그것들의 멤버까지 허용된다.  하지만, 각각에 대해 모든 접근 제한자가 적용될 수 있는 것은 아니고 상황에 따라 같은 클래스의 정의라도 허용되는 범위가 달라진다.

ex) 일반 클래스 정의는 public, internal만 사용될 수 있지만, 클래스 내부에 정의되는 또 다른 클래스(중첩 클래스)에는 5가지 접근 제한자를 모두 명시할 수 있다.

 

접근 제한자의 그 밖의 특징

명시되지 않은 경우에 기본적으로 갖게 되는 제한자가 정의되어 있다는 점이다.

ex) class 정의에서 접근 제한자를 생략한 경우 기본적으로 internal로 설정되는 반면, class 내부의 멤버에 대해서는 private으로 설정된다. 이로인해 public으로 명시하지 않은 멤버를 외부에서 사용할 수 없다.

 

접근 제한자를 잘못 설정해도 C# 컴파일러가 명시적으로 에러를 발생시키므로 곧바로 인식할 수 있다.

마지막으로, 객체지향 프로그래밍 관점에서 접근 제한자는 정보 은닉에 중요한 역할을 한다.

정보 은닉

객체지향에서 캡슐화를 다룰 때면 언제나 정보 은닉(information hiding)이라는 개념이 합께 등장한다.

클래스 입장에서 '정보;라고 불리는 것은 메머 변수를 일컫는데, 외부에서 이 멤버 변수에 직접 접근할 수 없게 만드는 것이 바로 정보 은닉에 해당한다. 

일반적으로 캡슐화를 잘 했다면 정보 은닉도 함께 지켜지는 것이 보통이지만 그 역은 성립하지 않을 수 있다.

온갖 잡다한 기능을 넣은 클래스에서 멤버 변수를 외부에 노출시키지 않는다면 정보 은닉이 성립하기 때문이다.

 

개발을 하다 보면 멤버 변수를 무조건 감추는 것이 능사는 아니다.

멤버 변수 자체가 클래스의 고유 특성을 반영하고 있다면 외부에서 그 변수의 값에 접근(access)할 필요가 있기 때문이다.

여기서 접근이라는 단어에는 읽기(read)와 쓰기(write)라는 두 가지 의미가 있는데, 필드에 읽고 쓰기가 적용될 때는 관례적으로 get과 set이라는 단어를 각각 사용한다.

멤버 변수에 대해 get/set 기능을 하는 메서드를 특별히 접근자 메서드(getter), 설정자 메서드(setter)라고 한다.

class Circle
{
    double pi = 3.14;
    public double GetPi()
    {
        return pi;
    }
    
    public void SetPi(double value)
    {
        pi = value;
    }
    //생략
}
Circle o = new Circle();
o.SetPi(3.14159);	//쓰기
double piValue = o.GetPi();	//읽기

멤버 변수를 반환하거나 설정하는 작업만 한다는 점을 제외하고는 접근자 메서드와 설정자 메서드의 구현은 일반 메서드의 구현과 비교해서 특별한 것이 없다.

이를 활용하면 외부에서 읽기만 가능(read-only)하도록 만들 수 있다. 즉,SetPi 메서드를 제거하기만 하면 된다.

class Circle()
{
    public double pi = 3.14;
    //이하 구현 생략
}
Circle o = new Circle();
o.pi = 3.14156;	//쓰기
double piValue = o.pi;	//읽기

오직 필드의 값을 읽고 쓰는 데만 관심이 있다면 굳이 접근자/설정자 메서드를 둘 필요가 없다. 

접근자/설정자 메서드가 나오게 된 이유 중 하나는 향후 코드에 대한 유지보수를 쉽게 하기 위해서이다. 

 

프로그램을 실행하다 어느 지점에 pi 값이 유효한 범위를 벗어나 이상하게 문제를 발견했다고 가정하면, 이때 설정자 메서드 없이 필드로 직접 노출했다면 그 필드를 사용하는 코드를 모조리 찾아야 하지만, 설정자 메서드를 정의해서 사용한다면 진단 목적의 코드를 넣어서 문제를 쉽게 발견할 수 있다.

class Circle
{
    double pi = 3.14;
    
    public void SetPi(double value)
    {
        if(value <= 3 || value >= 3.15)
        {
            Console.WriteLine("문제 발생");
        }
            pi = value;
    }
//생략
}
Circle o = new Circle();
o.SetPi(3.14159);
o.SetPi(3.5);	//출력 : 문제 발생

호출스택(call stack)을 구하는 방법이나 통합 개발 환경 등을 이용해 디버거(debugger)를 사용할 수 있는 때가 되면 설정자 메서드를 구현해 뒀을 때 훨씬 더 쉽게 문제를 파악할 수 있다.

 

정보 은닉의 원칙

  • 특별한 이유를 제외하고는 필드를 절대 public으로 선언하지 않는다.(그럴 만한 이유가 있는가?)
  • 접근이 필요할 때는 접근자/설정자 메서드를 만들어 외부에서 접근하는 경로를 클래스 개발자의 관리하에 둔다.

프로퍼티

접근자/설정자 메서드를 둬서 필드 접근엥 대한 단일 창구를 제공하는 것은 바람직하지만 호출을 위한 메서드 정의를 일일히 코드로 작성하기는 번거롭다. 이 같은 단점을 보완하기 위해 특별히 프로퍼티(property)라는 문법을 제공한다.

 

- 프로퍼티도 속성으로 번역되는데 이 경우 객체지향에서 말하는 속성과 혼동될 수 있다.

- 객체지항에서 말하는 속성(attribute)은 C#에서 필드(field)에 해당하고, C#의 속성(property)은 접근자/설정자 메서드에 대한 편리한 구문에 해당한다.

- 경우에 따라 C# 프로퍼티는 보통 public으로 되는 경우가 많아서 '공용 속성'이라고 구분해서 부르기도 한다.

 

프로퍼티 정의는 필드를 접근자/설정자 메서드로 정의하던 것과 비교하면 간단하다.

class 클래스_명
{
    접근_제한자 타입 프로퍼티_명
    {
        접근_제한자 get
        {
            // 코드
            return 프로퍼티의_타입과_일치하는_유형의_표현식;
        }
        접근 제한자 set
        {
            //value라는 문맥 키워드를 사용해 설정하려는 값을 표현
        }
    }
}

class 클래스의 pi 필드를 프로퍼티를 이용해 접근자/설정자 메서드를 대체 (프로퍼티 사용 예제)

class Circle
{
    double pi = 3.14;
    
    public double pi
    {
        get{return pi;}
        set{pi = value;}
    }
    
    //생략
}

Circle o = new Circle();
o.pi = 3.14159;	//쓰기
double piValue = o.pi;	//읽기

설정자 메서드는 사용자가 전달하는 값을 매개변수명으로 구분할 수 있지만 프로퍼티의 정의에서는 매개변수가 없으므로 C# 컴파일러가 프로퍼티에 대입되는 값을 가리킬 수 없다는 문제가 발생한다.

이 문제를 해결하기 위해 별도의 set 블록 내부에서만 사용할 수 있는 'value' 예약어가 도입된 것이다.

get/set과 프로퍼티를 사용하는 코드의 관계

읽기 전용 프로퍼티는 간단하게 set 블록의 코드만 제거하면 구현할 수 있다.

또한 get/set에도 접근 제한자를 지정할 수 있기 때문에 때로는 set을 없애지 않고 private으로 설정할 때도 있다.

클래스 내부에서는 해당 프로퍼티의 set 구문을 사용할 수 있으면서도 외부에서는 설정할 수 없기 때문에 적절한 캡슐화 수준을 유지할 수 있다.

마지막으로 프로퍼티는 메서드의 특수한 변형에 불과하다.

ex) Circle 클래스에서 정의한 Pi 프로퍼티는 C# 컴파일러가 빌드하는 시점에 자동으로 다음과 같은 메서드로 분리해서 컴파일한다.

double pi = 3.14;
public void set_Pi(double value)
{
    this.pi = value;
}

public double get_Pi()
{
    return this.pi;
}

또한 프로퍼티를 사용하는 코드도 그에 맞게 변경해서 빌드한다.

Circle o = new Circle();
o.set_Pi(3.14159);	//쓰기
double piValue = o.get_Pi();	//읽기

C#의 프로퍼티는 접근자/설정자 메서드를 간편하게 만들어주는 도우미 성격의 구문이다.

 

-외국 개발자들은 이를 두고 'sytactic sugar'라는 표현을 쓰기도 한다. 

-한글로 의역하자면  '간편 표기법' 정도이다.


Reference

시작하세요! C# 12 프로그래밍 기본 문법부터 실전 예제까지