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