객체지향 개발 5대 원칙 SOLID

SOLID 원칙

객체지향 설계에서 지켜줘야할 5대 원칙에 대해 정리 해봅니다.

  • 틀린 부분이 있을 수 있습니다.

왜 사용하는가?

  • 이런 원칙에 따라서 개발을 한다면, 더 유지보수 하기 쉽고 확장이 쉬운 소스코드를 짤 수 있다.

SRP 단일 책임 원칙

클래스와 메소드는 한가지의 일만 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Champion {
    String name;

    Champion(String name) {
        this.name = name;
    }

    public void skill() {
        if (name.equals("레이스")) {
            System.out.println("페이징");
        } else if (name.equals("지붕이")) {
            System.out.println("돔쉴드");
        }
    }
}
  • 다음과 같은경우는, Champion 이란 클래스는 두개의 책임을 지고 있는것을 볼 수 있다.
  • 즉 단일 책임 원칙을 어기고 있다.
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
abstract public class Champion {
    String name;

    Champion(String name) {
        this.name = name;
    }

    abstract public void skill();
}

public class Wraith extends Champion{
    Wraith(String name) {
        super(name);
    }

    @Override
    public void skill() {
        System.out.println("페이징");
    }
}

public class Gibraltar extends Champion{
    Wraith(String name) {
        super(name);
    }

    @Override
    public void skill() {
        System.out.println("둠쉴드");
    }
}
  • 하지만 다음과 같이 바꾼다면, Champion 클래스에서는 한개의 책임만 지고 있는것을 확인 할 수 있다.

OCP 개방 폐쇄원칙

자신의 확장에는 개방되어 있고, 주변의 변화에는 닫혀 있어야 한다.

  • 기능의 확장이나 변경이 필요한 경우, 기존 구성요소는 수정이 일어나지 않아야 한다.
  • 기존의 구성요소를 확장 해서 재사용 해야한다.

  • 운전자가 각각의 차종에 의존되어 있다고 해보자.
1
2
3
* 운전자
    * K5
    * 아방이
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

public class Car {
    private String name;

    Car(String name) {
        this.name = name;
    }

    public String getName(){
        return this.name;
    }
}



public class CarDriver {
    public static void main(String[] args) {
        List<Car> cars = new ArrayList<>(Arrays.asList(new Car("K5"), new Car("아방이")));

        for (Car car : cars) {
            if (car.getName().equals("K5")) {
                System.out.println("K5가 달립니다.");
            } else if (car.getName().equals("아방이")) {
                System.out.println("아방이는 멈춥니다.");
            }
        }
    }
}
  • K5, 아방이 외 다른 차종이 추가된다면?
    • 운전자 클래스에서 수정 이 일어나야 한다.
    • 변화에 따라서 의존적으로 변할 수 밖에 없는 구조가 된다.
  • 하지만 자동차라는 인터페이스나 클래스를 하나 더 두게 된다면?
1
2
3
4
* 운전자
    * 자동차
        * K5
        * 아방이
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
35
36
37
38
39
40
41
42

// Car interface
public interface Car {

    boolean checkCarName(String name);

    void carMethod();
}

// K5
public class K5 implements Car {

    @Override
    public boolean checkCarName(String name) {
        return "K5".equals(name);
    }

    @Override
    public void carMethod() {
        System.out.println("K5는 달린다.");
    }
}



// CarDriver
public class CarDriver {
    public static void main(String[] args) {
        List<Car> cars = new ArrayList<>(Arrays.asList(new K5()));
        carRun(cars, "K5");
        carRun(cars, "아방이");
    }

    private static void carRun(List<Car> cars, String name) {
        for (Car car : cars) {
            if (car.checkCarName(name)) {
                car.carMethod();
            }
        }
    }
}

  • 자동차 클래스는 다른 차종에 대해서 확장하기가 수월해진다.
    • 그냥 새로운 클래스만 하나더 만들어주면 된다.
  • 그리고 운전자는 K5 외의 차종 추가와 같은 변화에 대해서 의존적이지 않게 된다.

LSP 리스코프 치환 법칙

서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.

  • 간단하게 생각하면 IS-A 법칙을 잘 지키면 된다.
  • 상속은 조직, 계층도가 아닌 분류도가 되어야 한다.
1
2
3
4
5
6
public class Father {
}

public class Son extends Father{

}
  • 아들은 아빠이다? 잘못되었다.
    • 아들은 자신의 기반 타입인 아빠 로 교체할 수 없게 된다.
1
2
3
4
5
abstract public class Champion {
}

public class Wraith extends Champion{
}
  • 레이스는 챔피언이다 자연스럽다
    • 리스코프 치환 법칙을 잘 지키고 있는것을 볼 수 있다.

ISP 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다.

  • 만약 정윤성 이라는 클래스가 있다고 해보자.
  • 정윤성 은 여러가지 역할을 지니고 있다.
    • 친구
    • 교육생
    • 아들…등등
  • 이것 모든 역할을 정윤성 이라는 클래스에 다 때려박는다면, 친구의 역할만 필요한 클라이언트는 아들의 역할을 하는 메소드에도 의존 관계를 맺게 된다.

  • 그렇다고 SRP 원칙을 지키겠다고, 전부 따로 구현을 한다면 굉장히 많은 클래스를 만들게 될것이다.

  • 그러면 어떻게 할까?
    • 각 역할에 따른 인터페이스를 만들어서 관리해주면 된다.
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
35
36
37
38
39
40
package oop;

public class Person {
    public void eat() {
        System.out.println("밥 먹기");
    }

    public void sleep() {
        System.out.println("잠 자기");
    }
}

public interface Friend {
    void play();
}

public interface Student {
    void learn();
}

public class JungYoonSung extends Person implements Student, Friend{
    @Override
    public void learn() {
        System.out.println("공부하기");
    }

    @Override
    public void play() {
        System.out.println("놀기");
    }

    public static void main(String[] args) {
        Student studentJYS = new JungYoonSung();
        studentJYS.learn();

        Friend friendJYS = new JungYoonSung();
        friendJYS.play();
    }
}

  • 이제 Student 의 역할이 필요한 경우에는 Student로 만들면 된다.
    • Student studentJYS = new JungYoonSung();
    • 이렇게 만들어진 studentJYS는 사용하지 않는 메소드인 play 메소드에 의존적이지 않게 된다.
  • Person에는 공통된 기능을 하는 메소드 eat , sleep 를 구현해 두었다.
    • 상위 클래스가 풍부할수록 재활용성이 늘어나 소스코드가 깔끔해진다.

DIP 의존성 역전 법칙

자기보다 변하기 쉬운 것에 의존 하지마라.

  • 구체적인 것보다 추상적인것에 의존하는것이 좋다.
    • 관계를 느슨하게 만들어 두는것이 좋다.
  • 자동차가 스노우 타이어에 의존하고 있다고 해보자.
    • 스노우 타이어는 계절이 바뀌면 사용하지 않는다.
    • 굉장히 변하기 쉬운 객체인 것이다.
  • 자동차가 타이어 인터페이스에 의존하고 있다고 해보자.
    • 의존 관계를 타이어 인터페이스를 통해서 역전 시킨다.
    • 타이어스노우 타이어 보다 추상적 이게 된다.
    • 만약 스노우 타이어 에서 일반 타이어로 바뀌더라도, 자동차 객체에는 영향을 미치지 않게 한다.

Reference

  • http://www.nextree.co.kr/p6960/
  • https://lktprogrammer.tistory.com/38
  • https://sehun-kim.github.io/sehun/solid/
  • https://woovictory.github.io/2019/05/10/What-is-SOLID/