본문 바로가기
전공/C# 프로그래밍

7강. 객체지향 프로그래밍과 클래스 (2)은닉성(캡슐화)

by 임 낭 만 2023. 4. 17.

객체지향 프로그래밍

은닉성 (캡슐화)

은닉성(캡슐화) 의미

감추고 싶은 것은 감추고, 보여주고 싶은 것만 보여준다.

  • 클래스의 사용자에게 필요한 최소의 기능만 노출하고 내부를 감추는 것
  • 예를 들어 선풍기를 생각해 보면,
    • 버튼 3개(바람세기 조절)와 다이얼 2개(회전과 타이머)를 사용자에게 제공
    • 선풍기 케이스 안에 회로와 배선 등은 사용자에게 감춤
    • 만약, 선풍기의 회로와 배선을 사용자가 조작하도록 노출한다면 문제 발생
  • 캡슐화가 잘 된 클래스
    • 클래스의 이름 자체에서 제공되는 기능을 대략 파악 가능
    • 외부로 제공해야 할 기능에 대해서만 노출

접근 제한자(한정자) (Access Modifier)

  • 감추고 싶은 것은 감추고, 보여주고 싶은 것은 보여주도록 코드를 수식
    • 클래스 안에 필드, 메소드, 프로퍼티 등 모든 요소에 사용 가능
    • C#에서는 총 여섯 가지 접근 제한자 제공

접근 한정자의 종류

접근한정자

  • 객체간의 상호 작용이 중심인 OOP에서는 각 객체는 다른 객체에게 자신의 내부 사정(필드, 메소드, 프로퍼티)을 공유하지 않음
  • 다른 객체에게 공유해야 하는 멤버만 접근한정자를 이용하여 공개
  • 접근한정자는 멤버 (필드, 메소드 등등)를 외부에 어떤 수준으로 공개할지 지정

접근 제한자 사용 형식

  • 어셈블리 (Assembly)
    • .NET 에서는 EXE 또는 DLL 형식의 C# 파일을 어셈블리라고 함
    • 이론 상 어셈블리는 하나 이상의 모듈로 구성 (모듈 하나당 한 개의 파일)
    • 일반적으로 1개의 (EXE/DLL 모듈) 파일로 구성된 어셈블리가 사용됨
  • 접근 제한자로 수식하지 않은 클래스의 멤버는 무조건 private 으로 자동 지정

using System;

namespace AccessModifier
{    
    class WaterHeater
    {
        protected int temperature;
        public void SetTemperature(int temperature)
        { // -5 ~ 42 사이의 값만 할당하고, 그 외의 값은 예외발생
            if (temperature < -5 || temperature > 42)
            {
                throw new Exception("Out of temperature range");
            }
            // temperature 필드는 클래스 내부에서 접근 가능
            this.temperature = temperature;
        }
        internal void TurnOnWater()
        {
            Console.WriteLine($"Turn on water : {temperature}");
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            try
            {
                WaterHeater heater = new WaterHeater();
                heater.SetTemperature(20);
                heater.TurnOnWater();

                heater.SetTemperature(-2);
                heater.TurnOnWater();

                // 42 값보다 큰 값이 인수로 사용, 예외발생
                heater.SetTemperature(50);
                heater.TurnOnWater();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

정보 은닉 (Information Hiding)

  • 외부에서 클래스의 멤버 변수에 직접 접근 불가
    • 특별한 이유를 제외하고는 필드를 public 으로 선언하지 않음
    • 접근이 필요할 때는 접근자(getter)/설정자(setter) 메소드 이용해 외부 접근을 관리

using System;

namespace Hiding
{
    class Circle
    {
        double pi = 3.14;

        public double GetPi()
        { // 접근자
            return pi;
        }
        public void SetPi(double value)
        { // 설정자
            if (value <= 3 || value >= 3.15)
            {
                Console.WriteLine("문제 발생");
            }
            pi = value;
        }
        // ...
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            Circle o = new Circle();
            o.SetPi(3.14159);
            o.SetPi(3.5); // 출력: 문제 발생
            double piValue = o.GetPi();
            Console.WriteLine($"piValue: {piValue}");
        }
    }
}

프로퍼티 (Property)

  • C# 에서는 접근자/설정자를 쉽게 정의하고 사용하도록 프로퍼티 문법 제공
    • 설정자 set 의 암묵적 매개변수로 “value” 예약어 사용

using System;

namespace Property
{    
    class BirthdayInfo
    {
        private string name;
        private DateTime birthday;

        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        public DateTime Birthday
        {
            get { return birthday; }
            set { birthday = value; }
        }
        public int Age  // 읽기 전용 프로퍼티
        {
            get { return new DateTime(DateTime.Now.Subtract(birthday).Ticks).Year; }
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            BirthdayInfo birth = new BirthdayInfo();
            birth.Name = "홍길동";
            birth.Birthday = new DateTime(1991, 6, 28);
            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birth : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

자동구현 프로퍼티

  • C# 3.0부터 단순히 필드를 읽고 쓰기만 할 때 자동구현 프로퍼티 사용 가능
    • C# 7.0 부터는 자동구현 프로퍼티 선언과 동시에 초기화 수행 가능

using System;

namespace AutoImplementedProperty
{
    class BirthdayInfo
    {
        public string Name { get; set; } = "Unknown";
        public DateTime Birthday { get; set; } = new DateTime(1, 1, 1);
        public int Age  // 읽기 전용 프로퍼티
        {
            get { return new DateTime(DateTime.Now.Subtract(Birthday).Ticks).Year; }
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            BirthdayInfo birth = new BirthdayInfo();
            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birth : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");

            birth.Name = "홍길동";
            birth.Birthday = new DateTime(1991, 6, 28);

            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birth : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

프로퍼티와 생성자

  • 객체를 생성할 때 프로퍼티를 이용해 각 필드를 초기화
    • <프로퍼티 = 값> 목록에 객체의 모든 프로퍼티가 올 필요는 없음
    • 초기화하고 싶은 프로퍼티만 넣어서 초기화

using System;

namespace ConstructorWithProperty
{
    class BirthdayInfo
    {
        public string Name { get; set; } = "Unknown";
        public DateTime Birthday { get; set; } = new DateTime(1, 1, 1);
        public int Age  // 읽기 전용 프로퍼티
        {
            get { return new DateTime(DateTime.Now.Subtract(Birthday).Ticks).Year; }
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            BirthdayInfo birth = new BirthdayInfo()
            {     
                Name = "홍길동",
                Birthday = new DateTime(1991, 6, 28)
            };
            
            Console.WriteLine($"Name : {birth.Name}");
            Console.WriteLine($"Birth : {birth.Birthday.ToShortDateString()}");
            Console.WriteLine($"Age : {birth.Age}");
        }
    }
}

초기화 전용 자동 구현 프로퍼티

  • 프로퍼티를 객체를 생성할 때 초기화 후, 중간에 변경 못 하도록 설정
    • 자동 구현 프로퍼티에서 set 키워드 대신에 init 키워드 사용

using System;

namespace InitOnly
{
    class Transaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
    }

    class MainApp
    {
        static void Main(string[] args)
        {            
            Transaction tr1 = new Transaction { From = "Alice", To = "Bob", Amount = 100 };
            Transaction tr2 = new Transaction { From = "Bob", To = "Charlie", Amount = 50 };
            Transaction tr3 = new Transaction { From = "Charlie", To = "Alice", Amount = 50 };            
	  //tr1.From = "Charlie"; 값 할당 시 컴파일 에러 발생
            Console.WriteLine($"{tr1.From, -10} -> {tr1.To,-10} : {tr1.Amount,-10}");
            Console.WriteLine($"{tr2.From,-10} -> {tr2.To,-10} : {tr2.Amount,-10}");
            Console.WriteLine($"{tr3.From,-10} -> {tr3.To,-10} : {tr3.Amount,-10}");
        }
    }
}

레코드 형식의 불변 객체

  • 클래스는 참조 형식이기 때문에 객체 사이의 필드 복사, 비교, 출력 등에 있어 추가적 구현 필요
  • record 키워드를 사용하는 레코드 형식 사용
    • 값을 담는 용도의 클래스의 역할
    • 컴파일 시 복사, 비교, 출력 등의 메서드 자동추가
    • 따라서, record 형식은 class + “기본 생성 코드”
  • 레코드 형식의 불변 객체

using System;

namespace Record
{
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }        
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = new RTransaction { From = "Bob", To = "Charlie", Amount = 50 };
            
            Console.WriteLine($"{tr1.From,-10} -> {tr1.To,-10} : {tr1.Amount,-10}");
            Console.WriteLine($"{tr2.From,-10} -> {tr2.To,-10} : {tr2.Amount,-10}");            
        }
    }
}

with를 이용한 레코드 복사

  • with 키워드를 사용해 두 record 객체 사이의 깊은 복사를 수행
    • 깊은 복사와 동시에 일부 필드 값 변경 가능
using System;

namespace WithExp
{
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
    }

    class MainAoo
    {
        static void Main(string[] args)
        {
            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = tr1 with { To = "Charlie" };     // with 사용을 통한 깊은 복사
            RTransaction tr3 = tr1 with { From = "Charlie", Amount = 50 };  // 일부 필드 값 수정

            Console.WriteLine($"{tr1.From,-10} -> {tr1.To,-10} : {tr1.Amount,-10}");
            Console.WriteLine($"{tr2.From,-10} -> {tr2.To,-10} : {tr2.Amount,-10}");
            Console.WriteLine($"{tr3.From,-10} -> {tr3.To,-10} : {tr3.Amount,-10}");
        }
    }
}

레코드 객체 비교하기

  • 클래스에서는 자신과 다른 객체를 비교하기 위해 Equals() 메소드 사용
    • 필드들을 일일이 비교하기 위해 Equals() 메소드 재정의 (override) 필요
    • 재정의 없이 Equals() 메소드 사용 시 객체의 참조 주소 값만 비교
  • 레코드 객체는 컴파일러가 Equals() 메소드를 자동으로 구현

using System;

namespace RecordComp
{
    class CTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }

        public override bool Equals(object obj)
        {
            CTransaction target = (CTransaction)obj;
            if (this.From == target.From && this.To == target.To && this.Amount == target.Amount)
                return true;
            else
                return false;
        }
    }
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            CTransaction trA = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            CTransaction trB = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            Console.WriteLine(trA.Equals(trB));

            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            Console.WriteLine(tr1.Equals(tr2));
        }
    }
}

using System;

namespace RecordComp
{
    class CTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
    }
    record RTransaction
    {
        public string From { get; init; }
        public string To { get; init; }
        public int Amount { get; init; }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            CTransaction trA = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            CTransaction trB = new CTransaction { From = "Alice", To = "Bob", Amount = 100 };
            Console.WriteLine(trA.Equals(trB));

            RTransaction tr1 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            RTransaction tr2 = new RTransaction { From = "Alice", To = "Bob", Amount = 100 };
            Console.WriteLine(tr1.Equals(tr2));
        }
    }
}

무명 형식

  • 형식의 선언과 동시에 객체를 할당
    • 객체를 만들고 다시는 그 형식을 사용하지 않을 때 사용
    • 무명 형식의 프로퍼티에 할당된 값은 변경 불가 (읽기만 가능)

using System;

namespace AnonymousType
{
    class MainApp
    {
        static void Main(string[] args)
        {
            var a = new { Name = "홍길동", Age = 123 };
            Console.WriteLine($"Name: {a.Name}, Age: {a.Age}");

            var b = new { Subject = "수학", Scores = new int[] { 90, 80, 70, 60 } };
            Console.Write($"Subject: {b.Subject}, Scores: ");
            
            foreach (var score in b.Scores)
                Console.Write($"{score} ");
            Console.WriteLine();
        }
    }
}

 

댓글