[CS] SOLID 원칙
SOLID 원칙이란 무엇인가?
객체지향 설계의 5대 원칙인 SOLID 원칙은 유지보수가 용이하고 확장 가능한 소프트웨어를 개발하는 데 중요한 가이드라인입니다. 이 원칙들은 각각의 약어로 구성되어 있으며, 로버트 C. 마틴(Robert C. Martin)이 제안한 개념입니다.
1. SRP - 단일 책임 원칙 (Single Responsibility Principle)
하나의 클래스는 하나의 책임만 가져야 한다.
목적
- 클래스를 변경해야 하는 이유는 단 하나뿐이어야 한다.
같은 클래스 안에 다른 이유 때문에 변경될 메서드를 넣지 않도록 한다. 클래스를 설명하는 글에 ‘그리고’라는 단어가 포함되면 SRP가 깨질 수 있다. 모든 모듈이나 클래스는 소프트웨어가 제공하는 기능의 단일 부분에 대해서만 책임을 져야 하며, 그 책임을 클래스로 완전히 캡슐화해야 한다.
한 메서드를 변경해도 다른 메서드에 영향을 미치지 않게 하여 불필요하게 다시 컴파일하는 일이 없게 하는게 목표다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 나쁜 예
public class ReportManager {
public void GenerateReport() { }
public void SaveToFile(string path) { }
public void PrintReport() { }
}
// 좋은 예
public class ReportGenerator {
public void Generate() { }
}
public class ReportSaver {
public void SaveToFile(string path) { }
}
public class ReportPrinter {
public void Print() { }
}
2. OCP - 개방-폐쇄 원칙 (Open-Closed Principle)
소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
목적
- 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다.
개방/폐쇄 원리는 자식 클래스를 통해 코드를 확장해야 하며, 원래 클래스는 변경할 필요가 없다는 것이다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 나쁜 예
public class ShapeDrawer {
public void Draw(string shapeType) {
if (shapeType == "Circle") { /* ... */ }
else if (shapeType == "Rectangle") { /* ... */ }
}
}
// 좋은 예
public interface IShape {
void Draw();
}
public class Circle : IShape {
public void Draw() { /* ... */ }
}
public class Rectangle : IShape {
public void Draw() { /* ... */ }
}
public class ShapeDrawer {
public void Draw(IShape shape) {
shape.Draw();
}
}
3. LSP - 리스코프 치환 원칙 (Liskov Substitution Principle)
자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
목적
- 상속 구조에서 다형성을 활용하되, 하위 클래스가 상위 클래스의 행위를 깨지 않도록 한다.
부모 클래스의 인스턴스를 해당 자식 클래스 중 하나의 인스턴스로 교체할 수 있게 설계해야 한다. 부모 클래스가 무언가를 할 수 있다면 자식 클래스도 그것을 할 수 있어야 한다. 대체할 수 없다면, 자식이 아닌 별도의 클래스여야 한다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 나쁜 예
public class Bird {
public virtual void Fly() { }
}
public class Ostrich : Bird {
public override void Fly() {
throw new Exception("타조는 날 수 없습니다.");
}
}
// 좋은 예
public abstract class Bird { }
public abstract class FlyingBird : Bird {
public abstract void Fly();
}
public class Sparrow : FlyingBird {
public override void Fly() { }
}
public class Ostrich : Bird {
// Fly 메서드 없음
}
4. ISP - 인터페이스 분리 원칙 (Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다.
목적
- 인터페이스를 작고 특정한 용도로 분리하여 불필요한 의존을 없앤다.
해당 원칙에 따르면 몇 개의 큰 인터페이스가 있는 편보다는 작은 인터페이스가 많은 편이 바람직하다. 큰 인터페이스에 포함된 모슨 행위를 강요하지 않고 작은 인터페이스의 합성을 통해 필요한 행위들로 구축할 수 있다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 나쁜 예
public interface IMachine {
void Print();
void Scan();
void Fax();
}
public class OldPrinter : IMachine {
public void Print() { }
public void Scan() { throw new NotImplementedException(); }
public void Fax() { throw new NotImplementedException(); }
}
// 좋은 예
public interface IPrinter {
void Print();
}
public interface IScanner {
void Scan();
}
public class SimplePrinter : IPrinter {
public void Print() { }
}
5. DIP - 의존 역전 원칙 (Dependency Inversion Principle)
고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
목적
- 구체화된 구현이 아닌 추상화(인터페이스, 추상 클래스)에 의존함으로써 결합도를 낮춘다.
- 컴파일 타임이 아닌 런타임에 객체를 선택하게 한다.
의존성 역전의 목표는 구상적인 것에 결합하기보다는 추상적인 것에 결합하는 것이다. 이 원칙을 논의할 때 이해해야할 몇 가지 핵심 용어는 다음과 같다.
- 의존성 역전: 의존체들을 역전시키는 원칙
- 의존성 주입: 의존체들을 역전시키는 행위
- 생성자 주입: 생성자를 통해 의존성 주입을 수행
- 파라미터 주입: 세터와 같은 메서드의 파라미터를 통해 의존성 주입을 수행
의존성 주입을 논의할 때 객체를 실제로 인스턴스화하는 시점이 중요한 고려사항이다. 목표는 주입을 통해 객체를 작성하는 것이지만, 어느 시점에서는 객체를 인스턴스화 해야 한다. 결과적으로 설계 결정은 이 인스턴스화를 수행할 시기를 중심으로 이루어진다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 나쁜 예
public class EmailSender {
public void SendEmail(string message) { }
}
public class Notification {
private EmailSender emailSender = new EmailSender();
public void Notify(string message) {
emailSender.SendEmail(message);
}
}
// 좋은 예
public interface IMessageSender {
void Send(string message);
}
public class EmailSender : IMessageSender {
public void Send(string message) { }
}
public class Notification {
private IMessageSender messageSender;
public Notification(IMessageSender sender) {
messageSender = sender;
}
public void Notify(string message) {
messageSender.Send(message);
}
}