C#와 SOLID 원칙
Source: Dev.to
소프트웨어 개발에서, 로버트 C. 마틴이 정의한 SOLID 원칙은 유연하고 가독성이 높으며 유지보수가 쉬운 객체‑지향 시스템을 만들기 위한 다섯 가지 기본 지침입니다. C#에서는 이러한 원칙이 클래스 설계 규칙으로 흔히 사용됩니다.
단일 책임 원칙 (SRP)
클래스는 하나의 책임만 가져야 하며, 즉 변경되는 이유도 하나만 있어야 합니다. 클래스가 여러 작업을 수행하게 되면(예: 데이터를 저장하고 보고서를 생성) 유지보수가 어려워지고 오류가 발생하기 쉬워집니다.
개방/폐쇄 원칙 (OCP)
클래스는 확장에 열려 있어야 하지만 수정에는 닫혀 있어야 합니다. 새로운 동작은 기존 클래스 코드를 변경하지 않고 추가되어야 하며, 일반적으로 추상 클래스나 인터페이스를 사용합니다.
// 잘못된 예: 새로운 결제 수단을 추가하려면 클래스를 수정해야 함
public class PaymentService
{
public void Pay(string type)
{
if (type == "CreditCard") { /* ... */ }
else if (type == "PayPal") { /* ... */ }
}
}
// 올바른 예: 새로운 수단을 추가하려면 새로운 클래스를 생성해야 함
public interface IPayment
{
void Pay();
}
public class CreditCardPayment : IPayment
{
public void Pay() { /* ... */ }
}
public class PayPalPayment : IPayment
{
public void Pay() { /* ... */ }
}
Liskov Substitution Principle (LSP)
파생 클래스는 기본 클래스를 대체할 수 있어야 합니다. 서브클래스는 기본 클래스가 기대되는 모든 상황에서 올바르게 동작해야 하며, 기본 클래스의 계약을 깨서는 안 됩니다.
public abstract class Bird
{
public abstract void Fly();
}
public class Sparrow : Bird
{
public override void Fly() => Console.WriteLine("Sparrow is flying");
}
// Wrong: Penguins cannot fly, but Fly() is forced
public class Penguin : Bird
{
public override void Fly() => throw new NotImplementedException();
}
Penguin 클래스는 Bird 추상화가 모든 새가 날 수 있다고 가정하기 때문에 LSP를 위반합니다. 더 나은 설계는 비행 새와 비비행 새를 IFlyingBird와 INonFlyingBird와 같은 별개의 인터페이스로 구분하는 것입니다.
인터페이스 분리 원칙 (ISP)
클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요받아서는 안 됩니다. 크고 “뚱뚱한” 인터페이스보다 작고 집중된 인터페이스를 선호하세요.
// Wrong: All animals are forced to walk, fly, and swim
public interface IAnimal
{
void Walk();
void Fly();
void Swim();
}
// Correct: Split into smaller interfaces
public interface IWalkable { void Walk(); }
public interface IFlyable { void Fly(); }
public interface ISwimmable { void Swim(); }
public class Dog : IWalkable, ISwimmable
{
public void Walk() { /* ... */ }
public void Swim() { /* ... */ }
}
Dependency Inversion Principle (DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 두 모듈 모두 추상화(인터페이스 또는 추상 클래스)에 의존해야 합니다. 이 원칙은 의존성 주입의 기반이 됩니다.
// Wrong: Service depends directly on a concrete logger
public class Service
{
private readonly ConsoleLogger _logger = new ConsoleLogger();
}
// Correct: Service depends on an abstraction
public interface ILogger
{
void Log(string msg);
}
public class ConsoleLogger : ILogger
{
public void Log(string msg) => Console.WriteLine(msg);
}
public class Service
{
private readonly ILogger _logger;
public Service(ILogger logger) => _logger = logger;
}
SOLID 적용의 장점
- 코드 가독성과 유지보수성 향상
- 변경에 강한 아키텍처 구축
- 단위 테스트 간소화
- 코드 중복 감소 및 재사용성 증가
원칙 요약
- SRP – 클래스는 하나의 책임만 가져야 합니다.
- OCP – 확장은 열려 있어야 하고, 수정은 닫혀 있어야 합니다.
- LSP – 서브클래스는 기반 클래스와 교체 가능해야 합니다.
- ISP – 작고 집중된 인터페이스를 선호합니다.
- DIP – 구체적인 구현이 아니라 추상에 의존합니다.