SOLID 원칙이란?
객체지향 프로그래밍 설계의 다섯가지 기본 원칙
유지보수와 확장이 쉬운 프로그램을 만들기 위한 목적
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- OCP(Open/Close Principle) : 개방/폐쇄 원칙
- LSP(Liskov Substitute Principle) : 리스코프 치환 원칙
- ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle) : 의존성 역전 원칙
1. SRP(Single Responsibility Principle) : 단일 책임 원칙
- 모듈이나 클래스는 하나의 책임을 가져야 한다
- 어떤 변화에 의해 클래스를 변경해야하는 이유는 오직 하나 뿐이어야 한다.(하나의 변경 원인만 가져아한다.)
(한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있다.)
ex) 출력과 편집을 제공하는 Docs class
Class Docs { Page page = new Page; public Report printReport() {} public Report editReport() {} } |
위 클래스는 문서편집기가 제공해야하는 기능을 제공하는 책임을 담당하고 있다.
언뜻 보면 문서편집기를 하나의 덩어리로 본다면 단일 책임 원칙을 잘 지킨 패턴이라고 볼 수 있을수 도 있을 것 같다.
위 클래스에 변경 가능한 부분을 생각해보면 편집 형식(페이지 번호, 정렬, 페이지 방향 등)이 있을 수 있고,
출력 형식(홀수/짝수 페이지만, 특정 페이지만, 출력 용지 규격, 페이지 방향 등)이 있을 수 있다.
만약, 출력창에서 가로/세로 페이지 형식을 바꿔 출력하는 기능을 추가하기 위해
Class Docs { Page page = new Page; public Report printReport(boolean isPageVertical) { page.setPageDir(isPageVertical); } public Report editReport() {} } |
이와 같이 소스코드를 수정하고 기능을 사용하였더니 출력창에서만 페이지의 가로/세로가 변경된 것이 아니라
편집창에서도 가로/세로가 변경되는 현상이 나타날 수 있다.
Page 객체를 편집기능과 출력기능에 공유하니 출력에서만 가로/세로를 변경하는게 아니라 편집창에서까지 이어졌다.
따라서 아래와 같이
Class DocsPrinter { Page page = new Page; public Report printReport(boolean isPageVertical) { page.setPageDir(isPageVertical); } } Class DocsEditor { Page page = new Page; public Report editReport() {} } |
문서 출력의 책임과 문서 편집의 책임을 분리하여 설계한다면 기존 설계 방식보다 영향도를 줄이며 변경에 대응할 수 있을 것이다.
※ SRP 설계시 주의 사항
- 책임의 명확한 정의(추상화), 변경의 이유
추상화를 통해 같이 수정해야할 것들을 묶고, 따로 수정해야할 것은 분리시킨다. - 과도한 분리에 빠지지 않기
단일 책임 원칙을 고려하다보면 마치 클래스 하나에 메서드 하나를 설계하는게 바람직한건가(?)라고 생각할 수 도 있겠지만, 단일 책임 원칙은 모듈의 응집도를 높이고 결합도를 낮추는데 있다. 즉, 하나의 클래스를 보고 이 클래스가 어떤 목적과 기능을 수행하는지 명확하게 알아야하기에(클래스 여러개를 거쳐가며 파악해야한다면 응집도가 낮은것), 책임을 마구잡이로 분산시키는 것만이 SRP를 잘 지킨 설계는 아니다.
SRP의 장점
- 낮은 결합도로 유지보수에 유리함
- 코드의 재사용성 증가
- 응집도 향상(코드 파악, 클래스 파악 쉬워짐)
- 쉬운 테스트
2. OCP(Open/Close Principle) : 개방/폐쇄 원칙
- 기존의 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
- 기능 수정에 대해서는 폐쇄적이고 확장에 대해서는 열려있는 구조
- 추상화, 다형성과 연관이 깊다.
* 추상화 : '구체적이지 않은' 정도 또는 '추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징'이라고 정의한다.
Open :확장에 열려있다. -> 상속, 오버라이딩, 오버로딩과 같은 개념을 통한 기능의 확장
Close : 변경에 닫혀있다. -> 기존의 소스 코드 수정에 폐쇄적
=> 변경시 기존 소스코드는 변경하지 않고 기존 클래스 상속을 통한 오버라이딩으로 새로운 기능 제공
=> 즉, 기존 기능 변경이 아닌 새로운 기능의 확장
ex) JDBC : DB의 종류가 추가되어도 기존 DB 연결에 영향을 주지 않고 새롭게 확장 시켜 나갈 수 있음
기존의 DB 연결 스펙에서 다른 DB까지 연결해야 하는 상황에서 상속을 통해 새로운 DB를 연결하는 클래스를 설계한다면
기존 DB 연결 기능에 영향을 주지 않고 다른 DB 연결까지 가능한 기능 확장 가능
[장점] : 재사용성, 유지 보수성, 유연성
[단점] : 설계의 어려움과 복잡성, 오버 엔지니어링
3. LSP(Liskov Substitute Principle) : 리스코프 치환 원칙
- 부모 클래스의 인스턴스는 자식 클래스의 인스턴스로 교체될 수 있어야한다.
- 자식객체의 확장시 부모객체가 의도한 대로 오버라이딩 이루어져야함(올바른 상속)
(부모클래스의 일반 메서드를 의도와 다르게 오버라이딩 하지 않게 주의해야한다.) - 상속, 다형성과 가장 깊은 관계가 있다.
ex) 직사각형과 정사각형의 상속 관계
Rectangle 클래스
public class Rectangle { public int width; public int height; public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } |
Square 클래스
public class Square extends Rectangle{ @Override public void setWidth(int Width) { super.setWidth(width); super.setHeight(getWidth()); } @Override public void setHeight(int height) { super.setHeight(height); super.setWidth(getHeight()); } } |
하위 클래스의 인스턴스를 상위클래스 인스턴스로 대체했을 때 너비 구하기
public class Main { public static void main(String[] args) { Rectangle rectangle = new Rectangle(); rectangle.setWidth(10); rectangle.setHeight(5); System.out.println(rectangle.getArea()); rectangle = new Square(); //하위 클래스 인스턴스인 정사각형으로 대체 rectangle.setWidth(10); rectangle.setHeight(5); System.out.println(rectangle.getArea()); } } |
output
50
25
리스코프 치환 원칙에 의하면 부모 객체를 호출하는 동작에서 자식 객체가 부모객체를 완전히 대체 할 수 있어야합니다.
그런데 지금 자식 객체로 대체하니 결과가 50이 아닌 25가 나왔습니다.
자식 객체가 부모 객체를 완전히 대체하지 못하였습니다.
이는, 정사각형과 직사각형이 올바른 상속관계가 아니라는 것을 의미합니다.
why?. 분명 사각형의 성질로는 정사각형과 직사각형이 상속관계를 이루는데 왜 올바른 상속관계가 아니라는 거죠?
직사각형의 소스코드를 보면 width와 height가 각각 존재합니다. 하지만 정사각형은 width와 height가 각각 별도로 존재할 필요가 없습니다. 하지만 상속을 받게 되면 부모 클래스의 성질을 그대로 가져오기 때문에 정사각형 클래스는 마치 직사각형처럼 width와 height가 별도로 각각 존재하는 사각형이 되어버렸습니다.
그렇다면 제대로된 설계는?
상위타입으로 Shape 인터페이스 정의
interface Shape { int getArea(); } |
Rectangle 클래스에서 Shape 인터페이스를 상속받아 getArea 오버라이딩
public class Rectangle implements Shape { public int width; public int height; public Rectangle(int width, int height){ this.width=width; this.height=height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } @Override public int getArea() { return width * height; } } |
Square 클래스에서 Shape 인터페이스를 상속받아 getArea 오버라이딩
public class Square implements Shape { public int length; public Square(int length){ this.length=length; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } @Override public int getArea() { return length * length; } } |
위와 같이 설계하고
도형의 넓이를 구해보면
public class Main { public static void main(String[] args) { Shape shape = new Rectangle(10, 5); System.out.println(shape.getArea()); shape = new Square(5); //하위 클래스 인스턴스인 정사각형으로 대체 System.out.println(shape.getArea()); } } |
output
50
25
이제 Rectangle과 Square는 상속관계가 아니므로 결과가 다르다고 해서 LSP에 위반됬다고 생각할게 아니라
Shape의 getArea가 의도한 각 구현체의 넓이를 올바르게 구했으므로 LSP에 위배되지 않다고 볼 수 있다.
※ LSP의 장점 또는 LSP를 지켜야하는 이유
- 올바른 상속관계 형성 -> 다형성을 통한 기능 확장 용이 -> 유지보수 용이
- 부모 클래스와 하위 클래스 간의 일관성 형성 -> 코드 가독성 향상
- 코드의 재사용성 증가(하위 클래스가 상위 클래스 대체 가능하므로 상위 클래스 타입으로 여러 하위 클래스 처리 가능)
4. ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
- 인터페이스를 사용하는 사용자를 기준으로 분리
- 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙
- 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 한다.
- 의존성을 약화시키는 목적
- 몇개의 큰 인터페이스가 있는 편 보다는 작은 인터페이스가 많은 편이 바람직하다.
ex) 자동차 인터페이스
interface Car { public void drive() ; public void reverse(); public Gas getFuelState(); public void fillFuel(Gas gas); } |
위의 자동차 인터페이스를 보면 전진,후진, 연료량 확인, 연료 채우기라는 자동차가 가져야하는 기능을 갖추고 있다.
그래서 아래와 같은 자동차의 상속 관계를 만들었다.
디젤, 가솔린, LPG 자동차 모두 Car 인터페이스를 통해 자동차가 갖춰야할 기능들을 모두 갖추게 되었다.
허나, 전기차의 등장 이후 getFuelState, fillFuel 기능이 필요가 없어졌다.
전기차는 내연기관 자동차와 달리 연료 주입이 아닌 배터리 충전이다.
Gas를 리턴값이나 매개변수로 갖는 연료 충전, 연료 상태를 확인하는 메서드를 전기차에서는 사용하지 못한다.
만약 위와 같은 구조에서 전기차를 구현한다면 아래와 같을 것이다.
class ElectricCar implements Car { public void drive() { //drive 로직... } public void reverse() { //reverse 로직... } public Gas getFuelState() { return null; } public void fillFuel(Gas gas) {} public void chargingBattery(Battery battery) { //배터리 충전 } public Battery getBatteryState() { //배터리 상태 } public Battery getBatteryLife() { //배터리 수명 } public Battery changeBattery() { //배터리 교체 } } |
위와 같이 getFuelState, fillFuel을 반드시 구현해야하므로 메서드를 비워놓고 배터리와 관련된 메서드를 추가할 것이다.
위 클래스에서 보듯이 전기차는 사용하지도 않는 getFuelState, fillFuel을 구현해야하므로 번거로움이 생긴다.
만약 더 많은 메서드가 존재했다면 더 큰 버건로움이 생겼을 것이다.
=> 인터페이스 분리 원칙을 준수한 설계
interface CarOperate { public void drive() ; public void reverse(); } interface GasEngine { public Gas getFuelState(); public void fillFuel(Gas gas); } interface BatteryEngine { public void chargingBattery(Battery battery); public Battery getBatteryState(); public Battery getBatteryLife(); public Battery changeBattery(); } |
위와 같이 자동차 조작 인터페이스와 연료 충전 인터페이스를 분리하여 설계한다면
내연기관 자동차와 전기 자동차가 갖는 서로 다른 속성으로 인해 불필요한 메서드 강제 오버라이딩을 막을 수 있다.
따라서 아래와 같이 getFuelState, fillFuel 없이 전기차 충전 기능만 오버라이딩 할 수 있다.
class ElectricCar implements CarOperator, BatteryEngine { public void drive() { //drive 로직... } public void reverse() { //reverse 로직... } public void chargingBattery(Battery battery) { //배터리 충전 } public Battery getBatteryState() { //배터리 상태 } public Battery getBatteryLife() { //배터리 수명 } public Battery changeBattery() { //배터리 교체 } } |
5. DIP(Dependency Inversion Principle) : 의존성 역전 원칙
- 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
즉, 상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다는 것이다.
DIP이 장점 : 확장성 용이, 객체 간의 관계를 느슨하게 만들어줌.
ex) 사용자의 카드 객체를 전달받아 결제 구현을 해야할 때 파라미터로 삼성카드, 현대카드로 설정을 한다면 카드사마다 메서드를 만들어야함. 하지만 카드 객체를 통해 할수 있는 공통된 행위를 정의한 인터페이스를 만들고 각 카드사의 카드 객체를 이 인터페이스를 상속 받게 한다면 카드 결제 메서드에 카드 인터페이스 타입을 선언한 메서드 하나 가지고 여러 카드사의 카드 결제를 진행할 수 있다.
'OOP' 카테고리의 다른 글
객체지향 프로그래밍이란 : OOP (0) | 2024.06.30 |
---|---|
[진행중]상속보다 합성을(composition over inheritance) (0) | 2023.12.31 |
객체지향 설계의 SOLID 원칙 (0) | 2023.11.15 |