이해하기 많이많이 힘들었습니다... 잘못된 부분이 있으면 피드백 주세요!
SOLID 원칙
시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조 → 좋은 설계
유지 보수와 이해가 쉬운 소프트웨어가 되도록, 유연한 대처와 확장성 있는 시스템 구조를 만드는 것이 중요하다. 크기가 커질 때 복잡성을 줄이고 문제가 생길 수 있는 부분을 제거한다. 여러 디자인 패턴이 SOLID 설계 원칙에 입각해 만들어져 표준화 작업부터 설계까지 다양하게 적용된다.
⇒ 코드를 확장하고, 유지 보수 관리가 쉬우며, 복잡성을 제거해 개발의 생산성을 높일 수 있다.
SRP 단일 책임 원칙 (Single Responsibility)
“there should never be more than one reason for a class to change”
→ 클래스가 변경되는 이유는 하나, 단 하나의 책임만 가져야 한다.
클래스는 하나의 기능만 가지고, 그 책임을 수행하는데 집중해야 한다는 원칙. 한 클래스에 기능이 여러개 있다면 기능 변경이 일어났을 때 수정해야 할 코드가 많아진다. 한 책임에 대한 변경이 다른 책임의 변경으로 연쇄 작용이 일어나는 것.
SRP 적용 방법
각 책임을 개별 클래스로 분할하여 하나의 책임만 맡도록 설계한다.
단순히 책임만 분리하는 게 아니라 클래스 간 관계의 복잡도를 줄이도록 설계하는 것이 중요하다.
내가 개발한 것 중에 어떤 것이 SRP를 지키지 못했는지 구분하기 어려울 때는 하나의 요소를 변경시키는 방법이 있다. 어떤 기능을 변경했을 때, 다른 클래스에서 변경에 대한 변화가 발생할 경우 SRP를 지키지 못한 것이다.
→ 연쇄 작용을 막기 위해 SRP 적용
책임 범위를 선정하는 것은 개발자마다 기준이 다르다. 이렇게 해야 한다는 완벽한 교과서적인 방법은 없지만, 항상 책임을 분리한다는 것을 잊지 말아야 한다. 개발을 처음 할 때, 그리고 리팩터링을 하면서 제일 먼저 생각해야 하는 건 내가 개발하고 있는 이곳의 역할이 무엇이냐는 것이다. 그 역할을 정확히 구분하고 책임을 분리할 수 있도록 노력해야 한다.
SRP 장점
- 테스팅
- 지금 기능에 대한 테스트만 진행하면 되니 테스트 케이스가 적다.
- 다른 기능에 대한 테스트는 해당 클래스에서만 확인하면 된다.
- 결합도
- 책임이 많아질수록 다른 역할끼리 강하게 결합될 수 있다.
- 하나의 책임만 주어졌다면 해당 기능에 문제가 생겼을 때 그 클래스만 수정하면 된다.
- 가독성
- 다른 기능까지 모두 포함되지 않으니 코드 가독성이 향상 된다.
SRP 설계 시 주의점
1. 클래스 이름
- 어떤 기능을 담당하는지 알 수 있게 작명하는 것이 좋다.
- 변수 이름을 선정할 때도, 함수 이름을 정할 때도 어떤 기능을 하는지 명확히 알 수 있어야 한다.
- 클래스 또한 마찬가지로, 지금 이 클래스가 어떤 책임을 가지고 있는지 이름에서 드러나야 한다.
- 처음 공부를 시작할 때 대부분 int a, char c, int arr[10]처럼 변수 이름에 의미를 두지 않고 작성하는 사람이 많다. 하지만 어느 정도 개발 공부를 하고 나면 그 다음으로 변수, 함수 이름을 제대로 정하라는 말을 많이 듣는다.하나의 책임을 분리시킨다는 SRP는 그만큼 클래스 이름에서 그 책임에 대해 나타내야 한다. 어떤 기능의 클래스인지 제대로 추상화 시켜야 한다.
- 실제로 1학년 때 선배가 개발하는 모습을 옆에서 직접 바라볼 때 메서드 이름을 뭘로 할까 고민을 하며 두세번 변경하는 모습을 보았다. 그때 보고 배운 행동이었지만, 이름이라는 것은 그 본질을 나타내는 것으로 다른 사람이 봤을 때도 명확히 알아야 한다는 것을 이해한 순간 중요성이 더 와닿았다.
2. 결합도, 응집도
- 무조건 책임을 분리한다고 SRP가 적용되는 것은 아니다.
- 지금 클래스에서 하나의 책임만 담당하고 있으면 그 내부에서 응집도는 높아질 것이다. 다른 클래스와 책임을 분리하고 있으니 외부와 결합도는 낮아질 것이다.
- 응집도와 결합도가 갖추어지지 않았다면 제대로 책임이 분리됐는가를 생각해봐야 한다.
- 높은 응집도와 낮은 결합도는 소프트웨어 설계의 기준이 되는 요소 중 하나로 작용한다. 응집도는 하나의 모듈 안에서 그 요소들 간의 연관 정도, 결합도는 모듈과 모듈 사이에서 일어나는 연관 정도를 말한다.
OCP 개방 폐쇄 원칙 (Open-Closed)
“you should be able to extend a classes behavior, without modifying it”
→ 확장에는 열려있어야 하며, 수정에는 닫혀있어야 한다.
수정에 닫혀있어야 한다는 것은 클래스 자체를 수정을 하지 말아야 한다는 것이 아니다. 에러나 코드 자체에 문제가 생길 경우 수정은 언제든지 이루어질 수 있다. 하지만 우리의 수정으로 기존에 구성된 다른 모듈에는 영향을 주지 않아야 한다. 요구사항이 변경되거나 추가 될 경우 클래스를 확장해 나가며, 그 외 다른 수정은 일어나지 않게 하는 것이 OCP의 핵심이다.
Open - 확장에는 열려 있어야 한다. → 기능을 추가하고, 유연하게 변경할 수 있어야 한다.
Closed - 수정에는 닫혀있어야 한다. → 기존의 코드는 변경되지 않아야 한다.
추상화를 생각하며 OCP를 바라보면 쉽게 이해할 수 있다.
추상화를 할 때 구체적인 사항을 알고 있지 않아도 기능을 사용할 수 있도록 해야 한다. 이름만 보고도 어떤 기능을 하는지 식별 될 수 있어야 하는 것이다. Grady Booch에 의하면 “추상화란 다른 모든 종류의 객체로부터 식별 될 수 있는 객체의 본질적인 특징” 이라고 정의한다. 그 행위를 본질적으로 정의할 수 있다면, 다른 행위와 쉽게 구분할 수 있다. 확장이 필요할 때 어떤 것을 수정해야 할지 제대로 이해하고 다가간다면 다른 행위를 위한 모듈에서는 수정을 할 필요가 없다.
OCP 적용 방법
- 확장할 것과 변하지 않을 것을 구분한다.
- 두 모듈이 만나는 지점에 인터페이스를 정의한다.
- 구현체보다 인터페이스에 의존하도록 코드를 작성한다.
추상클래스나 인터페이스를 활용한 추상화된 클래스를 상속하면, 새로운 것이 추가 되었을 때 다른 모듈에 변경을 주지 않고도 쉽게 확장할 수 있다.
먼저 공통된 특징을 모아 추상화 한 뒤, 상속한 클래스에서 구현해보자. 그리고 새로운 것을 추가시키고 싶을 때는 추상화했던 모듈을 상속해서 구현하자. 기존에 구현해두었던 다른 클래스에는 영향을 주지 않는다. 그저 추상화되었던 것을 가져와 우리가 새로운 것을 직접 구현한다.
→ 새로 기능을 추가하며 확장시켰고, 다른 모듈을 수정하지 않아도 완성했다.
OCP 적용 시 주의점
1. 관계 조절
- 확장할 것과 변하지 않을 모듈을 분리하는 과정에서 그 조절이 중요하다.
- 크기 조절에 실패하면 관계가 더 복잡해질 수 있다.
2. 인터페이스 설계
- 인터페이스 자체가 변경되는 것은 위험하다.
- 그 인터페이스를 상속하는 다른 모듈에서 전부 수정을 해야 한다.
- 인터페이스 변경이 아닌 상속을 통해 확장을 하는 것이 중요하다.
인터페이스를 정의할 때 여러 경우의 수를 생각해야 한다.
LSP 리스코프 치환 원칙 (Liskov Substiution)
“functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it”
→ 기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 파생 클래스의 객체를 알지 못한 채 사용할 수 있어야 한다.
다른 것과 달리 이름에서 그 의미를 이해하기 힘든 리스코프 치환 원칙은 “하위 클래스는 상위 클래스를 표현할 수 있어야 한다”는 의미를 가지고 있다. 바바라 리스코프(Barbara Liskov)가 1987년 처음 소개하였다.
부모클래스를 상속 받으면 자식클래스는 부모클래스에 존재하는 필드와 메서드를 모두 가지게 된다. 자식클래스가 부모클래스의 타입으로 형 변환을 해도 오류가 뜨지 않고 원하는 동작을 수행할 수 있다.
업캐스팅이 가능한 상태 ⇒ LSP가 지켜지고 있는 상태로 볼 수 있다.
상속은 궁극적으로 다형성을 통한 확장성 획득을 목표로 한다. OCP를 설명할 때도 확장을 위해 상속을 이용했다. 인터페이스를 이용하게 되면 다형성을 얻을 수 있다. 결국 상속은 다형성과도 확장성과도 깊은 연관이 있는 것이다.
[JAVA - 상속과 인터페이스의 관계 인터페이스 잘 구현하기]
위 글을 작성하면서 List객체를 만들어서 예시를 들었다.
ArrayList와 LinkedList로 모두 List 객체를 인스턴스 할 수 있었다. 바로 이 방법이 LSP를 이용한 방법이다.
List를 상속받은 자식 클래스로 상위 클래스를 표현할 수 있는 것. 업캐스팅을 해도 문제가 없는, LSP 원칙이 지켜지는 것이다. 업캐스팅이 가능하다는 건 단순히 업캐스팅을 하면 LSP를 지킨다는 것이 아니다. 업캐스팅을 해도 오류가 나지 않는 상태를 지켜야 한다.
LSP 적용 방법
- 같은 일을 하는 개체는 하나의 모듈로
- 같은 연산이지만, 차이가 있으면 공통의 인터페이스로
- 공통된 연산이 없으면 별개의 모듈로
LSP를 지키기 위해 그 개념과 방법을 알아보는데 OCP와 비슷한 점이 많이 보인다. LSP는 OCP를 구성하기 위한 구조가 된다. LSP를 바탕으로 제작하면 OCP를 지키며 제작할 수 있게 된다.
LSP 설계 시 주의점
1. 행동 규약
- 부모클래스의 행동 규약을 자식클래스가 위반하면 안 된다.
- 메서드 오버라이딩을 할 때 잘못 정의하면 LSP를 위배할 수 있다.
오버라이딩을 할 때 잘못 정의하는 경우는 시그니처를 변경하거나, 의도와 다르게 사용하는 경우가 있다. 부모를 상속할 때 기존에 보장하던 조건을 수정하거나 적용시키지 않을 때(null로 만드는 경우) 부모 클래스를 사용하는 코드에서 오류를 발생시킨다.
2. Is-A 관계
- 상속을 통한 재사용은 부모와 자식 간 is-a관계가 있을 경우로만 제한 되어야 한다.
- ‘상위 개념’은 ‘하위 개념’이다 (o), ‘하위 개념’은 ‘상위 개념’이다 (x)
ISP 인터페이스 분리 원칙 (Interface Segregation)
“clients should not be forced to depend upon interfaces that they do not use”
→ 사용하지 않는 인터페이스에 의존해서는 안된다. 사용에 맞게 분리해야 한다.
한 클래스에서 자신이 사용하지 않는 클래스는 구현하지 말아야 한다. 인터페이스의 사용에 맞게 분리해야 한다. 사용에 맞게 분리해야 한다는 것은 첫번째로 설명했던 SRP와 비슷하다. 다른 점이 있다면 SRP는 클래스 단위로 분리하는 것이고, ISP는 인터페이스 단위로 분리하는 것이다.
한 인터페이스 안에 여러 책임이 존재할 경우, 구현할 클래스는 사용하지 않을 책임이 함께 들어있을 수 있다. 만약 이 인터페이스가 책임 별로 나누어 개별화된다면 필요한 인터페이스만 이용해 구현하면 된다.
인터페이스를 분리하여 목적과 용도에 적합한 인터페이스만 제공할 수 있게 하는 것이 ISP의 목적이다.
ISP 적용 방법
- 클래스 인터페이스를 통한 분리
- 객체 인터페이스를 통한 분리
ISP 설계 시 주의점
- 인터페이스 설계
- 한번 인터페이스를 분리해 구성한 후, 다시 인터페이스를 분리하지 말아야 한다.
- 인터페이스는 한번 구성되면 변하면 안 되는 정책 개념이다.
인터페이스를 분리하여 이미 그 인터페이스를 사용하고 있다.
만약 이 인터페이스를 다시 분리시키면, 그 인터페이스를 사용하고 있는 다른 모듈에서 문제가 생긴다.
OCP에서 인터페이스를 설계하는 단계부터 여러 경우의 수를 생각해 정의해야 한다고 했다. 인터페이스는 한번 구성되어 사용하고 있는 순간, 해당 인터페이스 자체에서 수정을 최소화해야 한다. 예기치 못한 문제가 생겼을 경우 수정을 해야 할 일이 생기겠지만, 그럴 경우에도 해당 인터페이스를 사용하는 모든 것을 수정해야 한다.
DIP 의존성 역전 원칙 (Dependency Inversion)
“high level modules should not depend upon low level modules. both should depend upon abstractions. abstractions should not depend upon details. details should depend upon abstractions.”
→ 상위 모듈이 하위 모듈에 의존하는 대신, 모두 추상화에 의존해야 한다.
구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 것이다. 사용 관계는 바뀌지 않고 추상을 매개로 메시지를 주고받는다. 여기서 주의해야 하는 점은 통신을 할 때 추상성이 낮은 클래스가 아니라 높은 클래스와 통신을 해야 한다.
인터페이스를 상속받아 구현하고 있는 하위 모듈이 있다. 그리고 우리는 그 하위 모듈을 사용하며 의존하는 것이 아닌, 상위 모듈=인터페이스를 통해 의존해야 한다. 인터페이스는 실제 구현체가 아닌데 왜 의존을 해야 할까? 하위 모듈의 구체적인 내용에 의존하면, 하위 모듈에 변화가 생길 때마다 상위 모듈의 코드를 수정해야 한다.
→ 상위 타입의 객체로 통신하라는 것
적용 방법
Grady Booch는 “잘 구조화된 객체 지향 설계는 인터페이스를 통한 서비스를 제공하는 레이어로 구성되어 있다”고 한다.
의존이 발생할 경우 인터페이스=추상화된 상위 모듈을 통해서 서비스를 제공하는 것이다.
List<Integer> list = new ArrayList()<>;
Set<Integer> set = new HashSet()<>;
Map<String, String> map = new HashMap()<>;
각 객체를 인스턴스 할 때, 인터페이스 타입으로 선언하고 있다.
업캐스팅으로 알고 있는 이 방법 또한 DIP를 지키는 선언이다.
reference
"객체 지향 설계의 5가지 원칙 - S.O.L.I.D", Inpa Dev,
https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-SOLID
"객체지향 개발 5대 원리: SOLID", 넥스트리(NEXTREE.IO), https://www.nextree.co.kr/p6960/
"SOLID 원칙을 위한 완전한(Solid) 가이드",개발뚱 , https://dev-gold.tistory.com/104
'Language > JAVA' 카테고리의 다른 글
응집도(Conhension)와 결합도(Coupling), 그리고 캡슐화의 중요성 (0) | 2025.01.14 |
---|---|
[자바] 자바 가상 머신, JVM(Java Virtual Machine) 자세히 이해하기 (0) | 2025.01.13 |
객체 지향 프로그래밍(Object-Oriented-Programming) 자세히 이해하기 (0) | 2025.01.08 |
[자바/JAVA] 상속과 인터페이스의 관계, 인터페이스 잘 구현하기 (0) | 2025.01.08 |
[자바/JAVA] 자바로 그래프(Graph) 직접 구현해보기 -인접 행렬, 인접 리스트 (1) | 2025.01.03 |