객체 지향 설계 5 원칙: SOLID

  • by

프로그램을 설계할 때 모듈 결합도가 낮아지고 모듈 나의 응집도를 높여야 한다는 원칙이 있다.

모듈 간의 상호 의존성을 낮추고 있는 부분이 조금 달라진다고 해서, 시스템 전체가 망가지지 않도록 하고, 관련성이 있는 요소를 하나의 모듈에 집중시켜 재이용 및 메인터넌스성을 높이는 것이다.

오브젝트 지향적인 관점으로부터 상기 원칙을 지키기 위해 나온 개념이 오브젝트 지향 설계 5 원칙, SOLID이다.

1. SRP(단일 책임 원칙) : 하나의 모듈은 하나의 책임만 가지고 있습니다.

나(파)는 학생이든, 어머니의 딸이든, 누군가의 친구이기도 하다.

각각의 사회적 역할에서 나(사람)의 행위를 Person 객체에 정의해 보았다.

public class Person {
    public void study() {
        System.out.println("공부합니다.

"); } public void play() { System.out.println("함께 놉니다.

"); } public void hyodo() { System.out.println("효도합니다.

"); } }

물론 「나」로서 모두 행위이지만, 하나의 클래스에 모여 있는 것으로 분명히 할 수 없다.

게다가 졸업을 해서 더 이상 학생이 아닌가, 갑자기 전 세계적 괴롭힘이 되면 study() 와 play() 는 퇴치 곤란의 method 가 된다.

졸업을 하고 학생이 아니게 되었지만, 효행하는 주체로서의 클래스를 바꾸어 버리면, 단일 책임 원칙을 위반하게 된다.

어떤 클래스를 변경해야 하는지는 하나만 있어야 합니다.

_로버트 C. 마틴

public class Student extends Person {
    public void study() {
        System.out.println("공부합니다.

"); } }
public class Friend extends Person {
    public void play() {
        System.out.println("함께 놉니다.

"); } }
public class Daughter extends Person {
    public void hyodo() {
        System.out.println("효도합니다.

"); } }

역할과 책임에 따라 수업을 나누었습니다.

아무래도 같은 사람인 만큼 공통의 속성이나 메소드가 있는 것 같고, Person이라고 하는 상위 클래스를 계승하는 형태를 취했다.

SRP의 핵심은 하나의 역할 또는 책임을 가진 속성/방법/모듈/패키지 등을 개별적으로 분리하는 것이다.

git에 풀 요청을 올릴 때도 그 이유는 하나인 것이 예쁘다.

같은 공부를 해도 국어국문학과 학생과 영어영문학과 학생이 하는 공부는 다르다.

전공에 따라 다른 공부를 하는 방법을 구현하고 싶다며 다음과 같이 하면 단일의 책임(행위) 원칙을 위반한 것이다.

필드나 메소드는 책임에 따라 다른 의미를 가져서는 안됩니다.

public class CollegeStudent {
    String major;
    
    public CollegeStudent(String major) {
        this.major = major;
    }
    
    public void study() {
        if (this.major.equals("국어국문학과")) {
            System.out.println("국문을 공부합니다.

"); } else if (this.major.equals("영어영문학과")) { System.out.println("영문을 공부합니다.

"); } } }

다시 한 번, 한 가지 역할과 책임을 가진 클래스로 나누었습니다.

추상 클래스를 두고 책임에 따라 다른 방법으로 구현하도록 했다.

public abstract class CollegeStudent {
    public abstract void study();
}
public class KoreanStudent extends CollegeStudent {
    @Override
    public void study() {
        System.out.println("국문을 공부합니다.

"); } }
public class EnglishStudent extends CollegeStudent {
    @Override
    public void study() {
        System.out.println("영문을 공부합니다.

"); } }

2. OCP(개폐 원칙) : 자신의 확장에는 열려 있고 주변의 변화에는 닫혀 있다.

iPad를 들고 애플 연필로 필기하는 대학생이 있다.


계속 수기로 적자라면 손이 너무 아파서 말이 많은 교수라고 쓰는 것도 많아 노트북 타자 필기를 하기로 했다.


OCP 위반이다.

노트북과 iPad는 대학생의 입장에서 똑같이 필기를 위한 도구인데, 그것이 조금 변화했다고 사용하고 있는 method의 이름도 바뀌어 버렸다.

주위의 변화에 ​​닫히지 않은 것이다.


「필기 툴」이라고 하는 상위 클래스를 작성해, 대학생 오브젝트가 필기 툴이라고 하는 주변의 변화에 ​​닫고 있도록(듯이) 했다.

iPad와 노트북은 그를 계승해, 필기 툴이라면 바로 해야 하는 것 「필기()」를 각각의 방법으로 구현하게 했다.

이에 따라 필기구는 기존 iPad, 노트북 뿐만 아니라 공책, 휴대전화(?) 등 확장에 열려 있게 됐다.

이것을 마찬가지로 확인할 수 있는 예는, JPA 인터페이스와 그 구현입니다.


Java 애플리케이션에서 ORM을 이용하기 위해서는 JPA 인터페이스만을 알면 된다.

구체적인 구현체로 Hibernate 를 사용하는지, EclipseLink 를 사용하는지 JPA 가이드로 정의한 method 를 사용하는 것만으로 그만 둡시다.

「구현체」라는 주위의 변화에 ​​응용은 닫혀 있다.

반면에 JPA는 다양한 유형의 구현을 가질 수 있으며 자체 확장으로 열렸습니다.

JVM도 마찬가지입니다.

실제 하드웨어를 돌리는 OS가 무엇이든, JRE만이 구성되어 있으면 Java 코드는 어디에서나 동작한다.

소스 코드는 OS의 변화에 ​​닫혀 있으며, JVM은 다양한 OS의 확장에 열려있다.

구체적인 것을 잇는 인터페이스가 하나의 쿠션으로서 일하고, 이러한 확장 폐쇄 원칙을 지킬 수 있도록 해주는 것 같다.

3. LSP(리스스코프 대체 원칙) : 하위 유형은 상위 유형으로 대체 가능해야 합니다.

  • 서브 클래스 is a kind of 부모 클래스: 서브타입은 부모 타입의 일종입니다.

  • 구현 클래스 is able to 인터페이스 : 구현 타입은 인터페이스 가능하지 않으면 안됩니다.

상속에 관한 위의 두 문장을 잘 지키는 선으로 객체 설계를 했다면, 리스스코프 치환 원칙을 잘 지킨 것으로 보인다.

OOP 상속과 관련된 상위/하위 개념은 조직이나 계층 구조가 아닙니다.

분류‘의 개념이므로 서브 클래스는 부모 클래스의 서브 세트라고 표시됩니다.

사람을 하나의 상위 클래스, 여성과 남자를 서브 클래스로 볼 수 있습니다.

여자와 남자는 각각 사람의 분류이므로 사람이 할 수 있는 일은 무엇이든 할 수 있어야 한다.

‘여자’는 ‘남자’가 할 수 없어도 ‘사람’이 할 수 있는 것은 가능해야 한다는 의미다.

4. ISP(인터페이스 분리 원칙) : SRP의 다른 솔루션

한 번의 예를 다시 가져오자. Student, Daughter, Friend 각각의 역할을 하는 클래스로 분리하는 대신 각 역할을 인터페이스로 분할하는 방법있다.

public interface Student {
    public void study();
}
public interface Daughter {
    public void hyodo();
}
public interface Friend {
    public void play();
}

그리고 각 인터페이스는 Person 객체에 의해 구현됩니다.

public class Person implements Student, Friend, Daughter {
    @Override
    public void hyodo() {
        System.out.println("효도합니다.

"); } @Override public void play() { System.out.println("같이 놉니다.

"); } @Override public void study() { System.out.println("공부합니다.

"); } }

각 역할을 수행하는 객체가 필요할 때마다 참조 변수 유형을 인터페이스로 원하는 메서드를 호출할 수 있습니다.

클래스 분리가 아닌 인터페이스 분리로 코드를 청소했습니다.

동일한 문제에 대한 또 다른 해결책입니다.

그러나, ISP를 이용한 방법은, 1 클래스내에 인터페이스의 구현이 모여 있어, 복수의 구현을 써야 하기 때문에, SRP의 방식보다 예쁘게 보이지 않는다.

아무래도 클래스를 나누는 SRP를 사용하는 것이 국가 규칙처럼.

# 인터페이스 최소주의 원칙

부모 클래스는 풍부할수록 좋고, 부모 인터페이스는 작을수록 좋다.

ISP에 관해 함께 나오는 원칙이다.

상위 클래스가 풍부하다는 말은, 상위 클래스를 계승하는 서브 클래스의 공통점을 최대한으로!
많이 묶어 올렸다는 말이다.

따라서 상속이라는 특성을 매우 유용하게 사용할 수 있습니다.

참조 변수형을 편하게 상위형으로 해 두면 그것을 사용하지만, 하위형의 필드/메소드가 필요한 때에 최소한으로 형 캐스트를 해 해당 속성을 이용하는 것이다.

부모 인터페이스가 작다는 단어는 특정 역할을 수행하는 인터페이스의 SRP 개념과 관련이 있습니다.

앞의 예에서 Daughter와 Student의 기능을 한 번에 맞춘 인터페이스가 나오면 인터페이스 최수주 원칙을 위반한 것이다.

5. DIP(의존 역전 원칙) : 추상적인 것, 변하지 않는 것에 의지해야 한다.

  • 구체적인 것 추상적으로 의존해야 한다.

  • 변화하다 변함없는 것에 의존해야 한다.

여기서 「구체적인 것」이란, 상위 클래스나 인터페이스가 아니라, 서브 클래스와 구체적인 구현체를 말한다.

추상적이고 변하지 않는 것은 그 반대이다.

“concrete”, 즉 구현이 확실하고 자주 변화하는 클래스에 의존하지 말아야 한다는 원칙입니다.

OCP 학생 – 필기구가 DIP를 위반한 예입니다.

(DIP와 OCP는 SRP와 ISP처럼 관련성이 높습니다.

) iPad는 필기 도구를 구현한 콘크리트 구현입니다.

필기 도구 구현은 iPad, 노트북 또는 노트북으로 만들 수 있습니다.

쉽게 바뀐다는 것이다.

대학생이 이렇게 쉽게 바뀌는 구체적인 클래스에 의존하면 코드를 제대로 쓰는 것이 어려워진다.

재사용도 어렵고, 유지 보수의 난이도도 올라간다.

그러므로 변함없는, 즉 추상화 된 부모 클래스와 인터페이스에 따라 구현 클래스의 변화에 ​​영향을 최소화 하는 것이 바람직하다.

자신보다 변하기 쉬운 것에 의존하지 마십시오.

상위 클래스/인터페이스/추상 클래스일수록 변화할 확률은 적다.

객체 종속성을 추가하는 경우에는 이러한 것에 의존해야 한다는 것이 DIP입니다.

마무리

SOLID의 원칙을 준수하여 프로젝트를 설계하면 일반적으로 소스 파일 수가 많아지고 심각한 경우 프로그램 자체의 성능이 저하될 수 있습니다.

그러나 원칙을 준수하도록 설계된 코드는 이해, 유지 보수 및 확장에 유리하며, 이를 통해 얻은 이점은 그 이상입니다.

오브젝트 지향 5 원칙에 대해 ChatGPT가 설명한 것과 정리한다.