2022. 11. 21. 23:45ㆍProgramming/Java
[Java] 객체지향 설계 - SOLID
객체 지향 설계, 우리가 사는 세상을 객체들끼리의 상호작용으로 보고 이를 프로그래밍 적으로 설계하여 표현을 하는 것이다. 이러한 것을 객체지향 설계라고 하며 해당 설계를 진행할 때 지켜져야 하는 5개의 원칙이 존재한다. 그리고 이 원칙을 SOLID 원칙이라고 하며 오늘은 이에 대해 공부한 내용들을 정리하고자 한다.
SOLID 원칙
프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용해서 설계를 진행하면 된다. 원칙들은 각 원칙의 앞글자를 따와서 가져왔으며 아래와 같다
- SRP : 단일 책임 원칙 (Single responsebility principle)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- OCP : 개방 폐쇄 원칙 (Open/close principle)
- 소프트웨어 요소는 확장에 열려 있으나 변경에는 닫혀 있어야 한다.
- LSP : 리스코프 치환 원칙 (Liskov substituion principle)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- ISP : 인터페이스 분리 원칙 (Interface segregation principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
- DIP : 의존관계 역전 원칙 (Dependency inversion principle)
- 프로그래머는 추상화에 의존을 해야 하며, 구체화에 의존하면 안 된다.
- 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
SRP - 단일 책임 원칙
클래스는 클래스마다 하나의 책임만을 가진다.
하나의 책임이란 것은 상황에 따라 클 수도 적을 수도 있다. 이때 책임의 기준은 변경이다. 변경을 하면서 수정 부분이 적다면 해당 원칙을 잘 따른 것이다. 변경이 있을 때 파급 효과가 적으면 SRP를 잘 적용한 것이다. 다만 SRP를 적용할 때 역할에 하나의 책임만 구현하려고 따로 구현하면 해당 객체의 응집도가 떨어지는 경우도 존재한다. 고로 설계를 할 때 역할을 나눠야 할 부분과 아닌 부분을 잘 나눠야 한다. 바로 이 지점에서 아까 책임의 크기가 달라지는 것을 의미한다.
- 결제 버튼의 위치 변경
- if 결제 버튼에 결제의 기능까지 포함 : 결제 기능까지 교체
- else : 결제 기능은 유지하고 버튼의 위치만 변경
- 클래스 요소로 살펴보자
카페를 하나의 객체로 보고 설계를 했을 경우이다. 카페에 있는 사람들이 하는 일들을 메서드로 구현을 한 것이다.
public class Person {
public void make_drink(){}
public void order_something(){}
public void drink_something(){}
public void clean_table(){}
}
그럼 모든 사람들을 person으로 구현하여 각각의 사람에게 이름을 주는 것도 하나의 방법이지만 나중에 세밀화하여 해당 객체를 만들면 person에서의 코드는 점차 길어질 것이다. 그러니 종업원과 손님으로 나눠서 각 클래스에서 각자 할 일을 하는 책임을 만들자
// 바리스타, 종업원의 클래스
public class barista {
public void clean_table(){}
public void make_drink(){}
}
public class customer {
public void order_something(){}
public void drink_something(){}
}
해당 방법으로 진행을 하면 손님과 종업원에게 각자 역할을 부여하여 본인이 할 수 있는 일만 시킬 수 있다.
OCP - 개방/폐쇄 원칙
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
프로그램을 만들고 유지를 하다 보면, 사용자의 피드백을 받아서 기존 기능을 개선하거나 혹은 더 나은 처리방식을 찾아 수정하는 경우가 존재한다. 다만 이럴 경우 하나의 기능을 수정할 때 그 기능을 수행하는 다른 모듈들을 전부 수정해야 한다면 수정의 난이도는 점점 높아진다. 그렇기에 해당 원칙이 생기게 되었다.
수정(확장)할 것과 그대로 둘 것을 구분한다.
수정과 유지의 중간에서 인터페이스를 구현하고 해당 인터페이스에 코드들이 유지를 하도록 한다.
LSP - 리스코프 치환 원칙
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다. 이때 다형성을 지원하기 위한 원칙, 인터페이스라는 추상체를 구현한 구현체를 믿고 사용하려면 해당 원칙이 필요하다.
- 각 기능은 기능에 맞는 이름을 가져야 한다. 지폐를 넣으라 하고 동전만 받는다면 해당 기능은 올바르게 이루어진 것이 아니다. 그리고 자동차의 액셀을 밟았는데 후진을 해버 리거나 브레이크를 걸어버리면 그건 사용자가 해당 제품에 대한 신뢰를 할 수가 없다. 그러므로 해당 원인과 결과가 올바르게 나오도록 프로그램의 정확성을 유지해야 한다.
ISP - 인터페이스 분리 원칙
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
카페라는 하나의 장소를 생각해보자. 단순히 카페를 생각하면 사람들이 커피를 사고 마시는 장소로 생각하면 끝이다
.
그러나 해당 카페에는 종업원 / 손님 / 커피 / 디저트 / 화장실 / 청소... 다양한 각각의 객체가 상호작용을 하며 카페라는 커다란 객체를 이룬다. 고로 만약 카페라는 커다란 인터페이스를 만들어서 그 안에서 모든 것을 동작하게 하려면 코드의 줄단위도 커지고 해당 객체들의 세밀 성을 표현하지 못할 수도 있다. 또한 중간에 하나의 객체가 등장하거나 수정이 되는 경우 연관이 되는 다른 객체도 영향을 받을 수도 있다.
그러므로 카페라는 하나의 객체를 구현하는 것보다 각각의 역할을 하는 객체를 여러 개 구현해서 해당 객체들의 상호작용을 만드는 게 좋다. 이런 식으로 따로 추상체를 만들어서 구현을 하게 된다면 해당 카페에서 손님의 인터페이스가 달라져도 카페나 종업원 등 다른 객체에 영향을 주지 않는다.
- 카페 → 종업원 / 메뉴 / 손님 인터페이스로 분리
- 자동차 → 운전 / 정비 인터페이스로 분리
- 사용자 → 운전자 / 정비사 인터페이스로 분리
DIP - 의존관계 역전 원칙
추상화에 의존하고 구체화에 의존하면 안 된다. → 의존성 주입
구현 클래스(class)에 의존하지 말고 추상화(inteface)에 의존하라는 것이다.
사람마다 좋아하는 음료수를 선언한다고 생각해보자. 아래의 코드는 둘 다 아이스 아메리카노를 선언하는 코드이다.
Drink IceAmericano1 = new Americano(500,Temperature.ICE);
Americano IceAmericano2 = new Americano(500,Temperature.ICE);
다만 첫 번째 코드는 Drink라는 인터페이스로 선언이 된 코드이며 두 번째 코드는 Americano 구체화된 클래스로 구현이 된 객체이다. 그렇다면 나중에 좋아하는 음료가 바뀐다면 어떻게 될까? Americano를 통해 선언이 된 아이스 아메리카노 2는 객체의 타입이 Americano이므로 전부다 수정을 해야 하지만 Drink를 통해 선언한 객체는 일부분만 변경하면 된다.
Drink iceLatte = new Latte(500, Temperature.ICE);
즉 인터페이스에 의존을 해야 사용자는 역할에 따라 능동적으로 변할 수 있다. 그러니 꼭 구현체보단 추상화를 먼저 생각해보자.
참고
'Programming > Java' 카테고리의 다른 글
[Java] Thread / Thread 클래스, Runnable 인터페이스 이용 (0) | 2022.12.03 |
---|---|
[Java] Java와 JVM (0) | 2022.11.30 |
[Java] Stream이란? (0) | 2022.11.15 |
[Java] 객체지향 (0) | 2022.11.14 |
[Java] static 개념 및 정리 (0) | 2022.06.26 |