SOLID란 로버트 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것

시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용한다.

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

단일 책임 원칙이란 모든 클래스는 하나의 책임을 가지며, 클래스는 그 책임을 완전히 캡슐화해야 한다.

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다.

액터란 시스템이 동일한 방식으로 변경되기를 원하는 사용자 집단(1 or more)

SRP를 설계할 때는 설계하는 해당 클래스에만 초점을 맞춘 미시적 관점이 아니라 거시적인 관점에서 해당 클래스에 어떤 액터들이 의존하게 되는지를 생각해봐야 한다.

개방-폐쇄 원칙(OCP, Open-Closed Principle)

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다.

기능을 추가하거나 변경해야 할 때 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다.

  • 확장에 열려 있다.

    모듈의 동작을 확장할 수 있다. 애플리케이션의 요구 사항이 변경될 때, 이 변경에 맞게 새로운 동작을 추가해 모듈을 확장할 수 있다. 즉 모듈이 하는 일을 변경할 수 있다.

  • 수정에 대해 닫혀 있다.

    모듈의 소스 코드나 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있다. 그 모듈의 실행 가능한 바이너리 형태나 링크 가능한 라이브러리를 건드릴 필요가 없다.

추상화를 통한 개방-폐쇄 원칙 (OCP, Open Close Principal)

객체 지향 프로그래밍 언어에서는 고정되기는 해도 제한되지는 않은, 가능한 동작의 묶음을 표현하는 추상화가 가능하다. 모듈은 추상화를 조작할 수 있다. 이런 모듈은 고정된 추상화에 의존하기 떄문에 수정에는 닫혀 있고, 새 파생 클래스를 만들어 확장할 수 있다.

예 : 인터페이스를 통합 확장(인터페이스를 통한 파생클래스를 만드는 자유로운 확장. 즉, 개방)과 폐쇄(파생 클래스에 의해서 사용자 기능(모듈)이 수정없이 확장된다) 컴파일 타임의 의존성을 인터페이스에 고정시키고, 런타임 의존성을 변경시키게 함으로써 추가 구현 클래스를 생성하더라도 동작은 추가(개방)되지만 기존의 코드는 수정되지 않음(폐쇄)을 의미한다.

모듈은 확장할 수 있고 그 확장은 모듈의 변경을 초래하지 않는다

개방 폐쇄 원칙 OCP 은 관리가능(maintainable)하고, 재사용(reusable) 가능한 코드를 만드는 기반이다. 잘 설계된 코드는 기존 코드의 변경 없이 확장 가능하다. OCP 를 가능케 하는 중요 메커니즘은 추상화와 다형성이다. 추상화와 다형성을 가능케 하는 키 메커니즘이 상속이다. 추상 기반 클래스의 순수 가상 함수로 부터 클래스를 상속 파생 시킴으로써 추상화된 다형 인터페이스를 만들어 낼 수 있다. - 로버트 C. 마틴

개방 폐쇄 원칙(OCP)

리스코프 치환 원칙(LSP, Liskov Substitution Principle)

Child classes should never break the parent class’ type definitions.

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바뀔 수 있어야 한다.

컴퓨터 프로그램에서 자료형 S가 자료형 T의 하위형이라면 필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체(치환)할 수 있어야 한다.

LSP 위반의 예

  • 직사각형 클래스로부터 정사각형 클래스를 파생하는 경우
  • 포유류 클래스로부터 오리너구리 클래스를 파생하는 경우(‘모든 포유류는 태생이다’라고 가정할 경우)
class Mammal {
 
    private int age;
    private String name;

    Mammal(int age, String name) {
        this.age = age;
        this.name = name;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    public int getAge() {
        return this.age;
    }
 
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public Mammal reproduce() {
        return new Mammal(0, this.name + " 새끼");
    }

    public boolean isLactating() {
        return true;
    }
}

// 오리너구리
class Platypus extends Mammal {
    public Mammal reproduce() {
        return new Mammal(0, this.name + " 알");
    }
}

class Client {
    public boolean reproduceVerifier(Mammal m) {
        if(m.reproduce().getName().endsWith("새끼")) {
            throw new Exception("Bad reproduce!");
        }
 
        return true;
    }
 
    public void testSquareArea() {
        Platypus m = new Platypus(0, "오리너구리");

        assertTrue(c.reproduceVerifier(r));
    }
}

대체 가능성

오리너구리/포유류 체계는 리스코프 치환 원칙을 위반한다.

  • 잘못된 분석으로 인해 발생하는 실패의 원인(대부분의 포유류가 태생을 하지만 일부는 그렇지 않다.)
  • Mammal Class의 서브타입으로 ViviparousMammal(태생의 포유류)와 Monotreme(알 낳는 포유류 : 단공류)를 두어 해결할 수 있다.
  • Platypus를 Monotreme 안에 포함 시킬 수 있다.

리스코프 치환 원칙의 강제

  • 하위형에서 메서드 인수의 반공변성
  • 하위형에서 반환형의 공변성
  • 하위형에서 메소드는 상위형 메소드에서 지정한 예외 외의 다른 예외를 던지는 것을 비허용
  • 하위형에서 선행조건은 강화될 수 없음 (선행 조건 : 루틴이나 클래스에서 다른 루틴을 호출하거나 객체를 생성하기 에 반드시 true여야 하는 특성)
  • 하위형에서 후행조건은 약화될 수 없음 (후행 조건 : 루틴이나 클래스를 호출하고 난 에 반드시 true여야 하는 조건 어설션은 선행조건과 후행조건이 참인지 동적 검사 가능)
  • 하위형에서 상위형의 불변조건은 반드시 유지되어야 함 (불변 조건 : 실행하기 전, 중, 후 모두 항상 만족해야 하는 조건)

제네릭에서의 공변성, 반공변성, 무공변성

변성 의미
공변성(Covariant) T'가 T의 서브타입이면, C<T`>는 C<? extends T>의 서브타입이다.
반공변성(Contravariant) T`가 T의 서브타입이면, C<T>는 C<? super T>의 서브타입이다.
무공변성, 불변성(Invariant) C와 C<T`>는 아무런 관계가 없다.

예시

public class Main {
    public static void main(String[] args) {
        Platypus p1 = new Platypus("오구", 30);
        Platypus p2 = new Platypus("아기 오구", 6);
        Echidna e1 = new Echidna("뚜지", 30);
        BlackBear b1 = new BlackBear("검은곰", 40);

        List<Animal> animals = new ArrayList<>();
        animals.add(p1);
        animals.add(p2);
        animals.add(e1);
        animals.add(b1);

        List<Monotreme> monotremes = new ArrayList<>();
        monotremes.add(p1);
        monotremes.add(p2);
        monotremes.add(e1);

        List<Platypus> platypuses = new ArrayList<>();
        platypuses.add(p1);
        platypuses.add(p2);

        howToCry(animals);

        howToReproduce(animals);
        howToReproduce(monotremes);

        // <? super Monotreme> : Monotreme 의 서브타입을 사용할 수 없다.
        // java: incompatible types:
        // java.util.List<com.company.Platypus> cannot be converted to 
        // java.util.List<? super com.company.Monotreme>
        // howToReproduce(platypuses);
    }

    // 공변성 : 특정 타입에서 확장한 타입만을 허용한다.
    // Animal 의 서브타입을 허용한다.
    private static void howToCry(List<? extends Animal> animals) {
        for(var animal : animals)
            animal.crying();
    }

    // 반공변성 : 특정 타입을 포함한 슈퍼 타입만을 허용한다.
    // Platypus 의 확장 타입을 허용하지 않는다.
    private static void howToReproduce(List<? super Monotreme> list) {
        for(var ele : list) {
            System.out.println(monotreme.getClass().getName());
        }
    }
}

public interface Animal {
    void crying();
}

public abstract class Mammal implements Animal{
    String name;
    int age;
}

// 태생의 포유류
public abstract class ViviparousMammal extends Mammal{
    public void reproduce() {
        System.out.println(this.name + "는(은) 태생입니다.");
    }
}

// 단공류
public abstract class Monotreme extends Mammal{
    public void reproduce() {
        System.out.println(this.name + "는(은) 알에서 태어납니다.");
    }
}

// 검은 곰
public class BlackBear extends ViviparousMammal{
    BlackBear(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void crying() {
        System.out.println(name + ": 그르렁!");
    }
}

// 가시두더지
public class Echidna extends Monotreme {
    public Echidna(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void crying() {
        System.out.println(this.name + ": 끾끾끾");
    }
}

// 오리너구리
public class Platypus extends Monotreme {
    public Platypus(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void crying() {
        System.out.println(name + ": 으아아아악!");
    }
}

인터페이스 분리 원칙(ISP, Interface Segregation Principle)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙이다.

큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다. 이와 같은 작은 인터페이스들을 역할 인터페이스라고도 부른다.

인터페이스 분리 원칙을 통해 시스템의 내부 의존성을 약화시켜 리팩토링, 수정, 재배포를 쉽게 할 수 있다.

의존관계 역전 원칙(DIP, Dependency Inversion Principle)

프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

의존관계 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭한다. 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전 시킴으로써 상위 계층의 하위 계층의 구현으로부터 독립한다.

  1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
  2. 추상화는 세부 사항에 의존하면 안된다. 세부사항이 추상화에 의존해야 한다.

참고

  1. 위키백과

  2. 객체지향 개발 5대 원칙 (SOLID)

  3. SOLID Design

댓글남기기