포스트

[CS] SOLID 원칙

[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);
    }
}

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.