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()}장"); } }
댓글을 사용할 수 없습니다.