SOLID란?
로버트 마틴이 2000년대 초반에 이름붙인 객체 지향 프로그래밍/설계의 5가지 기본 원칙의 앞글자를 딴 약어 SOLID
객체지향의 4대 원칙 - 캡슐화, 상속, 추상화, 다형성
다형성 - 똑같은 클라이언트 코드로 안에 들어있는 존재에 따라 다른 동작이 수행되는 것
단일 책임 원칙 (Single responsibility principle, SRP)
하나의 클래스는 하나의 책임만을 가져야 한다.
하나의 클래스가 여러개의 책임을 가진다면, 그 클래스의 코드가 변경되어야할 이유가 여러개가 생긴 것이다.
단일 책임 원칙을 지키는 코드는 각각의 클래스가 응집력이 높기 때문에, 코드의 재사용성이 높아지고, 캡슐화를 통해 한 클래스의 변경이 다른 클래스에 영향을 미치지 않도록 만든다.
개방-폐쇄 원칙 (Open/closed principle, OCP)
소프트웨어는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
- 꼭 인터페이스가 필요한 것은 아니다.
- 아래 개념을 활용해서도 개방-폐쇄 원칙을 지킬 수 있다.
- Enum
- 디자인 패턴
- 이벤트 기반 프로그래밍
개방 폐쇄 원칙을 지키는 코드는 클라이언트 코드가 추상화에 의존하고 있기 때문에, 확잘될 때와 변경될 때 모두 다른 코드에 영향을 주지 않게 만든다.
리스코프 치환 원칙 (Liskov substitution principle)
부모 클래스가 할 수 있는 행동은 자식 클래스도 할 수 있어야 한다.
리스코프 치환 원칙이 깨진 상황과 그 문제점
public class Parent {
public void someMethod(int input) {
// 어떤 input이든 상관 없음
System.out.println("Parent 정상적으로 호출됨.");
}
};
public class Child extends Parent {
@Override
public void someMethod(int input) {
if(input <= 0)
throw new RuntimeException("양수만 받을 수 있어요");
System.out.println("Child 정상적으로 호출됨.");
}
};
-> Parent 에서는 어떤 input 이든 상관 없었는데 Child 에서는 Input 이 0 이하일 경우 예외를 던지고 종료된다.
-> 부모 클래스가 할 수 있는걸 자식이 할 수 없는 상황이다.
-> 이게 무슨 문제냐고? 이제 Client 의 코드를 보며 문제점을 찾아보자.
public class Client {
public void someClientMethod(Parent parentOrChild) {
parentOrChild.someMethod(-1);
}
}
public class LspExampleMain {
public static void main(String[] args) {
Client client = new Client();
// Parent parent = new Parent();
Child child = new Child();
client.someClientMethod(child);
}
}
-> Client 입장에선 parentOrChild 에 Parent 가 들어있는지 Child 가 들어있는지 모름
-> Child 의 someMethod 로 -1 을 넘기니까, 역시 예외가 터지는 것을 볼 수 있다.
해결책으로 세가지 정도가 떠오를 수 있겠다.
- Child 대신 Parent 인스턴스를 넣어준다.
- 파라미터로 양수만 넣어준다.
- instanceof 로 어떤 인스턴스인지 구분하여 호출한다.
그럼 이제 살펴보자.
- Child 대신 Parent 인스턴스를 넣어준다?
- Parent 로 타입이 정해져버리는 것은 다형성을 없애기 때문에 적절한 해결이 되지 못한다.
- 파라미터로 양수만 넣어준다?
- Parent 에서는 음수에서도 동작을 해야하는데, 상속으로 인해서 Parent 의 기능이 제한되는 것은 해결책이 되지 못한다.
- instanceof 로 어떤 인스턴스인지 구분하여 호출한다?
- 아래의 코드를 보자. 이것도 해결책이 되지 못한다. 타입에 따른 로직을 분리했기 때문에 다형성이 깨졌다.
- Parent 와 Child 외에 상속이 더 진행된다면 Client 의 코드에 if 가 추가될 것이다.
public class Client {
public void someClientMethod(Parent parentOrChild) {
// parentOrChild.someMethod(-1);
if(parentOrChild instanceof Parent && false == parentOrChild instanceof Child)
parentOrChild.someMethod(-1);
else if(parentOrChild instanceof Child)
parentOrChild.someMethod(Math.abs(-1));
}
}
A instanceof B 에서, A 가 B 와 동일한 타입이어야만 true 를 반환한다고 오해할 수 있는데, A 가 B 의 자식 타입이더라도 true 를 반환한다는 것을 유념하자.
그렇다면 해결첵은?
1. Parent 와 Child 는 애초에 상속 관계를 가지지 말았어야 한다.
2. 상속관계를 역전시켜서 Child 가 Parent 의 부모가 되었어야 한다.
2번의 방법을 설명하자면
Child 에서 input 으로 양수만 받을 것이고, 자식인 Parent 에서는 양수를 처리할 수 있기 때문이다.
따라서, 리스코프치환원칙을 지키지 않는 상속은 다형성을 깨트리기 때문에
-> 상속을 하지 않는 것이 적절하다.
계약에 의한 설계
사전 조건은 자식 클래스에서 더 강해지면 안된다.
위의 예제에서 Child 에서 Input 의 범위를 제한하는 것을 말한다.
계약에 의한 설계의 내용은 이 것 말고도 많다. 지금 말고 시간이 될 때 찾아보자.
접근제어자에서도 발견할 수 있는 리스코프 치환 원칙
public class Parent {
public void someMethod(int input) {}
};
public class Child extends Parent {
private void someMethod(int input) {}
};
-> 다형성 X
public class Parent {
private void someMethod(int input) {}
};
public class Child extends Parent {
public void someMethod(int input) {}
};
-> 다형성 O
'백엔드 데브코스' 카테고리의 다른 글
[Java] 도서관리 프로그램을 만들면서 배운것들 - Enum, Builder 디자인패턴 (0) | 2023.10.06 |
---|---|
[Java] SOLID란? (2) - 객체 지향 프로그래밍의 5가지 기본 원칙 (0) | 2023.09.29 |
[Java] 의존성을 주입해주는 주체 - jar, @Profile, @Order (0) | 2023.09.27 |
[Java] 의존 주입 패턴 (0) | 2023.09.27 |
[Java] 의존 역전 (0) | 2023.09.26 |