1. 객체지향 프로그래밍의 5가지 설계 원칙, SOLID
SOLID란 객체 지향 프로그래밍을 하면서 지켜야하는 5대 원칙으로 각각 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LSP(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)의 앞글자를 따서 만들어졌다. SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 되는 것으로 알려져있다.
2. 원칙
1) 단일 책임의 원칙 (SRP, Single Responsibility Principle)
하나의 클래스는 하나의 책임만 가져야한다.
클래스는 그 책임을 완전히 캡슐화해야 함을 말한다.
책임이란 기준이 모호하기 때문에 변경 을 책임의 기준으로 삼으면 설계에 용이할 수 있다.
어떠한 역할에 대해 변경사항이 발생했을때, 영향을 받는 기능만 모아둔 클래스라면 동일한 책임을 지닌 기능이 모인 집합으로써 SRP 원칙이 적용된 설계로 볼 수 있을것 같다.
이처럼 변경사항이 있을때, 애플리케이션의 파급 효과가 적으면 SRP 원칙을 잘 따른것으로 볼 수 있다.

위의 남자 클래스를 보면 그냥 보아도 이 사람이 해야 할 역할, 책임이 매우 많은 것을 볼 수 있습니다. 즉, 남자라고 클래스의 범위를 너무 애매하게 잡았기 때문에 하나의 클래스가 많은 역할과 책임을 가지게 된 것인데요.

그래서 위처럼 남자라는 것을 각 역할에 맞게 잘 분리하면 역할과 책임이 알맞게 잘 분리된 것을 볼 수 있습니다.
메소드가 SRP를 지키지 못한 경우
class 강아지 {
final static Boolean 수컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
void 소변보다() {
if (this.성별 == 수컷) {
// 한쪽 다리를 들고 소변을 보다.
} else {
// 뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
}
}
}
위의 강아지 클래스 코드를 보면 소변보다() 메소드에서 수컷, 암컷 모두 구현하려고 해서 단일 책임 원칙을 위반하고 있는 것을 볼 수 있습니다.
abstract class 강아지 {
abstract void 소변보다();
}
class 수컷강아지 extends 강아지 {
void 소변보다() {
// 한쪽 다리를 들고 소변을 본다.
}
}
class 암컷강아지 extends 강아지 {
void 소변보다() {
// 뒷다리 두 개로 앉은 자세로 소변을 본다.
}
}
그래서 위와 같이 강아지 라는 추상 클래스를 두고 수컷강아지, 암컷강아지 클래스가 각자 자신의 특징에 맞게 소변보다() 메소드를 구현해서 사용하는 것으로 리팩터링 할 수 있습니다.
이렇게 객체지향 4대 특성인 캡상추다(캡슐화, 추상화, 상속, 다형성)와 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화임을 알 수 있습니다.
2) 개방 - 폐쇄 원칙 (Open-Closed Principle, OCP)
개방 폐쇄 원칙(Open-Closed Principle, OCP)은 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙으로, 각각이 갖는 의미는 다음과 같다.
- 확장에 대해 열려 있다: 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
- 수정에 대해 닫혀 있다: 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
즉, 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.

위의 그림을 보면 운전자가 기어가 수동 or 자동이냐에 따라 행동이 달라지는 것을 볼 수 있는데요. 이렇게 어떤 변화가 있을 때 바로 운전자에게 영향이 오기 때문에 이러한 설계는 개방 폐쇄 원칙에 위배됩니다.

이렇게 상위 클래스 또는 인터페이스를 중간에 둠으로써 다양한 자동차가 생긴다고 해도 객체 지향 세계의 운전자는 운전 습관에 영향을 받지 않게 됩니다. 다양한 자동차가 생긴다고 하는 것은 자동차 입장에서는 자신의 확장에는 개방되어 있는 것이고, 운전자 입장에서는 주변의 변화에 폐쇄돼 있는 것입니다.
데이터베이스의 개방 폐쇄 원칙의 좋은 예

JDBC가 개방 폐쇄 원칙의 가장 좋은 예입니다. 데이터베이스가 MySQL에서 오라클로 바뀌더라도 Connection을 설정하는 부분만 변경해주면 됩니다.
* JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다. JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.
즉, 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀 있는 것입니다. 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것입니다.
개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.
따라서 객체지향 프로그래밍에서 개방 폐쇄 원칙은 반드시 지켜야 할 원칙이다.
3) 인터페이스 분리 원칙 (Interface segregation principle, ISP)
단일 책임 원칙(SRP)에서는 하나의 역할(책임)만 하도록 다수의 클래스로 분할하였습니다

인터페이스 분리 원칙은 각 역할에 맞게 인터페이스로 분리하는 것입니다.
(ex 어머니한테는 아들, 여자친구한테는 남자친구)
결론적으로는 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다.
하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다.
- 상위 클래스는 풍성할수록 좋다.
- 풍성할수록 하위 클래스에게 많은 기능을 확장시켜주는 것이고, 형변환, 코드 중복을 줄여줍니다.
- 인터페이스 내에 메소드는 최소한 일수록 좋다.
- 인터페이스는 하위 클래스에게 구현을 강제하도록 하는 역할입니다. 즉, 최소한의 기능만 제공하면서 하나의 역할에 집중하라는 뜻입니다.
또 다른 예제를 살펴보자.
예를 들어 사용자가 비밀번호를 변경할 때 입력한 비밀번호가 기존의 비밀번호와 동일한지 검사해야 하는 로직을 다른 Authentication 로직에 추가해야 한다고 가정하자.
그러면 우리는 다음과 같은 isCorrectPassword라는 퍼블릭 인터페이스를 SHA256PasswordEncoder에 추가해줄 것이다.
@Component
public class SHA256PasswordEncoder implements PasswordEncoder {
@Override
public String encryptPassword(final String pw) {
...
}
public String isCorrectPassword(final String rawPw, final String pw) {
final String encryptedPw = encryptPassword(rawPw);
return encryptedPw.equals(pw);
}
}
하지만 UserService에서는 비밀번호 암호화를 위한 encryptPassword() 만을 필요로 하고, 불필요하게 isCorrectPassword를 알 필요가 없다.
현재 UserService는 PasswordEncoder를 주입받아 encrpytPassword에만 접근 가능하므로 인터페이스 분리가 잘 된 것 처럼 보인다.
하지만 새롭게 추가될 Authentication 로직에서는 isCorrectPassword에 접근하기 위해 구체 클래스인 SHA256PasswordEncoder를 주입받아야 하는데 그러면 불필요한 encryptPassword에도 접근 가능해지고, 인터페이스 분리 원칙을 위배하게 된다.
물론 PasswordEncoder에 isCorrectPassword 퍼블릭 인터페이스를 추가해줄 수 있지만, 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공한다는 인터페이스 분리 원칙을 지키기 위해서라도 이미 만든 인터페이스는 건드리지 않는 것이 좋다.
그러므로 위의 상황을 해결하기 위해서는 비밀번호를 검사를 의미하는 별도의 인터페이스(PasswordChecker)를 만들고, 해당 인터페이스로 주입받도록 하는 것이 적합하다.
public interface PasswordChecker {
String isCorrectPassword(final String rawPw, final String pw);
}
@Component
public class SHA256PasswordEncoder implements PasswordEncoder, PasswordChecker {
@Override
public String encryptPassword(final String pw) {
...
}
@Override
public String isCorrectPassword(final String rawPw, final String pw) {
final String encryptedPw = encryptPassword(rawPw);
return encryptedPw.equals(pw);
}
}
클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다. 그리고 이렇게 인터페이스를 클라이언트의 기대에 따라 분리하여 변경에 의해 의한 영향을 제어하는 것을 인터페이스 분리 원칙이라고 부른다.
4) 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
리스코프 치환 원칙은 1988년 바바라 리스코프가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 하위 타입은 상위 타입을 대체할 수 있어야 한다는 것이다. 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다.
서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
- 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.
- 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.
위의 두 문장을 잘 지키고 있다면 이미 리스코프 치환 원칙을 잘 지키고 있다고 할 수 있습니다.
리스코프 치환 원칙 위배

- 아버지 - 딸(계층도/조직도)
리프코프 치환 원칙 만족

- 동물 - 펭귄 구조(분류도)
즉, 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
5) 의존 역전 원칙 (Dependency Inversion Principle, DIP)
의존 역전 원칙이란 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다는 것이다. 객체 지향 프로그래밍에서는 객체들 사이에 메세지를 주고 받기 위해 의존성이 생기는데, 의존성 역전의 원칙은 올바른 의존 관계를 위한 원칙에 해당된다. 여기서 각각 고수준 모듈과 저수준 모듈이란 다음을 의미한다.
- 고수준 모듈 : 변경이 없는 추상화된 클래스(또는 인터페이스)
- 저수준 모듈 : 변하기 쉬운 구체 클래스
의존 역전 원칙이란 결국 추상화에 의존하며 구체화에는 의존하지 않는 설계 원칙을 의미한다.

자동차가 타이어에 의존하면 어떻게 될까요? 자동차 타이어는 자주 바뀌게 되는 것 중 하나입니다. 이렇게 자주 바뀌는 것에 의존하면 자동차는 영향을 받게 되어 있습니다. 즉, 자동차 자신보다 더 자주 변하는 스노우타이어에 의존하기에 좋지 않음을 알 수 있습니다.

자동차가 구체적인 타이어가 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 타이어가 변경되어도 자동차가 영향을 받지 않습니다.
이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.
상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존 역전 원칙이다.
위의 내용들을 읽으면서 느꼈겠지만 5가지의 객체 지향 설계 원칙인 SOLID가 얘기하는 핵심은 결국 추상화이다. 구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 우리는 유연하고 확장가능한 애플리케이션을 만들 수 있는 것이다.
Reference
https://devlog-wjdrbs96.tistory.com/380
https://hckcksrl.medium.com/solid-%EC%9B%90%EC%B9%99-182f04d0d2b
'개발공부 > 개념정리' 카테고리의 다른 글
| OAuth (0) | 2022.06.16 |
|---|---|
| 브라우저에서 서버까지의 응답에 대한 흐름 이해하기! (0) | 2022.06.14 |
| OOP(Object oriented Programming) 객체지향 프로그래밍 (0) | 2022.06.13 |
| Redis (Remote Dictionary Server) (2) | 2022.06.08 |
| HTTP, HTTPS, SSL, TLS (0) | 2022.06.07 |