SOLID 원칙

HEETAE HEO·2022년 6월 19일
0
post-thumbnail

SOLID 원칙이란?

SOLID 원칙은 SRP(단일 책임 원칙), OCP(개방 폐쇄 원칙), LSP(리스코프 치환 법칙),
ISP(인터페이스 분리 원칙), DIP(의존성 역전 원칙)을 줄여서 SOLID 원칙이라 합니다.

SOLID 원칙의 목적

SOLID의 궁극적인 목적은 변경에 유연해야 한다는 것입니다.

SRP(Single Responsibility Principle)

소프트웨어 모듈은(클래스) 변경의 이유가 단 하나여야만 한다.

변경의 이유가 단 하나여야만 한다라는 것은 하나의 모듈은 오직 하나의 액터만 책임져야 한다는 뜻입니다. 소스 파일에 다양하고 많은 메서드를 포함하면 병합이 자주 발생할 것 입니다. 특히 메서드가 서로 다른 액터를 책임진다면 병합이 발생할 가능성은 확실히 더 높습니다. 이 문제를 해결하는 방법은 서로 다른 액터를 뒷받침하는 코드를 분리하는 것 입니다. 그러므로 하나의 모듈은 하나의 기능만을 가지게하고 그 하나의 책임을 수행하는데 집중되어 있어야한다는 원칙입니다.

// User 클래스는 SRP를 따르지 않는다. 
class User{
	private db: Database;
    private name: string;
    private birth: Date;
    
    constructor(name: string, birth: Date){
        this.db Database.connect();
    }
    
    getUser(){
        return this.name + "(" + this.birth + ")";
    }
    
    save() {
    	this.db.users.save({ name: this.name, birth: this.birth});
    }
}

User 클래스는 User에 관한 책임만을 가져야만 합니다. 즉 User클래스는 사용자 모델과 관련된 속성을 정의해야합니다. 하지만 데이터 접근 기능과 저장 기능까지 정의를 하고 있기 때문에 이 Class는 단일 책임 원칙에 따르지 않았다고 할 수 있습니다.

class User{
    constructor(private name:string, private birth: Date) {}
    
    getUser(){
    	return this.name + "(" + this.birth +")";
    }
}

class UserRepository {
	private db : Database;
    
    constructor() {
    	this.db = Database.connect();
    }
    
    save(user: User) {
    	this.db.users.save(JSON.stringify(user));
    }
}

SRP(단일책임원칙)을 지켜지지 않던 User코드를 새롭게 수정하여 User 클래스는 데이터 모델과 관련된 속성을 정의하는 책임만 존재하게 되므로 SRP(단일책임원칙)를 만족하게 됩니다.

OCP(Open-Closed Principle)

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

소프트웨어 개체의 행위는 확장될 수 있어야 하지만, 이때 개체를 변경해서는 안된다는 원칙입니다. 만약 요구사항을 살짝 확장하는데 소프트웨어를 엄청나게 수정해야 한다면 그 소프트웨어 아키텍처는 엄청난 실패에 맞닥뜨린 것이라고 합니다. 즉 OCP는 시스템의 아키텍처를 떠받치는 원동력 중 하나입니다.
OCP는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는데 있습니다.

//OCP를 만족

class Card {
	private code: String;
    private expiration: Date;
    protected monthlyCost: number;
    
    
    constructor(code: String, expiration: Date, monthlyCost: number){
    	this.code= code;
        this.expiration = expiration;
        this.monthlyCost = monthlyCost;
    }
    
    getCode() : String{
    	return this.code;
    }
    
    getExpiration(): Date{
    	return this.expiration;
    }
    
    monthlyDiscount(): number{
    	return this.monthlyCost * 0.02;
    }
}

상속을 통한 확장
class GoldCard extends Card{
	monthlyDiscount(): number {
    	return this.monthlyCoas * 0.05;
    }
}

class SilverCard extends Card {
	monthlyDiscount(): number {
    	return this.monthlyCost * 0.03;
    }
}

Card 클래스를 상속받아 재정의하는 것입니다. 이렇게 된다면 확장에 열려있고 변경에 닫혀있습니다. 객체지향의 특징인 다형성까지 가져갈 수 있습니다.

LSP(Liskov Substitution Principle)

상호 대체 가능한 구성요소를 이용해 소프트웨어를 만들 수 있으려면 이들 구성 요소는 반드시 서로 치환 가능해야한다.

어떠한 프로그램에서 상호 대체 가능한 객체가 있을 때 서로 치환하더라도 프로그램의 행위가 변하지 않아야 한다는 뜻입니다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처에 별도 매커니즘을 추가해야 할 수 있기 때문에 중요한 원칙입니다.

쉽게 말하자면 자식 클래스는 부모 클래스의 유형정의를 깨트리면 안된다는 것입니다.

바로 코드로 확인해보겠습니다.

abstract class Address {
	addressee: string;
    country: string;
    postalCode: string;
    city: string;
    street: string;
    house: number;
    
    abstract writeAddress(): string;
}

class KoreaAddress extends Address {
  writeAddress(): string {
    return "Formatted Address Korea" + this.city;
  }
}

class UKAddress extends Address {
  writeAddress(): string {
    return "Formatted Address UK" + this.city;
  }
}

class USAAddress extends Address {
  writeAddress(): string {
    return "Formatted Address USA" + this.city;
  }
}

// PrintAddress 메서드에서 받을 파라미터는 치환이 가능하다.
class AddressWriter {
  PrintAddress(writer: Address): string {
    return writer.writeAddress();
  }
}

개방 폐쇄 원칙의 예제와 비슷합니다. Address 추상 클래스는 주소에 대한 속성을 가진 클래스입니다.나라별 주소를 표기하는 법이 모두 다르기 때문에 Address 추상 클래스를 상속받아 재정의하여 구현을 강제하고 있습니다.AdressWriter 클래스는 주소를 표기하는 클래스입니다.여기서 리스코프 치환 법칙을 만족하는 코드를 볼 수 있습니다. 즉, 다양한 나라의 클래스가 와도 프로그램의 행위가 변하지 않게 됩니다.

ISP(Interface Segregation Principle)

사용하지 않는 것에 의존하지 말아야한다.

인터페이스 분리 원칙은 클래스에 의해 구현되는 더 작고 더 구체적인 일련의 인터페이스를 작성해야 한다고 명시합니다. 일반적으로, 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해롭기 때문입니다. 쉽게 말해 불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠질 수 있다는 것입니다. 따라서 인터페이스를 분리하여 동작을 제공해야 합니다. 그리고 각 인터페이스는 단일 동작을 제공합니다.

// 잘못 사용된 ISP 
interface Printer {
    copyDocument();
    printDocument(document: Document);
    stapleDocument(document: Document, tray: Number);
}


class SimplePrinter implements Printer {

    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: Number) {
        //...
    }

}

위에 작성된 printer 인터페이스를 사용하면 인쇄 및 복사는 가능하지만 staple부분은 구현할 수 없습니다.


interface Printer {
    printDocument(document: Document);
}


interface Stapler {
    stapleDocument(document: Document, tray: number);
}


interface Copier {
    copyDocument();
}

class SimplePrinter implements Printer {
    public printDocument(document: Document) {
        //...
    }
}


class SuperPrinter implements Printer, Stapler, Copier {
    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: number) {
        //...
    }
}

잘못사용된 코드와는 다르게 메서드를 보다 구체적인 인터페이스로 그룹화 하는 대체 접근 방식을 보여줍니다. 불필요한 기능에 의존하지 않게 되어 훨씬 깔끔한 코드가 되었습니다.

DIP(Dependency Inversion Principle)

고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대 의존해서는 안된다. (상위 수준 클래스가 하위 수준 구성 요소에 의존해서는 안 되며 대신 추상화에 의존해야 한다고 명시합니다.)

의존성 역전 원칙은 구체적이며 변동성이 큰 코드는 절대로 언급하지 말아야 한다는 것입니다.유연성이 극대화된 시스템은 소스 코드 의존성이 abstraction에 의존하며 concretion에는 의존하지 않는 시스템을 말합니다.더 구체적으로 말하면 import, include와 같은 구문은 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻입니다.보통 안정성이 보장된 플랫폼이나 환경에서는 이 원칙을 무시해도 괜찮습니다.우리가 의존하지 않도록 하는 것은 바로 변동성이 큰 구체적인 요소이기 때문에 현재 개발 중이라 변경될 수밖에 없는 모듈들이기 때문입니다.

// 잘못사용된 예
class CarWindow {
    open() {
        //... 
    }

    close() {
        //...
    }
}


class WindowSwitch {
    private isOn = false;

    constructor(private window: CarWindow) {

    }

    onPress() {
        if (this.isOn) {
            this.window.close();
            this.isOn = false;
        } else {
            this.window.open();
            this.isOn = true;
        }
    }
}
DIP를 제대로 사용한 예
interface IWindow {
    open();
    close();
}

class CarWindow implements IWindow {
    open() {
        //...
    }

    close() {
        //...
    }
}


class WindowSwitch {
    private isOn = false;

    constructor(private window: IWindow) {

    }

    onPress() {
        if (this.isOn) {
            this.window.close();
            this.isOn = false;
        } else {
            this.window.open();
            this.isOn = true;
        }
    }
}

DIP를 만족시키기 위해 고수준에서 저수준을 의존하는 것을 인터페이스를 통해 역전시킵니다.이제 더 이상 고수준을 구현하는 WindowSwitch 클래스는 저수준 세부사항을 구현하는 코드에 의존하지 않게 되며 소스 코드의 의존성은 제어의 흐름과는 반대 방향으로 역전되게 됩니다. 이러한 이유로 이 원칙을 의존성 역전이라 부릅니다.

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글