객체지향 개발 5대원리 SOLID

Terror·2024년 9월 25일
0

들어가기 전에...

  • 항상 정답이란 없다, 이러한 원칙은 효율적이고 효과적이기 때문에 사용하는것이고 이미 그 유용성이 증명되었기 때문에 우리가 사용하는것이다
  • 입증된 객체지향 디자인 원리들을 이용하면 좀 더 유지보수하기 쉽고, 유연하고, 확장이 쉬운 소프트웨어를 만들 수 있습니다
  • 그럼 이에대해 알아보자

5가지 원리의 핵심내용

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

정의

  • 작성된 클래스는 "하나의 기능만 가지며" 클래스가 제공하는 모든 서비스는 " 하나의 수행을 책임"을 수행하는데 집중되어야 한다는 원칙
  • 다시말해 "어떤 변화에 의해 클래스를 변경해야 하는 이유는 하나뿐" 이여야 한다는 뜻이다

예시

// 자주 변동이 일어나지않는 기타의 정보 클래스 (변동이 일어나지 않는 정보에 대해서만 책임을 가지고있음)
public class GuitarInfo {
    private Integer serialNumber;

    public GuitarInfo(Integer serialNumber) {
        this.serialNumber = serialNumber;
    }

    public Integer getSerialNumber() {
        return serialNumber;
    }

    public void setSerialNumber(Integer serialNumber) {
        this.serialNumber = serialNumber;
    }
}
// 자주 변동이 일어나는 기타의 정보 클래스 (자주 변동이 일어나는 정보에 대해서만 책임을 가지고 있음)
package SOLID;

public class GuitarSpec {
    private Integer price;
    private String model;

    public GuitarSpec(Integer price, String model) {
        this.price = price;
        this.model = model;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
}
// 기타를 만드는 책임만 가지고있음
package SOLID;

public class Guitar {
    private final GuitarInfo guitarInfo;
    private final GuitarSpec guitarSpec;

    public Guitar(GuitarInfo guitarInfo, GuitarSpec guitarSpec) {
        this.guitarInfo = guitarInfo;
        this.guitarSpec = guitarSpec;
    }

    public GuitarInfo getGuitarInfo() {
        return guitarInfo;
    }

    public GuitarSpec getGuitarSpec() {
        return guitarSpec;
    }
}
// 기타 행동제어,상태변화에 대해서만 책임을 가짐
public class GuitarManager {
    public void updatePrice(Guitar guitar, Integer newPrice) {
        guitar.getGuitarSpec().setPrice(newPrice);
    }
}
// 실제 활용 예시
public class Main {

    public static void main(String[] args) {
        GuitarManager gm = new GuitarManager();
        GuitarInfo guitarInfo = new GuitarInfo(2148123);
        GuitarSpec guitarSpec1 = new GuitarSpec(1000,"첫번째 기타");
        Guitar guitar1 = new Guitar(guitarInfo,guitarSpec1);
        System.out.println(guitar1.getGuitarSpec().getPrice());
        gm.updatePrice(guitar1,5000);
        System.out.println(guitar1.getGuitarSpec().getPrice());
    }
}

적용이슈

  • 클래스는 자신의 이름이 나타내는 일을 해야합니다
  • 각 클래스는 "하나의 개념"을 나타내어야 합니다
  • 단순히 책임을 분리한다고 SRP가 적용되는것은 아닙니다
  • 각 개체간의 응집력이 있다면, 병항이 순 작용의 수단이 되고 결합력이 있다면 분리가 순 작용의 수단이 됩니다

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

정의

  • 버틀란트 메이어박사가 1998년 객체지향 소프트웨어 설계라는 책에서 정의한 내용입니다
  • 소프트웨어의 구성요소 (컴포넌트, 클래스, 모듈, 함수)는 "확장에는 Open", "변경에는 Close"의 원리입니다
  • 이것은 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화 해야 한다는 의미로, 요구사항의 변경이나 추가사항이 발생하더라도, "기존 구성요소는 수정이 일어나지 말아야하며", "기존 구성 요소를 쉽게 확장해서 재사용할 수 있어야함" 입니다
  • 로버트 C. 마틴은 OCP는 관리가능하고 재사용 가능한 코드를 만드는 기반이며, OCP를 가능케 하는 중요 메커니즘은 추상화와 다형성이라고 설명하고 있습니다

예시

public class ViolinInfo implements Info {
    private Integer serialNumber;

    public ViolinInfo(Integer serialNumber) {
        this.serialNumber = serialNumber;
    }

    @Override
    public Integer getSerialNumber() {
        return serialNumber;
    }

    @Override
    public void setSerialNumber(Integer serialNumber) {
        this.serialNumber = serialNumber;
    }
}
public class ViolinSpec implements Spec {
    private Integer price;
    private String model;

    public ViolinSpec(Integer price, String model) {
        this.price = price;
        this.model = model;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
}
public class Violin implements MusicalInstrument {
    private final Info info;
    private final Spec spec;

    public Violin(Info info, Spec spec) {
        this.info = info;
        this.spec = spec;
    }

    public Info getInfo() {
        return info;
    }

    public Spec getSpec() {
        return spec;
    }
}
public class ViolinManager implements Manager {

    private final MusicalInstrument musicalInstrument;

    public ViolinManager(MusicalInstrument musicalInstrument) {
        this.musicalInstrument = musicalInstrument;
    }
    public void updatePrice(Integer newPrice) {
        musicalInstrument.getSpec().setPrice(newPrice);
    }

    @Override
    public void play() {
        System.out.println("바이올린을 킵니다");
    }
}
  • 이후 기타도 동일하게 수정해준다
public class Main {

    public static void main(String[] args) {
        // guitar
        Info guitarInfo = new GuitarInfo(12426);
        Spec guitarSpec = new GuitarSpec(1000,"어쿠스틱 기타");
        MusicalInstrument guitar = new Guitar(guitarInfo,guitarSpec);
        Manager guitarManager = new GuitarManager(guitar);
        guitarManager.play();
        guitarManager.updatePrice(10000);
        System.out.println(guitar.getSpec().getPrice());

        Info violinInfo = new ViolinInfo(771512);
        Spec violinSpec = new ViolinSpec(715000,"100년산 바이올린");
        MusicalInstrument violin = new Violin(violinInfo,violinSpec);
        Manager violinManager = new ViolinManager(violin);
        violinManager.play();
        violinManager.updatePrice(7000000);
        System.out.println(violin.getSpec().getPrice());
    }
}
  • 우리는 앞으로, 새로운 악기가 추가되어도 Info,Spec,Manager,MusicalInstriment 인터페이스를 활용하여 새로운 악기 클래스를 만들고 구현만 시켜주면 기존코드의 수정을 하지않고, 새로운 클래스를 만들어서 활용만 해주면된다

적용이슈

  • 확장되는 것과, 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 관계가 더 복잡해질 수 있습니다
  • 설계자의 좋은 자질 중 하나는, 이러한 크기 조절과 같은 갈등 상황을 잘 포착하여 비장한 결단을 내릴줄 아는 능력에 있습니다

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

  • 정의
  • "서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다"라는 뜻입니다
  • 즉 서브타입은 언제나 기반 타입과 호환 될 수 있어야합니다
  • 다시 다른말로하면, "서브타입은 기반타입이 약속한 규약을 지켜야합니다"
  • 예를들어 Animal 클래스에서 makeSound()를 만들고, 이를 Cat 클래스에서 상속받아서 오버라이딩 하는데 오버라이딩할때 makeSound()에서 소리를 나게하는게 아니라, 예외를 던지게 한다는 방식을 어겼다고 볼 수 있습니다 ( 자세한 설명은 아래에...)
  • 구현상속,인터페이스상속 이든 궁극적으로 다형성을 통한 확장성 획득을 목표로합니다
  • LSP원리도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미합니다
  • 다형성과 확장성을 극대화 하위 클래스를 사용하는것보다는 상위의 클래스를 (Animal을 상속받고있는 Cat을 쓰는것보다는 Animal 클래스를 사용하는것이, 더 나아가서는 인터페이스 개념으로까지 통용됨) 사용하는것이 더 좋습니다
  • 상속은 다형성과 따로 생각할 수 없습니다
  • 그리고 다형성으로 인한 확장 효과를 얻기 위해서는 서브 클래스가 기반 클래스와 클라이언트간의 규약(인터페이스)를 어겨서는 안됩니다
  • 따라서 이 구조는 다형성을 통한 확장의 원리인 OCP를 제공합니다
  • 즉 LSP는 OCP를 구성하는 구조가 됩니다
  • OOP의 원리는 이렇게 서로가 서로를 이용하기도 하고, 포함하기도 하는 특징이 있습니다
  • LSP는 규약을 준수하는 상속구조를 제공합니다
  • LSP를 바탕으로 OCP는 확장하는 부분에 다형성을 제공해 변화에 열려있는 프로그램을 만들 수 있도록 합니다

리스코브 치환의 원칙 지키는 사례

class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

리스코브 치환의 원칙 어기는 사례

class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

class Bird extends Animal {
    @Override
    public void makeSound() {
        throw new UnsupportedOperationException("Birds don't make sounds");
    }
}

적용방법

  • 만약 두 개체가 동일한 일을한다면, 둘을 하나의 클래스로 표현하고 이들을 구분 할 수 있는 필드를 둡니다
// ComputerType Enum (컴퓨터 타입을 구분하는 enum)
public enum ComputerType {
    A_COMPUTER,
    B_COMPUTER
}

// Computer 클래스
public class Computer {
    private ComputerType type;

    public Computer(ComputerType type) {
        this.type = type;
    }

    public void playComputer() {
        switch (type) {
            case A_COMPUTER:
                System.out.println("A 컴퓨터를 합니다");
                break;
            case B_COMPUTER:
                System.out.println("B 컴퓨터를 합니다");
                break;
            default:
                throw new UnsupportedOperationException("Unknown computer type");
        }
    }
}
  • 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현합니다
class AComputer implements Computer {
    @Override
    public void playComputer() {
        System.out.println("A 컴퓨터를 합니다");
    }
}

class BComputer implements Computer{
    @Override
    public void playComputer() {
        System.out.println("B 컴퓨터를 합니다");
    }
}

interface Computer {
    public void playComputer();
}
  • 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만듭니다
class Computer {
    public void playComputer() {
        System.out.println("컴퓨터를 합니다");
    }
}

class Mouse {
    public void playMouse() {
        System.out.println("마우스를 합니다");
    }
}
  • 두 개체가 하는일에 추가적으로 무언가를 한다면 구현상속을 사용합니다
package SOLID.computer;

class Computer extends Machine{
    public void playComputer() {
        System.out.println("컴퓨터를 합니다");
    }
}

class Mouse extends Machine{
    public void playMouse() {
        System.out.println("마우스를 합니다");
    }
}

class Machine {
    public void on() {
        System.out.println("전원을 킵니다");
    }
}

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

정의

  • ISP원리는 한 클래스는 자신이 사용하지않는 인터페이스는 구현하지 말아야 한다는 원리입니다
  • 즉 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스 만을 사용해야 합니다
  • ISP는 하나의 일반적인 인터페이스 보다는, 여러개의 구체적인 인터페이스가 낫다, 라고 정의할 수도 있습니다
  • 만약 어떤 클래스를 이용하는 클라이언트가 여러개고, 이들이 해당 클래스의 특정 부분집합만을 이용한다면 이들을 따로 인터페이스로 빼내어 클라이언트가 기대하는 메시지만을 전달할 수 있도록 합니다
  • SPR가 클래스의 단일 책임을 강조한다면
  • ISP는 인터페이스의 단일책임을 강조합니다
  • 하지만 ISP는 어떤 클래스 혹은 인터페이스가 여러 책임, 여러 역할을 갖는 것을 인정합니다
  • 이러한 경우 ISP가 사용되는데 SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면
  • ISP에서는 인터페이스 분리를 통해 같은 목표에 도달합니다

예시

package SOLID.isp.powerControl;

public class Main {
    public static void main(String[] args) {
        PowerControlManager powerControlManager = new PowerControlManager();
        PowerControlStateManager powerControlStateManager = new PowerControlStateManager();
        Tv tv = new Tv(powerControlManager,powerControlStateManager);
        tv.on();
        tv.off();
        tv.getOn();
        tv.getOff();
        Ramp ramp = new Ramp(powerControlManager,powerControlStateManager);
        ramp.on();
        ramp.off();
        ramp.getOn();
        ramp.getOff();
    }
}

interface PowerControl {
    void on();
    void off();
}

interface PowerControlState {
    void getOn();
    void getOff();
}

class PowerControlStateManager implements PowerControlState {
    @Override
    public void getOn() {
        System.out.println("전원이 켜져있는값 반환");
    }

    @Override
    public void getOff() {
        System.out.println("전원이 꺼져있는값 반환");
    }
}

class PowerControlManager implements PowerControl {
    @Override
    public void on() {
        System.out.println("전원을 킵니다");
    }

    @Override
    public void off() {
        System.out.println("전원을 끕니다");
    }
}

class Tv implements PowerControlState,PowerControl {
    private final PowerControl powerControl;
    private final PowerControlState powerControlState;

    public Tv(PowerControl powerControl,PowerControlState powerControlState) {
        this.powerControl = powerControl;
        this.powerControlState = powerControlState;
    }

    @Override
    public void getOn() {
        powerControlState.getOn();
    }

    @Override
    public void getOff() {
        powerControlState.getOff();
    }

    @Override
    public void on() {
        powerControl.on();
    }

    @Override
    public void off() {
        powerControl.off();
    }


}

class Ramp implements PowerControlState,PowerControl {
    private final PowerControl powerControl;
    private final PowerControlState powerControlState;

    public Ramp(PowerControl powerControl,PowerControlState powerControlState) {
        this.powerControl = powerControl;
        this.powerControlState = powerControlState;
    }

    @Override
    public void getOn() {
        powerControlState.getOn();
    }

    @Override
    public void getOff() {
        powerControlState.getOff();
    }

    @Override
    public void on() {
        powerControl.on();
    }

    @Override
    public void off() {
        powerControl.off();
    }
}

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

정의

  • 의존 관계의 역전이란 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전입니다
  • 실제 사용관계는 바뀌지 않으며, 추상을 매개로 메세지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙입니다
  • DIP의 키워드는 "IoC","훅 메서드","확장성"입니다
  • 이 3가지 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트간의 커뮤니케이션을 효율적이게 합니다

    Caller: 호출 하는쪽 (컨트롤러라고 봐도될듯)
    Callee: 호출 당하는쪽(서비스로 봐도될듯)

  • 이를 위해 Callee 컴포넌트는 Caller 컴포넌트들이 등록할 수 있는 인터페이스를 제공합니다
  • 따라서 자연스럽게 Callee는 Caller들의 컨테이너 역할이 됩니다
  • Callee 컴포넌트는 정의된 훅 메서드를 구현합니다
  • 이로써 DIP를 위한 준비가 완료되고 이 상태에서 다음과 같은 시나리오가 전개됩니다
  • Caller는 Callee에 자신을 등록합니다
  • Callee는 Caller에게 정보를 제공할 적당한 시점에 Caller의 훅 메서드를 호출합니다
  • 바로 이 시점은 Caller와 Callee의 호출관계가 역전되는 IoC시점이 됩니다
  • DIP는 비동기적으로 커뮤니케이션이 이루어져도 될 경우, 컴포넌트 간의 커뮤니케이션이 복잡할 경우, 컴포넌트 간의 커뮤니케이션이 비효율적일 경우에 사용됩니다
  • DIP는 복잡하고 지난한 컴포넌트간의 커뮤니케이션 관계를 단순화하기 위한 원칙입니다
  • 실 세계에서도 헐리우드 원칙에서와 같이 귀찮도록 자주 질문과 요청을 하는 동료에게도 써먹어 볼만한 원칙입니다.

훅 메서드
슈퍼클래스에서 디폴트 기능을 정의해두거나, 비워뒀다가 서브클래스에서 선택적으로 오버라이드 할 수 있도록 만들어준 메서드를 읽컫습니다

마치며

  • 마지막 DIP에 대해서는 아직 면밀히 파악하지못하여, 다음 포스팅이나 언젠가 똑바로 정리하고자한다

참조 블로그

https://www.nextree.co.kr/p6960/

profile
테러대응전문가

0개의 댓글