Post

[DesignPattern] SOLID원칙(SOLID)

SOLID 원칙: 객체지향 설계의 기본 원칙

SOLID는 객체지향 설계 원칙의 앞글자를 딴 것으로, 이를 통해 유연하고 확장 가능한 소프트웨어를 개발할 수 있습니다. 이번 글에서는 SOLID 원칙에 대해 자세히 알아보고, 각각의 원칙을 예시 코드와 함께 살펴보겠습니다.

SRP: Single Responsibility Principle (단일 책임 원칙)


한 클래스는 단 하나의 책임을 가져야 한다는 원칙입니다. 이는 클래스가 변경되어야 하는 이유가 하나여야 함을 의미합니다. 만약 클래스가 여러 책임을 갖게 되면, 한 책임이 변경되면 다른 책임에도 영향을 미칠 수 있습니다.

예시 코드를 통해 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 잘못된 예시
class UserManager {
    public void createUser() {
        // 사용자 생성 로직
    }

    public void sendEmail() {
        // 이메일 전송 로직
    }

    public void generateReport() {
        // 리포트 생성 로직
    }
}

위 코드는 단일 책임 원칙을 위반합니다. createUser, sendEmail, generateReport 메서드는 각각 사용자 생성, 이메일 전송, 리포트 생성과 관련된 다른 책임을 갖고 있습니다. 이를 개선하기 위해 각 책임에 해당하는 클래스를 만들어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 개선된 예시
class UserManager {
    public void createUser() {
        // 사용자 생성 로직
    }
}

class EmailService {
    public void sendEmail() {
        // 이메일 전송 로직
    }
}

class ReportGenerator {
    public void generateReport() {
        // 리포트 생성 로직
    }
}

위 코드에서는 각 클래스가 하나의 책임을 갖도록 분리하여 변경 사항이 다른 클래스에 영향을 미치지 않도록 했습니다.

OCP: Open/Closed Principle (개방-폐쇄 원칙)


소프트웨어 요소는 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다는 원칙입니다. 즉, 기존의 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 합니다.

예시 코드를 통해 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
// 잘못된 예시
class DiscountCalculator {
    public double calculateDiscount(double price, String discountType) {
        if (discountType.equals("Christmas")) {
            return price * 0.2;
        } else if (discountType.equals("NewYear")) {
            return price * 0.1;
        }
        return 0;
    }
}

위 코드는 새로운 할인 유형이 추가될 때마다 DiscountCalculator 클래스를 변경해야 합니다. 이를 개선하기 위해 확장에는 열려있고 변경에는 닫혀있도록 설계해야 합니다.

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
// 개선된 예시
interface Discount {
    double calculateDiscount(double price);
}

class ChristmasDiscount implements Discount {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.2;
    }
}

class NewYearDiscount implements Discount {
    @Override
    public double calculateDiscount(double price) {
        return price * 0.1;
    }
}

class DiscountCalculator {
    public double calculateDiscount(double price, Discount discount) {
        return discount.calculateDiscount(price);
    }
}

위 코드에서는 Discount 인터페이스를 도입하여 새로운 할인 유형을 추가할 때 Discount 인터페이스를 구현하는 새로운 클래스를 만들기만 하면 되므로 기존 코드를 변경할 필요가 없습니다.

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
26
27
28
29
30
31
32
// 잘못된 예시
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

위 코드에서는 Square 클래스가 Rectangle 클래스의 서브 클래스이지만, Square는 사실상 직사각형이 아닌 정사각형을 나타내기 때문에 LSP를 위반합니다.

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
// 개선된 예시
class Shape {
    protected int width;
    protected int height;

    public int getArea() {
        return width * height;
    }
}

class Rectangle extends Shape {
    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

class Square extends Shape {
    public void setSideLength(int length) {
        this.width = length;
        this.height = length;
    }
}

위 코드에서는 직사각형(Rectangle)과 정사각형(Square)이 모두 Shape 클래스를 상속받아 getArea() 메서드를 사용할 수 있도록 하였습니다.

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
26
27
28
29
30
// 잘못된 예시
interface Worker {
    void work();
    void eat();
}

class Programmer implements Worker {
    @Override
    public void work() {
        // 프로그래머의 작업
    }

    @Override
    public void eat() {
        // 프로그래머의 식사
    }
}

class Robot implements Worker {
    @Override
    public void work() {
        // 로봇의 작업
    }

    @Override
    public void eat() {
        // 로봇은 식사를 할 필요가 없음
    }
}

위 코드에서는 Robot 클래스가 eat() 메서드를 구현하지만, 로봇은 식사를 할 필요가 없기 때문에 이는 ISP를 위반합니다.

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
// 개선된 예시
interface Worker {
    void work();
}

interface Eater {
    void eat();
}

class Programmer implements Worker, Eater {
    @Override
    public void work() {
        // 프로그래머의 작업
    }

    @Override
    public void eat() {
        // 프로그래머의 식사
    }
}

class Robot implements Worker {
    @Override
    public void work() {
        // 로봇의 작업
    }
}

위 코드에서는 Worker 인터페이스와 Eater 인터페이스를 분리하여 각각의 인터페이스에 필요한 메서드만 구현하도록 하였습니다.

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
// 잘못된 예시
class LightBulb {
    public void turnOn() {
        // 전구를 켜는 로직
    }

    public void turnOff() {
        // 전구를 끄는 로직
    }
}

class LightSwitch {
    private LightBulb lightBulb;

    public LightSwitch() {
        this.lightBulb = new LightBulb();
    }

    public void toggle() {
        if (lightBulb.isOn()) {
            lightBulb.turnOff();
        } else {
            lightBulb.turnOn();
        }
    }
}

위 코드에서는 LightSwitch 클래스가 LightBulb 클래스에 직접 의존하고 있으므로 DIP를 위반합니다.

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
34
// 개선된 예시
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        // 전구를 켜는 로직
    }

    @Override
    public void turnOff() {
        // 전구를 끄는 로직
    }
}

class LightSwitch {
    private Switchable device;

    public LightSwitch(Switchable device) {
        this.device = device;
    }

    public void toggle() {
        if (device.isOn()) {
            device.turnOff();
        } else {
            device.turnOn();
        }
    }
}

위 코드에서는 LightSwitch 클래스가 Switchable 인터페이스에 의존하도록 개선되었습니다. 이렇게 하면 LightSwitch 클래스는 구체적인 LightBulb 클래스에 직접 의존하지 않고, Switchable 인터페이스에 의존하게 되므로 의존 역전 원칙을 준수하게 됩니다.

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.