SOLID 원칙
SOLID 란,
객체지향 프로그래밍 ( OOP ) 에서 코드의 유지보수성 , 확장성 , 유연성을 높이기 위해 고안된 5대 설계 원칙을 말한다.
로버트 C. 마틴 ( Robert C. Martin , "Uncle Bob" )이 제안했고, 이후 좋은 소프트웨어 설계의 기본 원칙으로 널리 사용한다.
S - 단일 책임 원칙 ( Single Responsibility Principle )
클래스는 하나의 역할만 가져야 한다.
즉 , 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되도록 클래스를 개별적으로 설계하는 원칙이다.
▼안좋은 예시
class User
{
public void SaveToDB()
{
// DB 저장 로직
}
public void SendEmail()
{
// 이메일 발송 로직
}
}
User 클래스가 DB 와 메일까지 다 떠맡기 때문에 좋지 못하다
User 클래스가 DB 저장 + 이메일 발송 두 가지 일을 하고 있다.
이렇게 되면 "저장 방식"이 바뀔 때도, "메일 방식"이 바뀔 때도 이 클래스를 수정해야 하는 번거로움이 있다.
즉, 변경 사유가 두 가지 이상이 되면 유지보수가 어려워진다.
▼좋은 예시
class User
{
// 사용자 정보만 관리
}
class UserRepository
{
public void Save(User user)
{
// DB 저장 로직
}
}
class EmailService
{
public void Send(User user)
{
// 이메일 발송 로직
}
}
→ User 는 "사용자 정보" 관리만 담당한다.
→ UserRepository 는 "DB 저장"만 담당한다.
→ EmailService 는 "메일 발송"만 담당한다.
이렇게 책임을 분리하면, 한 기능을 수정해도 다른 기능에 영향이 없다.
따라서 코드가 깔끔하고 유지보수하기 쉬워진다.
O - 개방 / 폐쇄 원칙 ( Open / Closed Principle )
확장에는 열려 있고 , 수정에는 닫혀 있어햐 한다.
즉 , 새로운 기능은 추가할 수 있지만 기존 코드는 수정하지 않아야 한다.
기능 추가시 기존의 코드를 수정하기보다는 추가적인 코드를 작성해 기능을 추가할 수 있어야 한다는 원칙이다.
▼안좋은 예시
class Payment
{
public void Pay(string type)
{
if (type == "kakao")
{
// 카카오 결제
}
else if (type == "paypal")
{
// 페이팔 결제
}
}
}
→ 새로운 결제 수단이 추가될 때마다 if 문을 고쳐야 한다
새로운 결제 수단 ( 예시 : TossPay ) 를 추가하려면 Payment 클래스 내부 코드를 수정해야 한다.
이렇게 되면 클래스가 자꾸 수정되면서, 기존에 있던 기능이 망가질 위험이 생긴다.
즉, 확장에 닫혀 있고 수정에 열려 있는 구조가 된다.
▼좋은 예시
interface IPayment
{
void Pay();
}
class KakaoPay : IPayment
{
public void Pay()
{
// 카카오 결제
}
}
class PayPal : IPayment
{
public void Pay()
{
// 페이팔 결제
}
}
class PaymentService
{
public void Process(IPayment payment)
{
payment.Pay();
}
}
→ 인터페이스 기반 → 새로운 결제 수단을 클래스만 추가하면 된다.
IPaymen 라는 추상 인터페이스에 의존하도록 변경한다.
새로운 결제 방식이 필요하면 IPayment 를 구현한 새로운 클래스만 추가하면 된다.
기존 PaymentService 는 수정하지 않아도 된다
확장에는 열려 있고, 수정에는 닫혀있게 된다
L - 리스코프 치환 원칙 ( Liskov Substitution Principle )
자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 한다.
다형성의 특징을 이용하기 위해 상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면,
상태에서 부모의 메서드를 사용해도 프로그램이 동작해야 한다는 원칙이다.
▼안좋은 예시
class Bird
{
public virtual void Fly()
{
Console.WriteLine("새가 날아갑니다.");
}
}
class Penguin : Bird
{
public override void Fly()
{
Console.WriteLine("펭귄은 날 수 없어요.");
}
}
Penguin 을 Bird 처럼 사용하면 예외 발생
Bird 를 상속한 모든 새가 반드시 Fly() 를 구현해야 한다.
하지만 펭귄은 실제로 날 수 없는데, 억지로 Fly() 를 구현해야 하니까 의미가 맞지 않는다.
나중에 코드에서 Penguin.Fly() 를 호출하면 이상한 메세지가 나와서 부모 클래스와 자식 클래스의 일관성이 깨진다.
▼좋은 예시
abstract class Bird
{
// 공통 기능 (예: 알 낳기 등)
}
class FlyingBird : Bird
{
public void Fly()
{
Console.WriteLine("새가 날아갑니다!");
}
}
class Penguin : Bird
{
public void Swim()
{
Console.WriteLine("펭귄이 수영합니다!");
}
}
→ Bird 를 상황별로 나눠서 설계 → Penguin 은 문제없이 사용 가능하다.
날 수 있는 새와 날 수 없는 새를 아예 다른 클래스 ( FlyingBird , Penguin )로 나눈다.
이제 Penguin 은 불필요하게 Fly() 를 구현하지 않아도 된다.
부모 클래스의 정의와 자식 클래스의 동작이 일치한다.
I - 인터페이스 분리 원칙 ( Interface Segregation Principle )
큰 인터페이스 하나보다는, 작은 인터페이스 여러 개가 낫다.
프로그램의 유지보수에서 발생할 수 있는 인터페이스의 분리나 수정으로 인한 많은 양의 코드 수정을 막기 위해 불필요한 정보까지 가질 수 있는 하나의 거대한 인터페이스보다, 상황에 맞도록 소규모로 분리된 인터페이스를 사용할 수 있어야 한다는 원칙이다.
▼안좋은 예시
interface IWorker
{
void Work();
void Eat();
}
class Robot : IWorker
{
public void Work()
{
Console.WriteLine("로봇이 작업을 합니다.");
}
public void Eat()
{
// 로봇은 밥을 먹지 않지만, 인터페이스 때문에 억지로 구현
Console.WriteLine("로봇은 밥을 먹을 수 없습니다... (이상한 동작)");
}
}
로봇은 밥을 먹지 못하는데, Eat() 메서드를 강제로 구현해야 하는 상황이 발생
결국 불필요한 코드가 들어가고, 클래스 의미가 어긋난다.
▼좋은 예시
interface IWorkable
{
void Work();
}
interface IEatable
{
void Eat();
}
class Human : IWorkable, IEatable
{
public void Work()
{
Console.WriteLine("사람이 일을 합니다.");
}
public void Eat()
{
Console.WriteLine("사람이 밥을 먹습니다.");
}
}
class Robot : IWorkable
{
public void Work()
{
Console.WriteLine("로봇이 작업을 합니다.");
}
}
IWorker 를 작업 ( Work ) 과 식사 ( Eat ) 로 나눈다.
사람은 두 인터페이스를 모두 구현할 수 있고, 로봇은 IWorkable 만 구현할 수 있다.
필요한 기능만 선택적으로 구현할 수 있어서 코드가 깔끔해진다.
D - 의존 역전 원칙 ( Dependency Inversion Principle )
고수준 모듈은 저수준 모듈에 의존하면 안되고, 둘 다 추상화에 의존해야 한다.
객체가 객체를 참조하거나 의존 관계를 맺을 때 , 세부구현된 객체보다 상위 객체를 참조함으로서,
세부구현된 클래스의 변화 발생 시에도 유연하게 동작할 수 있는 구조를 맺는 원칙이다.
▼안좋은 예시
class MySQLDatabase
{
public void Save()
{
// MySQL 저장
}
}
class OrderService
{
private MySQLDatabase db = new MySQLDatabase();
public void SaveOrder()
{
db.Save();
}
}
OrderServie 가 MySQLDatabase 라는 구체 클래스에 직접 의존한다.
나중에 DB를 MongoDB로 바꾸고 싶으면 OrderService 내부 코드를 고쳐야 한다.
즉 , 고수준 모듈 ( OrderService )이 저수준 모듈 ( MySQLDatabase )에 종속되어 확장성이 떨어진다.
▼좋은 예시
interface IDatabase
{
void Save();
}
class MySQLDatabase : IDatabase
{
public void Save()
{
// MySQL 저장
}
}
class MongoDatabase : IDatabase
{
public void Save()
{
// MongoDB 저장
}
}
class OrderService
{
private readonly IDatabase db;
public OrderService(IDatabase database)
{
db = database;
}
public void SaveOrder()
{
db.Save();
}
}
OrderService 는 DB 구체 클래스 ( MySQL , Mongo 등 )를 몰라도 된다 → 교체가 자유롭다
OrderService 는 IDatabase 라는 추상화에만 의존한다.
실제로 어떤 DB ( MySQL , MongoDB 등 )를 쓸지는 외부에서 주입 ( Dependency Injection ) 해주면 된다.
이제 DB 를 바꿔도 OrderService 는 수정할 필요가 없다.
즉 , 고수준 모듈과 저수준 모듈 모두 추상화에 의존하도록 구조가 개선된다.
정리
- SRP : 한 클래스 , 한 책임
- OCP : 수정하지 말고 확장으로 해결하자
- LSP : 부모 대신 자식 써도 문제 없어야 한다
- ISP : 필요한 인터페이스만 나눠서 구현하자
- DIP : 구체 구현이 아니라 추상화 ( 인터페이스 ) 에 의존하자
프로그래밍 회고
인지
- 구현해야 할 프로그램의 요구사항을 정확하게 인지하고 나열한다.
- 플레이어 캐릭터를 구현하라는 기획을 받게 되면, 먼저 플레이어가 가져야 할 기능에 집중할 필요가 있다.
- 이동이 필요한지 , 공격을 해야 하는지 등의 전체적인 기능과 동작에 대해 나열한다.
분석
- 인지 단계에서 나열된 정보들에 대한 세부내용을 정리한다.
- 문제의 입력과 출력 , 데이터 형식을 식별하고 해결을 위해 필요하다면 추가 요구사항이나 제약사항을 고려해야 한다.
설계
이전 단계를 통해 도출된 데이트를 기반으로 기능의 절차를 단계적으로 기술한다.
이는 "알고리즘"을 작성한다고 표현한다.
흔히 상향식 설계 방식에 따라 작성하는 것이 선호되고 있다.
'⭐C Sharp > 13. 디자인 패턴' 카테고리의 다른 글
| Factory Method (0) | 2025.09.30 |
|---|---|
| 의존성 주입 ( Dependency Injection ) (0) | 2025.09.29 |
| Gang of Four (0) | 2025.09.29 |
| 게임에 자주 쓰이는 패턴 (0) | 2025.09.29 |
| 디자인 패턴 ( Design Pattern ) (0) | 2025.09.29 |