[F-Lab 모각코 챌린지 25일차] SOLID

부추·2023년 6월 25일
0

F-Lab 모각코 챌린지

목록 보기
25/66

TIL

객체지향 설계 5원칙 : SOLID



프로그램을 설계할 땐 모듈 간 결합도는 낮추고, 모듈 내 응집도를 높여야 한다는 원칙이 있다.

모듈 간의 상호 의존성, 즉 결합도를 을 낮춰 한 부분이 조금 변한다고 해서 전체 시스템이 망가지지 않게 해야하고, 연관성이 있는 요소들을 하나의 모듈로 집중시켜 잘 캡슐화하는 것이다. 반대로 응집도가 낮고 결합도가 높으면 객체 코드 수정이 일어날 때 프로젝트 여기저기에 흩어진 객체의 흔적을 찾느라 힘들어진다. 변화의 전파가 커진다는 뜻이다. 이는 유지보수의 비용을 높이는 잘못된 설계다!

객체지향적 관점으로 위 원칙을 지키기 위해 나온 개념이 객체지향 설계 5원칙, SOLID이다. 일단 오늘은 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("효도합니다.");
    }
}

물론 '나'로서 모두 하는 행위이긴 하지만, 하나의 class에 몰려있으니 깔끔하지 못하다. 게다가 졸업을 해서 더이상 학생이 아니라거나, 갑자기 전 세계적 왕따가 됐다면 study()와 play()는 처치 곤란의 method가 된다. 졸업을 해서 학생이 아니게 되었는데, 효도하는 주체로서의 class를 바꿔버리면 단일 책임 원칙을 위반하게 된다. 게다가 학생으로서 Person이 사용되는 코드는 전~부 수정의 대상이 되어버린다.

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. _ 로버트 C.마틴

public class Student extends Person {
	@Override
    public void do() {
        System.out.println("공부합니다.");
    }
}
public class Friend extends Person {
	@Override
    public void do() {
        System.out.println("함께 놉니다.");
    }
}
public class Daughter extends Person {
	@Override
    public void do() {
        System.out.println("효도합니다.");
    }
}

역할과 책임에 따라 클래스를 분리했다. 상위 Person클래스를 두었는데 이것이 최선의 선택일지는 프로젝트의 맥락에 따라 달려있다. 아무튼 상속을 통한 다형성을 달성하기 위해 메소드 역시 do()로 통일한 것 역시 확인하자!

SRP의 핵심은 하나의 역할 혹은 책임을 가진 property / method / module / package 등을 따로 분리하는 것이다. 이들을 분리해서 하나의 클래스에 하나의 책임만을 집어넣으면 응집도가 높아진다.


같은 공부를 해도 국어국문학과 학생과 영어영문학과 학생이 하는 공부는 다르다. 전공에 따라 다른 공부를 하는 method를 구현하고 싶다고 해서, 아래와 같이 하면 단일 책임(행위) 원칙을 위반한 것이다. 필드나 method가 책임에 따라 다른 의미를 가져선 안된다.

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(개방 폐쇄 원칙) : 자신의 확장엔 열려있고, 주변의 변화엔 닫혀있다.

아이패드를 갖고 애플펜슬로 필기를 하는 대학생이 있다.

계속 수기로 적자니 손이 너무 아프고, 말이 많은 교수님이라 적을 것도 많아서 노트북 타자필기를 하기로 했다.
-> OCP 위배다. 노트북과 아이패드는 대학생 입장에서 똑같이 필기를 위한 도구인데, 그게 좀 변화했다고 해서 사용하는 method 이름도 바뀌어버렸다. 주변의 변화에 닫혀있지 않은 것이다.

'필기도구'라는 상위 class를 만들어, 대학생 객체가 필기도구라는 주변의 변화에 닫혀있도록 했다. 아이패드와 노트북은 그를 상속받아, 필기도구라면 마땅히 해야할 일 "필기()"를 각자의 방식으로 구현하도록 했다. 이로써 필기도구는 기존의 아이패드, 노트북 뿐만 아니라 공책, 핸드폰(?) 등 확장에 열려있게 됐다.

이를 비슷하게 확인해 볼 수 있는 예시가 바로 JPA 인터페이스와 그 구현체들이다.

자바 어플리케이션에서 ORM을 이용하기 위해선 JPA 인터페이스만 알면 된다. 구체적 구현체로 Hibernate를 쓰든, EclipseLink를 쓰든 JPA 가이드에서 정의한 method를 사용하기만 하면 그만이다. "구현체"라는 주변 변화에 어플리케이션은 닫혀있다. 한편 JPA는 여러 종류의 구현체를 가질 수 있음으로써 자신의 확장엔 열리게 되었다.

JVM도 마찬가지이다. 실제 하드웨어를 돌리는 OS가 무엇이든, JRE만 구성되어있다면 자바 코드는 어디서든 동작한다. 소스코드는 OS 변화에 닫혀있고, JVM은 다양한 OS의 확장에 열려있다.

구체적인 것들을 묶는 인터페이스가 하나의 쿠션으로 작용하여 이와같은 확장 폐쇄 원칙을 지킬 수 있게 해주는 것 같다.



3. LSP(리스코프 치환 원칙) : 하위 타입은 상위 타입으로 대체 가능해야 한다.

  • 하위 클래스 is a kind of 상위 클래스 : 하위 타입은 상위 타입의 한 종류이다.
  • 구현 클래스 is able to 인터페이스 : 구현 타입은 인터페이스 할 수 있어야 한다.'

상속과 관련한 위 두 문장을 잘 지키는 선에서 객체 설계를 했다면 리스코프 치환 원칙을 잘 지켰다고 볼 수 있다. OOP의 상속과 관련된 상위/하위 개념은 조직이나 계층이 아닌 "분류"의 개념이기 때문에, 하위 클래스는 상위 클래스의 부분집합으로 표시된다.

사람을 하나의 상위 클래스로, 여자와 남자를 하위 클래스로 볼 수 있다. 여자와 남자는 각각 사람의 분류이기 때문에, 사람이 할 수 있는 일은 뭐든 할 수 있어야 한다. "여자"는 "남자"가 할 수 있는 일은 할 수 없어도 "사람"이 할 수 있는 일은 가능해야 한다는 뜻이다.



4. ISP(인터페이스 분리 원칙)

1번의 예시를 다시 한 번 들고와보자. Student, Daughter, Friend 각각의 역할을 하는 클래스로 분리하는 대신 각 역할을 interface들로 나누는 방법이 있다.

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("공부합니다.");
    }
}

각 역할을 하는 객체가 필요해질때마다, 참조 변수 타입을 인터페이스로 하여 원하는 method를 호출할 수 있다. 클래스 분리가 아닌 인터페이스 분리로 코드를 깔끔하게 만들었다. 같은 문제에 대한 다른 해결방식인 것이다.


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

상위 클래스는 풍성할수록 좋고, 상위 인터페이스는 작을수록 좋다. ISP와 관련해서 함께 나오는 원칙이다.

상위 클래스가 풍성하다는 말은, 상위 클래스를 상속받는 하위 클래스의 공통점을 최대한! 많이 묶어 올려보냈다는 말이다. 이렇게 되면 다형성의 이점을 누릴 수 있다. 상속을 이용하는 이유는 "다형성"때문인데, 다형성은 클라이언트 코드 입장에서 구상 클래스의 구현을 알지 못해도 프로그램이 잘 동작하는 특징이다. 풍성한 상위 클래스에 대해 하위 클래스가 각 메소드를 잘 구현해놓으면 클라이언트 코드의 수정을 최소화하면서 다양하게 확장된 기능을 이용할 수 있다.

상위 인터페이스가 작다는 말은, 특정 인터페이스로 작동하기 위해 필요한 구현을 최소화한다는 말이다. 인터페이스는 "어떤 기능을 할 수 있는" 책임을 클래스에 추가하기 위해 사용한다. 사용 목적으로 만들어진 인터페이스의 책임 메소드들은 public인데, 이는 캡슐화가 안된, 겉으로 드러나는 요소들이다. 또한 사용하지 않는 인터페이스 요소가 있을 경우 그것의 수정에 같이 영향을 받을 위험성도 높다. 따라서 상위 인터페이스는 작을수록 좋다.



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

  • 구체적인 것이 추상적인 것에 의존해야 한다.
  • 변하는 것이 변하지 않는 것에 의존해야 한다.

여기서 '구체적인 것'이란 상위 클래스나 인터페이스가 아닌 하위 클래스와 구체적 구현체를 말한다. 추상적이고 변하지 않는 것은 그 반대다. "concrete", 즉 구현이 확실하고 자주 변하는 클래스에 의존을 하지 말라는 원칙이다.


OCP의 학생-필기도구가 DIP를 위반한 예시이다. (DIP와 OCP는 연관성이 높다)아이패드는 필기도구를 구현한 concrete 구현체이다. 필기도구 구현체는 아이패드가 될 수도 있고, 노트북이 될 수도 있고, 공책이 될 수도 있다. 쉽게 변한다는 말이다. 대학생이 이같이 쉽게 변하는 구체 클래스에 의존하면 큰일난다!!!!! 필기도구 클래스가 수정되거나 변할 때 영향을 직접적으로 받기 때문이다. 구현보다 인터페이스에 맞춰 하는 설계는 수정에 대한 일종의 ..

따라서 변하지 않는, 즉 추상화된 상위 클래스나 인터페이스에 의존하여 구현 클래스의 변화에 영향을 최소화 하는 것이 바람직하다.

자신보다 변하기 쉬운 것에 의존하지 마라.
구현체보다 인터페이스에 맞춰 설계하라.

상위 클래스 / 인터페이스 / 추상 클래스일수록 변화할 확률이 적다. 객체 의존성을 추가할 것이라면 이와 같은 것들에 의존해야 한다는 것이 DIP이다.

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글