Design Pattern

[04] SOLID 원칙

y0ngha 2021. 12. 9. 23:51

SOLID 원칙은 객체 지향 설계 원칙으로 총 5가지의 원칙이 합쳐져 SOLID라고 부른다.

 

SOLID 원칙

  1. SPR(Single Responsibility Principle) : 단일 책임 원칙
  2. OCP(Open Closed Principle) : 개방 폐쇄 원칙
  3. LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  4. ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  5. DIP(Dependency Inversion Principle) : 의존 역전 원칙

함수와 데이터 구조를 Class로 배치하는 방법, 클래스를 서로 결합하는 방법, 소프트웨어는 변경하기 쉬워야 하며 아키텍처는 형태에 독립적으로 하자는 원칙이다.

목적

변경에 유연해야 한다.

 

SRP

"소프트웨어 모듈은 변경의 이유가 단 하나여야만 한다."

즉, 모듈은 오직 하나의 내용만 책임을 져야한다. 라는 내용이다.

 

이 내용에 대해 설명을 하는건 코드로 설명하도록 하겠다.

(예시 코드는 모두 ES6 문법을 따라가며, 이해를 돕기 위해 Typescript로 코드를 진행하겠다.)

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
class User {
    private _database: Database;
    private _name: string;
    private _age: number;
    
    constructor(name: string, age: number) {
        this._database = Database.connect();
        this._name = name;
        this._age = age;
    }
 
    getUser(): string {
        return this._name + "(" + this._age + "살)";    
    }
 
    save(): boolean {
        this._database.save(
            {
                name: this._name,
                age: this._age
            }
        );
        return true;
    }
}
cs

위 코드는 간단하게 User의 정보를 담고 있는 Class다.

하지만 위 코드는 SRP 원칙에 위배되었다.

왜냐하면 User class는 User 정보에 대해서만 책임이 있어야하는데 위 클래스는 Database에 연결을 하거나, 저장하는 등 다른 이벤트를 책임지고 있기 때문이다.

 

아래와 같이 코드를 바꿔주도록 하자.

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
class DatabaseRepository {
    private _database: Database;
 
    constructor() {
        this._database = Database.connect();
    }
 
    save<T>(data: T): boolean {
        this._database(JSON.stringify(data));
        return true;
    }
}
 
class User {
    private _name: string;
    private _age: number;
    
    constructor(name: string, age: number) {
        this._name = name;
        this._age = age;
    }
 
    getUser(): string {
        return this._name + "(" + this._age + "살)";
    }
}
 
class UserRepository {
    private _databaseRepository: DatabaseRepository;
    
    constructor() {
        this._databaseRepository = DatabaseRepository();
    }
    
    saveUser(user: User) {
        this._databaseRepository.save<User>(user);
    }
}
cs

이 원칙은 관심사의 분리(SoC) 원칙에서 파생된 것으로 알고 있다.

OCP

"소프트웨어 객체는 확장에 열려 있어야 하고, 변경에는 닫혀 있어야한다."

즉, 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템에 너무 많은 영향을 받지 않도록 해야 한다.

 

만약 개발자가 어떤 객체를 작성하였는데, 해당 객체의 내용 하나를 바꿨다가 프로그램 자체가 동작을 하지 않는 증상은 없어야 한다는 것이다.

 

OCP 원칙에 대해 성립하는 코드를 보도록 하겠다.

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
class ShinhanCardBase {
    private _cardNumber: string;        // 카드 번호
    private _expirationDate: Date;        // 유효기간
    private _annualFee: number;            // 연회비
    
    constructor(cardNumber: string, expirationDate: Date, annualFee: number) {
        this._cardNumber = cardNumber;
        this._expirationDate = expirationDate;
        this._annualFee     = annualFee;
    }
 
 
    getCardNumber(): string {
        return this._cardNumber;
    }
 
    getExpiration(): Date {
        return this._expiration;
    }
 
    getAnnualFeeDiscount(): number {
        return this._annualFee * 0.05;
    }
}
 
class PlatinumShinhanCard extends ShinhanCardBase {
    getAnnualFeeDiscount(): number {
        return this._annualFee * 0.08;
    }
}
 
class VipShinhanCard extends ShinhanCardBase {
    getAnnualFeeDiscount(): number {
        return this._annualFee * 0.2;
    }
}
cs

Base Class를 생성하고, 이 Class를 이용해 시스템을 확장하고, 확장되는 내용에 대해 바꾸지 않아도 되는 정보는 닫혀있게(private, protected) 해주는 것이다.

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
28
29
30
31
abstract class Phone {
    phoneNumber: string;
    deviceType: string;
    newsAgency: string;
 
    abstract phoneInfo(): string;
}
 
class SKTPhone extends Phone {
    phoneInfo(): string {
        return "SKT " + this.deviceType;
    }
}
 
class KTPhone extends Phone {
    phoneInfo(): string {
        return "KT " + this.deviceType;
    }
}
 
class LGUPlusPhone extends Phone {
    phoneInfo(): string {
        return "LG U+ " + this.deviceType;
    }
}
 
class PhoneWriter {
    printPhoneInfo(phone: Phone) {
        console.log(phone.phoneInfo());
    }
}
cs

PhoneWriter Class에 통신사 3개의 핸드폰중 어떤 것을 넣어도 동일한 동작을 하게끔 설계를 하는 것이다.

 

개방 폐쇄의 원칙(OCP)와 매우 유사한 원칙이다.

ISP

"사용하지 않는 것에 대해 의존하지 않아야한다."

즉, 더 작고 더 구체적인 일련의 Interface를 작성하라는 내용이다.

 

먼저 잘못된 코드를 보도록 하자.

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
interface PhoneLogic {
    call(toPhoneNumber: string);
    messageSend(toPhoneNumber: string, message: string);
    shutdown();
    reboot();
}
class PhoneCommunication implements PhoneLogic {
    call(toPhoneNumber: string) {
        /**
         * toPhoneNumber로 전화를 거는 로직
         */
 
        ...
    }
 
    messageSend(toPhoneNumber: string, message: string) {
        /**
         * toPhoneNumber로 message를 보내는 로직
         */
         
        ...
    }
 
 
    shutdown() { };
    reboot() { };
}
cs

PhoneCommunication은 오로지 상대 핸드폰과 연결하기 위한 Class다.

근데 PhoneLogic Interface를 구현하고 있기 때문에 사용되지 않는 shutdown, reboot 함수를 정의했어야 했다.

이와 같이 사용되지 않는 것을 모두 빼자는 의미이다.

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
interface IPhoneCommunication {
    call(toPhoneNumber: string);
    messageSend(toPhoneNumber: string, message: string);
}
 
 
interface IPowerControll {
    shutdown();
    reboot();
}
 
class PhoneCommunication implements IPhoneCommunication {
    call(toPhoneNumber: string) {
        /**
         * toPhoneNumber로 전화를 거는 로직
         */
 
        ...
    }
 
    messageSend(toPhoneNumber: string, message: string) {
        /**
         * toPhoneNumber로 message를 보내는 로직
         */
         
        ...
    }
}
 
class PhonePowerControll implements IPowerControll {
    shutdown() {
        /**
         * 강제종료
         */
 
        ...
    };
 
    reboot() { 
        /**
         * 강제종료 후 다시 시작
         */
 
        ...
    };
}
cs

이 원칙은 관심사의 분리(SoC) 원칙에서 파생된 것으로 알고 있다.

DIP

"고수준 정책을 구현하는 코드는, 저수준 세부 사항을 구현하는 코드에 의존되서는 안된다."

Entity는 구체화가 아닌 추상화에 의존해야 한다.

 

잘못된 코드를 보도록 하자.

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
/**
 * 저수준 세부사항 구현 Class
 */
class CarWindow {
    open() {
        /**
         * 창문이 올라가는 로직
         */
        
        ...
    }
 
    close() {
        /**
         * 창문이 내려가는 로직
         */
        
        ...
    }
}
 
/**
 * 고수준 세부사항 구현 Class
 */
class CarWindowSwitch {
    private _isOpen: boolean = false;
 
    constructor(private _carWindow: CarWindow) { }
 
    onPress() {
        this._isOpen = !this._isOpen;
 
        if(this._isOpen) {
            this._carWindow.open();
        } else {
            this._carWindow.close();
        }
    }
}
cs

저수준 세부사항 구현코드가 변경이 되면, 고수준 코드에도 문제가 발생을 하니 이를 추상화 Interface를 통해 빼고 고수준 세부사항 구현 Class에서는 추상화된 Interface만 사용하자는 것이다.

 

위 잘못된 코드를 바로 고치려면 아래와 같이 고쳐주도록 하자.

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
/**
 * 세부사항 추상화 Interface
 */
interface IWindow {
    open();
    close();
}
 
/**
 * 저수준 세부사항 구현 Class
 */
class CarWindow implements IWindow {
    open() {
        /**
         * 창문이 올라가는 로직
         */
        
        ...
    }
 
    close() {
        /**
         * 창문이 내려가는 로직
         */
        
        ...
    }
}
 
/**
 * 고수준 세부사항 구현 Class
 */
class CarWindowSwitch {
    private _isOpen: boolean = false;
 
    constructor(private _window: IWindow) { }
 
    onPress() {
        this._isOpen = !this._isOpen;
 
        if(this._isOpen) {
            this._window.open();
        } else {
            this._window.close();
        }
    }
}
cs

 

클린 아키텍쳐에 대해 이것 저것 공부를 하고 있는데, 이 원칙을 지키는 것 만으로도 진짜 머리가 아플거같다.