3. 캡슐화 - 접근 제어자
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" 난이도를 새로 만들고 싶으면:
- addInsaneLevel() 함수 새로 만들어야 하고
- addBonusByDifficulty() 안의 if 문도 수정해야 하고
- 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# 클래스 방식의 비교
비교 요약
- C 구조체 방식은 객체지향처럼 유사하게 구현할 수 있다
하지만, C 언어는 클래스, 접근 제어자, 상속, 다형성 등 객체지향의 핵심 문법을 지원하지 않음 - 구조체 + 함수 조합은 간접적 함수 인터페이스로 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()}장");
}
}