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 완벽 준수