업데이트:

4 분 소요

개요

SOLID란 객체지향 프로그래밍 및 설계의 다섯 가지 기본 원착을 두문자어 기억술로 소개한 것이다.
이 다섯가지 원칙을 프로그래밍 및 설계에 적용한다면 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들 수 있고, 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때 까지 적용할 수 원칙이기도 하다.

직접 프로젝트를 하다보면 이러한 원칙을 쉽사리 잊어버리고는 한다.
좋은 프로그램을 만들고 깔끔한 코드를 짤 수 있게끔 지켜지는 원칙임을 인지하고 있기에, 기억하고 다시 되새기려 포스트를 작성하게 됐다.

SOLID

SRP, Single responsibility principle(단일 책임 원칙)

단일 책임 원칙의 의미는 아래와 같다.

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

캡슐화 ?

캡슐화는 클래스의 내부 변수(속성, 데이터)와 메서드(행위)를 하나로 패키징해야 한다는 OOP안의 중요한 개념이다.
java나 c++에서는 접근지정자를 통해 내부 정보를 은닉하고 다른 곳에서 접근할 수 없도록 한다.

객체를 캡슐화하면 코드의 중복을 방지하고 내부 동작에 대해 외부에서 알 필요가 없어진다.
캡슐화의 이점과, 그 이유에 관해서는 여기서 자세히 알 수 있다.

다시 단일 책임 원칙으로 돌아와서, 단일 책임 원칙을 위반한다면, 즉 하나의 클래스가 여러 책임을 지고 있다면 개발자가 책임 중 하나를 변경했을 때 다른 책임에 영향을 줘 버그가 발생할 가능성이 높아진다.

public class cup{
    public Action drink(){
        ...
    }
    public Species getContent(){
        ...
    }
    public Action wash(){
        ...
    }
}

위의 코드는 SRP를 위반한다. 책임을 분리해 아래와 같이 작성되어야 한다.

public class cup{
    private Species getContent(){
        ...
    }
public class human{
    private Action drink(){
        ...
    }
    private Action wash(){
        ...
    }
}
}

이에 반해 단일 책임 원칙을 따른다면 아래와 같은 이점이 있다.

  • 용이한 단위 테스트
  • 줄어든 종속성에 의한 낮은 결합도
  • 용이한 검색
  • 용이한 구현과 이해

OCP, Open/closed principle(개방-폐쇄 원칙)

위키에서 확인한 개방-폐쇄 원칙의 의의는 다음과 같다.

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

조금 어렵게 들리지만, OCP는 기존의 코드를 변경하지 않고도 기능을 추가할 수 있게 설계되어야 한다는 원칙이다.
컵 안의 내용물을 확인하는 다음 클래스 예시를 통해 쉽게 이해해보자.

public class Main{
    public static void main(String args[]){
        CoffeCup cup1 = new CoffeeCup();
        JuiceCup cup2 = new JuiceCup();
        cup1.getContent()
        cup2.getContent()

    }
}
public class JuiceCup{
    
    public void getContent(){
        System.out.println("Juice in here");
    }

}
public class CoffeeCup{
    public void getContent(){
        System.out.println("Coffee in here");
    }
}

위와 같은 MyCups 클래스에서는 새로운 종류의 컵이 생긴다면 다른 그 컵에 맞는 새로운 객체를 생성해야 한다.

public class Main{
    public static void main(String args[]){
        Cup cup1 = new CoffeeCup();
        Cup cup2 = new JuiceCup();

        cup1.getContent()
    }

}
public class Cup{
    public String ContentType = "";
    public void setContentType(String type){
        this.ContentType = type;
    }
    public void getContentType(){
        System.out.println(ContentType + " in here")
    }

}
public class CoffeeCup extends Cup{
    public CoffeCup(){
        setContentType("Coffee");
    }
}
public class JuiceCup extends Cup{
    public JuiceCup(){
        setContentType("Juice");
    }
}

OCP 원칙을 통해 새로 만든 클래스에서는 새로운 컵이 생겼을 경우에도 Cup 클래스를 통한 추상화로 Cup클래스의 getContentType() 메서드를 통해 내용물을 확인할 수 있다.

새로운 컵이 생길 때 마다 그에 맞는 새로운 타입의 컵을 만들어 주어야 하는 기존의 코드에 비해 추상화를 통해 새로운 컵을 만들어 주게 된다면 기존의 코드를 변경하지 않고도 기능을 확장 시킬 수 있다.

이렇듯 OCP 원칙을 따르지 않는다면 if-else문이 늘어나 변경과 확장에 따라 코드가 더욱 복잡해지며, 그에 따라 리팩터링과 유지보수가 어려워진다.

LSP, Liskov substitution principle(리스코프 치환 원칙)

리스코프 치환원칙은 다른 원칙들에 비해 그 이름으로 내용을 유추해내기 어려움이 있다.
이 원칙이 말하는 내용은 다음과 같다.

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

리스코프 치환 원칙은 다형성에 관한 원칙을 말한다.

다형성 ?

다형성이란 하나의 객체가 한 가지 타입 뿐만 아니라 여러 가지 타입을 가질 수 있는 성질을 의미한다.
자바에서는 부모 클래스 타입의 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하고 있다.
실제로 위의 내용을 이곳에서 확인해보면 다음과 같다.

class Parent { ... }
class Child extends Parent { ... }
...
Parent pa = new Parent(); // 허용
Child ch = new Child();   // 허용
Parent pc = new Child();  // 허용
Child cp = new Parent();  // 오류 발생.

이와 같이 부모 클래스 Parent는 자식 클래스 Child를 가질 수 있지만 반대의 경우는 허용되지 않는 모습을 볼 수 있다. 이는 자식 클래스에서 사용할 수 있는 속성(멤버 변수)의 개수가 부모 클래스의 속성보다 항상 적거나 같아야 하기 때문이다.

다시 돌아와서, 리스코프 치환 원칙은 이러한 다형성과 그 사용에 관한 원칙이다.
리스코프 치환 원칙에 관한 예시를 먼저 보자.

public class Parent{
    public parentMethod(){
        ...
    }
    ...
}
public class Child extends Parent{
    public childMethod(){
        ...
    }
    ...
public class Main{
    public static void someMethod(Parent p){
        p.parentMethod();
    }
    public static void main(String[] args) {
        parentMethod(new Parent());// 허용
        parentMethod(new Child());// 허용
    }
}
}

예시에서 Main 클래스의 someMethod는 그 인자로 Parent 타입을 받지만, 그 안에 Parent 클래스를 상속 받는 Child 클래스를 인자로 넣어도 정상적으로 작동된다.
리스코프 치환 원칙은 이렇게 부모클래스의 subclass를 인자로 넣어도 작동되어야 한다는 것을 의미한다.

이렇게 리스코프 치환원칙이 제대로 지켜지지 않는다면 다형성에 기반한 개방 폐쇄의 원칙 또한 지켜지지 않아 리스코프 치환원칙을 지키는 것이 중요하게 여겨진다.
리스코프 치환원칙에 관한 더 정확한 설명은 이곳에 있다.

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

인터페이스 분리의 원칙의 의의는 다음과 같다.

클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
인터페이스에 관한 간략한 설명은 이전에 작성한 포스트에 있다.
알다시피, 인터페이스를 사용하기 위해서는 그 안에 있는 추상 메서드를 모두 오버라이드 해줘야 한다.

하지만 어떤 클래스는 인터페이스 안에 있는 모든 추상 메서드를 사용하지 않는 경우가 있는데, 이는 뚱뚱한 인터페이스라 불리며 인터페이스 분리의 원칙을 위배하게 된다.

이러한 인터페이스는 더 잘게 쪼개져야 함을 의미한다. 하지만 인터페이스를 너무 잘게 쪼개서도 안된다. 인터페이스가 너무 잘게 쪼개진다면 이를 구현하는 구현 클래스들은 너무 많은 인터페이스를 상속 받아야하기 때문이다.

따라서 인터페이스는 어떤 기능이나 역할을 기준으로 적절히 나뉘어져야 한다.

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

의존관계 역전 원칙은 다음과 같은 내용을 가진다.

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

이 원칙은 코드의 확장성과 재사용성을 추구를 위한 원칙이다. 상위 모듈과 하위 모듈이 모두 추상화에 의존하면서 프로그램은 그 자체로 유연해지고 그만큼 확장과 유지보수에 용이성을 증대시켜준다.

조금더 쉽게 말하자면 객체는 객체가 아닌 인터페이스에 의존해야하고 변화의 가능성이 큰 것에 의존하지 않아야 한다 고 할 수 있겠다.

참고문서

SOLID
https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)
캡슐화
https://bperhaps.tistory.com/entry/캡슐화란-무엇인가-어떤-이점이-있는가
단일 책임 원칙
https://yoongrammer.tistory.com/96
개방 폐쇄 원칙
https://steady-coding.tistory.com/378
다형성
http://www.tcpschool.com/java/java_polymorphism_concept
리스코프 치환 원칙
https://koseungbin.gitbook.io/wiki/books/undefined/part-2.-di/solid/liskov-substitution-principle
인터페이스 분리 원칙
https://velog.io/@tataki26/인터페이스-분리-원칙

댓글남기기