Java

[OOP] 객체지향 개발 5대 원칙, 'SOLID'란 무엇인가?

ImKDM 2022. 4. 13. 14:10
728x90

SOLID란 무엇인가


개발자 면접에서 많이 등장하는 개념으로 좋은 객체지향 프로그래밍을 하는데 준수해야 하는 5가지 원칙을 말한다. 

이와 같은 원리들을 적용하면 코드의 유지보수가 굉장히 수월해진다. 

단일 책임의 원칙 (Single Responsibility Principle)  [SRP]


하나의 클래스에 너무 많은 데이터, 기능, 책임을 부여하면 안 되고, 모든 클래스는 각각 하나의 책임만 가져야 한다.

 

SRP를 위반한 좋지 못한 케이스

public class man {

    public eat() {}                   // 인간으로서 역할
    public takeClothes() {}
    public goToBathroom() {}

    public void kiss() {}              // 남자친구 역할
    public void giveHerPresent() {}

    public void fireGun() {}           // 군인 역할
    public void train() {}
    public void beOnDuty() {}

    public void massage() {}           // 아들 역할
    public void praise() {}
    
}

 

만약 Man 이라는 클래스를 만들었는데, '사람', '남자 친구', '군인', '아들'이라는 다양한 역할(책임)을 몽땅 넣으면 단일 책임의 원칙에 위반된다. 혹시 군인 역할에 추가/수정이 생기면 다른 역할에도 영향이 갈 수밖에 없고, 해당 클래스에 오류가 발생했을 때 모든 코드를 확인해야 한다. 그리고 코드가 지저분해질 수 있다. 

 

이를 해결하기 위해선 Man의 각 역할에 맞게 잘 분리하여 별도의 클래스들을 만들면 된다. class BoyFriend, class Solider, class Son 으로 나누면 각각의 기능과 책임에 충실할 수 있다. 하나의 클래스에 하나의 책임을 부여하는 것이 단일 책임의 원칙이다.

개방 폐쇄의 원칙 (Open-Closed Principle)  [OCP]


자신의 확장에는 열려 있고, 주변의 변화에는 닫혀 있어야 하는 원칙이다. 

 

'1톤 트럭'과 '마티즈'는 기어 작동 방식이 다르다. '1톤 트럭'은 수동 기어를 사용하고, '마티즈'는 자동 기어이다. 만약 운전자가 '1톤 트럭'과 '마티즈'와 직접 관계를 맺는다면, 운전자 클래스는 해당 자동차의 기어 시스템에 따라 코드 구현이 달라질 것이다.

 

OCP를 위반한 좋지 못한 케이스

class Driver {

    if( '1톤 트럭' ) {
        기어 = '수동';
    } else if( '마티즈' ) {
        기어 = '자동';
            .
            .
            .
    }
}

// 만약 BMW, 포르쉐, 쏘나타, 볼보... 등 다양한 자동차가 추가된다면?

 

새로운 자동차가 추가될 때마다 IF 구문의 수는 끝없이 길어질 것이고, 이는 코드의 가독성 및 유지보수에 좋지 못하다. OCP를 충족하려면, 새로운 객체의 추가 등장으로 확장에는 개방적이지만, 이로 인한 자신의 코드는 변화하지 못하도록 폐쇄적이어야 한다. 다시 말해, 추가할 때는 잘 오픈이 되어 있는 반면에, 기존의 코드는 변경하지 않거나 수정하려면 고쳐야 할 코드 부분이 폐쇄적으로 한 곳에 모여 있어야 하는 것이다.

 

개방 폐쇄의 원칙의 좋은 예는 'JDBC'이다. 데이터 베이스를 Oracle에서 MySQL로 바꿔도 자바 프로그램이 Java DataBase Connectivity란 인터페이스를 통해 DB와 연결되기 때문에 직접 자바 프로그램에서 변경되는 부분이 적다. 이를 통해 자바 프로그램의 수정엔 폐쇄적이지만, 어떤 DB도 쉽게 추가할 수 있어 OCP를 만족시키는 좋은 코드가 된다.

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


서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있다는 원칙이다. 

 

쉽게 이야기해서 부모의 클래스를 상속받은 자식 인스턴스가 부모의 기능까지 사용할 수 있어야 한다는 의미이다. 상속을 하면 부모 클래스의 참조 변수로 하위 클래스의 인스턴스를 업 캐스팅을 통해 할당할 수 있다. 그런데 아버지와 딸의 관계를 보자. 아버지 클래스와 그 딸인 춘향이 클래스가 있다고 가정했을 때,

 

아버지 춘향이 = new 딸();    (X)

 

관계는 이상하다. 왜냐하면 아버지형 객체 참조 변수를 가진 춘향이가 아버지 객체가 가진 기능(메서드)를 할 수 있어야 하는데 딸은 아버지의 역할을 수행할 수 없다. 이런 관계에선 리스코프 치환 원칙을 위배한다고 볼 수 있다. 하지만 동물의 예는 반대이다.

 

동물 펭수 = new 펭귄();    (O)

 

펭수는 펭귄으로써 동물의 기능을 잘 수행할 수 있다. 상속 관계는 하위 분류가 상위 분류의 한 종류일 때 LSP를 준수한다. 이와 같이 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입했을 때 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.

 

또 하나의 예로 만약 '동물'이란 상위 클래스에 '걷기', '뛰기', '사냥하기' 메서드가 있다고 생각해보자. 이 동물 클래스를 상속하는 '강아지'와 '고양이' 클래스는 해당 메서드를 모두 상속할 수 있다. 하지만 같은 동물이지만 헤엄치는 '상어', '고래' 클래스는 부모 클래스의 기능을 구현할 수 없다. 부모의 행위를 자식이 거부해선 안된다.

 

LSP가 충족되어야 상속의 성질을 잘 지키는 것이고, 상속에 있어서 가장 중요한 기본 원칙이다. LSP가 충족되어야 OCP가 가능하기 때문에 잘 지켜야 한다.

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


어떤 클래스가 인터페이스를 구현하고 있을 때, 큰 범위의 하나의 인터페이스를 구현하는 것보다, 작고 구체적인 여러 개의 인터페이스를 구현해야 한다는 원칙이다.

 

만약 '뛰다', '물다', 짖다'의 기능을 가진 '동물'이란 인터페이스를 '강아지' 클래스가 구현한다고 생각해보자. 문제가 없다. 하지만 '고양이' 클래스로 동물 인터페이스를 구현하려면 '짖다'와 '물다' 기능을 구현하지 못한다. (고양이는 짖다보다 '운다'가, 물다보다 '할퀴다'가 더 어울린다고 치자). 하지만 인터페이스는 모든 기능을 구현해야 하기 때문에 고양이 클래스가 동물 인터페이스를 구현하면 문제가 발생한다.

 

따라서 여러가지 기능을 가진 인터페이스를 만들지 말고, "최소한의 기능만 제공하면서 하나의 역할에 집중하는" 인터페이스를 사용해야 한다. 인터페이스 내 메소드는 최소한일수록 좋다. 필요에 따라 여러 인터페이스를 복수로 구현하면 된다.

 

또 다른 예로, '수륙양용차' 인터페이스를 만들었다고 가정하자. 여기엔 '직진하기', '좌회전하기', '우회전하기', '배 조종하기', '배 우회전하기', '배 좌회전하기' 기능들이 있다. 하지만 자동차 클래스엔 보트 기능이 필요 없고, 보트 클래스엔 자동차 기능이 필요 없다. ISP를 충족시키려면 자동차와 보트 인터페이스를 따로 만들어야 한다. 만약 수륙양용차 클래스를 만들고 싶다면 '자동차'와 '보트' 인터페이스를 모두 구현하면 된다.

의존역전 원칙 (Dependency Inversion Principle)  [DIP]


구체적인 것이 추상화된 것에 의존해야 한다. 다시 말해 의존관계를 맺을 때 좀 더 일반적이고 추상적인 것에 의존하라는 원칙이다. 

 

만약에 '토끼'가 '당근'이라는 구체적인 데이터 타입에 의존한다고 생각해보자. 토끼는 당근만 먹고살지 않는다. 이렇게 먹이는 쉽게 바뀔 수 있고, 자주 바뀌는 것에 의존하면 토끼는 영향을 받게 된다. 때문에 토끼 자신보다 더 자주 변하는 당근에 의존하는 건 좋지 못하다.

 

그런데 만약 당근, 양상추, 무 등이 구현하고 있는 '채소'라는 상위 인터페이스에 토끼가 의존하고 있으면 어떨까? 토끼가 구체적인 당근이 아니라 추상화된 채소 인터페이스에만 의존하게 됨으로써 실제 먹이가 변경되어도 토끼가 직접 영향을 받지 않게 된다. 이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 "추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에서 영향을 받지 않게 하는 것"이 DIP이다.

 

상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높다. 따라서 의존관계를 맺을 때 하위 클래스나 구체적인 클래스가 아닌 가능한 추상화 모듈을 통해 의존해야 한다.

 


<지식 출처>

https://www.youtube.com/watch?v=KO2xdqOZSAs 

https://www.youtube.com/watch?v=pJL4EuA6aGc 

https://www.youtube.com/watch?v=fXpsmTK7aZk 

https://www.youtube.com/watch?v=UOo5Jl8Kk0o 

https://devlog-wjdrbs96.tistory.com/380

https://server-engineer.tistory.com/226