호호 스터디에서 Chapter 5: 설계 원칙: SOLID를 듣기 전에, 미리 책을 읽고 공부한 내용을 기록한 포스팅이다.
SOLID 원칙
어떻게 보면 귀에 딱지가 앉을 정도로 많이 들어서 너무 뻔하게 느껴질 수 있다.
하지만 막상 코드를 짤 때는 자신도 모르는 사이에 잘 지켜지지 않는 경우가 많다.
애초에, 객체의 값을 꺼내지 말고 메시지를 던지라는 것조차도 코드를 짜다보면 잘 안지켜지는 경우가 많은 것처럼 말이다.
이번 포스팅에서 SOLID 원칙을 살펴보긴 하겠지만, 제일 좋은 건 실전을 통한 많은 경험을 해보는 것인 듯하다 :)
단일 책임 원칙 (Single Responsibility Principle : SRP)
클래스는 단 한 개의 책임을 가져야 한다.
객체지향적인 코드를 짜기 위해 최대한 책임을 작게 나누어야 된다는 말에 부합하는 원칙이다.
위 코드를 보면, 입력을 받는 기능과 카드를 나누는 기능, 출력하는 기능을 동시에 하고 있다.
이렇게 되면 두 가지 문제점이 생긴다.
1. 변경에 유연하지 못하게 된다.
2. 재사용을 어렵게 만든다.
만약 A 객체에선 입력을 받는 기능만 이용하면 되고, B 객체에선 카드를 나누는 기능만 필요하다고 해보자.
단일 책임 원칙을 지켰다면 A 객체와 B 객체는 서로 다른 클래스를 의존했겠지만, 지금 코드는 A 객체와 B 객체 둘 다 위 코드를 의존해야 한다.
만약 위 코드에 수정이 발생할 경우, 단일 책임 원칙을 지켰다면 A, B 둘 중 하나의 객체만 변경해도 되지만, 지금 상황에선 A 객체와 B 객체 모두 수정해야 될 여지가 생겨버리는 것이다.
이는 의존성을 높여 코드의 질을 해치는 것이기도 하다.
위처럼 의존성이 높을 경우, 불필요하게 많은 클래스들을 사용해야될 수 있으며, 코드가 복잡해져 재사용이 힘들어지고 구조가 어려워질 수 있다.
의존성이 높은 코드가 객체지향적이지 못한 이유는 Chapter 2에서 충분히 공부했다!
https://kth990303.tistory.com/274
단일 책임 원칙을 잘 지키기 위해선,
클래스 설계를 먼저 하기보단, 어떤 기능(메서드)가 필요한지 생각해본 후 그 메서드들을 누가 실행할지 생각해보면 좋다.
개방-폐쇄 원칙 (Open-Closed Principle : OCP)
기능을 변경하거나 확장할 수 있으면서, 그 기능을 사용하는 코드는 수정하지 않는다.
이 말이 좀 어렵게 느껴질 수 있는데, 추상화의 장점을 설명하는 원칙이다.
아래 예시를 보자.
private final Transportation transportation;
public 생성자(Bus bus) {
this.transportation = bus;
}
어떤 객체에서 교통수단을 이용해야될 일이 생겼다고 하자.
위와 같이 지하철, 버스, 택시는 하나의 교통수단으로 이용할 수 있으므로
Transportation이라는 인터페이스를 만들고, 지하철, 버스, 택시가 그 인터페이스를 구현하도록 만들었다면,
위 코드에서 bus가 아닌 taxi로 변경이 발생해도 taxi의 기능을 사용하는 Transportation 코드는 수정이 일어나지 않는다.
즉, 기능을 변경하거나 확장할 때에도, 그 기능을 사용하는 코드는 수정이 발생하지 않게 한 것이므로,
확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다는 OCP 원칙을 지킨 것이다!
즉, 개방 폐쇄 원칙(OCP)는 다형성과 추상화를 통해 변경의 유연함을 보여주는 원칙이라 볼 수 있다.
우리는 다형성과 추상화의 장점을 Chapter 3에서 열심히 공부했었다.
https://kth990303.tistory.com/280
구조가 복잡해지고 코드량이 많아질수록 추상화를 잘 이용하면
변경에 유연해지는 코드를 만들 수 있다.
리스코프 치환 원칙 (Liskov Substitution Principle : LSP)
상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.
개방 폐쇄 원칙에서 설명했던 내용과 유사하게, 추상화에 관련된 내용이다.
추상화를 잘못하면 발생하는 문제에 대해 얘기하는 원칙으로, 대표적인 예시로 정사각형-직사각형 예시가 있다.
@Override
public void 길쭉하게_만들기(Rectangle rec) {
rec.setHeight(rec.getHeight() + 10);
}
정사각형은 직사각형에 속하므로 Is-a 관계에 해당되어 상속 관계로 구현했다고 가정하자.
Rectangle 클래스를 사용하는 `길쭉하게_만들기` 메서드에 Square를 넘겨준다면?
더 이상 가로, 세로 길이가 같지 않게 되므로 Square에서 벗어나게 된다.
따라서 아래와 같이 수정했다고 가정하자.
@Override
public void 길쭉하게_만들기(Rectangle rec) {
if (rec instanceOf Square) {
throw new IllegalArgumentException("정사각형은 넣을 수 없습니다.");
}
rec.setHeight(rec.getHeight() + 10);
}
이렇게 instanceof 연산자를 사용한다는 것 자체가 추상화의 장점을 제대로 활용하고 있지 못하다는 의미이다.
(일단 setter 메서드 쓴 것 자체부터가 굉장히 위험하다...)
instanceof 연산자를 사용하여 구현한다면, 정말 제대로 추상화를 이용한 것이 맞는지 의심해보아야 한다.
상위 타입의 객체를 하위 타입의 객체로 치환해도 문제가 일어나지 않아야 되는데,
위 상황에선 정사각형으로 치환할 경우 문제가 발생하게 되므로 리스코프-치환 원칙 (LSP) 위반이다.
이렇게 리스코프 치환 원칙은 잘못 추상화하는 경우를 조심하자고 얘기해주는 원칙이다.
리스코프 치환 원칙을 지키기 위해선, 하위 타입이 상위 타입의 명세에서 벗어난 기능을 수행하는지 잘 체크해보아야 한다.
위 코드에서 정사각형의 길이를 setter로 함부로 변경시키는 기능을 추가하지 않는다면 정사각형-직사각형 상속 관계는 좋은 설계일 가능성도 존재하는 것이다.
인터페이스 분리 원칙 (Interface Segregation Principle : ISP)
인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
특정 인터페이스가 여러 api를 가지고 있다면, 이 api들을 사용하는 여러 소스코드들을 다시 컴파일해야 되는 문제가 발생한다.
여기까지 읽으면 SOLID 원칙의 SRP(단일 책임 원칙)과 이어진다는 느낌을 받을 것이다.
다행히 JAVA는 JVM이 컴파일 과정에 링크 과정을 수행하지 않고, .class 파일을 로딩하는 과정에서 동적으로 링크 과정을 발생시키기 때문에 `사용하지 않는 인터페이스 변경에 의해 발생하는 소스 재컴파일` 문제는 발생하지 않는다고 한다.
하지만 위와 같이 인터페이스를 분리하지 않아 의존성이 높아진다는 문제점은 언어와 무관하게 항상 존재한다.
단일 책임 원칙과 마찬가지로, 인터페이스 분리 원칙도 자신이 사용하는 메서드에만 의존할 수 있도록 코드를 설계해야 한다.
클라이언트가 사용하는 기능을 중심으로 인터페이스를 분리하여
변경에 유연한 코드를 작성하도록 하자.
의존 역전 원칙 (Dependency Inversion Principle : DIP)
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
눈치 빠른 사람들은 `추상 타입`이라는 부분에서 추상화 관련이자, 개방 폐쇄 원칙과 관련이 있을 것이란 생각을 했을 수도 있다.
DIP 원칙의 대표적인 예시인 자동차-타이어-스노우타이어 예시이다.
만약 추상화를 하지 않고, 자동차가 스노우타이어를 사용하여 스노우 타이어에 의존하는 코드를 만들었다고 해보자.
이렇게 코드를 만들었더니 갑자기 DIP 원칙이 한마디 한다.
"야, 내가 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다고 했잖아!"
이는 자동차(고수준 모듈)가 스노우타이어(저수준 모듈)에 의존하는 클린하지 못한 코드임을 DIP 원칙이 말해준 것이다.
자동차가 스노우타이어를 사용할 때도 있고, 일반 타이어를 사용할 때도 있고, 광폭 타이어를 사용할 때도 있어서,
타이어를 갈아끼울 때마다 자동차 기능도 변경의 여지가 생길 수 있는 것이다.
즉, 확장에는 열려 있어야 하고 변경에는 닫혀있어야 한다는 개방-폐쇄 원칙 (OCP)도 어길 여지가 생기는 것이다.
어떻게 해야될지 모르겠는 우리 초보개발자에게 DIP 원칙이 한마디 더 해준다.
"추상 타입을 만들어서 자동차가 저수준 모듈로, 추상 타입이 고수준 모듈로 되도록 바꿔봐!
그럼 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다는 원칙을 지킬 수 있겠네."
그래서 우리가 위와 같이 타이어 인터페이스를 만들어주고, 이 인터페이스의 구현체로 여러 종류의 타이어들을 만들어준 것이다.
이렇게 하면 보다 고수준 모듈을 의존할 수 있게 되고,
자동차(저수준 모듈)이 타이어(고수준 모듈)에서 정의한 추상타입을 의존할 수 있게 된다.
물론 실제 런타임에서는 자동차가 스노우타이어를 의존하는 것이기 때문에, 런타임에서의 의존을 역전시키는 것은 아니다.하지만 소스 코드 상에서 다형성과 추상화를 이용한 역전을 발생시켜 변경에 유연하게 만들어주게 해주는 원칙이 바로 의존 역전 원칙 (DIP)인 것이다!
Chapter 2. 캡슐화와 의존성,
Chapter 3 다형성과 추상화
관련 내용을 원칙으로 정의한 느낌.
하지만 실제로 코드를 짜다보면 나도 모르게 이 원칙들을 잘 지키지 못하는 경우들이 많다.
코딩은 이론도 이론이지만, 그만큼 실전 또한 매우 중요하기 때문에 실전에서 많이 적용해보면 좋을 듯하다 ㅎㅎ
이제 코드를 설계하고 구현할 때, 정말 SOLID 원칙을 통해 변경에 유연하게 작성했는지 고민하고 뜯어고치면서 성장하는 시간을 가져보자 :)
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[ERROR] 406 에러 _ Not Acceptable (스프링 직렬화/역직렬화) (2) | 2022.04.23 |
---|---|
[호호 스터디] DI와 서비스 로케이터 _객체지향과 디자인 패턴 Chapter 6 (0) | 2022.03.27 |
[JAVA] VO(Value Object)로 원시값을 포장해보자 (2) | 2022.03.16 |
[JAVA] Cache를 이용한 재사용으로 성능을 높이자 (0) | 2022.03.16 |
[호호 스터디] 재사용: 상속보단 조립_ 객체지향과 디자인 패턴 Chapter 4 (2) | 2022.03.12 |