늦은 프로그래밍 이야기

SOLID 원칙 본문

내일배움캠프/객체지향 프로그래밍

SOLID 원칙

한정규 2022. 11. 21. 14:43

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

 - 작성된 클래스는 하나의 기능만 가지며, 하나의 책임(변화의 축)을 수행하는데 집중되어 있어야 한다는 원칙.

 - 클래스를 변경해야 하는 이유가 오직 하나여야 한다.

 - 하나의 책임의 기준은 변경이다.

 - 변경이 발생하였을 때, 변경해야 될 부분이 적으면, 단일 책임 원칙을 잘 따른 것이다.

 - 한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유롭다.

 - 책임을 적절히 분배함으로써 코드의 가독성 향상, 유지보수가 용이하다.

 

응집도

 - 단일 책임 원칙은 "응집도(cohesion)와 관련이 있다.

 - 응집도 : 하나의 클래스는 하나의 추상적인 개념을 나타내야 한다. 또한 각 클래스는 클래스의 목적과 의미를 한 줄로 기술할 수 있어야 한다. 만약 클래스를 간단하게 기술할 수 없다면, 아마도 하나 이상의 추상적인 개념을 나타내고 있을 것이다. 클래스에 추가된 책임들은 클래스의 설명에 부합해야 한다.

 

적용방법

 - 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 한다.

 - 책임만 분리하는 것이 아니라 분리된 두 클래스 간의 관계의 복잡도를 줄이도록 설계한다.

 - 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 갖고 있다면 각각의 클래스들의 공유되는 요소를 부모 클래스로 정의하여 부모 클래에 위임한다.

 

적용예시

SRP 적용 전

class Galaxy {
    private String serialNumber;
    private String cpu;
    private String memory;
    private int battery;
    private double weight;
}

 - serialNumber는 변화요소가 아니고 고유정보라고 할 수 있다.

 - cpu, memory, battery, weight는 모두 특성 정보군으로 변경이 발생 할 수 있는 부분. 변화요소.

 - 특성 정보군에 변화가 발생하면 항상 해당 클래스를 수정해야 하는 상황이 발생하므로 이 부분이 SRP적용의 대상이 된다.

 

SRP 적용 후

// 스펙만 관리
class GalaxySpec {
    private String cpu;
    private String memory;
    private int battery;
    private double weight;
}

class Galaxy {
    private String serialNumber;
    private GalaxySpec spec;
    public Galaxy(String serialNumber, GalaxySpec spec) {
        this.serialNumber = serialNumber;
        this.spec = spec;
    }
}

 - 변화가 예상되는 특성 정보군을 분리한다.

 - 따라서 특성 정보에 변경이 일어나면 GalaxySpec 클래스만 변경하면 된다.


OCP (Open Close Principle : 개방 폐쇄의 원칙)

 - 확장에는 열려있고, 변경에는 닫혀 있어야 한다는 원칙.

 - 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야한다.

 - 변경이나 추가사항이 발생 하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며 기존 기성요소를 쉽게 확장해서 재사용할 수 있어야 한다.

 - 중요 메커니즘은 추상화와 다형성. 객체지향의 장점을 극대화하는 아주 중요한 원리.

 

템플릿 메소드 패턴 (Template Method Pattern)

 

적용방법

 - 변경될 것과 변하지 않을 것을 엄격히 구분한다.

 - 두 모듈이 만나는 지점에 인터페이스를 정의한다.

 - 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성한다.

 

적용예시

OCP 적용 전

class GalaxySpec {
    private String cpu;
    private String memory;
    private int battery;
    private double weight;
}

class Galaxy {
    private String serialNumber;
    private GalaxySpec spec;
    public Galaxy(String serialNumber, GalaxySpec spec) {
        this.serialNumber = serialNumber;
        this.spec = spec;
    }
}

 - SRP원칙을 적용하여 변경이 예상되는 부분을 뽑아 새로운 클래스에 변화요소들을 하나로 모았다.

 - 하지만 갤럭시 외의 새로운 핸드폰들이 생겨나면 변경이 발생할 수 있다.

 

OCP 적용 후

class Phone {
    private String serialNumber;
    private PhoneSpec spec;
    public Phone(String serialNumber, PhoneSpec spec) {
        this.serialNumber = serialNumber;
        this.spec = spec;
    }
}

class PhoneSpec {
    private String cpu;
    private String memory;
    private int battery;
    private double weight;
}

class Galaxy extends Phone

class IPhone extends Phone

class 샤오미 extends Phone

class Sony extends Phone

 - 추가될 핸드폰들의 공통 속성을 모두 담은 Phone 클래스 생성. 추상화 작업.

 - 새로운 핸드폰이 추가되면서 발생하는 부분을 분리하여 수정을 최소화.


LSP (Liskov Substitution Principle :  리스코브 치환의 원칙)

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

 - 상속은 궁극적으로는 다형성을 통한 확장성 획득을 목표로 한다.

 - 다형성과 확장성을 극대화 하려면 하위 클래스를 사용하는 것보다는 상위의 클래스(인터페이스)를 사용하는 것이 좋다.

 - 일반적으로 선언은 기반 클래스로, 생성은 구체 클래스로 대입하는 방법을 사용.

 

적용방법

 - 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.

 - 똑같은 연산을 제공하지만, 이를 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현한다. (인터페이스 상속)

 - 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만든다.

 - 만약 두 개체가 하는일에 추가적으로 뭔가를 더 한다면 구현 상속을 사용한다.

 

적용예시

class Rectangle {
    ...
}

class Square extends Rectangle {
    ...
}

public static void main(String[] args) {
    Ractangle rectangle = new Rectangle();
    ...
    Rectangle square = new Square();
    ...

 


ISP (Interface Segregation principle : 인터페이스 분리의 원칙)

 - 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원칙.

 - 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용.

 - 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다.

 - 인터페이스의 단일책임을 강조.

 - 인터페이스 분리를 통해 변화에의 적응성을 획득.

 

적용예시

ISP 적용 전

public interface Phone {
    void call(String phoneNumber);
    void pay(String cardName);
    void wirelessCharge();
}

public class Galaxy implements Phone {
    @Override
    public void call(String phoneNumber) {
         // ..
    }

    @Override
    public void pay(String cardName) {
        // ..
    }

    @Override
    public void wirelessCharge() {
        // ..
    }
}

public class SkyPhone implements Phone {
    @Override
    public void call(String phoneNumber) {
         // ..
    }

    @Override
    public void pay(String cardName) {
        // ..
    }

    @Override
    public void wirelessCharge() {
        // ..
    }
}

 - call, pay, wirelessCharge 기능들이 하나의 인터페이스에 작성이 되었다.

 - wirelessCharge 기능을 사용하지 않는 객체를 추가했을 때 문제가 발생한다.

 

ISP 적용 후

public interface Phone {
    void call(String phoneNumber);
}

public interface WirelessCharge {
    void wirelessCharge();
}

public interface Payment{
    void pay(String cardName);
}

public class Galaxy implements Phone, WirelessCharge, Payment {
    @Override
    public void call(String phoneNumber) {
         // ..
    }

    @Override
    public void pay(String cardName) {
        // ..
    }

    @Override
    public void wirelessCharge() {
        // ..
    }

}

public class SkyPhone implements Phone {
    @Override
    public void call(String phoneNumber) {
         // ..
    }
}

 - 여러 개의 구체적인 인터페이스를 작성하여 클래스마다 필요한 인터페이스만 사용한다.


DIP (Dependency Inversion Principle : 의존성 역전의 원칙)

 - 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전.

 - 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙.

 

적용방법

 - 변하기 쉬운 것과 어려운 것을 구분 해야한다.

 - 변하기 쉬운 것 : 구체적인 행동

 - 변하기 어려운 것 : 흐름이나 개념 같은 추상적인 것.

 - 상위 클래스와 하위 클래스를 바로 의존하게 하는 것이 아니라 둘 사이에 존재하는 추상적인 개념을 통해 의존해야 한다.

 

적용예시

public interface Phone {
    void call(String phoneNumber);
}

public class SmartPhone implements Phone {
    @Override
    public void call(String phoneNumber) {
        System.out.println("스마트폰 : " + phoneNumber);
    }
}

public class PublicPhone implements Phone {
    @Override
    public void call(String phoneNumber) {
        System.out.println("공중 전화 : " + phoneNumber);
    }
}

public class InternetPhone implements Phone {
    @Override
    public void call(String phoneNumber) {
        System.out.println("인터넷 전화 : " + phoneNumber);
    }
}


public class Person {
    private Phone phone;
  
    public void setPhone(Phone phone) {
        this.phone = phone; // 폰이 계속 바뀜.
    }

    public void call(String phoneNumber) {
        phone.call(phoneNumber);
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setPhone(new SmartPhone());
        person.setPhone(new InternetPhone());



        // 코드 수정 X
        person.call("01012341234"); // 스마트폰 전화,
    }
}

 - 구체적인 행동 : 스마트폰으로 전화를 건다. 공중전화로 전화를 건다. 인터넷전화로 전화를 건다.

 - 추상적인 행동 : 전화를 건다.

 - 추상적 개념인 call을 작성한 Phone이라는 인터페이스를 매개로 상위 클래스인 Person과 하위 클래스들의 메시지를 주고 받는다.


'내일배움캠프 > 객체지향 프로그래밍' 카테고리의 다른 글

3계층 구조  (0) 2022.11.28
Comments