SOLID 원칙은 객체 지향 설계의 5가지 핵심 원칙 입니다.
이 원칙을 따르면 유지 보수성과 확장성 측면에서 더 나은 설계를 할 수 있습니다.
이제 5가지 원칙에 대해서 그 원칙이 무엇이고, 어떻게 지켜야하고, 위반한, 잘 지킨 예시는 어떤것이 있는지 하나하나 알아보겠습니다.
| 구분 | 약어 | 영문명 | 설명 |
|---|---|---|---|
| S | SRP | Single Responsibility Principle | 단일 책임 원칙 |
| O | OCP | Open Closed Principle | 개방 폐쇄 원칙 |
| L | LSP | Liskov Substitution Principle | 리스코프 치환 원칙 |
| I | ISP | Interface Segregation Principle | 인터페이스 분리 원칙 |
| D | DIP | Dependency Inversion Principle | 의존 역전 원칙 |
SRP - Single Responsibility Principle
단일 책임 원칙: 클래스는 하나의 책임 만 가져야 하며, 변경 이유도 하나여야 한다.
이 말은 한 클래스가 여러 이유로 변경되지 않도록 해야한다는 의미입니다.
예를 들어서
@Service
public class UserService {
public void register(User user) {
validateUser(user); // 1. 유효성 검증
userRepository.save(user); // 2. 사용자 저장
mailService.sendWelcomeMail(user.getEmail()); // 3. 이메일 발송
}
}
이 코드에서는 UserService 가 바뀌는 이유가 유효성 검사 검증 기준이 변경되거나, 사용자 저장 방식이 바뀌거나 등등 다양합니다.
→ 따라서 이 클래스는 SRP, 단일 책임 원칙을 지키고 있지 않습니다.
그러므로 코드를 아래와 같이 개선할 수 있습니다.
@Service
public class UserService {
private final UserValidator validator;
private final MailService mailService;
public void register(User user) {
validator.validate(user);
userRepository.save(user);
mailService.sendWelcomeMail(user.getEmail());
}
}
UserValidator : 유효성 검사 책임UserRepository : 데이터 저장 책임MailService : 이메일 발송 책임→ 각각은 하나의 이유로만 변경됨 → SRP 준수
OCP - Open Closed Principle
개방 폐쇄 원칙: 소프트웨어 요소는 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.
예시와 함께 OCP 에 대해서 더 알아보겠습니다.
public class DiscountService {
public int getDiscountPrice(String grade, int price) {
if ("NORMAL".equals(grade)) {
return price - 1000;
} else if ("VIP".equals(grade)) {
return price - 3000;
} else {
return price;
}
}
}
이런 코드라면 새로운 할인 정책이 추가될 때마다 if-else문 을 추가하는 번거로움을 겪게 됩니다. 확장에는 열려 있고 수정에는 닫혀 있어야 하는 OCP 를 위반하게 된 것이죠.
따라서 아래와 같이 코드를 개선할 수 있습니다.
interface PaymentMethod {
void pay();
}
class CardPayment implements PaymentMethod {
@Override
public void pay() {
// 카드 결제 처리
}
}
class CashPayment implements PaymentMethod {
@Override
public void pay() {
// 현금 결제 처리
}
}
class PaymentProcessor {
void pay(PaymentMethod method) {
method.pay();
}
}
PaymentMethod 라는 인터페이스를 통해 결제 방식 추가가 자유롭습니다.PaymentProcessor 는 수정할 필요가 없습니다.LSP - Likov Substituition Principle
리스코프 치환 원칙: 하위 클래스는 상위 클래스를 완벽하게 대체할 수 있어야 한다.
이 의미는 부모 클래스 객체를 사용하는 곳에 자식 클래스 객체를 넣어도 동일하게 작동해야 한다는 의미입니다. 자식 클래스가 부모의 규칙을 깨거나 예외를 발생시키면 LSP 가 위반됩니다. 아래의 예시와 함께 더 자세히 알아보겠습니다.
class Bird {
public void fly() {
// 날 수 있음
}
public void layEgg() {
// 알 낳기
}
}
class Chicken extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("닭은 날 수 없습니다.");
}
}
Bird 는 fly() 가 가능한 객체로 설계됨 (부모 클래스의 규칙)Chicken 는 Bird 를 상속하지만 fly() 불가능 (= 자식 클래스가 부모 클래스의 규칙을 깸)Bird 를 사용하는 코드에서 Chicken 를 넣으면 에러 발생따라서 아래와 같이 코드를 개선할 수 있습니다.
interface Bird {
void layEgg();
}
interface Flyable {
void fly();
}
class Parrot implements Bird, Flyable {
public void layEgg() {
// 알 낳기
}
public void fly() {
// 날기
}
}
class Chicken implements Bird {
public void layEgg() {
// 알 낳기
}
}
Bird 는 공통 기능인 layEgg() 만 포함합니다.Flyable 은 날 수 있는 새에만 필요한 기능인 fly() 를 분리합니다.Chicken 는 둘 다 구현 → 날 수 있는 새Chicken 는 Bird 만 구현 → 날 수 없는 새ISP - Interface Segregation Principle
인터페이스 분리 원칙: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다.
이 원칙은 하나의 거대한 인터페이스보다, 작고 명확한 인터페이스 여러 개로 나누는 것이 좋다는 의미를 포함하고 있습니다.
아래의 예시와 함께 더 자세히 알아보겠습니다.
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() { }
public void eat() {
throw new UnsupportedOperationException("로봇은 밥 안 먹음");
}
}
Robot 은 eat() 이 필요 없음Worker 를 구현하므로 eat() 을 구현해야 함따라서 아래와 같이 코드를 개선할 수 있습니다.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() { }
public void eat() { }
}
class Robot implements Workable {
public void work() { }
}
DIP - Dependency Injection Principle
의존 역전 원칙: 고수준 모듈은 저수준 모듈에 의존하면 안된다. 둘 다 추상화에 의존해야 한다.
DIP 를 위반하면, 고수준 모듈(비즈니스 로직)이 저수준 모듈(구현 클래스)에 직접 의존하게 되고 이러면 변경에 취약해집니다. 따라서 인터페이스(추상화) 를 통해 서로 느슨하게 연결해야 합니다.
아래의 예시와 함께 더 자세히 알아보겠습니다.
class MySQLDatabase {
public void save(String data) {
// MySQL에 저장
}
}
class DataService {
private MySQLDatabase db = new MySQLDatabase();
public void save(String data) {
db.save(data);
}
}
DataService (비지니스 로직) 는 MySQLDatabase (구현 클래스) 에 직접 의존DataService 도 수정해야 함따라서 아래와 같이 코드를 개선할 수 있습니다.
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) {
// MySQL에 저장
}
}
class DataService {
private final Database db;
public DataService(Database db) {
this.db = db;
}
public void save(String data) {
db.save(data);
}
}
Database 라는 인터페이스에 의존 → 실제 구현은 외부에서 주입SOLID 원칙은 객체 지향 설계에 있어서 "필수 규칙" 이 아니라, "권장되는 설계 지침" 입니다.
이 말은 SOLID 원칙을 반드시 따라야 하는 것은 아니라는 의미 입니다.
첫번재 이유
프로젝트 규모가 작거나 단기간에 끝나는 프로젝트라면, SOLID 원칙을 엄격히 적용하면, 과설계가 발생할 수 있습니다.
두번재 이유
모든 원칙을 다 지키려고 하면 코드 구조가 복잡해져서 협업이나 유지보수가 어려워질 수 있습니다.
결론
SOLID 원칙을 모두 적용하려면 많은 시간과 노력이 요구됩니다.
따라서 성능, 유지보수성 등의 우선 순위를 고려하여 이에 따라 필요한 정도로 적당히 원칙을 적용하는 것이 중요합니다.