8. SOLID - OCP

1. SOLID 원칙
1. SOLID
OOP, 객체 지향 프로그래밍 설계의 기본 원칙
SOLID | Eng. | Kor. |
SRP | Single Responsibility Principle | 단일 책임 원칙 |
OCP | Open-Closed Principle | 개방-폐쇄 원칙 |
LSP | Liskov Substitution Principle | 리스코프 치환 원칙 |
ISP | Interface Segregation Principle | 인터페이스 분리 원칙 |
DIP | Dependency Inversion Principle | 의존성 역전 원칙 |
2. 개방-폐쇄 원칙 (OCP, Open-Closed Principle)
1. 개념
"Software entities ... should be open for extension, but closed for modification."
새로운 기능이 필요할 때, 기존 코드를 수정하지 않고, 최소한의 변경(새로운 코드 추가)만으로 기능을 확장할 수 있어야 합니다.
기존 코드를 수정하면 버그 발생 위험이 증가하고, 기능 간의 결합도가 강해져 구현이 깨지기 쉽습니다.
유지보수 측면에서도 재앙같은 존재입니다.
2. 용어
확장에는 열려(Open) 있고 수정에는 닫혀(Close) 있는 상태 → 개방-폐쇄 원칙 (OCP, Open-Closed Principle)
3. 고려사항
OCR 예제들은 DIP 예제와 유사하여, 두 관점을 한번에 고려해 문제점과 해결책을 살펴봅니다.
3. 도형(Shape) 클래스 예시
1. 가정
- 프로그램에 버튼을 누르거나, 특정 동작을 진행하면 "도형"이 표현된다고 가정해보자
(게임에서 새로운 오브젝트(나무, 돌, 벽, 몬스터 등)가 등장하듯이)
- 도형의 위치, 색, 속성 등과 기능을 구현하고, 이를 사용할 때 어떤 구조로 구현해야 할까?
2. ❌ OCP를 위반한 코드
using System; namespace OCPExample { // ✅ 도형 클래스들을 정의한다. public class Circle { public void Draw() { Console.WriteLine("Drawing a circle"); } } public class Rectangle { public void Draw() { Console.WriteLine("Drawing a rectangle"); } } public class Triangle // 🚨 새로운 도형을 추가한다 가정하고, 문제점을 파악한다. { public void Draw() { Console.WriteLine("Drawing a triangle"); } } // ❌ OCP 위반 클래스 public class ShapeManager { public void Render(object shape) { if (shape is Circle) { ((Circle)shape).Draw(); } else if (shape is Rectangle) { ((Rectangle)shape).Draw(); } else if (shape is Triangle) // 새로운 도형이 추가될 때마다, Render 구현체의 수정이 필요하다 { ((Triangle)shape).Draw(); } else { throw new Exception("Unsupported shape"); } } } class Program { static void Main(string[] args) { ShapeManager manager = new ShapeManager(); manager.Render(new Circle()); manager.Render(new Rectangle()); manager.Render(new Triangle()); // 🚨 새로운 도형을 추가한다 가정한다. } } }
3. ❌ OCP를 위반한 코드 문제점 파악
- 새로운 도형을 추가할 때마다 ShapeManager의 코드를 수정해야 합니다. → OCP 위반
- if-else 문이나 switch 문을 사용 → 새로운 도형이 추가될 때마다 코드 수정 필요합니다
- 클래스를 수정하는 사람이 여럿이라면? 모든 구성원이 각자 모든 코드를 수정한다면?
4. ✅ OCP가 지켜진 코드
using System; namespace OCPComplianceExample { // ✅ 추상 클래스 정의 public abstract class Shape { public abstract void Draw(); } // ✅ 기존 도형 클래스 public class Circle : Shape { public override void Draw() { Console.WriteLine("Drawing a circle"); } } public class Rectangle : Shape { public override void Draw() { Console.WriteLine("Drawing a rectangle"); } } public class Triangle : Shape // 🚨 새로운 도형을 추가되어도, 기존 코드 수정 없이 확장 가능 { public override void Draw() { Console.WriteLine("Drawing a triangle"); } } // ✅ 개방-폐쇄 원칙 준수 클래스 public class ShapeManager { public void Render(Shape shape) { shape.Draw(); // 다형성을 통해 새로운 도형이 추가되더라도 Render 수정할 필요 없음 } } class Program { static void Main(string[] args) { ShapeManager manager = new ShapeManager(); manager.Render(new Circle()); manager.Render(new Rectangle()); manager.Render(new Triangle()); // 기존 코드 수정 없이 새로운 도형 추가 가능 } } }
5. ✅ OCP를 준수한 코드에서 개선된 부분 파악
- 새로운 도형이 추가될 때 ShapeManager 수정이 필요 없음 → OCP 준수
- 다형성을 사용해 새로운 도형을 쉽게 추가 가능
- if-else 문 없이 인터페이스나 추상 클래스 기반 설계

4. 결제 시스템(Payment) 예시
using System; // 결제 인터페이스 정의 public interface IPayment { void Pay(decimal amount); } // 신용카드 결제 클래스 public class CreditCardPayment : IPayment { public void Pay(decimal amount) { Console.WriteLine($"신용카드를 사용하여 {amount}원을 결제하였습니다."); } } // 결제 처리 클래스 public class PaymentProcessor { public void ProcessPayment(IPayment payment, decimal amount) { payment.Pay(amount); // 다형성을 통해 결제 방식에 관계없이 처리 가능 } }
IPayment 인터페이스를 정의하고, 구체적인 새로운 결제 방식(CreditCardPayment, PayPalPayment, ApplePayPayment 등) 이 추가되더라도 기존 PaymentProcessor class 코드의 수정 없이 인터페이스를 구현만으로 추가 기능 확장 구현 가능
5. 몬스터 클래스 예시
1. OCP를 지키지 않은 간단한 예제
using System; namespace GameExampleWithoutOCP { public class MonsterHandler { public void HandleMonster(string type) { if (type == "좀비") { Console.WriteLine("좀비 몬스터 스폰"); } else if (type == "흡혈귀") { Console.WriteLine("흡혈귀 몬스터 스폰"); } else if (type == "드래곤") // 새로운 몬스터 추가 시 MonsterHandler 구현부 소스 코드 수정 필요 { Console.WriteLine("드래곤 몬스터 스폰"); } } } class Program { static void Main(string[] args) { MonsterHandler handler = new MonsterHandler(); handler.HandleMonster("좀비"); handler.HandleMonster("흡혈귀"); handler.HandleMonster("드래곤"); // 새로운 몬스터를 추가하고 싶다면, MonsterHandler 코드 수정 필요 } } }
기능을 확장할 때 기존 코드를 수정해야 한다.
- 새로운 몬스터(dragon)가 추가될 때마다 MonsterHandler 클래스의 HandleMonster() 메서드 구현부를 수정해야 합니다.
- if-else 또는 switch-case가 점점 커지면서 결합도(coupling)가 높아지고 코드가 복잡해져 유지보수 및 확장성이 떨어집니다.
2. OCP를 지키지 않은 예제
using System; namespace GameExampleWithoutOCP { // 부모 클래스 정의 public class Monster { public string Type { get; set; } } // 하위 클래스 정의 public class Zombie : Monster { public Zombie() { Type = "좀비"; } } public class Vampire : Monster { public Vampire() { Type = "흡혈귀"; } } public class Dragon : Monster // 새로운 몬스터를 추가한다고 가정합니다. { public Dragon() { Type = "드래곤"; } } // 개방-폐쇄 원칙 위반 클래스 public class MonsterHandler { public void HandleMonster(Monster monster) { if (monster is Zombie) { Console.WriteLine("몬스터가 좀비로 등장한다."); } else if (monster is Vampire) { Console.WriteLine("몬스터가 흡혈귀로 등장한다."); } else if (monster is Dragon) // 새로운 몬스터 추가 시 handler 구현부 수정 필요 { Console.WriteLine("몬스터가 드래곤으로 등장한다."); } } } class Program { static void Main(string[] args) { MonsterHandler handler = new MonsterHandler(); handler.HandleMonster(new Zombie()); handler.HandleMonster(new Vampire()); handler.HandleMonster(new Dragon()); // 새로운 몬스터를 추가하고 싶다면, MonsterHandler 코드 수정 필요 } } }
3. OCP를 준수한 예제
using System; namespace GameExampleWithOCP { // 부모 클래스 정의 (추상 클래스) public abstract class Monster { // 모든 몬스터가 공격 기능을 가지도록 정의 public abstract void Handle(); public abstract void Attack(); } // 하위 클래스 정의 public class Zombie : Monster { public override void Handle() { Console.WriteLine("Handling a zombie"); } public override void Attack() { Console.WriteLine("Zombie bites the player!"); } } public class Vampire : Monster { public override void Handle() { Console.WriteLine("Handling a vampire"); } public override void Attack() { Console.WriteLine("Vampire drains the player's health!"); } } public class Dragon : Monster { public override void Handle() { Console.WriteLine("Handling a dragon"); } public override void Attack() { Console.WriteLine("Dragon breathes fire at the player!"); } } // 개방-폐쇄 원칙을 준수한 클래스 구현체 public class MonsterHandler { // 몬스터 핸들링과 공격 처리 public void HandleMonster(Monster monster) { monster.Handle(); // 몬스터의 기본 동작 처리 monster.Attack(); // 몬스터의 공격 처리 } } class Program { static void Main(string[] args) { MonsterHandler handler = new MonsterHandler(); handler.HandleMonster(new Zombie()); handler.HandleMonster(new Vampire()); handler.HandleMonster(new Dragon()); // 새로운 몬스터 추가에도 handler 구현체 수정 필요 없음 } } }
5. 네트워크 클래스 예시
1. 가정
TCP, UDP, HTTP, WebSocket등 다양한 프로토콜을 사용하는 통신 구조 구현
프로토콜에 구애받지 않는 연결, 요청, 응답 등을 구현한다면?
2. ❌ OCP를 위반한 코드
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Net.Http; using System.Threading.Tasks; using System.Net.WebSockets; using System.Threading; namespace NetworkExampleWithoutOCP { class Program { static void Main(string[] args) { NetworkManager manager = new NetworkManager(); manager.SendMessage("TCP", "127.0.0.1", 8080, "Hello TCP!"); manager.SendMessage("UDP", "127.0.0.1", 8080, "Hello UDP!"); manager.SendMessage("HTTP", "https://example.com", 80, "Hello HTTP!"); manager.SendMessage("WebSocket", "ws://example.com", 80, "Hello WebSocket!"); } } // ❌ 문제점: 개방-폐쇄 원칙 위반 // - 새로운 프로토콜이 추가될 때마다 NetworkManager의 if문를 수정해야 함 // - 수정 시 버그 발생 위험 증가, 결합도 증가 public class NetworkManager { public void SendMessage(string protocol, string ipAddress, int port, string message) { if (protocol == "TCP") { TcpClient client = new TcpClient(); client.Connect(IPAddress.Parse(ipAddress), port); NetworkStream stream = client.GetStream(); byte[] data = Encoding.UTF8.GetBytes(message); stream.Write(data, 0, data.Length); Console.WriteLine("TCP 메시지 전송: " + message); stream.Close(); client.Close(); } else if (protocol == "UDP") { UdpClient client = new UdpClient(); IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); byte[] data = Encoding.UTF8.GetBytes(message); client.Send(data, data.Length, endPoint); Console.WriteLine("UDP 메시지 전송: " + message); client.Close(); } else if (protocol == "HTTP") { HttpClient client = new HttpClient(); var content = new StringContent(message, Encoding.UTF8, "application/json"); var response = client.PostAsync(ipAddress, content).Result; Console.WriteLine("HTTP 메시지 전송: " + response.Content.ReadAsStringAsync().Result); } else if (protocol == "WebSocket") { ClientWebSocket client = new ClientWebSocket(); client.ConnectAsync(new Uri(ipAddress), CancellationToken.None).Wait(); byte[] data = Encoding.UTF8.GetBytes(message); client.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, CancellationToken.None).Wait(); Console.WriteLine("WebSocket 메시지 전송: " + message); client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None).Wait(); } else { Console.WriteLine("알 수 없는 프로토콜: " + protocol); } } } }
3. ❌ OCP를 위반한 코드의 문제점 파악
- 결합도가 높음
: NetworkManager가 모든 프로토콜의 구체적인 구현에 직접 의존 → 변경에 취약 - 확장에 닫혀 있음
: 새로운 프로토콜 추가 시 if-else 또는 switch 구문을 수정해야 함 → 수정 시 기존 코드에 영향 발생 가능 - 수정 시 버그 발생 위험 증가
: 하나의 메서드에서 너무 많은 로직을 처리 → 코드의 복잡성 증가
4. ✅ OCP가 지켜진 코드
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Net.Http; using System.Threading.Tasks; using System.Net.WebSockets; using System.Threading; namespace NetworkExampleWithOCP { class Program { static void Main(string[] args) { // TCP 클라이언트 실행 INetworkClient tcpClient = new TcpNetworkClient(); NetworkManager manager = new NetworkManager(tcpClient); manager.Connect("127.0.0.1", 8080); manager.SendMessage("Hello TCP!"); manager.Disconnect(); // UDP 클라이언트 실행 INetworkClient udpClient = new UdpNetworkClient(); manager = new NetworkManager(udpClient); manager.Connect("127.0.0.1", 8080); manager.SendMessage("Hello UDP!"); manager.Disconnect(); // HTTP 클라이언트 실행 INetworkClient httpClient = new HttpNetworkClient(); manager = new NetworkManager(httpClient); manager.Connect("https://example.com", 80); manager.SendMessage("Hello HTTP!"); manager.Disconnect(); // WebSocket 클라이언트 실행 INetworkClient webSocketClient = new WebSocketNetworkClient(); manager = new NetworkManager(webSocketClient); manager.Connect("ws://example.com", 80); manager.SendMessage("Hello WebSocket!"); manager.Disconnect(); } } // ✅ 추상화: 모든 네트워크 클라이언트는 이 인터페이스를 구현해야 함 public interface INetworkClient { void Connect(string ipAddress, int port); void SendMessage(string message); void Disconnect(); } // ✅ TCP 소켓 클라이언트 구현 public class TcpNetworkClient : INetworkClient { private TcpClient _client; private NetworkStream _stream; public void Connect(string ipAddress, int port) { _client = new TcpClient(); _client.Connect(IPAddress.Parse(ipAddress), port); _stream = _client.GetStream(); Console.WriteLine("TCP 연결 성공"); } public void SendMessage(string message) { if (_stream != null) { byte[] data = Encoding.UTF8.GetBytes(message); _stream.Write(data, 0, data.Length); Console.WriteLine("TCP 메시지 전송: " + message); } } public void Disconnect() { _stream?.Close(); _client?.Close(); Console.WriteLine("TCP 연결 종료"); } } // ✅ UDP 소켓 클라이언트 구현 public class UdpNetworkClient : INetworkClient { private UdpClient _client; private IPEndPoint _endPoint; public void Connect(string ipAddress, int port) { _client = new UdpClient(); _endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); Console.WriteLine("UDP 연결 성공"); } public void SendMessage(string message) { byte[] data = Encoding.UTF8.GetBytes(message); _client.Send(data, data.Length, _endPoint); Console.WriteLine("UDP 메시지 전송: " + message); } public void Disconnect() { _client?.Close(); Console.WriteLine("UDP 연결 종료"); } } // ✅ HTTP 클라이언트 구현 public class HttpNetworkClient : INetworkClient { private HttpClient _client; public void Connect(string ipAddress, int port) { _client = new HttpClient(); Console.WriteLine("HTTP 연결 성공"); } public async void SendMessage(string message) { var content = new StringContent(message, Encoding.UTF8, "application/json"); var response = await _client.PostAsync("https://example.com", content); Console.WriteLine("HTTP 메시지 전송: " + await response.Content.ReadAsStringAsync()); } public void Disconnect() { _client?.Dispose(); Console.WriteLine("HTTP 연결 종료"); } } // ✅ WebSocket 클라이언트 구현 public class WebSocketNetworkClient : INetworkClient { private ClientWebSocket _client; public async void Connect(string ipAddress, int port) { _client = new ClientWebSocket(); await _client.ConnectAsync(new Uri("ws://example.com"), CancellationToken.None); Console.WriteLine("WebSocket 연결 성공"); } public async void SendMessage(string message) { byte[] data = Encoding.UTF8.GetBytes(message); await _client.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, CancellationToken.None); Console.WriteLine("WebSocket 메시지 전송: " + message); } public async void Disconnect() { await _client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done", CancellationToken.None); Console.WriteLine("WebSocket 연결 종료"); } } // ✅ 개방-폐쇄 원칙을 준수하는 클래스 public class NetworkManager { private readonly INetworkClient _networkClient; public NetworkManager(INetworkClient networkClient) { _networkClient = networkClient; } public void Connect(string ipAddress, int port) { _networkClient.Connect(ipAddress, port); } public void SendMessage(string message) { _networkClient.SendMessage(message); } public void Disconnect() { _networkClient.Disconnect(); } } }
5. ✅ 개선된 사항
✔️ HttpNetworkClient → HTTP 프로토콜 처리
✔️ WebSocketNetworkClient → WebSocket 프로토콜 처리
✔️ 기존 코드 수정 없이 새로운 프로토콜을 쉽게 추가 가능 → OCP 완벽 준수
댓글을 사용할 수 없습니다.