콘텐츠로 이동

9장. 클래스와 객체

8장까지 배운 내용으로 기능별로 코드를 메서드로 나누어 관리할 수 있게 되었습니다. 하지만 실제 프로그램에서는 관련된 데이터와 기능을 하나로 묶어서 관리하면 더욱 편리합니다. 예를 들어, 학생의 이름, 나이, 성적을 따로 변수로 관리하는 것보다 "학생"이라는 하나의 단위로 묶어서 다루는 것이 자연스럽습니다.

이것이 바로 **객체지향 프로그래밍(Object-Oriented Programming, OOP)**의 핵심 개념입니다. **클래스(Class)**는 객체를 만들기 위한 설계도이고, **객체(Object)**는 클래스를 기반으로 만들어진 실체입니다.

이 장에서 배울 내용

  • 클래스 정의하기 (필드, 속성, 메서드, 생성자)
  • 객체 생성하고 사용하기
  • 접근 제한자로 정보 보호하기
  • 정적 멤버 이해하기

9.1 클래스 정의

클래스는 객체를 생성하기 위한 틀(Template) 또는 청사진(Blueprint)입니다. 현실 세계의 사물이나 개념을 프로그래밍 언어로 모델링한 것으로, 데이터(필드, 속성)와 동작(메서드)을 하나로 묶어 정의합니다. 이는 데이터와 함수가 분리되어 있던 절차적 프로그래밍과의 근본적인 차이점입니다.

클래스와 객체의 관계:

클래스는 "설계도"이고, 객체는 그 설계도를 바탕으로 만들어진 "실제 제품"입니다. 예를 들어: - 클래스: "자동차" 설계도 (바퀴 4개, 엔진 1개, 색상, 주행() 메서드 등) - 객체: 빨간색 아반떼, 파란색 소나타 (실제로 메모리에 존재하는 자동차 인스턴스)

하나의 클래스로부터 여러 개의 객체를 생성할 수 있으며, 각 객체는 독립적인 데이터를 가지지만 같은 구조와 동작을 공유합니다.

클래스의 기본 구조:

class 클래스이름
{
    // 필드 (Fields): 객체의 상태를 저장하는 변수

    // 속성 (Properties): 필드에 대한 캡슐화된 접근

    // 생성자 (Constructors): 객체를 초기화하는 특별한 메서드

    // 메서드 (Methods): 객체의 동작을 정의하는 함수
}

간단한 클래스 예제:

// Person 클래스 정의
class Person
{
    // 필드: 객체의 데이터
    public string Name;
    public int Age;

    // 메서드: 객체의 동작
    public void Introduce()
    {
        Console.WriteLine($"안녕하세요, 제 이름은 {Name}이고 {Age}살입니다.");
    }
}

// 객체 생성 및 사용
Person person1 = new Person();
person1.Name = "홍길동";
person1.Age = 25;
person1.Introduce();
// 출력: 안녕하세요, 제 이름은 홍길동이고 25살입니다.

// 다른 객체 생성
Person person2 = new Person();
person2.Name = "김철수";
person2.Age = 30;
person2.Introduce();
// 출력: 안녕하세요, 제 이름은 김철수이고 30살입니다.

명명 규칙과 컨벤션:

C#에서 클래스와 그 멤버들은 다음과 같은 명명 규칙을 따릅니다: - 클래스 이름: PascalCase (각 단어의 첫 글자 대문자) - Person, BankAccount, ShoppingCart - 필드: camelCase (첫 글자 소문자) 또는 _camelCase (언더스코어 + 소문자) - 속성: PascalCase - Name, Age, FirstName - 메서드: PascalCase - Introduce(), CalculateTotal(), Withdraw() - 매개변수: camelCase - userName, accountNumber

9.1.1 필드와 속성

필드(Field)는 클래스 내부에 선언된 변수로, 객체의 상태(State)를 저장합니다. 속성(Property)은 필드에 대한 캡슐화된 접근 방법을 제공하여, 데이터의 유효성 검증이나 읽기 전용/쓰기 전용 제어를 가능하게 합니다.

필드(Field):

필드는 클래스 레벨에서 선언된 변수로, 객체의 데이터를 직접 저장합니다. 각 객체 인스턴스는 자신만의 독립적인 필드 복사본을 가집니다.

class BankAccount
{
    // 필드 선언
    public string accountNumber;
    public string owner;
    public double balance;
}

// 사용 예제
BankAccount account = new BankAccount();
account.accountNumber = "123-456-789";
account.owner = "홍길동";
account.balance = 10000.0;

Console.WriteLine($"계좌: {account.accountNumber}");
Console.WriteLine($"소유자: {account.owner}");
Console.WriteLine($"잔액: {account.balance}원");

필드의 문제점과 캡슐화:

필드를 public으로 직접 노출하면 다음과 같은 문제가 발생할 수 있습니다:

class BankAccount
{
    public double balance;  // 문제: 직접 접근 가능
}

BankAccount account = new BankAccount();
account.balance = -1000;  // ❌ 음수 잔액이 허용됨 - 데이터 무결성 위반!

이러한 문제를 해결하기 위해 속성(Property)을 사용합니다.

속성(Property):

속성은 필드에 대한 캡슐화된 접근을 제공하는 특별한 멤버입니다. get 접근자(Accessor)는 값을 읽을 때, set 접근자는 값을 쓸 때 호출됩니다. 이를 통해 데이터 검증, 계산, 읽기 전용/쓰기 전용 제어 등을 구현할 수 있습니다.

전체 속성(Full Property) 구문:

class BankAccount
{
    // private 필드 (백킹 필드, Backing Field)
    private double balance;

    // public 속성
    public double Balance
    {
        get 
        { 
            return balance; 
        }
        set 
        { 
            // 유효성 검증: 음수 잔액 방지
            if (value >= 0)
            {
                balance = value;
            }
            else
            {
                Console.WriteLine("잔액은 음수가 될 수 없습니다.");
            }
        }
    }
}

// 사용
BankAccount account = new BankAccount();
account.Balance = 10000;  // set 접근자 호출
Console.WriteLine($"잔액: {account.Balance}원");  // get 접근자 호출

account.Balance = -500;  // 출력: 잔액은 음수가 될 수 없습니다.

자동 구현 속성(Auto-Implemented Property):

단순히 필드를 읽고 쓰기만 하는 경우, C# 3.0부터 도입된 자동 구현 속성을 사용하면 코드를 간결하게 작성할 수 있습니다:

class Person
{
    // 자동 구현 속성 - 컴파일러가 자동으로 백킹 필드 생성
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

// 사용
Person person = new Person();
person.Name = "홍길동";
person.Age = 25;
Console.WriteLine($"{person.Name}, {person.Age}세");

읽기 전용 속성과 초기화:

class Book
{
    // 읽기 전용 속성 (get만 있음)
    public string Title { get; }
    public string Author { get; }

    // 생성자에서만 초기화 가능
    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }
}

Book book = new Book("C# 프로그래밍", "홍길동");
Console.WriteLine($"{book.Title} - {book.Author}");
// book.Title = "다른 제목";  // ❌ 컴파일 오류: set 접근자 없음

계산 속성(Computed Property):

속성은 저장된 값을 반환하는 것이 아니라, 계산된 값을 반환할 수도 있습니다:

class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    // 계산 속성: 넓이는 저장하지 않고 계산
    public double Area
    {
        get { return Width * Height; }
    }

    // C# 6.0 식 본문 속성 (Expression-bodied Property)
    public double Perimeter => 2 * (Width + Height);
}

Rectangle rect = new Rectangle();
rect.Width = 5;
rect.Height = 3;
Console.WriteLine($"넓이: {rect.Area}");      // 출력: 넓이: 15
Console.WriteLine($"둘레: {rect.Perimeter}"); // 출력: 둘레: 16

속성 초기화 구문(C# 6.0+):

class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; } = 0;  // 기본값 초기화
    public DateTime CreatedDate { get; } = DateTime.Now;  // 읽기 전용 + 초기화
}

9.1.2 메서드

메서드(Method)는 클래스 내부에 정의된 함수로, 객체의 동작(Behavior)을 나타냅니다. 7장에서 학습한 메서드의 모든 개념이 클래스 메서드에도 적용됩니다.

class Calculator
{
    // 메서드: 두 수를 더함
    public int Add(int a, int b)
    {
        return a + b;
    }

    // 메서드: 두 수를 뺌
    public int Subtract(int a, int b)
    {
        return a - b;
    }

    // 메서드: 두 수를 곱함
    public int Multiply(int a, int b)
    {
        return a * b;
    }
}

// 사용
Calculator calc = new Calculator();
Console.WriteLine($"10 + 5 = {calc.Add(10, 5)}");
Console.WriteLine($"10 - 5 = {calc.Subtract(10, 5)}");
Console.WriteLine($"10 × 5 = {calc.Multiply(10, 5)}");

인스턴스 메서드와 객체 상태:

인스턴스 메서드는 객체의 필드와 속성에 접근하여 객체의 상태를 읽거나 변경할 수 있습니다:

class BankAccount
{
    private double balance;

    public double Balance
    {
        get { return balance; }
        private set { balance = value; }
    }

    // 입금 메서드
    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            balance += amount;
            Console.WriteLine($"{amount}원이 입금되었습니다. 잔액: {balance}원");
        }
    }

    // 출금 메서드
    public bool Withdraw(double amount)
    {
        if (amount > 0 && balance >= amount)
        {
            balance -= amount;
            Console.WriteLine($"{amount}원이 출금되었습니다. 잔액: {balance}원");
            return true;
        }
        else
        {
            Console.WriteLine("출금 실패: 잔액이 부족합니다.");
            return false;
        }
    }
}

// 사용
BankAccount account = new BankAccount();
account.Deposit(10000);   // 출력: 10000원이 입금되었습니다. 잔액: 10000원
account.Withdraw(3000);   // 출력: 3000원이 출금되었습니다. 잔액: 7000원
account.Withdraw(10000);  // 출력: 출금 실패: 잔액이 부족합니다.

this 키워드:

this 키워드는 현재 객체 인스턴스를 참조합니다. 주로 매개변수와 필드의 이름이 같을 때 구분하기 위해 사용됩니다:

class Person
{
    private string name;
    private int age;

    public void SetInfo(string name, int age)
    {
        this.name = name;  // this.name은 필드, name은 매개변수
        this.age = age;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"{this.name}, {this.age}세");
    }
}

9.1.3 생성자

생성자(Constructor)는 객체가 생성될 때 자동으로 호출되는 특별한 메서드로, 객체를 초기화하는 역할을 합니다. 생성자는 클래스와 같은 이름을 가지며, 반환 타입이 없습니다.

기본 생성자:

class Person
{
    public string Name;
    public int Age;

    // 기본 생성자 (매개변수 없음)
    public Person()
    {
        Name = "이름 없음";
        Age = 0;
        Console.WriteLine("Person 객체가 생성되었습니다.");
    }
}

// 사용
Person person = new Person();  // 생성자 호출
// 출력: Person 객체가 생성되었습니다.

매개변수가 있는 생성자:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 매개변수가 있는 생성자
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public void Introduce()
    {
        Console.WriteLine($"안녕하세요, {Name}입니다. {Age}살입니다.");
    }
}

// 사용
Person person = new Person("홍길동", 25);
person.Introduce();
// 출력: 안녕하세요, 홍길동입니다. 25살입니다.

생성자 오버로딩:

클래스는 여러 개의 생성자를 가질 수 있습니다(오버로딩):

class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    // 기본 생성자
    public Rectangle()
    {
        Width = 1;
        Height = 1;
    }

    // 정사각형을 위한 생성자
    public Rectangle(double size)
    {
        Width = size;
        Height = size;
    }

    // 직사각형을 위한 생성자
    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

// 다양한 방법으로 객체 생성
Rectangle rect1 = new Rectangle();           // 1×1
Rectangle rect2 = new Rectangle(5);          // 5×5 정사각형
Rectangle rect3 = new Rectangle(4, 6);       // 4×6 직사각형

Console.WriteLine($"rect1 넓이: {rect1.GetArea()}");  // 출력: 1
Console.WriteLine($"rect2 넓이: {rect2.GetArea()}");  // 출력: 25
Console.WriteLine($"rect3 넓이: {rect3.GetArea()}");  // 출력: 24

생성자 체이닝 (this 생성자):

한 생성자에서 다른 생성자를 호출하여 코드 중복을 줄일 수 있습니다:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }

    // 가장 완전한 생성자
    public Person(string name, int age, string email)
    {
        Name = name;
        Age = age;
        Email = email;
    }

    // this를 사용하여 위의 생성자 호출
    public Person(string name, int age) : this(name, age, "이메일 없음")
    {
    }

    public Person(string name) : this(name, 0, "이메일 없음")
    {
    }
}

Person p1 = new Person("홍길동", 25, "hong@example.com");
Person p2 = new Person("김철수", 30);
Person p3 = new Person("이영희");

9.2 객체 생성과 사용

객체(Object)는 클래스를 기반으로 메모리에 생성된 실체(Instance)입니다. new 키워드를 사용하여 객체를 생성하며, 이를 인스턴스화(Instantiation)라고 합니다.

객체 생성의 메모리 관점:

C#에서 객체는 힙(Heap) 메모리에 할당되며, 참조 타입(Reference Type)입니다. 변수는 스택(Stack)에 저장되고, 실제 객체는 힙에 저장됩니다. 변수는 힙에 있는 객체의 주소(참조)를 저장합니다.

class Car
{
    public string Model { get; set; }
    public string Color { get; set; }
    public int Year { get; set; }

    public Car(string model, string color, int year)
    {
        Model = model;
        Color = color;
        Year = year;
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"{Year}년식 {Color} {Model}");
    }

    public void Drive()
    {
        Console.WriteLine($"{Model}이(가) 주행합니다.");
    }
}

// 객체 생성 및 사용
Car myCar = new Car("아반떼", "흰색", 2023);
myCar.DisplayInfo();  // 출력: 2023년식 흰색 아반떼
myCar.Drive();        // 출력: 아반떼이(가) 주행합니다.

// 여러 객체 생성
Car car1 = new Car("소나타", "검은색", 2022);
Car car2 = new Car("그랜저", "은색", 2024);

car1.DisplayInfo();  // 출력: 2022년식 검은색 소나타
car2.DisplayInfo();  // 출력: 2024년식 은색 그랜저

객체 초기화 구문:

C# 3.0부터는 객체를 생성하면서 속성을 초기화하는 간편한 구문을 제공합니다:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string City { get; set; }
}

// 객체 초기화 구문
Person person = new Person
{
    Name = "홍길동",
    Age = 25,
    City = "서울"
};

Console.WriteLine($"{person.Name}, {person.Age}세, {person.City}");

null과 객체 참조:

참조 타입인 객체 변수는 null 값을 가질 수 있습니다. null은 "아무것도 참조하지 않음"을 의미합니다:

Person person = null;  // 객체를 참조하지 않음

// person.Name = "홍길동";  // ❌ NullReferenceException 발생!

// null 체크
if (person != null)
{
    person.Name = "홍길동";
}

// null 조건부 연산자 (C# 6.0+)
person?.Introduce();  // person이 null이 아닐 때만 호출

여러 객체를 다루기:

class Student
{
    public string Name { get; set; }
    public int Score { get; set; }

    public Student(string name, int score)
    {
        Name = name;
        Score = score;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"{Name}: {Score}점");
    }
}

// 학생 객체 배열
Student[] students = new Student[3];
students[0] = new Student("홍길동", 85);
students[1] = new Student("김철수", 92);
students[2] = new Student("이영희", 78);

// 모든 학생 정보 출력
foreach (Student student in students)
{
    student.PrintInfo();
}
// 출력:
// 홍길동: 85점
// 김철수: 92점
// 이영희: 78점

9.3 접근 제한자

접근 제한자(Access Modifier)는 클래스의 멤버에 대한 접근 범위를 제어하여 캡슐화를 구현하는 핵심 메커니즘입니다. 정보 은닉(Information Hiding) 원칙을 실현하여 객체의 내부 구현을 외부로부터 보호합니다.

접근 제한자의 종류:

접근 제한자 설명 접근 범위
public 모든 곳에서 접근 가능 제한 없음
private 같은 클래스 내부에서만 접근 가능 클래스 내부
protected 같은 클래스와 파생 클래스에서 접근 가능 클래스 + 상속
internal 같은 어셈블리 내에서만 접근 가능 같은 프로젝트
protected internal 같은 어셈블리 또는 파생 클래스 조합 (OR)
private protected 같은 어셈블리의 파생 클래스에서만 조합 (AND)

9.3.1 public

public은 가장 개방적인 접근 제한자로, 어디서든 접근할 수 있습니다. 주로 클래스의 공개 인터페이스(Public Interface)를 정의하는 데 사용됩니다.

class Calculator
{
    // public 메서드 - 어디서든 호출 가능
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Subtract(int a, int b)
    {
        return a - b;
    }
}

Calculator calc = new Calculator();
int result = calc.Add(10, 5);  // ✅ 접근 가능
Console.WriteLine(result);     // 출력: 15

9.3.2 private

private는 가장 제한적인 접근 제한자로, 같은 클래스 내부에서만 접근할 수 있습니다. 내부 구현을 숨기고 캡슐화를 강화하는 데 사용됩니다.

class BankAccount
{
    // private 필드 - 클래스 외부에서 접근 불가
    private double balance;

    // public 메서드를 통해서만 접근 가능
    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            balance += amount;  // 같은 클래스 내부에서는 접근 가능
        }
    }

    public double GetBalance()
    {
        return balance;  // 같은 클래스 내부에서는 접근 가능
    }

    // private 메서드 - 내부 구현 로직
    private bool ValidateAmount(double amount)
    {
        return amount > 0 && amount <= 1000000;
    }
}

BankAccount account = new BankAccount();
account.Deposit(10000);
// account.balance = 50000;  // ❌ 컴파일 오류: private 필드 접근 불가
Console.WriteLine($"잔액: {account.GetBalance()}원");

private의 중요성:

class Person
{
    private int age;  // private으로 보호

    public int Age
    {
        get { return age; }
        set 
        { 
            // 유효성 검증
            if (value >= 0 && value <= 150)
            {
                age = value;
            }
            else
            {
                Console.WriteLine("유효하지 않은 나이입니다.");
            }
        }
    }
}

Person person = new Person();
person.Age = 25;   // ✅ 정상
person.Age = -5;   // ❌ 검증 실패: 유효하지 않은 나이입니다.
person.Age = 200;  // ❌ 검증 실패: 유효하지 않은 나이입니다.

9.3.3 protected

protected는 같은 클래스와 파생 클래스(Derived Class, 상속받은 클래스)에서 접근할 수 있습니다. 상속을 염두에 둔 설계에서 사용됩니다.

class Animal
{
    protected string name;  // 파생 클래스에서 접근 가능

    public Animal(string name)
    {
        this.name = name;
    }

    protected void Breathe()  // 파생 클래스에서 사용 가능
    {
        Console.WriteLine($"{name}이(가) 숨을 쉽니다.");
    }
}

class Dog : Animal
{
    public Dog(string name) : base(name)
    {
    }

    public void Bark()
    {
        // protected 멤버에 접근 가능
        Console.WriteLine($"{name}이(가) 짖습니다: 멍멍!");
        Breathe();  // protected 메서드 호출
    }
}

Dog dog = new Dog("뽀삐");
dog.Bark();
// 출력:
// 뽀삐이(가) 짖습니다: 멍멍!
// 뽀삐이(가) 숨을 쉽니다.

// dog.name = "다른이름";  // ❌ 외부에서는 접근 불가
// dog.Breathe();          // ❌ 외부에서는 호출 불가

9.3.4 internal

internal은 같은 어셈블리(Assembly, 보통 같은 프로젝트) 내에서만 접근할 수 있습니다. 프로젝트 내부의 구현을 숨기면서도, 프로젝트 내 다른 클래스에서는 사용할 수 있게 합니다.

// 같은 프로젝트 내의 파일1
internal class InternalHelper
{
    internal void DoSomething()
    {
        Console.WriteLine("내부 헬퍼 메서드 실행");
    }
}

// 같은 프로젝트 내의 파일2
class MyClass
{
    public void UseHelper()
    {
        InternalHelper helper = new InternalHelper();
        helper.DoSomething();  // ✅ 같은 프로젝트 내에서는 접근 가능
    }
}

// 다른 프로젝트(다른 어셈블리)에서
// InternalHelper helper = new InternalHelper();  // ❌ 접근 불가

접근 제한자 선택 가이드:

  • public: 외부에 공개해야 하는 API (메서드, 속성)
  • private: 내부 구현 세부사항 (필드, 헬퍼 메서드)
  • protected: 상속 계층에서 공유할 멤버
  • internal: 같은 프로젝트 내에서만 사용할 타입이나 멤버

기본 접근 제한자:

  • 클래스 멤버: 명시하지 않으면 private
  • 클래스 자체: 명시하지 않으면 internal

9.4 정적 멤버 (Static Members)

정적 멤버(Static Member)는 클래스 자체에 속하며, 인스턴스를 생성하지 않고도 사용할 수 있는 멤버입니다. 모든 인스턴스가 공유하는 데이터나 유틸리티 기능을 구현할 때 사용됩니다.

인스턴스 멤버 vs 정적 멤버:

  • 인스턴스 멤버: 각 객체마다 독립적으로 존재. 객체.멤버 형태로 접근
  • 정적 멤버: 클래스에 하나만 존재. 클래스명.멤버 형태로 접근
class Counter
{
    // 인스턴스 필드: 각 객체마다 독립적
    public int instanceCount;

    // 정적 필드: 모든 객체가 공유
    public static int staticCount;

    public void Increment()
    {
        instanceCount++;
        staticCount++;
    }
}

Counter c1 = new Counter();
Counter c2 = new Counter();

c1.Increment();
c1.Increment();
c2.Increment();

Console.WriteLine($"c1 인스턴스 카운트: {c1.instanceCount}");  // 2
Console.WriteLine($"c2 인스턴스 카운트: {c2.instanceCount}");  // 1
Console.WriteLine($"정적 카운트: {Counter.staticCount}");      // 3 (공유됨)

9.4.1 정적 필드와 속성

정적 필드와 속성은 클래스 레벨에 존재하며, 모든 인스턴스가 공유하는 데이터를 저장합니다.

class BankAccount
{
    // 정적 필드: 모든 계좌가 공유하는 은행 이름
    public static string BankName = "국민은행";

    // 정적 필드: 생성된 총 계좌 수
    private static int totalAccounts = 0;

    // 정적 속성
    public static int TotalAccounts
    {
        get { return totalAccounts; }
    }

    // 인스턴스 필드
    public string AccountNumber { get; set; }
    public double Balance { get; private set; }

    // 생성자
    public BankAccount(string accountNumber)
    {
        AccountNumber = accountNumber;
        totalAccounts++;  // 계좌 생성 시 카운트 증가
    }
}

// 사용
Console.WriteLine($"은행: {BankAccount.BankName}");
Console.WriteLine($"현재 계좌 수: {BankAccount.TotalAccounts}");

BankAccount acc1 = new BankAccount("123-456");
BankAccount acc2 = new BankAccount("789-012");

Console.WriteLine($"현재 계좌 수: {BankAccount.TotalAccounts}");  // 2

// 모든 인스턴스에 적용
BankAccount.BankName = "우리은행";
Console.WriteLine($"은행: {BankAccount.BankName}");  // 우리은행

정적 필드의 활용 - 상수와 설정:

class MathHelper
{
    // 정적 상수 (const는 암묵적으로 static)
    public const double PI = 3.14159265359;

    // 정적 읽기 전용 필드
    public static readonly double E = 2.71828182846;

    public static double CircleArea(double radius)
    {
        return PI * radius * radius;
    }
}

double area = MathHelper.CircleArea(5);
Console.WriteLine($"원의 넓이: {area}");
Console.WriteLine($"파이: {MathHelper.PI}");
Console.WriteLine($"자연상수: {MathHelper.E}");

9.4.2 정적 메서드

정적 메서드는 인스턴스 없이 호출할 수 있는 메서드로, 주로 유틸리티 함수나 팩토리 메서드로 사용됩니다.

class MathUtility
{
    // 정적 메서드: 두 수 중 큰 값 반환
    public static int Max(int a, int b)
    {
        return a > b ? a : b;
    }

    // 정적 메서드: 세 수 중 큰 값 반환
    public static int Max(int a, int b, int c)
    {
        return Max(Max(a, b), c);
    }

    // 정적 메서드: 절댓값 계산
    public static int Abs(int value)
    {
        return value < 0 ? -value : value;
    }
}

// 인스턴스 생성 없이 직접 호출
int max = MathUtility.Max(10, 25);
Console.WriteLine($"최댓값: {max}");  // 출력: 최댓값: 25

int max3 = MathUtility.Max(5, 12, 8);
Console.WriteLine($"세 수 중 최댓값: {max3}");  // 출력: 세 수 중 최댓값: 12

정적 메서드의 제약:

정적 메서드는 인스턴스 멤버에 접근할 수 없습니다. 정적 멤버만 접근 가능합니다.

class Example
{
    private int instanceValue = 10;
    private static int staticValue = 20;

    public static void StaticMethod()
    {
        Console.WriteLine(staticValue);    // ✅ 정적 필드 접근 가능
        // Console.WriteLine(instanceValue);  // ❌ 인스턴스 필드 접근 불가
    }

    public void InstanceMethod()
    {
        Console.WriteLine(instanceValue);  // ✅ 인스턴스 필드 접근 가능
        Console.WriteLine(staticValue);    // ✅ 정적 필드도 접근 가능
    }
}

정적 메서드 활용 예제 - 변환 유틸리티:

class TemperatureConverter
{
    // 섭씨를 화씨로 변환
    public static double CelsiusToFahrenheit(double celsius)
    {
        return (celsius * 9 / 5) + 32;
    }

    // 화씨를 섭씨로 변환
    public static double FahrenheitToCelsius(double fahrenheit)
    {
        return (fahrenheit - 32) * 5 / 9;
    }
}

// 사용
double celsius = 25;
double fahrenheit = TemperatureConverter.CelsiusToFahrenheit(celsius);
Console.WriteLine($"{celsius}°C = {fahrenheit:F1}°F");  // 출력: 25°C = 77.0°F

double f = 98.6;
double c = TemperatureConverter.FahrenheitToCelsius(f);
Console.WriteLine($"{f}°F = {c:F1}°C");  // 출력: 98.6°F = 37.0°C

9.4.3 정적 생성자

정적 생성자(Static Constructor)는 클래스가 처음 사용될 때 자동으로 한 번만 호출되는 특별한 생성자입니다. 정적 필드를 초기화하거나 클래스 레벨의 설정을 수행하는 데 사용됩니다.

정적 생성자의 특징: - 매개변수를 가질 수 없음 - 접근 제한자를 가질 수 없음 (자동으로 private) - 클래스당 하나만 존재 - 직접 호출할 수 없음 (자동으로 호출됨)

class Configuration
{
    public static string AppName;
    public static string Version;
    public static DateTime LoadedTime;

    // 정적 생성자
    static Configuration()
    {
        AppName = "My Application";
        Version = "1.0.0";
        LoadedTime = DateTime.Now;
        Console.WriteLine("Configuration 클래스가 초기화되었습니다.");
    }

    public static void DisplayInfo()
    {
        Console.WriteLine($"앱 이름: {AppName}");
        Console.WriteLine($"버전: {Version}");
        Console.WriteLine($"로드 시간: {LoadedTime}");
    }
}

// 처음 사용 시 정적 생성자가 자동 호출됨
Console.WriteLine("프로그램 시작");
Configuration.DisplayInfo();  // 이 시점에 정적 생성자 실행
Configuration.DisplayInfo();  // 두 번째 호출 시에는 실행 안 됨

// 출력:
// 프로그램 시작
// Configuration 클래스가 초기화되었습니다.
// 앱 이름: My Application
// 버전: 1.0.0
// 로드 시간: 2024-01-15 오후 2:30:45
// 앱 이름: My Application
// 버전: 1.0.0
// 로드 시간: 2024-01-15 오후 2:30:45

정적 생성자 활용 예제:

class DatabaseConnection
{
    public static string ConnectionString;
    private static bool isInitialized;

    static DatabaseConnection()
    {
        // 데이터베이스 연결 설정 초기화
        ConnectionString = "Server=localhost;Database=mydb;";
        isInitialized = true;
        Console.WriteLine("데이터베이스 연결 설정이 초기화되었습니다.");
    }

    public static void Connect()
    {
        if (isInitialized)
        {
            Console.WriteLine($"연결 중: {ConnectionString}");
        }
    }
}

정적 클래스:

모든 멤버가 정적인 경우, 클래스 자체를 static으로 선언할 수 있습니다. 이는 유틸리티 클래스나 확장 메서드를 정의할 때 사용됩니다.

static class StringHelper
{
    public static bool IsNullOrEmpty(string str)
    {
        return string.IsNullOrEmpty(str);
    }

    public static string Reverse(string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        char[] chars = str.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }
}

// 사용 (인스턴스 생성 불가)
// StringHelper helper = new StringHelper();  // ❌ 컴파일 오류
bool isEmpty = StringHelper.IsNullOrEmpty("Hello");  // ✅
string reversed = StringHelper.Reverse("Hello");      // ✅
Console.WriteLine(reversed);  // 출력: olleH

9장 정리 및 요약

이 장에서는 객체지향 프로그래밍의 기초인 클래스와 객체에 대해 학습했습니다.

핵심 개념 정리

  1. 클래스와 객체
  2. 클래스는 객체를 생성하기 위한 청사진
  3. 객체는 클래스를 기반으로 생성된 실체(인스턴스)
  4. 하나의 클래스로 여러 객체 생성 가능

  5. 필드와 속성

  6. 필드: 객체의 상태를 저장하는 변수
  7. 속성: 필드에 대한 캡슐화된 접근 제공
  8. 자동 구현 속성으로 간결한 코드 작성

  9. 메서드와 생성자

  10. 메서드: 객체의 동작을 정의
  11. 생성자: 객체 초기화를 위한 특별한 메서드
  12. 생성자 오버로딩으로 다양한 초기화 방법 제공

  13. 접근 제한자

  14. public: 모든 곳에서 접근 가능
  15. private: 같은 클래스 내부에서만 접근
  16. protected: 클래스와 파생 클래스에서 접근
  17. internal: 같은 어셈블리 내에서 접근

  18. 정적 멤버

  19. 클래스 자체에 속하는 멤버
  20. 인스턴스 생성 없이 사용 가능
  21. 모든 인스턴스가 공유하는 데이터나 유틸리티 기능

실습 문제

문제 1: 도서 클래스

도서 정보를 관리하는 Book 클래스를 작성하세요.

class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int Pages { get; set; }

    public Book(string title, string author, int pages)
    {
        Title = title;
        Author = author;
        Pages = pages;
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"제목: {Title}");
        Console.WriteLine($"저자: {Author}");
        Console.WriteLine($"페이지: {Pages}");
    }
}

// 테스트
Book book1 = new Book("C# 프로그래밍", "홍길동", 500);
Book book2 = new Book("자료구조", "김철수", 450);

book1.DisplayInfo();
Console.WriteLine();
book2.DisplayInfo();

문제 2: 은행 계좌 클래스

입출금 기능이 있는 BankAccount 클래스를 작성하세요.

class BankAccount
{
    private double balance;
    public string AccountNumber { get; set; }
    public string Owner { get; set; }

    public double Balance
    {
        get { return balance; }
    }

    public BankAccount(string accountNumber, string owner)
    {
        AccountNumber = accountNumber;
        Owner = owner;
        balance = 0;
    }

    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            balance += amount;
            Console.WriteLine($"{amount:N0}원이 입금되었습니다. 잔액: {balance:N0}원");
        }
    }

    public bool Withdraw(double amount)
    {
        if (amount > 0 && balance >= amount)
        {
            balance -= amount;
            Console.WriteLine($"{amount:N0}원이 출금되었습니다. 잔액: {balance:N0}원");
            return true;
        }
        else
        {
            Console.WriteLine("출금 실패: 잔액이 부족합니다.");
            return false;
        }
    }
}

// 테스트
BankAccount account = new BankAccount("123-456-789", "홍길동");
account.Deposit(100000);
account.Withdraw(30000);
account.Withdraw(80000);  // 실패
Console.WriteLine($"현재 잔액: {account.Balance:N0}원");

문제 3: 학생 관리 클래스

학생 정보와 성적을 관리하는 클래스를 작성하세요.

class Student
{
    private static int totalStudents = 0;

    public static int TotalStudents
    {
        get { return totalStudents; }
    }

    public string Name { get; set; }
    public int StudentId { get; set; }
    private List<int> scores;

    public Student(string name, int studentId)
    {
        Name = name;
        StudentId = studentId;
        scores = new List<int>();
        totalStudents++;
    }

    public void AddScore(int score)
    {
        if (score >= 0 && score <= 100)
        {
            scores.Add(score);
        }
    }

    public double GetAverage()
    {
        if (scores.Count == 0)
            return 0;

        return scores.Average();
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"학생 이름: {Name}");
        Console.WriteLine($"학번: {StudentId}");
        Console.WriteLine($"평균 점수: {GetAverage():F2}");
    }
}

// 테스트
Console.WriteLine($"현재 학생 수: {Student.TotalStudents}");

Student s1 = new Student("홍길동", 2024001);
s1.AddScore(85);
s1.AddScore(90);
s1.AddScore(88);

Student s2 = new Student("김철수", 2024002);
s2.AddScore(92);
s2.AddScore(87);

s1.DisplayInfo();
Console.WriteLine();
s2.DisplayInfo();
Console.WriteLine();
Console.WriteLine($"현재 학생 수: {Student.TotalStudents}");

문제 4: 계산기 클래스

정적 메서드를 사용한 계산기 클래스를 작성하세요.

static class Calculator
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static int Subtract(int a, int b)
    {
        return a - b;
    }

    public static int Multiply(int a, int b)
    {
        return a * b;
    }

    public static double Divide(int a, int b)
    {
        if (b == 0)
        {
            Console.WriteLine("오류: 0으로 나눌 수 없습니다.");
            return 0;
        }
        return (double)a / b;
    }
}

// 테스트
Console.WriteLine($"10 + 5 = {Calculator.Add(10, 5)}");
Console.WriteLine($"10 - 5 = {Calculator.Subtract(10, 5)}");
Console.WriteLine($"10 × 5 = {Calculator.Multiply(10, 5)}");
Console.WriteLine($"10 ÷ 5 = {Calculator.Divide(10, 5)}");

다음 장 예고

10장 "상속과 다형성"에서는 객체지향 프로그래밍의 핵심 개념들을 심화 학습합니다: - 상속 (Inheritance) - 메서드 오버라이딩 (Method Overriding) - 추상 클래스 (Abstract Class) - 인터페이스 (Interface) - 다형성 (Polymorphism)

이를 통해 코드의 재사용성과 확장성을 극대화하는 방법을 배우게 됩니다!