Language/Java

[Java] SOLID

by Donghwan 2021. 9. 29.

목적

  • 변경에 유연합니다.
  • 이해하기 쉽습니다.
  • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 됩니다.

 

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

클래스는 단 한 개의 책임을 가져야 한다는 원칙으로 클래스가 변경되는 이유는 단 한 개여야 한다는 의미입니다. 응집도는 높고 결합도는 낮은 프로그램을 뜻합니다. 설계를 잘한 프로그램은 새로운 요구사항과 프로그램 변경에 영향을 받는 부분이 적습니다.

변경의 이유가 단 하나여야만 한다라는 것은 하나의 모듈은 오직 하나의 액터만 책임져야 한다는 뜻입니다. 그렇다면 '책임'은 무엇일까요? 해당 클래스에 어떤 '액터'들이 의존하게 되는지를 생각해봐야 올바르게 준수 할 수 있습니다. 여기서 '액터'란 시스템이 동일한 방식으로 변경되기를 원하는 사용자(클라이언트) 집단을 의미합니다.

public class Person {
    private String name;
    private int age;
    
    public void eat() { ... }
    
    public void sleep() { ... }
    
    public void coding() { ... }
}

위와 같은 Person 클래스는 SRP를 위반했습니다. 그 이유에 대해 알아보겠습니다. Person 클래스는 사람의 의미합니다. 그럼 이때 '액터'는 모든 사람들이라고 생각할 수 있습니다. 모든 사람은 공통적으로 이름을 가지고 있고 나이가 있습니다. 또 먹고 자는 행동도 모두 동일하게 수행하는 행동입니다. 하지만 코딩은 모든 사람들이 하는 행동은 아닙니다. 개발자이거나 개발에 취미가 있는 사람이 하는 행동이기 때문입니다. 아래와 같이 수정한다면 SRP를 지켰다고 볼 수 있습니다.

public class Person {
    private String name;
    private int age;
    
    public void run() { ... }
    
    public void eat() { ... }
    
    public void sleep() { ... }
}

public class Programmer extends Person {
    public void coding() { ... }
}

 

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

확장에는 열려 있지만 변경에는 닫혀 있어야 한다는 원칙입니다. 기존 코드를 수정하기보다 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 해야 합니다. 객체지향언어에서는 추상화를 통해 개방폐쇄원칙을 해결 할 수 있습니다. 추상화는 개방폐쇄원칙의 핵심요소입니다.

개방폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야 할 때 이미 제대로 동작하고 있던 원래 코드를 변경하지 않아도, 기존의 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능합니다. 이 의미는 추상화를 기반의 클래스를 만들면 이 추상화 자체는 수정에 닫혀있다고 할 수 있지만 필요에 따라 이 추상화의 파생 클래스들을 만드는 것으로 확장에는 열려 있는 설계를 가져갈 수 있다는 의미로 해석됩니다.

public class User {
    private Clazz clazz;

    public User(Clazz clazz) {
        this.clazz = clazz;
    }

    public void changeClazz(Clazz clazz) {
        this.clazz = clazz;
    }

    public void attack() {
        clazz.attack();
    }
}

abstract class Clazz {
    abstract void attack();
}

public class Warrior extends Clazz {
    @Override
    public void attack() {
        System.out.println("Power Strike");
    }
}

public class Magician extends Clazz {
    @Override
    public void attack() {
        System.out.println("Magic Claw");
    }
}

public class Main {
    public static void main(String[] args) {
        User user = new User(new Warrior());
        user.attack();
        user.changeClazz(new Magician());
        user.attack();
    }
}

//Result
//Power Strike
//Magic Claw

코드를 보면 게임상의 직업을 추상화한 Clazz라는 클래스를 통해 직업을 구성하게 된다면 새로운 직업 Archer를 생성하더라도 새로운 클래스를 생성할 뿐 실제적인 코드에 변경이 없습니다. 

간단하게 추상화를 클래스를 통해 하게된다면 템플릿 메서드 패턴이라고 할 수 있고 인터페이스를 통해 적용하면 전략 패턴이라고 할 수 있습니다.

 

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

상위 클래스를 상속받은 하위 클래스를 상위 클래스의 인스턴스 대신 전달하여도 동작에 이상이 없어야 한다는 원칙입니다. 상위 클래스와 하위 클래스는 is-a 관계를 성립해야 합니다. 상위 클래스를 통해 추상화된 공통의 개념을 가지고 있어야 합니다.

LSP에 가장 유명한 예제가 직사각형과 정사각형입니다. 직사각형은 가로와 세로가 독립적인 값을 가질 수 있지만 정사각형은 그럴 수 없습니다. 쉽게 정사각형은 직사각형입니다. 하지만 직사각형은 정사각형이 아니기 때문에 LSP에 위배 됩니다. 

LSP의 정의를 보면 OCP와 매우 관계가 깊습니다. OCP는 추상화를 통해 행동을 컨트롤하고 상속 또는 구현한 하위 클래스에서 동작 방식을 구현을 하고 있습니다. OCP에서 작성한 예시를 보면 Clazz를 입력 받아야할 부분에 Clazz를 상속받은 Warrior와 Magician을 주입하여도 동작에 이상이 없습니다. 또 Clazz를 직접 넣더라도 동작에 이상이 없습니다. 단 위 예제는 Clazz의 attack()은 내부 상세를 구현하지 않았기 때문에 어떠한 것도 출력 되지 않습니다.

결론적으로 LSP가 지켜지지 않으면 OCP를 위반하게 되므로 기능 확장을 위해 더 많은 부분을 수정해야 합니다.

 

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

자신이 사용하지 않는 것에 의존하지 않고 영향을 받지 말아야 합니다. 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스를 사용하여 구성하는 것이 좋습니다.

public interface Eat { ... }

public interface Walk { ... }

public interface Sleep { ... }

public class Human implements Eat, Walk, Sleep { ... }

 

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

고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안됩니다. 반대로 세부사항은 정책에 의존해야 합니다. 여기서 말하는 고수준이란 추상화된 개념을 의미하고 저수준은 구체화된 개념을 의미합니다. 추상화된 개념이란 쉽게 변하지 않는 고유의 행위 또는 정책을 의미합니다.

위 ISP의 예제를 보면 Human이라는 개념은 인간을 의미하고 먹고 자고 걷는 것은 변하지 않는 행위입니다. 하지만 먹는 방법, 걷는 방법, 자는 방법은 다양하게 존재할 수 있습니다. 그렇기 때문에 Human의 입장에서 내가 먹는 방법, 걷는 방법, 자는 방법을 구체적으로 몰라도 실행할 수 있는데 이때 Human이라는 클래스 내부에서 이러한 내용들을 구체적으로 안다면 OCP에 위배가 된다고 볼 수 있습니다. OCP와 함께 생각해 본다면 구체화는 추상화된 개념 뒤에 숨어 의존성을 역전하게 됩니다. 이 부분을 의존성 역전이라고 합니다. 주의할 점은 런타임에서의 의존을 역전시키는 것이 아니라 코드 단계에서의 의존을 역전시킨다는 것을 유의해야 합니다. 

 


참고자료

  • Clean Architecture
728x90
반응형

'Language > Java' 카테고리의 다른 글

[Java] 연산자 ( Operator )  (0) 2021.10.23
[Java] 변수 (Variable)  (0) 2021.10.16
[Java] 객체 지향 언어  (0) 2021.09.29
[Java] 제네릭 (Generic)  (0) 2021.04.21
[Java] 다형성  (0) 2021.01.01

댓글