객체지향 설계 원칙이란 소프트웨어 개발의 유지보수성과 확장성을 높이기 위해 도입된 일련의 설계 지침이다.
이 원칙은 시간이 지나도 변화에 유연하게 대응할 수 있는 구조 즉, 변경에 강하고 확장에 유연한 코드를 만들기 위해 등장했다.
초기 소프트웨어 개발에서는 아래와 같은 문제들이 자주 발생했다.
1. 코드의 강한 결합도(Tightly Coupled Code)
→ 하나의 클래스가 변경되면 여러 클래스가 연쇄적으로 영향을 받는 구조로, 작은 변경이 전체 시스템에 영향을 주는 경우가 많았음.
2. 유지보수의 어려움
→ 시간이 지날수록 코드가 복잡해지고, 누가 작성했는지 모르는 코드가 많아져 수정이 어렵고 버그도 많아짐.
3. 재사용성 부족
→ 특정 기능을 다른 프로젝트나 모듈에서 재활용하기 어려움.
4. 확장에 취약한 구조
→ 기능이 늘어날수록 기존 코드를 수정해야만 했고, 새로운 요구사항이 들어올 때마다 기존 코드가 깨지기 쉬웠음.
➡️이러한 문제들을 해결하기 위해 객체지향 프로그래밍(OOP)이 제안되었고, OOP 안에서도 더 좋은 설계를 위한 가이드라인으로써 SOLID 원칙을 중심으로 한 객체지향 설계 원칙들이 제안되었다.
SOLID 원칙은 로버트 C. 마틴이 제안한 객체지향 설계 5대 원칙이다.
SOLID의 용어 개념 이론들은 모두 자바의 클래스 객체 지향인 추상화, 상속, 인터페이스, 다형성 등의 개념들을 재정립한 것으로 보면 된다.
그리고 이 5가지 원칙들은 서로 독립된 개별적인 개념이 아니라 서로 개념적으로 연관되어 있다.
한 클래스는 하나의 책임만 가져야 한다.
➡️클래스는 오직 한가지 기능 또는 역할만을 수행해야하며, 변경 사유도 하나뿐이여야 한다.
public class UserManager {
public void createUser() { ... }
public void deleteUser() { ... }
public void saveToFile() { ... }
}
→ 위 클래스에서는 사용자 관리(createUser, deleteUser)와 파일관리(saveToFile)이라는 두가지 책임을 가진다.
public class UserService {
public void createUser() { ... }
}
public class FileSaver {
public void saveToFile() { ... }
}
→ 클래스가 각각 하나의 책임만 갖도록 분리
확장에는 열려있고, 변경에는 닫혀있어야 한다.
➡️기존 코드를 건드리지 않고 새로운 기능을 추가할 수 있어야 한다. 즉, 변경이 아닌 확장으로 해결하라는 의미이다.
public class DiscountService {
public double getDiscount(String userType) {
if (userType.equals("VIP")) return 2;
else if (userType.equals("BASIC")) return 1;
return 0;
}
}
→ 새로운 회원 등급이 생길 때마다 if문을 수정해야 한다.
public interface DiscountPolicy {
double getDiscount();
}
public class VipDiscount implements DiscountPolicy {
public double getDiscount() { return 2; }
}
public class BasicDiscount implements DiscountPolicy {
public double getDiscount() { return 1; }
}
→ 새로운 등급이 추가되어도 기존 코드는 변경하지 않고, 코드 추가만 하면 된다.
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
➡️자식 클래스를 사용하는 클라이언트 코드에서 부모 클래스와 동일하게 작동해야 한다. 즉, 부모 클래스와 자식 클래스 사이의 행위에는 일관성이 있어야한다.
public class Bird {
public void fly() { ... }
}
public class Ostrich extends Bird {
public void fly() { throw new UnsupportedOperationException(); }
}
→ 타조는 날 수 없는데 Bird를 상속받음
public interface Bird { }
public interface Flyable {
void fly();
}
→ 인터페이스 분리로 리스코프 위반 방지(다형성)
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
➡️인터페이스는 작고 명확하게 분리되어야 한다. 즉, 너무 많은 기능을 한 인터페이스에 몰아 넣지 말고, 필요한 기능만 분리해서 사용하라는 뜻이다.
public interface Worker {
void work();
void eat();
void sleep();
}
→ 여러 기능을 하나의 인터페이스에서 사용
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface sleepable {
void sleep();
}
클라이언트는 구체 클래스가 아닌 인터페이스나 추상 클래스에 의존해야 한다.
➡️구체 클래스가 아닌 추상화에 의존함으로써 유연한 시스템 구조를 만들 수 있다.
의존 역전 원칙은 객체 생성 방식에 있어 new를 직접 쓰지말고 DI(의존성 주입)를 활용하라는 원칙과도 연결 된다.
public class OrderService {
private MySQLDatabase db = new MySQLDatabase(); // 강한 의존
}
public class OrderService {
private final Database db;
public OrderService(Database db) {
this.db = db;
}
}
→ 인터페이스에 의존하고, 구체 구현은 외부에서 주입받음 (ex: 스프링의 DI)