객체 지향 5대 원칙 - SOLID

1. 시작하며

SOLID 원칙에 대해 공부할 기회가 생겨 읽은 내용을 정리하고자 한다. 이 글을 통해 주로 공부하였으므로 원문을 확인하는 것을 추천한다.

 

SOLID 원칙은 2000년대 초반 로버트 마틴이 제안한 프로그래밍 및 설계의 다섯 가지 원칙이다. 이를 마이클 페더스가 앞글자를 따서 SOLID라 명명하고 기본 개념을 구축하였다.

 

이 디자인 원칙은 코드를 더 유지보수하기 쉽고, 설계를 이해하기 쉬우며, 확장성이 뛰어난 소프트웨어를 만들 수 있도록 도와준다. 결과적으로 복잡성, 종속성을 감소시켜 개발자가 다른 영역에 영향을 주지 않고 한 영역을 변경할 수 있도록 한다.

2. SOLID 원칙이란

이미지: https://hackernoon.com/how-to-learn-solid-design-principles-in-5-minutes-952x33rm

  1. Single Responsibility
  2. Open/Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

Single-responsibility

  • There should never be more than one reason for a class to change
  • In other words, every class should have only one responsibility
  • 클래스는 단 하나의 책임만을 가져야 한다
  • Testing, 낮은 결합도, Organization 의 측면에서 도움이 된다.

아래와 같이 Book 이라는 클래스가 있다
Book 클래스에 name, author, text를 저장할 수 있고 text 를 조회하는 메서드가 있다.

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters

    // methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

Book class 는 이제 제대로 동작한다. 해당 클래스를 통해 많은 book 을 저장할 수 있다.
여기에 콘솔에 print를 출력하는 메소드를 추가해보자.

public class Book {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

그러나 위의 코드는 single responsibility 을 위반한다.
이를 수정하기 위해 text의 print만 처리하는 별도의 class 로 구현해보자.

public class BookPrinter {

    // methods for outputting text
    void printTextToConsole(String text){
        //our code for formatting and printing the text
    }

    void printTextToAnotherMedium(String text){
        // code for writing to any other location..
    }
}

이제 Book 에서 Print와 관련된 책임들은 분리되어 개발 완료하였다. BookPrinter class를 통해 Book class의 print 책임을 줄여줄 뿐만 아니라 text의 내용을 다른 미디어로도 전송할 수 있다.

Open–closed

  • Software entities ... should be open for extension, but closed for modification
  • 클래스는 확장에 대해 열려 있어야 하고, 변경에 대해 닫혀있어야 한다.
  • 기존 코드를 수정하고 잠재적인 버그를 일으키는 것을 방지한다.

Guitar Class 가 있다.

public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

기타에 불꽃 패턴을 적용하여 더 롹앤롤처럼 보이게 하고자 한다.
이때 Guitar Class 에 Flame 을 추가할 수 있지만 어떤 side effect 가 발생할지 알 수 없다. 따라서 Open–closed 원칙을 지키면서 간단하게 Guitar class 를 확장해보자.

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

Guitar 클래스 를 확장하여 기존 애플리케이션이 영향을 받지 않도록 할 수 있다.

Liskov substitution

  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
  • 기반 클래스의 메소드는 파생 클래스 객체의 상세를 알지 않고서도 사용될 수 있어야 한다.
  • Class A 가 Class B 의 하위 유형이면 프로그램의 동작을 방해하지 않고 B 를 A로 바꿀 수 있어야 한다.

Car interface 가 있다.

public interface Car {

    void turnOnEngine();
    void accelerate();
}

모든 자동차가 수행할 수 있는 작업, 즉 엔진을 켜고 가속하는 interface를 정의한다.
interface를 구현하고 메서드에 몇가지 기능을 제공한다.

public class MotorCar implements Car {

    private Engine engine;

    //Constructors, getters + setters

    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }

    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

위의 코드를 통해 이제 엔진을 켜고 가속할 수 있다.
그런데 우리는 전기자동차 시대에 살고 있으므로 엔진이 없는 전기자동차도 추가해야 한다.

public class ElectricCar implements Car {

    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }

    public void accelerate() {
        //this acceleration is crazy!
    }
}

엔진이 없는 자동차를 추가함으로써 프로그램의 동작을 변경하였다. 이는 Liskov 치환에 대한 위반이며 이 전의 두 가지 원칙보다 수정하기 힘들다.

한 가지 가능한 솔루션은 Car 의 엔진이 없는 상태를 고려하는 interface로 재작업 하는 것이다.

Interface segregation

  • Many client-specific interfaces are better than one general-purpose interface
  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다.
  • 더 큰 인터페이스를 더 작은 인터페이스로 분할함을 의미한다.
  • 이를 통해 구현 클래스가 관심 있는 메소드에 대해서만 관심을 가질 수 있다.

곰 사육사 interface가 있다.

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

열렬한 사육사로서 곰을 씻기고, 먹이는 것을 기쁘게 생각한다. 그러나 우리는 곰을 쓰다듬는 것이 위험하다는 것을 알고 있다. 불행히도 infterface가 다소 커서 곰을 쓰다듬는 코드를 구현하는 것 외에는 선택의 여지가 없다.

 

이제 BearKeeper interface를 개별 interface로 분할해보자.

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

interface 덕분에 우리에게 관심사가 있는 중요한 메서드만 자유롭게 구현 가능하다.

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

마지막으로 위험한 일은 맛간 사람들에게 맡길 수 있다.

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

더 나아가 BookPrinter class도 같은 방식으로 interface 분리를 사용할 수 있다. 하나의 print method를 가진 Printer interface를 구현하여 별도의 ConsoleBookPrinter, OtherMediaBookPrinter 클래스를 인스턴스화할 수 있다.

Dependency inversion

  • Depend upon abstractions, [not] concretions
  • 추상화에 의존해야지, 구체화에 의존하면 안된다.
  • 자주 변경되는 구체적인 것에 의존하지 말고 추상화된 것을 참조

구식으로 돌아가서 코드가 있는 Windows 98 컴퓨터에 생명을 불어 넣어 보자.

public class Windows98Machine {}

그러나 모니터와 키보드가 없는 컴퓨터가 무슨 소용이 있겠는가?
인스턴스화 해놓은 Windows98Computer 가 Monitor와 StandardKeyboard로 포장 될 수 있도록 생성자에 각각 추가해보자

public class Windows98Machine {

    private final StandardKeyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new StandardKeyboard();
    }

}

Windows98Computer 클래스 내에서 StandardKeyboard 와 Monitor 를 사용할 수 있게 되었다.

문제는 해결되었으나 StandardKeyboard와 Monitor를 선언함으로써 이 3개의 클래스는 긴밀하게 결합되었다.

이는 Windows98Computer 를 테스트하기 어렵게 만들 뿐만 아니라 StandardKeyboard를 다른 키보드로 전환하기 어렵게 만들었다. Monitor 역시 마찬가지이다.

 

따라서 좀 더 일반적인 Keyboard interface를 추가하고 이를 사용하여 StandardKeyboard 를 Windows98Machine를 분리해보자.

public interface Keyboard { }
public class Windows98Machine{

    private final Keyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine(Keyboard keyboard, Monitor monitor) {
        this.keyboard = keyboard;
        this.monitor = monitor;
    }
}

Windows98Machine class에 Keyboard 종속성을 쉽게 추가하기 위해 종속성 주입 패턴을 사용하였다.
StandardKeyboard clss를 수정하여 Keyboard interface를 구현하여 Windows98Machine class에 추가하기 쉽게 수정하였다.

public class StandardKeyboard implements Keyboard { }

이제 모든 class 들은 결합성이 분리되었고 Keyboard 추상화를 통해 communication 한다. Machine의 Keyboard 타입 역시 다른 interface 구현받아서 쉽게 교체할 수 있다. 이는 Monitor class 에도 적용 가능하다.

이제 종속성은 분리되었고 선택한 테스트 프레임워크를 사용하여 Windows98Machine을 자유롭게 테스트할 수 있다.

3. 마치며

SOLID 원칙을 적용하여 코드를 리팩터링 하여 코드 스멜을 제거할 수 있다. 이론 및 예제는 확인해보았지만 현업에서 막상 적용하려고 하니 막막함을 느낄 수 있었다. 앞으로 가야 할 길이 멀었다.

참고

반응형

댓글

Designed by JB FACTORY