SOLID 원칙이란?
객체지향 프로그래밍에서 유지보수가 쉽고 확장 가능한 코드를 작성하기 위한 5가지 설계 원칙입니다.
SOLID는 각 원칙의 앞글자를 딴 약자
- SRP: Single Responsibility Principle (단일 책임 원칙)
- OCP: Open-Closed Principle (개방-폐쇄 원칙)
- LSP: Liskov Substitution Principle (리스코프 치환 원칙)
- ISP: Interface Segregation Principle (인터페이스 분리 원칙)
- DIP: Dependency Inversion Principle (의존관계 역전 원칙)
1️⃣ SRP - 단일 책임 원칙
"한 클래스는 하나의 책임만 가져야 한다"
❌ 잘못된 예시
// 회원 관리, 이메일 발송, DB 저장을 모두 담당 (책임이 너무 많음!)
public class User {
private String name;
private String email;
public void saveToDatabase() {
// DB 저장 코드
}
public void sendEmail() {
// 이메일 발송 코드
}
public void generateReport() {
// 리포트 생성 코드
}
}
✅ 올바른 예시
// 각 클래스가 하나의 책임만 담당
public class User {
private String name;
private String email;
// 회원 정보만 관리
}
public class UserRepository {
public void save(User user) {
// DB 저장만 담당
}
}
public class EmailService {
public void sendEmail(User user) {
// 이메일 발송만 담당
}
}
왜 중요한가?
- 코드 수정 시 영향 범위가 줄어듦
- 테스트하기 쉬워짐
- 재사용성이 높아짐
2️⃣ OCP - 개방-폐쇄 원칙
"확장에는 열려있고, 수정에는 닫혀있어야 한다"
새로운 기능 추가 시 기존 코드를 수정하지 않고 확장할 수 있어야 합니다.
❌ 잘못된 예시
public class PaymentProcessor {
public void processPayment(String type) {
if (type.equals("CARD")) {
// 카드 결제 처리
} else if (type.equals("CASH")) {
// 현금 결제 처리
}
// 새로운 결제 수단 추가할 때마다 이 코드를 수정해야 함!
}
}
✅ 올바른 예시
// 인터페이스 정의
public interface Payment {
void process();
}
// 각 결제 수단을 별도 클래스로 구현
public class CardPayment implements Payment {
public void process() {
// 카드 결제 처리
}
}
public class CashPayment implements Payment {
public void process() {
// 현금 결제 처리
}
}
// 새로운 결제 수단 추가 시 기존 코드 수정 없이 확장만 하면 됨
public class KakaoPayment implements Payment {
public void process() {
// 카카오페이 처리
}
}
왜 중요한가?
- 기존 코드를 건드리지 않아 버그 발생 위험이 줄어듦
- 새로운 기능 추가가 쉬워짐
3️⃣ LSP - 리스코프 치환 원칙
"자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 한다"
❌ 잘못된 예시
public class Bird {
public void fly() {
System.out.println("날아갑니다");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
// 부모 클래스의 fly()를 제대로 대체하지 못함
}
}
✅ 올바른 예시
public abstract class Bird {
public abstract void move();
}
public class Eagle extends Bird {
@Override
public void move() {
System.out.println("날아갑니다");
}
}
public class Penguin extends Bird {
@Override
public void move() {
System.out.println("뒤뚱뒤뚱 걸어갑니다");
}
}
왜 중요한가?
- 다형성을 올바르게 사용할 수 있음
- 예상치 못한 동작을 방지
4️⃣ ISP - 인터페이스 분리 원칙
"클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다"
❌ 잘못된 예시
// 너무 많은 기능을 하나의 인터페이스에 담음
public interface Worker {
void work();
void eat();
void sleep();
}
// 로봇은 eat()과 sleep()이 필요 없는데 구현해야 함
public class Robot implements Worker {
public void work() { /* 작업 */ }
public void eat() { /* 불필요 */ }
public void sleep() { /* 불필요 */ }
}
✅ 올바른 예시
// 인터페이스를 역할별로 분리
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// 필요한 인터페이스만 구현
public class Human implements Workable, Eatable, Sleepable {
public void work() { /* 작업 */ }
public void eat() { /* 식사 */ }
public void sleep() { /* 수면 */ }
}
public class Robot implements Workable {
public void work() { /* 작업 */ }
// eat(), sleep() 구현 불필요!
}
왜 중요한가?
- 불필요한 의존성 제거
- 코드가 더 명확해짐
5️⃣ DIP - 의존관계 역전 원칙
"구체적인 것이 아닌 추상적인 것에 의존해야 한다"
❌ 잘못된 예시
// 구체적인 클래스에 직접 의존
public class UserService {
private MySQLDatabase database = new MySQLDatabase();
public void saveUser(User user) {
database.save(user);
// MySQL에서 Oracle로 바꾸려면 코드 수정 필요!
}
}
✅ 올바른 예시
// 인터페이스(추상)에 의존
public interface Database {
void save(User user);
}
public class MySQLDatabase implements Database {
public void save(User user) { /* MySQL 저장 */ }
}
public class OracleDatabase implements Database {
public void save(User user) { /* Oracle 저장 */ }
}
public class UserService {
private Database database; // 인터페이스에 의존
public UserService(Database database) {
this.database = database; // 외부에서 주입받음
}
public void saveUser(User user) {
database.save(user);
// DB 변경 시 UserService 코드 수정 불필요!
}
}
왜 중요한가?
- 결합도가 낮아져 유연한 구조가 됨
- 테스트가 쉬워짐 (Mock 객체 사용 가능)
- 스프링부트에 의존성 주입이 바로 DIP 원칙을 구현한 것
실전 예제 - 도서 관리 시스템
// SRP: 각 클래스가 하나의 책임만
public class Book {
private String title;
private String author;
}
// OCP: 새로운 할인 정책 추가 시 확장만 하면 됨
public interface DiscountPolicy {
int getDiscountAmount(int price);
}
public class StudentDiscount implements DiscountPolicy {
public int getDiscountAmount(int price) {
return price * 10 / 100; // 10% 할인
}
}
// ISP: 필요한 기능만 인터페이스로 분리
public interface Borrowable {
void borrow();
void returnBook();
}
public interface Reservable {
void reserve();
}
// DIP: 구체적인 클래스가 아닌 인터페이스에 의존
public class LibraryService {
private BookRepository repository; // 인터페이스에 의존
public LibraryService(BookRepository repository) {
this.repository = repository;
}
}
SOLID 원칙을 지키면 얻는 이점
이점설명| 🔧 유지보수 용이 | 코드 수정이 쉽고 안전해짐 |
| 🚀 확장성 | 새로운 기능 추가가 쉬워짐 |
| 🧪 테스트 용이 | 각 부분을 독립적으로 테스트 가능 |
| 🔄 재사용성 | 코드를 다른 곳에서도 활용 가능 |
| 👥 협업 효율 | 코드 이해가 쉬워져 팀 작업이 수월함 |
핵심 정리
꼭 기억해야 할 SOLID
- SRP → 한 클래스는 한 가지 일만
- OCP → 수정은 닫고, 확장은 열어두기
- LSP → 자식은 부모를 완벽히 대체 가능해야
- ISP → 인터페이스는 작고 명확하게
- DIP → 구체적인 것 말고 추상적인 것에 의존
내가 생각해본 적용 과정
코드 작성 시 스스로 질문하기
↓
이 클래스의 책임이 명확한가? → SRP
새 기능 추가 시 기존 코드 수정이 필요한가? → OCP
상속 관계가 올바른가? → LSP
인터페이스가 너무 크지 않은가? → ISP
구체 클래스에 직접 의존하고 있지 않은가? → DIP
찾아본 다른 원칙들
1. DRY(Don't Repeat Yourself) - 중복 코드를 만들지 말자,
2. KISS(Keep It Simple, Stupid) - 단순하게 만들자,
3. YAGNI(You Aren't Gonna Need It) - 필요 없는 기능은 만들지 말자
'-- 오늘 있었던 개발 일기' 카테고리의 다른 글
| 오늘의 개발 문제 : DTO 필드명 불일치 (0) | 2025.12.09 |
|---|---|
| 오늘의 개발 문제 : SOLID원칙에 우선순위 (0) | 2025.12.08 |
| 오늘의 개발 문제 : context에서 Zustand로 (0) | 2025.12.03 |
| 자바의 예외 처리(Exception) 구조 (0) | 2025.11.30 |
| 오늘의 개발 문제 : useState 초기값 설정 (0) | 2025.11.28 |