2024. 5. 31. 22:53ㆍCS
SOLID는 객체지향적 설계를 위한 원칙으로
5가지로 구성되어 있다.
1. SRP (Single Resposibility Principle) 단일 책임 원칙
2. OCP (Open Closed Principle) 개방 폐쇄 원칙
3. LSP (Liskob Substitution Principle) 리스코프 치환 원칙
4. ISP (Interface Segregation Principle) 인터페이스 분리 원칙
5. DIP (Dependency Inversion Principle) 의존 역전 원칙
SOLID를 사용하는 이유 :
1. 유지 보수성 향상
각 클래스가 맡은 책임이 명확해져 가독성이 올라가며 유지보수에 용이해진다.
2. 시스템의 확장성 증대
기존의 코드를 거의 수정하지 않고 새로운 기능을 쉽게 추가할 수 있게 하여 시스템의 확장성을 높인다.
3. 코드의 재사용성 증가
코드가 모듈화돼 다른 프로젝트에서도 재사용하기가 쉬워진다.
- SRP (Single Resposibility Principle) 단일 책임 원칙
단일 책임 원칙은 클래스가 하나의 책임을 가져야 한다는 원칙이다.
클래스가 하나의 역할에 집중하게 해서 유지보수성을 높이고 코드의 응집도를 높이는게 주 목적이다.
바로 예시로 설명해보자면
public class Car {
public void move() {
System.out.println("차를 운전합니다.");
}
public void stop() {
System.out.println("차를 멈춥니다.");
}
public void fillUp() {
System.out.println("주유합니다.");
}
public void turnOnAirConditioner() {
System.out.println("에어컨을 틉니다.");
}
}
간단하게 나타낸 자동차의 기능들이다. 하지만 SRP에 따르면 하나의 클래스가 하나의 책임을 가져야 했는데
이 코드는 차의 이동, 주유, 온도조절등 여러가지 책임을 맡고 있으므로 SRP가 지켜지지 않은 코드라고 볼 수 있다.
그렇다면 SRP를 지키기 위해서는 어떻게 해야 할까?
간단히 클래스를 나눠서 이동을 맡는 클래스, 주유를 맡는 클래스, 에어컨 기능을 맡는 클래스를 만들어 나누면 끝이다.
public class Move {
public void move() {
System.out.println("차를 운전합니다.");
}
public void stop() {
System.out.println("차를 멈춥니다.");
}
}
public class Fuel {
public void fillUp() {
System.out.println("주유합니다.");
}
}
public class AirConditioner {
public void turnOnAirConditioner() {
System.out.println("에어컨을 틉니다.");
}
}
각각의 책임을 나눠서 맡으니 클래스가 어떤 역할을 하는지 한눈에 알아보기 쉽게 됐다.
이제 Car클래스의 기능을 수정하고 싶다면 Car 클래스를 손댈 필요없이 각각의 책임을 맡은 클래스를 수정하면
끝이다. 간단한 예시여서 한 기능을 건들면 다른 기능을 수정해야하는 일이 발생하지 않지만
A B C 3가지 클래스가 있다 하고 SRP가 적용되지 않았다 가정해보자 만약 그렇다면
A를 수정시 B를 수정해야하고 B를 수정할시 C를 수정 그리고 C를 수정하면 A를 수정해야하는
A -> B -> C -> A 이러한 상황이 발생할 수 도 있다. 하지만 SRP를 적용하면 이러한 상황을 방지 할 수 있다.
마지막으로 SRP에서 책임의 범위는 단일책임 원칙이지만 꼭 하나의 책임만 가져야 하는것은 아니다.
상황에 따라 적절한 책임을 부여하여 코드의 변화를 줄이는것이 중요한 것이다.
- OCP (Open Closed Principle) 개방 폐쇄 원칙
개방 폐쇄 원칙은 확장엔 열려있고 수정에는 닫혀있다는 뜻이다. 이 말만 보고서는 확장에 열려있는데 수정에는 닫혀 있다?
이게 무슨 소린지 이해가 안될 수 도 있다. 이 말의 의미는 어떠한 기능을 추가할시 클래스를 확장을 통해 손쉽게 구현해야 하고
확장에 따른 수정을 최소화 해야 한다는 의미 이다.
바로 예시로 봐보자
한 운전자가 기름을 필요로하는 차만 가지고 있다 생각해보자
public class Driver {
private Car car;
public Driver(Car car) {
this.car = car;
car.charge();
}
}
public class Car {
public void charge() {
System.out.println("기름을 주유합니다.");
}
}
그런데 운전자가 수소차와 전기차를 구매했다 해보자
public class ElectricCar {
public void charge() {
System.out.println("전기를 충전합니다.");
}
}
public class HydrogenCar {
public void charge() {
System.out.println("수소를 충전합니다.");
}
}
그러면 수소차와 전기차에 따라 알맞게 Driver 클래스의 파라미터를 수정해야 할것이다.
public class Driver {
private ElectricCar electricCar;
public Driver(ElectricCar electricCar) {
this.electricCar = electricCar;
electricCar.charge();
}
}
이런식의 코드 수정이 일어난다 하지만 OCP는 확장에는 열려있지만 수정에는 닫혀있어야 한다.
그렇다면 어떻게 하면 OCP를 지킬 수 있을까?
정답은 Car클래스를 인터페이스로 만드는 것이다.
public interface Car {
public void charge();
}
public class ElectricCar implements Car{
public void charge() {
System.out.println("전기를 충전합니다.");
}
}
public class HydrogenCar implements Car{
public void charge() {
System.out.println("수소를 충전합니다.");
}
}
public class Driver {
private Car car;
public Driver(Car car) {
this.car = car;
car.charge();
}
}
이렇게 수소차와 전기차가 Car를 상속받게 하면 Driver 클래스에서의 코드 수정은 일어나지 않는다.
수정에는 닫혔다는것은 만족했고 확장에는 열려있게 만들어 진것을 확인해보자
public class HybridCar implements Car{
@Override
public void charge() {
System.out.println("하이브리드 차량을 충전합니다.");
}
}
이런식으로 기존 코드를 건드리지 않고 쉽게 확장이 가능하다.
- LSP (Liskob Substitution Principle) 리스코프 치환 원칙
리스코프 치환 원칙은 자식 클래스는 언제나 자신의 부모 클래스로 대체할 수 있어야 한다는 것이다.
예를 들어 부모 클래스가 자동차라 하고 자식 클래스가 비행기라 해보자 자식클래스는 언제나 부모 타입으로
대체 할 수 있어야 하는데 자식은 하늘을 나는데 부모는 하늘을 날지 못한다.
즉 부모의 의도는 자동차가 운전되는건데 자식클래스가 부모클래스의 의도를 위배 해버린것이다.
public class Car {
public void charge() {
System.out.println("연료를 충전합니다.");
}
public void startAndDrive() {
System.out.println("시동을 킵니다.");
System.out.println("운전합니다.");
charge();
System.out.println("시동을끕니다.");
}
}
public class ElectricCar extends Car {
public void charge() {
System.out.println("전기를 충전합니다.");
}
@Override
public void startAndDrive() {
System.out.println("시동을 킵니다.");
System.out.println("운전합니다.");
charge();
System.out.println("시동을끕니다.");
}
}
public class HybridCar extends Car {
@Override
public void charge() {
System.out.println("하이브리드 차량을 충전합니다.");
}
@Override
public void startAndDrive() {
System.out.println("시동을 킵니다.");
System.out.println("운전합니다.");
charge();
System.out.println("시동을끕니다.");
}
}
public class HydrogenCar extends Car {
public void charge() {
System.out.println("수소를 충전합니다.");
}
@Override
public void startAndDrive() {
System.out.println("시동을 킵니다.");
System.out.println("운전합니다.");
charge();
System.out.println("시동을끕니다.");
}
}
public class Driver {
private Car car;
public Driver(Car car) {
this.car = car;
car.startAndDrive();
}
}
이전 OCP 예시에서 아주 조금만 바꾼 예시이다 자식 클래스에서 부모 클래스의 의도를 어기지 않고 언제나
부모 클래스로 대체가 가능하기에 LSP를 잘 지킨 예시이다.
public class CarMain {
public static void main(String[] args) {
Car car = new Car();
Car electricCar = new ElectricCar();
Car hydrogenCar = new HydrogenCar();
HybridCar hybridCar = new HybridCar();
Driver driver = new Driver(car);
}
}
Driver 클래스에 부모클래스인 Car 클래스로 대체해도 아무 문제가 생기지 않는다.
또한 대표적인 예시로 자바의 List가 있다
package solid.lsp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class LSPEx {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("A", "B", "C"));
LinkedList<String> linkedList = new LinkedList<>(Arrays.asList("X", "Y", "Z"));
printList(arrayList);
printList(linkedList);
printArrayList(arrayList);
printLinkedList(linkedList);
}
public static void printList(List<String> list) {
for (String s : list) {
System.out.println(s);
}
}
public static void printArrayList(ArrayList<String> list) {
for (String s : list) {
System.out.println(s);
}
}
public static void printLinkedList(LinkedList<String> list) {
for (String s : list) {
System.out.println(s);
}
}
}
arrayList, LinkedList로 두 리스트를 만들었는데
printList에서 부모타입인 List로 대체해도 아무 문제없이 잘 작동한다.
- ISP (Interface Segregation Principle) 인터페이스 분리 원칙
ISP 원칙은 SRP 원칙과 유사한데 SRP가 클래스간의 책임을 분리한다면
ISP는 인터페이스간 책임을 분리하는 것이다.
예시를 들어보자
public interface Phone {
void call();
void message();
void smartPhoneFunction();
void oldPhoneFunction();
}
Phone 인터페이스에 전화, 문자기능 외 스마트폰만의 기능, 구형 핸드폰만의 기능을 만들어놨다
하지만 이대로 ISP를 위배하고 구현한다면
public class OldPhone implements Phone{
@Override
public void call() {
System.out.println("전화합니다.");
}
@Override
public void message() {
System.out.println("문자를 보냅니다.");
}
@Override
public void smartPhoneFunction() {
System.out.println("지원하지 않는 기능입니다.");
}
@Override
public void oldPhoneFunction() {
System.out.println("구형폰만의 기능입니다.");
}
}
이렇게 필요없는 메서드까지 구현을 해야한다. 하지만 인터페이스를 분리하고 구현을 한다면
public interface Phone {
void call();
void message();
}
public interface OldFunction {
void oldPhoneFunction();
}
public interface SmartPhoneFunction {
void smartPhoneFunction();
}
public class OldPhone implements Phone, OldFunction{
@Override
public void call() {
System.out.println("전화합니다.");
}
@Override
public void message() {
System.out.println("문자를 보냅니다.");
}
@Override
public void oldPhoneFunction() {
System.out.println("구형폰만의 기능입니다.");
}
}
public class smartPhone implements Phone, SmartPhoneFunction{
@Override
public void call() {
System.out.println("전화합니다.");
}
@Override
public void message() {
System.out.println("문자를 보냅니다.");
}
@Override
public void smartPhoneFunction() {
System.out.println("스마트폰만의 기능입니다.");
}
}
이렇게 원하는 기능만 구현할 수 있다.
즉 ISP는 사용자의 목적과 용도에 맞게 인터페이스를 만드는게 목적이다.
인터페이스는 클래스와 다르게 다중 상속이 가능하기에 가능한 원칙이다.
- DIP (Dependency Inversion Principle) 의존 역전 원칙
DIP는 고수준 모듈은 저수준 모듈에 의존하면 안 되고 둘다 추상화 된것에 의존해야 한다는 원칙이다.
즉 클래스에 의존하지 말고 추상화된 추상 클래스, 인터페이스에 의존하도록 만들어서
모듈간 결합도를 낮추는게 목적이다. 결합도가 낮으면 하위 클래스가 상위 클래스에 영향을 주지 않아
수정에 용이하다. 또한 코드가 모듈화 되어있기 때문에 재사용에도 용이하다.
결국은 추상화를 통해 OCP를 지키라는것과 일맥상통한다 생각한다.
바로 예제를 보자
셰프가 어떠한 도구를 통해 요리를 만들거다
public class Blender {
public void use() {
System.out.println("블렌더를 사용해 요리합니다.");
}
}
public class Knife {
public void use() {
System.out.println("칼을 사용해 요리합니다.");
}
}
public class Chef {
private Blender blender;
private Knife knife;
public Chef(Blender blender) {
this.blender = new Blender();
// this.knife = new Knife();
}
public void cook() {
System.out.println("요리 시작");
blender.use();
System.out.println("요리 끝");
}
}
추상화된 클래스를 이용하지 않고 저수준 모듈인 칼과 블렌더의 use 메서드에 의존하게 된다.
당연하게도 OCP또한 지켜지지 않은 코드이다.
고수준 모듈인 추상화된 인터페이스 CookingTool을 만들어 DIP를 만족하게 코드를 수정해볼것이다.
public interface CookingTool {
public void use();
}
public class Blender implements CookingTool{
public void use() {
System.out.println("블렌더를 사용해 요리합니다.");
}
}
public class Knife implements CookingTool{
public void use() {
System.out.println("칼을 사용해 요리합니다.");
}
}
public class Chef {
private CookingTool cookingTool;
public Chef(CookingTool cookingTool) {
this.cookingTool = cookingTool;
}
public void cook() {
System.out.println("요리 시작");
cookingTool.use();
System.out.println("요리 끝");
}
}
이러면 OCP또한 만족되고 하위 모듈 칼, 블렌더의 메소드에 의존하지 않게된다.
따라서 유지보수나 확장 수정에 용이 할것이다.
- 참고자료
💠 객체 지향 설계의 5가지 원칙 - S.O.L.I.D
객체 지향 설계의 5원칙 S.O.L.I.D 모든 코드에서 LSP를 지키기에는 어려움. 리스코프 치환 원칙에 따르면 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신하더라도 의도에 맞게 작동되어
inpa.tistory.com
SOLID 5원칙 - SRP 단일 책임 원칙(Single Responsibility Principle)
컴퓨터 프로그래밍에서 SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적
dding9code.tistory.com
객체지향 설계에서 꼭 필요한 SOLID 5대원칙(SRP/OCP/LSP/ISP/DIP)
SRP : Single Responsibility Principle, 단일책임 원칙 OCP : Open Closed Principle, 개방-폐쇄 원칙 LSP : Liskov Subtitution Principle, 리스코프 치환원칙 ISP : Interface Segregation Principle, 인터페이스 분리 원칙 DIP : Dependency
career-gogimandu.tistory.com
'CS' 카테고리의 다른 글
ER다이어그램 (Crow's Foot) (2) | 2024.06.06 |
---|---|
ER 다이어그램 (Chen 표기법) (0) | 2024.06.01 |