C# 중급 문법 (2) : 생성자와 캡슐화

안녕하세요! 지난번에는 C# 객체 지향 프로그래밍의 기본인 클래스, 추상화, 인스턴스화에 대해 알아보았습니다. 클래스라는 설계도로 객체를 만들고, 그 복잡한 내부를 숨기는 추상화의 중요성도 이해했죠.

이번 포스팅에서는 객체를 만들 때 필수적으로 사용되는 생성자(Constructor)와 객체의 데이터를 안전하게 보호하는 객체 지향의 핵심 원칙인 캡슐화(Encapsulation)에 대해 깊이 파고들어 보겠습니다. 이 두 가지 개념은 견고하고 유지보수가 쉬운 코드를 만드는 데 매우 중요합니다.


1. 생성자(Constructor): 객체의 '첫인상'을 결정하다! 🏗️

우리가 지난번에 Car 클래스를 만들고 new Car("아반떼", "흰색", 180);처럼 객체를 생성했었죠? 이때 new Car(...) 뒤에 나오는 것이 바로 생성자입니다.

생성자클래스의 인스턴스(객체)가 생성될 때 가장 먼저, 그리고 자동으로 실행되는 특별한 종류의 메서드입니다. 생성자의 주된 목적은 새로 생성될 객체의 초기 상태를 설정하고, 필요한 준비 작업을 수행하는 것입니다.

  • 생성자의 특징:
    • 클래스 이름과 동일: 생성자의 이름은 반드시 해당 클래스의 이름과 같아야 합니다.
    • 반환 타입 없음: 일반 메서드와 달리 void와 같은 반환 타입이 없습니다. (객체 자체를 반환하므로 명시하지 않음)
    • new 키워드와 함께 호출: new 연산자를 사용하여 객체를 생성할 때만 호출됩니다.
    • 오버로딩 가능: 여러 개의 생성자를 만들 수 있으며, 각각 다른 매개변수 목록을 가질 수 있습니다 (생성자 오버로딩).

예시 (Car 클래스에 다양한 생성자 추가):

public class Car
{
    public string Model { get; set; }
    public string Color { get; set; }
    public int MaxSpeed { get; private set; } // MaxSpeed는 외부에서 설정 못하게 private set 추가
    public int CurrentSpeed { get; private set; } // CurrentSpeed도 외부에서 직접 수정 못하게 private set 추가

    // 1. 기본 생성자 (매개변수가 없는 생성자):
    //    명시적으로 정의하지 않아도 컴파일러가 자동으로 생성해 줍니다.
    //    만약 다른 생성자를 정의하면 기본 생성자는 자동 생성되지 않으므로,
    //    필요하다면 직접 정의해야 합니다.
    public Car()
    {
        Model = "미정";
        Color = "은색";
        MaxSpeed = 150;
        CurrentSpeed = 0;
        Console.WriteLine("기본 설정의 자동차가 생성되었습니다.");
    }

    // 2. 매개변수가 있는 생성자:
    //    객체 생성 시 필요한 초기값을 받아 설정합니다.
    public Car(string model, string color, int maxSpeed)
    {
        Model = model;
        Color = color;
        MaxSpeed = maxSpeed;
        CurrentSpeed = 0; // 모든 새 자동차는 초기 속도가 0
        Console.WriteLine($"{color} {model} 자동차가 생성되었습니다. (최고 속도: {maxSpeed})");
    }

    // 3. 또 다른 매개변수가 있는 생성자 (생성자 오버로딩):
    //    같은 이름의 생성자지만 매개변수 타입이나 개수가 다릅니다.
    public Car(string model) : this(model, "흰색", 180) // 다른 생성자 호출 (코드 중복 방지)
    {
        Console.WriteLine($"{model} 기본 흰색 자동차가 생성되었습니다.");
    }

    // 메서드 (생략, 이전 예시와 동일)
    public void Accelerate(int amount)
    {
        CurrentSpeed += amount;
        if (CurrentSpeed > MaxSpeed) CurrentSpeed = MaxSpeed;
        Console.WriteLine($"{Color} {Model}이(가) {amount}만큼 가속하여 현재 속도: {CurrentSpeed}km/h");
    }

    public void Brake()
    {
        CurrentSpeed = 0;
        Console.WriteLine($"{Color} {Model}이(가) 정지했습니다.");
    }
}

생성자 사용 예시:

public class Program
{
    public static void Main(string[] args)
    {
        Car car1 = new Car(); // 기본 생성자 호출
        // 출력: 기본 설정의 자동차가 생성되었습니다.
        // car1.Model: "미정", car1.Color: "은색", car1.MaxSpeed: 150

        Car car2 = new Car("제네시스", "검정색", 250); // 매개변수 있는 생성자 호출
        // 출력: 검정색 제네시스 자동차가 생성되었습니다. (최고 속도: 250)
        // car2.Model: "제네시스", car2.Color: "검정색", car2.MaxSpeed: 250

        Car car3 = new Car("코나"); // 또 다른 매개변수 있는 생성자 호출
        // 출력: 흰색 코나 자동차가 생성되었습니다. (최고 속도: 180)
        // 출력: 코나 기본 흰색 자동차가 생성되었습니다.
        // car3.Model: "코나", car3.Color: "흰색", car3.MaxSpeed: 180

        car2.Accelerate(50);
    }
}

생성자를 통해 객체가 생성되는 시점에 필요한 초기값을 효과적으로 설정할 수 있습니다. 이는 객체의 '유효하지 않은' 상태를 방지하고, 항상 일관된 초기 상태를 보장하는 데 중요합니다.


2. 캡슐화(Encapsulation): 데이터를 안전하게 '포장'하다! 🎁

캡슐화는 객체 지향 프로그래밍의 두 번째 핵심 원칙으로, 관련 있는 데이터(속성)와 그 데이터를 다루는 메서드를 하나의 '캡슐'처럼 묶고, 외부에서는 이 '캡슐'의 내부 구현에 직접 접근하지 못하도록 보호하는 것을 의미합니다. 오직 **정의된 공개된 인터페이스(메서드)**를 통해서만 내부 데이터에 접근하고 변경할 수 있도록 하는 것이죠.

다시 자동차 비유로 돌아가 봅시다.

  • 자동차의 엔진, 변속기, 연료 탱크 등은 중요한 내부 부품입니다. 이 부품들은 외부에서 아무나 만지거나 조작할 수 없도록 '차체'라는 캡슐 안에 숨겨져 있습니다.
  • 우리는 운전석의 핸들, 액셀, 브레이크, 기어와 같은 *공개된 조작 장치(인터페이스)*를 통해서만 자동차의 내부 동작에 영향을 미칠 수 있습니다.

캡슐화의 목적:

  • 데이터 보호 (정보 은닉): 객체의 중요한 데이터가 외부에서 무분별하게 변경되는 것을 막아 데이터의 무결성(Integrity)을 유지합니다.
  • 유지보수성 향상: 내부 구현이 변경되더라도 외부에 노출된 인터페이스(메서드)만 동일하게 유지된다면, 해당 객체를 사용하는 외부 코드에 영향을 주지 않습니다.
  • 코드 응집도(Cohesion) 증가: 데이터와 관련 메서드가 한 곳에 묶여 있으므로, 코드의 가독성과 이해도가 높아집니다.

C#에서 캡슐화 구현: 접근 제한자(Access Modifiers)와 속성(Properties)

C#에서는 **접근 제한자(public, private, protected 등)**와 **속성(Property)**을 사용하여 캡슐화를 구현합니다.

  • private (가장 많이 사용): 해당 클래스 내부에서만 접근 가능합니다. 외부에서는 접근할 수 없습니다. (데이터 은닉에 사용)
  • public: 어디에서든 접근 가능합니다. (외부에 노출할 메서드나 속성에 사용)
  • protected: 해당 클래스와 이를 상속받은 자식 클래스에서만 접근 가능합니다.

캡슐화 예시 (Car 클래스 개선):

public class Car
{
    // 필드: private으로 선언하여 외부에서 직접 접근 불가
    private string _model; // 모델명
    private string _color; // 색상
    private int _maxSpeed; // 최고 속도
    private int _currentSpeed; // 현재 속도

    // 속성 (Property): 필드에 안전하게 접근하기 위한 '공개된 통로'
    // get: 필드 값을 읽을 때 사용
    // set: 필드 값을 쓸 때 사용 (여기서 유효성 검사 등 추가 로직 가능)
    public string Model
    {
        get { return _model; }
        set { _model = value; } // 'value'는 대입되는 값을 나타내는 키워드
    }

    public string Color
    {
        get { return _color; }
        set
        {
            // 색상 설정 시 유효성 검사 추가 (캡슐화의 장점)
            if (value == "빨강" || value == "파랑" || value == "초록" || value == "흰색" || value == "검정")
            {
                _color = value;
            }
            else
            {
                Console.WriteLine("유효하지 않은 색상입니다. 기본값으로 설정됩니다.");
                _color = "흰색"; // 유효하지 않으면 기본값으로
            }
        }
    }

    public int MaxSpeed
    {
        get { return _maxSpeed; }
        // set을 private으로 설정하여 외부에서는 최고 속도를 변경할 수 없게 함 (생성자에서만 설정 가능)
        private set
        {
            if (value > 0 && value <= 300) // 최고 속도 유효성 검사
            {
                _maxSpeed = value;
            }
            else
            {
                Console.WriteLine("유효하지 않은 최고 속도입니다. 기본값으로 설정됩니다.");
                _maxSpeed = 150;
            }
        }
    }

    public int CurrentSpeed
    {
        get { return _currentSpeed; }
        // set을 private으로 설정하여 외부에서 직접 현재 속도 변경 불가.
        // 오직 Accelerate()나 Brake() 같은 메서드를 통해서만 변경 가능.
        private set
        {
            _currentSpeed = value;
        }
    }

    // 생성자 (이전과 동일)
    public Car(string model, string color, int maxSpeed)
    {
        Model = model; // 속성의 set 접근자를 통해 값 할당
        Color = color; // 속성의 set 접근자를 통해 값 할당 (유효성 검사 적용)
        MaxSpeed = maxSpeed; // 속성의 private set 접근자를 통해 값 할당 (유효성 검사 적용)
        CurrentSpeed = 0;
        Console.WriteLine($"{Color} {Model} 자동차가 생성되었습니다. (최고 속도: {MaxSpeed})");
    }

    // 메서드 (이전과 동일)
    public void Accelerate(int amount)
    {
        CurrentSpeed += amount; // private set 접근자를 통해 내부에서만 변경
        if (CurrentSpeed > MaxSpeed) CurrentSpeed = MaxSpeed;
        Console.WriteLine($"{Color} {Model}이(가) {amount}만큼 가속하여 현재 속도: {CurrentSpeed}km/h");
    }

    public void Brake()
    {
        CurrentSpeed = 0; // private set 접근자를 통해 내부에서만 변경
        Console.WriteLine($"{Color} {Model}이(가) 정지했습니다.");
    }
}

캡슐화 적용 후 사용 예시:

public class Program
{
    public static void Main(string[] args)
    {
        Car myCar = new Car("아반떼", "빨강", 180);

        // 속성(Property)의 get 접근자를 통해 값 읽기
        Console.WriteLine($"내 차 모델: {myCar.Model}"); // 아반떼
        Console.WriteLine($"내 차 색상: {myCar.Color}"); // 빨강

        // 속성(Property)의 set 접근자를 통해 값 변경
        myCar.Color = "파랑"; // set 접근자 호출
        Console.WriteLine($"색상 변경 후: {myCar.Color}"); // 파랑

        myCar.Color = "보라"; // 유효하지 않은 색상 시도
        // 출력: 유효하지 않은 색상입니다. 기본값으로 설정됩니다.
        Console.WriteLine($"유효하지 않은 색상 시도 후: {myCar.Color}"); // 흰색 (기본값)

        // private set으로 설정된 속성은 외부에서 직접 변경 불가 (컴파일 에러 발생)
        // myCar.MaxSpeed = 200; // 에러!
        // myCar.CurrentSpeed = 50; // 에러!

        // 오직 메서드를 통해서만 내부 상태 변경
        myCar.Accelerate(30); // CurrentSpeed는 Accelerate 메서드를 통해서만 변경 가능
        myCar.Brake();        // CurrentSpeed는 Brake 메서드를 통해서만 변경 가능
    }
}

이처럼 캡슐화를 통해 MaxSpeed나 CurrentSpeed와 같은 중요한 데이터는 private set으로 보호되어 외부에서 직접 변경할 수 없게 됩니다. 오직 Accelerate()나 Brake()와 같은 메서드를 통해서만 안전하게 변경될 수 있도록 통제하는 것이죠. 또한 Color 속성처럼 set 접근자 내에서 데이터의 유효성을 검사하는 로직을 추가하여 데이터의 안정성을 더욱 높일 수 있습니다.


마무리하며: 객체 지향의 견고한 기둥, 생성자와 캡슐화! 🧱

 생성자는 객체가 태어나는 순간, 그 객체의 정체성과 초기 상태를 부여하는 중요한 역할을 합니다. 생성자를 통해 우리는 유효하고 일관된 상태의 객체를 만들 수 있습니다.

 캡슐화는 객체의 내부를 보호하고, 외부와의 상호작용을 통제하여 데이터의 무결성을 지키고 코드의 유지보수성을 향상시킵니다. 이는 객체 지향 프로그래밍의 가장 강력한 특징 중 하나이며, 잘 설계된 시스템의 기반이 됩니다.

 

 이 두 가지 개념을 잘 이해하고 활용하면, 여러분은 더욱 견고하고 확장 가능한 C# 코드를 작성할 수 있을 것입니다. 객체 지향의 재미와 실용성을 느끼셨기를 바랍니다!

생성자 오버로딩이나 캡슐화의 다양한 적용 방식에 대해 더 궁금한 점이 있으신가요? 언제든지 질문해주세요!