객체지향 프로그래밍 관련 얘기들을 주워 듣다 보면 SOLID라는 말을 한번 쯤 들어봤을 것이다. 나는 정보처리기사 준비를 하면서 처음 봤었던 것 같다.
SOLID란 현대의 객체지향 프로그래밍을 위한 OOP의 5가지 설계 원칙이다. 일종의 가이드라인이다.
S-(SRP, Single Responsibility Principle)
단일 설계 원칙, 하나의 클래스는 하나의 목적만 가져야 한다.
- SRP는 클래스의 응집도를 올려준다.
O - 개방-폐쇄 원칙 (OCP, Open/Closed Principle)
확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
- 기능을 변경할 때 기존 코드를 건드리지 않고 상속이나 구성(Composition)으로 확장해야 한다.
- 기존 공통 기능을 수정한다면, 다른 부분과 호환성이 일그러지며, 버그가 생길 확률이 매우 높다. 따라서, 부가 기능을 처음부터 확장 형식으로 뻗어나가는 것이 옳다.
L - 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
자식 클래스는 언제나 부모 클래스 대신 사용될 수 있어야 한다. 무조건 부모의 모든 메소드 모든 속성을 포함해야 한다.
- 예: 부모가 Bird일 때, Penguin이 상속받더라도 fly()가 있으면 안 되는 것처럼 동작이 일관돼야 함.
- Bird → NonFlyingBird → Penguin으로 설계하여 회피한다.
I - 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
- 다시 말해, 클래스는 상속(구현)받은 인터페이스의 메서드를 모두 사용해야 한다. 상속받은 메서드들 중 일부만 구현하고, 사용하면 안된다.
- 인터페이스을 잘게 쪼개 각 기능에 따라 여러개의 인터페이스를 만들고, 클래스를 이를 조합해서 구현한다.
- ISP를 지키지 않을 경우 → 결합도가 올라간다.
D - 의존 역전 원칙 (DIP, Dependency Inversion Principle)
상위 모듈이 하위 모듈에 의존하면 안 되며, 둘 다 추상화에 의존해야 한다.
- 여기서 말하는 하위 모듈은 단순히 자신의 자식 클래스를 의미하는 것이 아니다.
자신보다 추상화 레벨이 낮은 클래스를 자신의 구현부에서 사용하면 안됨을 의미한다.
개인적으로 5번째 규칙인 DIP가 가장 이해하기 어려웠다. 이는 내가 하위 모듈을 자식 클래스로 이해했기 때문이다. 여기서 하위 모듈은 자식 클래스가 아니라, 자신보다 추상화 레벨이 낮은 모든 클래스를 의미한다.
다음 예시를 보면 이해하기가 쉬울 것이다.
DIP 위반 예시
// 상위 모듈이 하위 모듈에 직접 의존
class MySQLDatabase {
save(data) {
console.log("Saving to MySQL:", data);
}
}
class UserService {
constructor() {
this.db = new MySQLDatabase(); // 구체 클래스에 직접 의존
}
createUser(user) {
this.db.save(user);
}
}
DIP를 적용한 예시 (인터페이스에 의존)
class Database {
save(data) {
throw new Error("Must override");
}
}
// 구체 구현 1
class MySQLDatabase extends Database {
save(data) {
console.log("MySQL 저장:", data);
}
}
// 구체 구현 2
class MongoDatabase extends Database {
save(data) {
console.log("MongoDB 저장:", data);
}
}
// 상위 모듈은 추상화에만 의존
class UserService {
constructor(database) {
this.db = database; // 어떤 DB든 상관없음
}
createUser(user) {
this.db.save(user);
}
Spring Framework의 자바bean의 의존성 주입도 이 DIP를 대신해서 맡기 위함이다.