본문 바로가기
Programming/OOP

SOLID 원칙

by TinKerBellBass 2017. 8. 17.
728x90
반응형

참고도서 

스프링 입문을 위한 자바 객체 지향의 원리와 이해
국내도서
저자 : 김종민
출판 : 위키북스 2015.04.08
상세보기



1. SRP(Single Responsibility Principle, 단일 책임 원칙)

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다." - 로버트 C. 마틴


하나의 클래스가 수 많은 역할과 책임을 맡아서 수행하고 있고, 그런 클래스에 의존하는 다양한 클래스가 있다고 가정하면,

-> 의존하고 있는 클래스 중 하나에 변화가 생기고 

-> 수 많은 역할과 책임을 맡고 있는 클래스에 변화가 생기고

-> 의존하고 있는 다양한 클래스들에게 영향이 미치게 된다.


남자 클래스가 여자친구에 대해 남자친구로서의 역할과 책임, 

직장상사에 대해 사원으로서의 역할과 책임을 가지고 있다고 가정하면, 


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
43
44
45
46
47
48
49
class 남자 {
 
    // 남자친구 역할과 책임
    void 기념일챙기기() {
        System.out.println("선물 뭐 줄까?");
    }
 
    // 사원 역할과 책임
    void 아부하기() {
        System.out.println("부장님 사랑합니다");
    }
}
 
class 여자친구 {
     
    // 여자친구의 역할과 책임은 기념일알리기
    // 여자친구가 피터팬에게 남자의 역할과 책임 중 하나인 남자친구로서의 
    // 기념일챙기기를 수행시키기 위해 피터팬에게 기념일이라고 메시지를 날리면
    // 피터팬은 남자의 역할과 책임 중 하나인 남자친구로서의 기념일챙겨주기를 수행한다
    void 기념일알리기() {
        남자 피터팬 = new 남자();
 
        // 여자친구로부터 기념일알리기 메시지를 받아서 
        // 피터팬은 남자의 역할과 책임 중 하나인 남자친구로서 역할과 책임을 충실히 수행함
        피터팬.기념일챙기기();
 
        // 여자친구는 피터팬이 남자의 역할과 책임 중 하나인 사원으로서의 아부하기는 필요없다
        // 피터팬.아부하기();를 수행할 수 있으므로 문제가 발생
        피터팬.아부하기();
    }
}
 
class 직장상사 {
     
    // 직장상사의 역할과 책임은 사원호출
    // 직장상사가 후크에게 남자의 역할과 책임 중 하나인 사원으로서의
    // 아부하기를 수행시키기 위해 후크 사원을 호출하는 메시지를 날리면
    // 후크는 남자의 역할과 책임 중 하나인 사원으로서의 아부하기를 수행한다
    void 사원호출() {
        남자 후크 = new 남자();
 
        // 직장상사로부터 사원호출 메시지를 받아서 
        // 후크는 남자의 역할과 책임 중 하나인 사원으로서 역할과 책임을 충실히 수행함
        후크.아부하기();
 
        // 여자친구와 마찬가지의 문제가 발생
        후크.기념일챙기기();
    }
}
cs


이와 같은 문제가 발생한다. 남자 클래스를 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//남자친구로서의 역할과 책임만 가짐
class 남자친구 {
 
    // 남자친의 역할과 책임
    void 기념일챙기기() {
        System.out.println("선물 뭐 줄까?");
    }
 
}
 
// 사원으로서의 역할과 책임만 가짐
class 사원 {
 
    // 사원의 역할과 책임
    void 아부하기() {
        System.out.println("부장님 사랑합니다.");
    }
}
 
class 여자친구 {
 
    // 여자친구의 역할과 책임은 기념일알리기
    // 여자친구가 남자친구 역할을 하는 피터팬에게 기념일이라고 메시지를 날리면
    // 피터팬은 기념일을 챙겨줘야 하는 남자친구의 역할과 책임을 수행해서 기념일을 챙겨준다
    void 기념일알리기() {
        남자친구 피터팬 = new 남자친구();
 
        // 여자친구로부터 기념일알리기 메시지를 받아서 
        // 피터팬은 남자친구로서 역할과 책임을 충실히 수행함
        피터팬.기념일챙기기();
 
        // 불가능, 피터팬은 남자친구로서의 역할과 책임은 가지고 있지만
        // 사원으로서의 역할과 책임은 가지고 있지 않다
        // 피터팬.아부하기();
    }
}
 
class 직장상사 {
 
    // 직장상사의 역할과 책임은 사원호출
    // 직장상사가 사원 역할을 하는 후크에게 사원호출이라고 메시지를 날리면
    // 후크는 아부해야 하는 사원의 역할과 책임을 수행해서 직장상사에게 아부한다
    void 사원호출() {
        사원 후크 = new 사원();
 
        // 직장상사로부터 사원호출 메시지를 받아서 
        // 후크는 사원으로서 역할과 책임을 충실히 수행함
        후크.아부하기();
 
        // 불가능, 후크는 사원으로서의 역할과 책임은 가지고 있지만
        // 남자친구로서의 역할과 책임은 가지고 있지 않다
        // 후크.기념일챙기기();
    }
}
cs


이렇게 SRP에 따라 클래스를 분리하면 혹시 발생할 지도 모를 문제를 사전에 차단할 수 있다.


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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class 건물{
    
    // 건물크기, 건물종류 속성이 하나의 역할과 책임을 지고 있지 않다
    String 건물크기;
    String 건물종류;
 
    // 건물크기구하기() 메소드가 하나의 역할과 책임을 지고 있지 않다
    void 건물크기구하기(){
        // 조건문으로 덕지덕지 도배하게 된다.
        if(건물종류.equals("단독주택")){
            건물크기 = "전체면적";
            System.out.println(건물크기);
        }
 
        if(건물종류.equals("아파트")){
            건물크기 = "층수+동수";    
            System.out.println(건물크기);
        }
    }
}
 
class 건물정보{
 
    public static void main(String[] args){
        
        건물 마이홈 = new 건물();
        마이홈.건물종류 = "단독주택";
        마이홈.건물크기구하기();
 
        건물 유어홈 = new 건물();
        유어홈.건물종류 = "아파트";
        유어홈.건물크기구하기();
    }
}
 
// SRP 원칙에 따라 변경 
class 건물{
    
    String 단독주택크기;
    String 아파트크기;
 
    void 단독주택크기구하기(){
        단독주택크기 = "전체면적";
        System.out.println(단독주택크기);
    }
 
    void 아파트크기구하기(){
        아파트크기 = "층수+동수";
        System.out.println(아파트크기);
    }
}
 
class 건물정보{
 
    public static void main(String[] args){
        
        건물 마이홈 = new 건물();
        마이홈.단독주택크기구하기();
 
        건물 유어홈 = new 건물();
        유어홈.아파트크기구하기();
    }
}
cs


이런 속성과 메소드가 있을 수 있다.


한번 더 리팩토링 하면,


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
abstract class 건물{
 
    abstract void 건물크기구하기();
}
 
class 단독주택 extends 건물{
 
    void 건물크기구하기(){
        System.out.println("전체면적");
    }
}
 
class 아파트 extends 건물{
 
    void 건물크기구하기(){
        System.out.println("층수+동수");
    }
}
 
class 건물정보{
 
    public static void main(String[] args){
        
        건물 마이홈 = new 단독주택();
        마이홈.건물크기구하기();
 
        건물 유어홈 = new 아파트();
        유어홈.건물크기구하기();
    }
}
cs


이렇게 설계할 수 있을 것이다.


2. OCP(Open Closed Principle, 개방 폐쇄 원칙)

"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다."
- 로버트 C. 마틴

"자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다." - 김종민


운전자가 마티즈를 운전하다가 쏘나타로 바꿔 운전하는 경우, 

운전자는 수동으로 조작하던 창문개방과 기어조작을 자동으로 조작해야 한다.

운전자에게 변화가 온 것이다. 


객체지향 세계에서는 차가 바뀌더라도 운전자에게 변화가 없게 할 수 있다.



이제 운전자는 그냥 자동차를 운전하는 법만 알며 된다. 

그 자동차가 마티즈에서 쏘나타로 바뀌더라도 운전자의 조작법에는 변화는 일어나지 않는다.


"자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다."
라는 저자의 말에 비추어 생각해 보자.

운전자가 운전하고 싶은 자동차 종류는 얼마든지 늘어날 수 있다.
새로운 자동차 레이를 운전하고 싶다면 레이 클래스를 만들어서 자동차 인터페이스를 구현시키기만 하면 된다.
즉 자동차는 자신의 확장에는 열려 있는 것이다.

자동차가 확장해서 새로운 자동차 종류를 늘리더라도
운전자의 조작법은 영향을 받지 않는다. 그냥 자동차 운전법만 알고 있으면 되고.
단지 탈 자동차 종류만 선택해 주면 된다.
즉 운전자는 주변의 변화에 대해서는 닫혀 있는 것이다.

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
43
44
45
interface 자동차{
 
    void 창문개방();
    void 기어조작();
}
 
class 마티즈 implements 자동차{
 
    void 창문개방(){
        System.out.println("창문개방 수동조작");
    }
    
    void 기어조작(){
        System.out.println("기어 수동조작");
    }
}
 
class 쏘나타 implements 자동차{
    
    void 창문개방(){
        System.out.println("창문개방 자동조작");
    }
    
    void 기어조작(){
        System.out.println("기어 자동조작");
    }
}
 
class 운전자{
 
    public static void main(String[] args){
        // 마티즈 타입의 객체를 만들어서 자동차 역할을 할 수 있는 마이카라고 한다
        자동차 마이카 = new 마티즈();
        // 자동차 역할을 하는 객체를 가지고 조작
        마이카.창문개방();
        마이카.기어조작();
 
        // 쏘나타 타입의 객체를 만들어서 자동차 역할을 할 수 있는 유어카라고 한다
        자동차 유어카 = new 쏘나타();
        // 자동차 역할을 하는 객체를 가지고 조작
        // 자동차의 종류가 바뀌었지만 운전자는 자동차 역할을 할 수  인터페이스의 조작법만 알고 있으면 된다.
        유어카.창문개방();
        유어카.기어조작();
    }
}
cs



3. LSP(Liskov Substitution Principle, 리스코프 치환 원칙)

"서브 타입은 언제나 자신의 기반타입(base type)으로 교체할 수 잇어야 한다." - 로버트 C. 마틴

"하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다."
- 김종민

객체지향 세계에서 상속은 조직도나 계층도가 아닌 분류도가 돼야 한다.

상속의 경우,
하위클래스(sub class) is a kind of 상위클래스(super class),
하위 분류는 상위 분류의 한 종류다.

인터페이스의 경우,
구현 클래스(implements) is able to 인터페이스(interface),
구현 분류는 인터페이스 할 수 있어야 한다.

계층도 상속의 예
class 아버지{ }
class 딸 extends 아버지{ }
두 클래스에 의해, 아버지 춘향이 = new 딸( );
딸(하위 클래스)은 아버지(상위 클래스)의 역할을 할 수 없으며,
딸 is a kind of 아버지도 성립하지 않는다. LSP 원칙에 어긋나는 것이다.

분류도 상속의 예
class 동물{ }
class 포유류 extends 동물{ }
두 클래스에 의해, 동물 사자 = new 포유류( );
포유류(하위 클래스)는 동물(상위 클래스)의 역할을 할 수 있으며,
포유류 is a kind of 동물도 성립한다. LSP 원칙을 지키고 있는 것이다.

인터페이스의 예
interface 날수있는{ }
class 요정 implements 날수있는{ }
날수있는 팅커벨 = new 요정( );
요정(구현 클래스)은 날 수 있어야(인터페이스) 하며,
요정 is able to 날수있는도 만족한다.

즉, LSP 따라 상속은 계층도/조직도가 아닌 분류도가 되어야 한다.


4. ISP(Interface Segregation Principle, 인터페이스 분리 원칙)

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

SRP는 단일 역할과 책임을 가져야 한다는 것이었다.
그 방법 중 하나로 앞에서는 다양한 역할과 책임을 가지는 클래스를 토막냈다.
클래스를 토막내지 않고 SRP를 지키게 하는 방법 중 하나가 ISP이다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
interface 남자친구 {
    void 기념일챙기기();
}
 
interface 사원 {
    void 아부하기();
}
 
class 남자 implements 남자친구, 사원 {
 
    @Override
    public void 아부하기() {
        System.out.println("부장님 사랑합니다.");
    }
 
    @Override
    public void 기념일챙기기() {
        System.out.println("선물 뭐 줄까?");
    }
}
 
class 여자친구 {
 
    void 기념일알리기() {
 
        // 남자친구의 역할을 할 수 있는 남자 객체를 만들어서 피터팬이라 한다
        남자친구 피터팬 = new 남자();
        // 피터팬은 남자친구로서 기념일 챙기기 역할을 수행할 수 있다
        피터팬.기념일챙기기();
 
        // 피터팬은 남자친구의 역할만 할 수 있는 남자이기 때문에
        // 사원으로서의 역할은 할 수 없다.
        // 피터팬.아부하기();
    }
}
 
class 직장상사 {
 
    void 사원호출() {
 
        // 사원 역할을 할 수 있는 남자 객체를 만들어서 후크라 한다
        사원 후크 = new 남자();
        // 후크는 사원으로서 아부하기 역할을 수행할 수 있다
        후크.아부하기();
 
        // 후크는 사원의 역할만 할 수 있는 남자이기 때문에
        // 남자친구로서의 역할은 할 수 없다.
        // 후크.기념일챙기기();
    }
}


 
여자친구는 남자친구 인터페이스를 통해서만 남자 클래스에 접근할 수 있고,
직장상사는 사원 인터페이스를 통해서만 남자 클래스에 접근할 수 있다.
상황에 따라 다르겠지만 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
41
42
43
44
45
46
47
48
// 조류가 가지는 공통 부분
class 조류 {
 
    void 먹이먹기() {
        System.out.println("냠냠 맛나게 먹는다");
    }
 
    void 잠자기() {
        System.out.println("쿨쿨 잘 잔다");
    }
}
 
// 날 수 있는 조류만 구현하게 한다
interface 날수있는 {
 
    void 훨훨난다();
}
 
// 조류의 역할과 기능을 상속, 날수있는 기능은 구현 안함
class 펭귄 extends 조류 {
}
 
// 조류의 역할과 기능을 상속, 날수있는 기능을 구현
class 독수리 extends 조류 implements 날수있는 {
 
    @Override
    public void 훨훨난다() {
        System.out.println("슈우웅 난다");
    }
}
 
class 조류연구{
    
    public static void main(String[] args) {
        
        펭귄 펭펭 = new 펭귄();
        펭펭.먹이먹기();
        펭펭.잠자기();
        // 날 수 없다
        // 펭펭.훨훨난다();
        
        독수리 독독 = new 독수리();
        독독.먹이먹기();
        독독.잠자기();
        // 날 수 있다
        독독.훨훨난다();
    }
}
cs


5. DIP(Dependency Inversion Principle, 의존 역전 원칙)

"고차원 모듈은 저차원 모듈에 의존하면 안 된다.
 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다."
"추상화된 것은 구체적인 것에 의존하면 안 된다.
 구체적인 것이 추상화된 것에 의존해야 한다."
"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라"
- 로버트 C. 마틴



위의 경우 자동차가 스노우타이어에 의존하고 있다.

만약 겨울이 끝나 스노우 타이어를 일반 타이어로 바꾸어야 한다면,

스노우 타이어에 의존하고 있는 자동차가 일반 타이어에 의존하도록 바꾸어 주어야 한다.

즉, 자주 변경되는 구체 클래스에 의존해서는 안 된다는 것이다.


타이어가 바뀌어도 자동차에 변화가 없게 할려면,

OCP 를 이용해서 바꾸면 된다.



이제 자동차는 스노우타이어와 일반타이어에 의존적이지 않다.

대신 무엇에도 의존하지 않던 스노우 타이어가 타이어 인터페이스에 의존적이게 되었다.

의존의 방향이 역전된 것이다.

그리고 자동차는 자신보다 변하기 쉬운 스노우타이어에 의존하던 관계를

중간에 추상화된 타이어 인터페이스를 추가해 두고 의존 관계를 역전시키고 있다.


스프링은 의존 관계를 스프링 컨테이너를 이용해 역전시켜 DIP를 극한까지 구현한 프레임워크이다.


728x90
반응형

댓글