1. 목표

  OOP는 왜 등장했을까?

더보기

1. 목표

 

문제점 파악: 기존의 절차 지향 프로그래밍(POP)의 한계와 문제점을 파악

해결책 이해: 객체 지향 프로그래밍(OOP)의 문제 해결 방법 이해

 

 

2. 용어

 

캡슐화(Encapsulation) - OOP의 주요 특성

접근 제어자(Access Modifier) - C# 프로그래밍 문법

 

 

3. 참고 자료

https://youtu.be/zgeCwYWzK-k?si=rJFngC_y71cOcVSm

 

 

 

 

 

2. <예제1> - 절차지향 문제점

시나리오 2개

더보기

1. 어느 함수에서나 접근 가능한 공유 데이터 문제

#include <stdio.h>
int score = 100; // 전역 변수 사용 → 모든 함수가 직접 접근 가능함 (❗ 절차지향의 문제점 1: 전역 변수 남용)

void addBonus() { score += 10; }     // 보너스를 더함 → score = 110 예상 (정상 동작)
void applyPenalty() { score -= 50; } // 패널티 적용 → score = 60 예상 (정상 동작)
void resetScore() { score = -9999; } // ❌ 실수로 잘못된 값 설정  
// 조건 없이 점수를 비정상적으로 초기화함 (❗ 절차지향의 문제점 2: 데이터 무결성 관리 실패)

int main() {
    addBonus();       // score = 110
    applyPenalty();   // score = 60
    resetScore();     // score = -9999 (❗ 예기치 않은 값 변경 발생)

    // 문제 시나리오:
    // - score가 -9999가 되어 출력됨 → 결과가 현실적으로 불가능함
    // - 어느 함수에서 문제가 발생했는지 추적이 어려움 (❗ 절차지향의 문제점 3: 디버깅/책임 분리 실패)

    printf("최종 점수: %d\n", score); // → 결과 신뢰 불가
    return 0;
}

 

 

1.1 데이터

int score = 100; // 전역 변수 사용 → 모든 함수가 직접 접근 가능함 (❗ 절차지향의 문제점 1: 전역 변수 남용)

 

 

1.2 프로시저(로직, 함수, 데이터를 가지고 조건문, 반복문 등을 통해 일정한 절차를 수행하는 부분)

void addBonus() { score += 10; }     // 보너스를 더함 → score = 110 예상 (정상 동작)
void applyPenalty() { score -= 50; } // 패널티 적용 → score = 60 예상 (정상 동작)
void resetScore() { score = -9999; } // ❌ 실수로 잘못된 값 설정  
// 조건 없이 점수를 비정상적으로 초기화함 (❗ 절차지향의 문제점 2: 데이터 무결성 관리 실패)

// void addScore( score += 5 } // 추가 구현된 함수가 score 데이터 접근이 쉬움

 

 

1.3 실행과 협업 문제

  • score 변수는 모든 함수에서 직접 접근 가능
    → 실수로 잘못된 값 입력 가능
  • resetScore( ) 함수처럼 의도하지 않거나 실수로 로직을 망치는 코드가 어디서든 등장할 수 있음
  • 여러명이 협업 중에, 어느 한 개발자가 추가한 함수의 예상치 못한 실행이 값을 수정해버림
    버그 발생

 

 

 

 

2. 결합도가 높은 구조의 수정 문제

#include <stdio.h>
#include <string.h>

int level = 0;

void addEasyLevel() { level += 1; }
void addMediumLevel() { level += 2; }
void addHardLevel() { level += 3; }
// void addInsaneLevel() { level += 5; } // 🔧 새 난이도 함수 추가 필요

void addLevelByDifficulty(const char* difficulty) {
    if (strcmp(difficulty, "easy") == 0) addEasyLevel();
    else if (strcmp(difficulty, "medium") == 0) addMediumLevel();
    else if (strcmp(difficulty, "hard") == 0) addHardLevel();
    // else if (strcmp(difficulty, "insane") == 0) addInsaneLevel(); // 🔧 조건 추가 필요
    else printf("알 수 없는 난이도: %s\n", difficulty);
}

int main() {
    addLevelByDifficulty("easy");
    addLevelByDifficulty("medium");
    addLevelByDifficulty("hard");
    addLevelByDifficulty("insane");  // ❌ 새 난이도 추가했는데 작동 안 함
    // 🔧 addInsaneLevel() 함수 만들고 위 조건에도 추가해야 동작함
    printf("최종 레벨: %d\n", level);
    return 0;
}

 

 

2.1 소스코드 수정 문제

  • 소스코드는 이해하기 쉽고 변경하기도 쉬워야 하는데, 데이터 구조를 변경하면 관련된 모든 코드를 수정해야 함.
  • 문제가 있는 일부를 수정하는 것보다, 새롭게 만드는 것이 빠를 수 있는 상황 발생
  • "insane" 난이도를 새로 만들고 싶으면:
    1. addInsaneLevel() 함수 새로 만들어야 하고
    2. addBonusByDifficulty() 안의 if 문도 수정해야 하고
    3. main()에서도 addInsaneLevel() 함수도 호출해야 함
      → 기능 하나 추가할 때 여러 군데 수정해야 하는 높은 결합도 예시.
  • 실수로 분기문에 빠뜨리면 (else if에 안 넣음) 함수는 있어도 호출이 안 됨 → 유지보수 어려움

 

 

2.2 디버깅 문제 - 어느 함수에서나 접근 및 변경 가능한 공유 데이터

 

  • 프로그램이 조금만 복잡해져도 어떤 프로시져가 데이터를 수정했는지 모든 경우의 시나리오를 검토해야 하기 때문에 문제점을 찾기 어려움

 

 

 

 

3. <예제1> - 절차지향 C언어 해결책

더보기

OOP처럼 구조화된 ScoreManager 구조체 버전을 만들어 해결하는 방법 예시

#include <stdio.h>

// ScoreManager 구조체를 사용하여 점수 데이터 캡슐화
// 절차지향의 문제점인 전역 변수 남용 및 데이터 무결성 문제를 구조체로 해결

typedef struct { int score; } ScoreManager;

// 초기 점수 설정 (음수 방지 검증 포함)
void ScoreManager_init(ScoreManager* sm, int s) { sm->score = s >= 0 ? s : 0; }

// 보너스 추가 (+10)
void ScoreManager_addBonus(ScoreManager* sm) { sm->score += 10; }

// 패널티 적용 (-50)
void ScoreManager_applyPenalty(ScoreManager* sm) { sm->score -= 50; }

// 점수를 0으로 초기화하며, 상태 메시지 출력
void ScoreManager_safeReset(ScoreManager* sm) { sm->score = 0; printf("점수가 안전하게 초기화되었습니다.\n"); }

// 현재 점수 조회 함수 (외부에서는 직접 접근 불가)
int ScoreManager_getScore(ScoreManager* sm) { return sm->score; }

int main() {
    ScoreManager manager;
    ScoreManager_init(&manager, 100);         // 초기 점수 설정
    ScoreManager_addBonus(&manager);          // 보너스 적용
    ScoreManager_applyPenalty(&manager);      // 패널티 적용
    ScoreManager_safeReset(&manager);         // 안전 초기화

    printf("최종 점수: %d\n", ScoreManager_getScore(&manager)); // 점수 출력
    return 0;
}

 

주요 개선점:

  • score는 구조체 내부에 숨기고, 외부에서는 오직 함수로만 접근합니다.
  • 초기화/보너스/패널티/초기화/조회 등 모든 작업을 전용 함수로 분리해 책임을 명확히 분리했습니다.
  • 안전한 사용을 위해 초기값 검증도 포함되어 있습니다. (아래 C# 예제 생성자 비교)
  • 그 외, static 키워드를 사용한 파일 내부 사용 가능하게 하여 외부 접근 제한하는 방법도 있습니다.

 

 

 

 


4. <예제1> - 객체지향 C# 해결책

더보기

위 해결 방향과, C# 객체지향 버전과 비교

using System;

// ScoreManager 클래스: 점수 관리 책임을 분리한 객체지향 구조
// 절차지향의 문제점(전역 변수, 무결성 파괴, 책임 불명확성 등)을 해결
class ScoreManager {
    private int score; // 점수를 외부에서 직접 접근하지 못하도록 private으로 캡슐화

    // 생성자: 초기 점수 설정, 음수 방지 처리 포함 (무결성 확보)
    public ScoreManager(int initialScore) {score = (initialScore >= 0) ? initialScore : 0;}

    public void AddBonus() => score += 10;     // 보너스 점수 추가 메서드 (캡슐화된 로직)
    public void ApplyPenalty() => score -= 50; // 감점 처리 메서드
    public void Reset() => score = 0;          // 점수 초기화 (직접 설정이 아닌 메서드로만 가능 → 보호)
    public int GetScore() => score;            // 현재 점수 반환 메서드 (읽기 전용 제공)
}

class Program {
    static void Main() {
        var manager = new ScoreManager(100); // ScoreManager 객체 생성 및 초기화

        // 점수 조작은 오직 메서드를 통해 수행됨 (데이터 보호, 예측 가능한 흐름)
        manager.AddBonus();       // +10
        manager.ApplyPenalty();   // -50
        manager.Reset();          // 0으로 초기화

        Console.WriteLine($"최종 점수: {manager.GetScore()}"); // 점수 출력
    }
}

 

5. C 구조체 방식과 C# 클래스 방식의 비교

더보기

비교 요약

  1. C 구조체 방식은 객체지향처럼 유사하게 구현할 수 있다
    하지만
    , C 언어는 클래스, 접근 제어자, 상속, 다형성 등 객체지향의 핵심 문법을 지원하지 않음
  2. 구조체 + 함수 조합은 간접적 함수 인터페이스로 OOP의 "캡슐화" 와 비슷하게 구현 가능하다.

 

 

 

 

비교 상세

 

항목 C 구조체 방식 C# 객체지향 클래스 방식
데이터 은닉 ❌ score는 구조체 내부지만 외부 접근 가능 ✅ private 필드로 외부 접근 차단
메서드 포함 여부 ❌ 함수는 구조체 외부에 존재 ✅ 메서드가 클래스 내부에 캡슐화됨
접근 제어자 (private, public) ❌ 없음 (명시 불가) ✅ 명시적 접근 제어 가능
캡슐화 🔸 간접적 함수 인터페이스로 흉내낼 수 있음 ✅ 직접적으로 구현 가능
상태 보호 ❌ 직접 구조체 필드 수정 가능 ✅ 메서드 통해서만 수정 가능
확장성 (상속, 다형성 등) ❌ 없음 ✅ 있음 (OOP 핵심 기능)
문법 구조 절차지향적 함수 기반 설계 객체 중심의 클래스 기반 설계
유지보수성과 테스트 용이성 제한적 구조화 → 테스트 어려움 클래스 단위 테스트 및 리팩토링 용이

 

 

 

 

 

🆚 객체지향(OOP)과 비교해서 나은 점

 

항목 절차지향 프로그래밍 (C 등) 객체지향 프로그래밍 (C++, C#, Java 등)
성능 빠르고 가볍다 (오버헤드 적음) 상대적으로 느릴 수 있음 (추상화, 가상 메서드 등)
구조 단순성 흐름이 직관적, 코드가 짧고 단순 복잡한 구조로 초보자가 이해하기 어려움
디버깅과 추적 메모리 구조가 명확, 함수 단위 추적 쉬움 객체 상태 추적이 어려울 수 있음
메모리 사용량 낮음 클래스, 객체 생성 등으로 메모리 사용 증가 가능
빠른 개발 작고 간단한 작업에 적합 작은 기능도 클래스 단위로 나누는 오버엔지니어링 우려
제어의 유연성 로우 레벨 제어가 가능 (포인터, 주소 등) 언어와 런타임이 많은 것을 숨김

 

 

 

 

결론: OOP vs 절차지향, 무엇이 더 나은가?

둘 다 필요하고, 상황에 따라 OOP의 장점을 활용하되, 성능이 중요한 부분엔 절차적 구조를 병행

  • 복잡한 도메인, 유지보수 중심의 소프트웨어: OOP
  • 작고 빠른 시스템, 하드웨어 근접 프로그래밍: 절차지향

 

 

 

 

 

6. 접근 제어자(Access Modifier)

더보기

객체지향 개요 (개발 구조 예시)

using System;

// 객체지향 리팩토링 개요 - 접근 제어자 중심 주석 포함 헤더 구조

class TemperatureSensor
{
    // private: 외부에서 직접 접근 불가, 클래스 내부에서만 사용 가능
    private int currentTemperature;

    // public: 외부에서 객체를 생성할 수 있도록 생성자 공개
    public TemperatureSensor(int initialTemperature); // 생성자

    // public: 외부에서 온도 값을 읽을 수 있도록 허용
    public int GetTemperature();                      // 온도 읽기

    // public: 외부에서 센서 값을 갱신할 수 있도록 허용
    public void UpdateTemperature(int newTemp);       // 온도 갱신
}

class Boiler
{
    // private: 외부에서 보일러 상태 직접 조작 불가 (캡슐화)
    private bool isOn;

    // internal: 같은 어셈블리 내에서는 접근 가능 (예: 테스트 프로젝트)
    // private set: 외부에서 읽을 수는 있지만 수정은 불가능
    internal int TargetTemperature { get; private set; } // 목표 온도 (internal)

    // public: 외부에서 보일러 인스턴스를 생성할 수 있도록 생성자 공개
    public Boiler(int targetTemp);                     // 생성자

    // public: 외부에서 보일러 상태를 읽을 수 있도록 허용
    public bool IsOn();                                // 보일러 상태 조회

    // public: 온도 센서에 따라 보일러가 작동하도록 제어하는 메서드
    public void CheckTemperatureAndOperate(TemperatureSensor sensor); // 온도에 따른 작동 제어

    // private: 외부에서 직접 보일러를 켜거나 끌 수 없음 → 내부 제어 용도
    private void TurnOn();                             // 내부용: 보일러 켜기
    private void TurnOff();                            // 내부용: 보일러 끄기
}

class Program
{
    // static: 객체 생성 없이 클래스 이름으로 직접 실행 가능
    // public 생략됨 → 내부 진입점으로 한정됨
    static void Main(); // 진입점
}

 

접근 제어자 사용 요약

 

접근 제어자 사용 위치 의미 및 역할
private TurnOn(), isOn 내부 구현 보호. 외부에서 직접 접근/수정 금지.
public CheckTemperatureAndOperate() 다른 객체나 사용자 코드에서 사용 가능.
internal TargetTemperature 같은 어셈블리 내 접근 허용 (예: 테스트 등).
protected (사용 안 됨) 상속 관계에서 자식 클래스만 접근 허용. 필요 시 확장 가능.

 

 

3. C# 객체지향 버전

using System;

class TemperatureSensor
{
    // private: 외부에서 직접 온도 수정 불가
    private int currentTemperature;

    public TemperatureSensor(int initialTemperature)
    {
        currentTemperature = initialTemperature;
    }

    // public: 외부에서 온도를 읽을 수 있음
    public int GetTemperature()
    {
        return currentTemperature;
    }

    // public: 외부에서 센서 값을 갱신할 수 있음 (예: 외부 환경 변화)
    public void UpdateTemperature(int newTemp)
    {
        currentTemperature = newTemp;
    }
}

class Boiler
{
    // private: 외부에서 직접 보일러 상태를 변경할 수 없음
    private bool isOn;

    // internal: 같은 프로젝트 내에서는 접근 가능 (예: 테스트 프로젝트 등)
    internal int TargetTemperature { get; private set; }

    public Boiler(int targetTemp)
    {
        TargetTemperature = targetTemp;
        isOn = false;
    }

    // public: 보일러의 상태를 읽기 위한 메서드
    public bool IsOn()
    {
        return isOn;
    }

    // public: 외부에서 온도 센서를 통해 동작 제어 가능
    public void CheckTemperatureAndOperate(TemperatureSensor sensor)
    {
        int current = sensor.GetTemperature();

        if (current < TargetTemperature)
        {
            TurnOn();  // 내부에서만 호출
        }
        else
        {
            TurnOff();
        }
    }

    // private: 외부에서는 보일러를 직접 켜거나 끌 수 없음 (안정성 확보)
    private void TurnOn()
    {
        isOn = true;
        Console.WriteLine("보일러가 켜졌습니다.");
    }

    private void TurnOff()
    {
        isOn = false;
        Console.WriteLine("보일러가 꺼졌습니다.");
    }
}

class Program
{
    static void Main()
    {
        TemperatureSensor sensor = new TemperatureSensor(18); // 현재 온도 18도
        Boiler boiler = new Boiler(20); // 목표 온도 20도

        // 현재 온도 확인 및 보일러 상태 제어
        boiler.CheckTemperatureAndOperate(sensor); // 온도 낮음 → 보일러 켜짐
        Console.WriteLine("보일러 상태: " + (boiler.IsOn() ? "켜짐" : "꺼짐"));

        // 온도 상승 후 다시 상태 점검
        sensor.UpdateTemperature(22); // 센서 온도 업데이트
        boiler.CheckTemperatureAndOperate(sensor); // 온도 높음 → 보일러 꺼짐
        Console.WriteLine("보일러 상태: " + (boiler.IsOn() ? "켜짐" : "꺼짐"));
    }
}

 

7. 접근 제어자(Access Modifier) - Public vs Private

더보기

접근 제어자 요약

접근 제어자 의미 및 역할  
private 내부 구현 보호. 외부에서 직접 접근/수정 금지. 클래스 내부에서만
public 다른 객체나 사용자 코드에서 사용 가능. 어디서든
internal 같은 어셈블리 내 접근 허용 (예: 프로젝트 등). 같은 프로젝트(.dll) 안에서만
protected 상속 관계에서 자식 클래스만 접근 허용. 필요 시 확장 가능. 자식 클래스에서만
캡슐화 수준 private < internal < protected < public
 

 

 

C# 객체지향 버전

using System;

class Person
{
    public string Name;         // public: 외부에서 누구나 접근 가능 (직접 읽고 쓸 수 있음)
    private int age;            // private: 클래스 내부에서만 접근 가능 (외부 접근 불가)

    public Person(string name, int age) {
        Name = name;            // 외부에서 접근 가능한 public 필드 초기화
        this.age = age;         // private 필드는 생성자 내부에서만 설정 가능
    }

    public void PrintInfo() {
        // private 필드인 age에 안전하게 접근하는 공개 메서드
        Console.WriteLine($"이름: {Name}, 나이: {age}");
    }
}

class Program
{
    static void Main() {
        var p = new Person("홍길동", 30);      // 객체 생성

        Console.WriteLine(p.Name);            // ✅ public → 직접 접근 가능
        // Console.WriteLine(p.age);         // ❌ private → 접근 불가 (컴파일 에러)

        p.PrintInfo();                        // ✅ 메서드를 통해 private 필드 간접 접근
    }
}

 

8. 접근 제어자(Access Modifier) - internal

더보기

접근 제어자 요약

 

접근 제어자 의미 및 역할  
private 내부 구현 보호. 외부에서 직접 접근/수정 금지. 클래스 내부에서만
public 다른 객체나 사용자 코드에서 사용 가능. 어디서든
internal 같은 어셈블리 내 접근 허용 (예: 프로젝트 등). 같은 프로젝트(.dll) 안에서만
protected 상속 관계에서 자식 클래스만 접근 허용. 필요 시 확장 가능. 자식 클래스에서만
캡슐화 수준 private < internal < protected < public
 

 

 

어셈블리(Assembly)란?

  • 어셈블리(Assembly)란? → .NET에서의 하나의 컴파일 결과물(.dll 또는 .exe 파일)
  • internal로 선언된 클래스/멤버는 그 파일(프로젝트) 안에서는 접근 가능하지만,
    다른 프로젝트에서 참조한 경우에는 접근할 수 없습니다.

 

 

장점

  • 라이브러리 내부에서만 사용하고 외부 개발자에게 불필요한 API를 노출하지 않음
  • 유지보수 시 외부에 영향 없이 내부 구현 변경 가능
  • InternalsVisibleTo 사용해 단위 테스트 프로젝트 접근 가능

 

 

예시

📁 Solution
├── 📦 MyLibrary (class library project)
│   └── Boiler.cs  ← ✅internal 클래스 정의
├── 📦 MyApp (main app project)
│   └── Program.cs  ← MyLibrary 참조
// 📦 MyLibrary 프로젝트
internal class Boiler {
    internal int TargetTemperature { get; private set; }
}
// 📦 MyApp 프로젝트
Boiler b = new Boiler(); // ❌ 접근 불가: Boiler는 internal이므로 외부 프로젝트에서 볼 수 없음

 

9. 접근 제어자(Access Modifier) - protected

더보기

접근 제어자 요약

접근 제어자 의미 및 역할  
private 내부 구현 보호. 외부에서 직접 접근/수정 금지. 클래스 내부에서만
public 다른 객체나 사용자 코드에서 사용 가능. 어디서든
internal 같은 어셈블리 내 접근 허용 (예: 프로젝트 등). 같은 프로젝트(.dll) 안에서만
protected 상속 관계에서 자식 클래스만 접근 허용. 필요 시 확장 가능. 자식 클래스에서만
캡슐화 수준 private < internal < protected < public
 

 

 

C# 객체지향 버전

using System;

class Animal
{
    protected string sound = "동물 소리"; // protected: 상속받은 클래스에서만 접근 가능

    public void MakeSound() {
        Console.WriteLine(sound); // 직접 접근 가능 (자기 클래스 내부)
    }
}

class Dog : Animal
{
    public void Bark() {
        Console.WriteLine(sound); // ✅ 가능: Animal의 protected 멤버에 자식 클래스에서 접근
    }
}

class Program
{
    static void Main() {
        var dog = new Dog();

        dog.Bark();          // 자식 클래스에서 protected 필드에 접근한 메서드 호출
        // Console.WriteLine(dog.sound); // ❌ 오류: 외부에서는 protected 멤버 접근 불가
    }
}

 

10. 추가 예제1

더보기

객체지향 리팩토링 개요 (개발 구조 예시)

using System;

// 헤더 구조만 정리한 ScoreManager 예제

class ScoreManager
{
    private int score;

    public ScoreManager(int initialScore); // 생성자

    public void AddBonus();      // 보너스 추가
    public void ApplyPenalty();  // 감점 적용
    public void Reset();         // 점수 초기화
    public int GetScore();       // 점수 조회
}

class Program
{
    static void Main(); // 메인 진입점
}

 

using System;

class ScoreManager
{
    private int score;

    public ScoreManager(int initialScore)
    {
        score = initialScore;
    }

    public void AddBonus()
    {
        score += 10;
    }

    public void ApplyPenalty()
    {
        score -= 50;
    }

    // 점수를 임의로 설정하지 않고, reset은 명확하게 정의된 값으로만
    public void Reset()
    {
        score = 0;
    }

    public int GetScore()
    {
        return score;
    }
}

class Program
{
    static void Main()
    {
        ScoreManager manager = new ScoreManager(100);

        manager.AddBonus();
        manager.ApplyPenalty();
        manager.Reset(); // 의도된 초기화만 가능

        Console.WriteLine($"최종 점수: {manager.GetScore()}");
    }
}

 

개선된 점


절차지향 문제 객체지향 해결 방식
전역 변수 → 무결성 깨짐 private 필드로 외부 접근 차단
값 변경 추적 어려움 모든 변경은 메서드를 통해 제어
의도치 않은 값 입력 가능 reset()은 값 범위와 의미를 명확히 정의함

 

11. 추가 예제2

더보기

1. 절차지향 문제점이 드러나는 C 예제

#include <stdio.h>

// 전역 변수 - 모든 함수가 접근 가능
int accountBalance = 10000;

// 입금 함수
void deposit(int amount) {
    accountBalance += amount;
    printf("입금: %d원\n", amount);
}

// 출금 함수
void withdraw(int amount) {
    if (accountBalance >= amount) {
        accountBalance -= amount;
        printf("출금: %d원\n", amount);
    } else {
        printf("잔액 부족: 출금 실패\n");
    }
}

// 외부 함수가 실수로 직접 값을 바꿈 (데이터 오염 사례)
void systemError() {
    // 예: 외부 함수가 실수로 잔액을 초기화함
    accountBalance = -9999;  // ❌ 무결성 파괴
    printf("시스템 오류 발생: 잔액이 손상되었습니다!\n");
}

int main() {
    deposit(5000);
    withdraw(3000);

    // 의도치 않은 함수 호출로 전역 데이터 손상
    systemError();

    // 이후 처리 결과도 신뢰할 수 없음
    withdraw(1000);

    printf("최종 잔액: %d원\n", accountBalance);

    return 0;
}

 

간단한 은행 계좌 시스템을 구현하며, 전역 변수 사용으로 인해 발생하는 데이터 일관성 문제, 유지보수 어려움, 디버깅의 복잡성 등을 잘 보여줍니다.

 

 

 

 

2. 이 예제를 객체지향 방식(OOP)으로 리팩토링하면?

  • Account 클래스 내부에 balance를 private으로 선언
  • 입금/출금은 deposit()/withdraw() 메서드로만 가능
  • 시스템 오류나 외부 함수에서 직접 접근 불가 → 무결성 보장
  • 객체지향 리팩토링 개요 (개발 구조 예시)
using System;

// 💡 BankAccount 클래스: 계좌 관리 책임
class BankAccount
{
    // 🔐 캡슐화된 필드
    private int balance;

    // 🏗 생성자: 초기화 시 유효성 검사 포함
    public BankAccount(int initialBalance) { }

    // ✅ 입금 처리 (검증 포함)
    public void Deposit(int amount) { }

    // ✅ 출금 처리 (검증 및 조건 포함)
    public void Withdraw(int amount) { }

    // 📖 잔액 조회
    public int GetBalance() { return 0; }
}

// 🧪 실행 테스트용 메인 클래스
class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount(10000);

        account.Deposit(5000);
        account.Withdraw(3000);

        Console.WriteLine($"최종 잔액: {account.GetBalance()}원");
    }
}

 

 

 

 

3. C# 객체지향 버전

using System;

class BankAccount
{
    private int balance;

    public BankAccount(int initialBalance)
    {
        if (initialBalance < 0)
        {
            throw new ArgumentException("초기 잔액은 음수일 수 없습니다.");
        }
        balance = initialBalance;
    }

    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("입금 금액은 0보다 커야 합니다.");
            return;
        }

        balance += amount;
        Console.WriteLine($"입금: {amount}원");
    }

    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("출금 금액은 0보다 커야 합니다.");
            return;
        }

        if (balance >= amount)
        {
            balance -= amount;
            Console.WriteLine($"출금: {amount}원");
        }
        else
        {
            Console.WriteLine("잔액 부족: 출금 실패");
        }
    }

    public int GetBalance()
    {
        return balance;
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount(10000);

        account.Deposit(5000);
        account.Withdraw(3000);

        // 외부에서 잔액을 마음대로 조작할 수 없음 (무결성 보호)
        // account.balance = -9999; ❌ 오류 발생 - private 접근 제한

        Console.WriteLine($"최종 잔액: {account.GetBalance()}원");
    }
}

 

12. 추가 예제3

더보기

1. 절차지향 문제점이 드러나는 C 예제

#include <stdio.h>

// 전역 변수 - 절차지향 방식
int customerCash = 20000;
int customerTicket = 0;

int sellerCash = 0;
int sellerTicket = 5;

const int ticketPrice = 10000;

// 티켓 구매 가능 여부 확인
int canBuyTicket(int cash, int tickets, int price) {
    return cash >= price && tickets > 0;
}

// 티켓 판매 함수
void sellTicket() {
    if (canBuyTicket(customerCash, sellerTicket, ticketPrice)) {
        customerCash -= ticketPrice;
        customerTicket += 1;

        sellerCash += ticketPrice;
        sellerTicket -= 1;

        printf("티켓을 구매했습니다!\n");
    } else {
        printf("티켓을 구매할 수 없습니다.\n");
    }
}

// 추가 기능: 환불 처리
void refundTicket() {
    if (customerTicket > 0) {
        customerCash += ticketPrice;
        customerTicket -= 1;

        sellerCash -= ticketPrice;
        sellerTicket += 1;

        printf("티켓을 환불했습니다.\n");
    } else {
        printf("환불할 티켓이 없습니다.\n");
    }
}

int main() {
    sellTicket(); // 첫 구매 시도
    refundTicket(); // 환불 시도

    // 문제점: sellTicket, refundTicket 모두 전역 변수를 직접 수정하고 있음
    // 어떤 함수가 어떤 데이터를 수정하는지 추적이 어려움
    // 예: customerCash가 줄었는데 어떤 함수에서 줄였는지 명확히 알기 어려움
    // => 디버깅, 테스트, 유지보수가 어려워짐
    
    printf("고객 잔액: %d원\n", customerCash);
    printf("고객 티켓 수: %d장\n", customerTicket);
    printf("판매자 잔액: %d원\n", sellerCash);
    printf("판매자 티켓 수: %d장\n", sellerTicket);

    return 0;
}


 

 

 

2. 이 예제를 객체지향 방식(OOP)으로 리팩토링하면?

  • Customer, Seller 객체를 만들고, 그들의 데이터를 private 필드로 은닉
  • 각 객체가 자신에 대한 입금/출금/티켓 관리만 담당
  • 데이터를 직접 변경하지 않고, 메서드 (BuyTicket(), RefundTicket())로만 상태 변경
  • 객체지향 리팩토링 개요 (개발 구조 예시)
class Customer {
    private int cash;
    private int ticketCount;

    public bool CanBuy(int price) { ... }
    public void Buy(int price) { ... }
    public void Refund(int price) { ... }
}

class Seller {
    private int ticketCount;
    private int cash;

    public bool HasTickets() { ... }
    public void SellTicket(int price) { ... }
    public void ReceiveRefund(int price) { ... }
}

 

 

 

 

3. C# 객체지향 버전
using System;

// 고객 클래스
class Customer
{
    private int cash;
    private int ticketCount;

    public Customer(int initialCash)
    {
        cash = initialCash;
        ticketCount = 0;
    }

    public bool CanBuy(int price) => cash >= price;

    public void Pay(int price)
    {
        cash -= price;
        ticketCount += 1;
    }

    public void Refund(int price)
    {
        cash += price;
        ticketCount -= 1;
    }

    public int GetCash() => cash;
    public int GetTicketCount() => ticketCount;
    public bool HasTicket() => ticketCount > 0;
}

// 판매자 클래스
class Seller
{
    private int cash;
    private int ticketStock;

    public Seller(int initialStock)
    {
        ticketStock = initialStock;
        cash = 0;
    }

    public bool HasTickets() => ticketStock > 0;

    public void Sell(int price)
    {
        ticketStock -= 1;
        cash += price;
    }

    public void AcceptRefund(int price)
    {
        ticketStock += 1;
        cash -= price;
    }

    public int GetCash() => cash;
    public int GetTicketStock() => ticketStock;
}

// 프로그램 실행
class Program
{
    static void Main()
    {
        const int ticketPrice = 10000;

        Customer customer = new Customer(20000);
        Seller seller = new Seller(5);

        // 티켓 구매 시도
        if (customer.CanBuy(ticketPrice) && seller.HasTickets())
        {
            customer.Pay(ticketPrice);
            seller.Sell(ticketPrice);
            Console.WriteLine("티켓을 구매했습니다.");
        }
        else
        {
            Console.WriteLine("티켓을 구매할 수 없습니다.");
        }

        // 환불 시도
        if (customer.HasTicket())
        {
            customer.Refund(ticketPrice);
            seller.AcceptRefund(ticketPrice);
            Console.WriteLine("티켓을 환불했습니다.");
        }
        else
        {
            Console.WriteLine("환불할 티켓이 없습니다.");
        }

        // 결과 출력
        Console.WriteLine($"\n고객 잔액: {customer.GetCash()}원");
        Console.WriteLine($"고객 티켓 수: {customer.GetTicketCount()}장");
        Console.WriteLine($"판매자 잔액: {seller.GetCash()}원");
        Console.WriteLine($"판매자 티켓 수: {seller.GetTicketStock()}장");
    }
}