개요
SOLID 원칙은 객체지향 프로그래밍에서 유지보수 가능하고 확장 가능한 소프트웨어를 설계하기 위한 5가지 핵심 설계 원칙으로,
개발자 면접과 실무에서 필수적으로 알아야 할 개념입니다.
초보개발자부터 시니어 개발자까지 모든 개발자가 반드시 알아야 할 객체지향 설계 원칙인 SOLID 원칙을 실전 예제와 함께 쉽게 설명하겠습니다.
Robert C. Martin이 제시한 이 원칙들은
SRP(Single Responsibility Principle), OCP(Open/Closed Principle), LSP(Liskov Substitution Principle), ISP(Interface Segregation Principle), DIP(Dependency Inversion Principle)의 앞글자를 따서 명명되었습니다.
SOLID 원칙 개념 정리
SOLID 원칙 5가지 구성요소
SOLID 원칙은 소프트웨어 아키텍처를 설계할 때 코드의 품질을 높이고 유지보수성을 향상시키는 다섯 가지 원칙으로 구성됩니다.
각 원칙은 독립적으로 적용할 수 있지만, 모두 함께 적용했을 때 시너지 효과를 발휘합니다.
원칙 | 이름 | 핵심 내용 |
---|---|---|
S | Single Responsibility Principle | 한 클래스는 하나의 책임만 가져야 한다 |
O | Open/Closed Principle | 확장에는 열려있고 변경에는 닫혀있어야 한다 |
L | Liskov Substitution Principle | 하위 클래스는 상위 클래스를 대체할 수 있어야 한다 |
I | Interface Segregation Principle | 인터페이스는 클라이언트에 특화되어야 한다 |
D | Dependency Inversion Principle | 추상화에 의존해야 하며 구체화에 의존하면 안 된다 |
1. SRP (Single Responsibility Principle) - 단일 책임 원칙
SRP 정의와 중요성
단일 책임 원칙은 한 클래스가 하나의 책임만 가져야 한다는 원칙입니다.
클래스가 변경되는 이유는 단 하나여야 하며, 여러 책임을 가진 클래스는 코드 리팩토링 시 복잡성을 증가시킵니다.
SRP 다이어그램 - 책임 분리 구조
❌ 잘못된 설계 (SRP 위반)
┌──────────────────────────────────┐
│ User 클래스 │
├──────────────────────────────────┤
│ 📝 사용자 정보 관리 │
│ + setName(String) │
│ + getEmail() │
│ │
│ 📧 이메일 발송 (책임 위반!) │
│ + sendEmail(String) │
│ │
│ 💾 데이터베이스 저장 (책임 위반!) │
│ + saveToDatabase() │
└──────────────────────────────────┘
문제점: 하나의 클래스가 3가지 서로 다른 책임을 가짐
✅ 올바른 설계 (SRP 준수)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User 클래스 │ │ EmailService │ │ UserRepository │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ 📝 사용자 정보 │ │ 📧 이메일 발송 │ │ 💾 데이터 저장 │
│ + setName() │ │ + sendEmail() │ │ + save() │
│ + getEmail() │ │ + validate() │ │ + delete() │
└─────────────────┘ └─────────────────┘ └─────────────────┘
단일 책임 단일 책임 단일 책임
자바 SOLID 원칙 - SRP 잘못된 예제
// SRP 위반 예제
public class User {
private String name;
private String email;
// 사용자 정보 관리 책임
public void setName(String name) {
this.name = name;
}
// 이메일 발송 책임 (SRP 위반!)
public void sendEmail(String message) {
// 이메일 발송 로직
System.out.println("Sending email to " + email + ": " + message);
}
// 데이터베이스 저장 책임 (SRP 위반!)
public void saveToDatabase() {
// 데이터베이스 저장 로직
System.out.println("Saving user to database");
}
}
SRP 개선된 예제
// SRP 준수 예제
public class User {
private String name;
private String email;
// 사용자 정보 관리만 담당
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
}
// 이메일 발송 전용 클래스
public class EmailService {
public void sendEmail(String email, String message) {
System.out.println("Sending email to " + email + ": " + message);
}
}
// 데이터베이스 저장 전용 클래스
public class UserRepository {
public void save(User user) {
System.out.println("Saving user to database");
}
}
파이썬 SOLID 원칙 - SRP 예제
# SRP 준수 파이썬 예제
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def get_user_info(self):
return f"Name: {self.name}, Email: {self.email}"
class EmailService:
def send_email(self, email, message):
print(f"Sending email to {email}: {message}")
class UserRepository:
def save(self, user):
print(f"Saving user {user.name} to database")
2. OCP (Open/Closed Principle) - 개방/폐쇄 원칙
OCP 정의와 적용 방법
개방/폐쇄 원칙은 소프트웨어 개체는 확장에는 열려있되 변경에는 닫혀있어야 한다는 원칙입니다.
기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 합니다.
OCP 다이어그램 - 확장 가능한 구조
❌ 잘못된 설계 (OCP 위반)
┌────────────────────────────────────┐
│ DiscountCalculator │
├────────────────────────────────────┤
│ calculateDiscount(type, amount) { │
│ if (type == "REGULAR") │
│ return amount * 0.1 │
│ if (type == "PREMIUM") │
│ return amount * 0.2 │
│ if (type == "VIP") │
│ return amount * 0.3 │
│ } │
└────────────────────────────────────┘
문제점: 새로운 고객 유형 추가 시 기존 코드 수정 필요 ⚠️
✅ 올바른 설계 (OCP 준수)
┌─────────────────────┐
│ DiscountPolicy │ ← 🔸 추상 인터페이스
│ <<interface>> │
│ + calculateDiscount │
└─────────────────────┘
↑
┌─────────────────────┼─────────────────────┐
│ │ │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Regular 10% │ │ Premium 20% │ │ VIP 30% │
│ Discount │ │ Discount │ │ Discount │
└──────────────┘ └──────────────┘ └──────────────┘
새로운 유형 추가 시 → 기존 코드 수정 없이 확장 가능! ✨
┌──────────────┐
│ Corporate 25%│
│ Discount │
└──────────────┘
OCP 위반 예제
// OCP 위반 예제
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equals("REGULAR")) {
return amount * 0.1;
} else if (customerType.equals("PREMIUM")) {
return amount * 0.2;
} else if (customerType.equals("VIP")) {
return amount * 0.3;
}
return 0;
}
}
OCP 개선된 예제
// OCP 준수 예제
public interface DiscountPolicy {
double calculateDiscount(double amount);
}
public class RegularCustomerDiscount implements DiscountPolicy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.1;
}
}
public class PremiumCustomerDiscount implements DiscountPolicy {
@Override
public double calculateDiscount(double amount) {
return amount * 0.2;
}
}
public class DiscountCalculator {
private DiscountPolicy discountPolicy;
public DiscountCalculator(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public double calculateDiscount(double amount) {
return discountPolicy.calculateDiscount(amount);
}
}
3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
LSP 정의와 중요성
리스코프 치환 원칙은 상위 클래스의 객체를 하위 클래스의 객체로 치환해도 프로그램의 정확성이 깨지지 않아야 한다는 원칙입니다.
이는 객체지향 설계에서 상속 관계의 올바른 사용을 보장합니다.
LSP 다이어그램 - 치환 가능성 구조
❌ 잘못된 설계 (LSP 위반)
┌─────────────────────────────────┐
│ Rectangle │
├─────────────────────────────────┤
│ + setWidth(int width) │
│ + setHeight(int height) │
│ + getArea() │
└─────────────────────────────────┘
↑ 상속
┌─────────────────────────────────┐
│ Square │
├─────────────────────────────────┤
│ + setWidth(int width) { │
│ this.width = width │
│ this.height = width ⚠️ │
│ } │
│ + setHeight(int height) { │
│ this.width = height │
│ this.height = height ⚠️ │
│ } │
└─────────────────────────────────┘
문제점: Rectangle을 Square로 치환할 수 없음 (행위가 다름)
✅ 올바른 설계 (LSP 준수)
┌─────────────────────┐
│ Shape │ ← 🔸 추상 부모 클래스
│ <<abstract>> │
│ + getArea() │
└─────────────────────┘
↑
┌─────────────────┼─────────────────┐
│ │ │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Rectangle │ │ Square │ │ Circle │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ width, height│ │ side │ │ radius │
│ + getArea() │ │ + getArea() │ │ + getArea() │
└──────────────┘ └──────────────┘ └──────────────┘
각 클래스가 독립적으로 동작하며 상위 클래스와 치환 가능 ✅
LSP 위반 예제
// LSP 위반 예제
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // LSP 위반!
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // LSP 위반!
}
}
LSP 개선된 예제
// LSP 준수 예제
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square extends Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
ISP 정의와 실무 적용
인터페이스 분리 원칙은 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다.
큰 인터페이스를 작은 인터페이스로 분리하여 각 클라이언트가 필요한 메서드만 사용할 수 있도록 합니다.
ISP 다이어그램 - 인터페이스 분리 구조
❌ 잘못된 설계 (ISP 위반)
┌─────────────────────────────────┐
│ Worker Interface │
├─────────────────────────────────┤
│ + work() │
│ + eat() │
│ + sleep() │
└─────────────────────────────────┘
↑ 구현
┌───────┴───────┐
│ │
┌─────────┐ ┌─────────┐
│ Human │ │ Robot │
├─────────┤ ├─────────┤
│ work() │ │ work() │
│ eat() │ │ eat() │ ← ⚠️ 불필요
│ sleep() │ │ sleep() │ ← ⚠️ 불필요
└─────────┘ └─────────┘
문제점: Robot이 사용하지 않는 메서드까지 구현해야 함
✅ 올바른 설계 (ISP 준수)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Workable │ │ Eatable │ │ Sleepable │
│<<interface>>│ │<<interface>>│ │<<interface>>│
│ + work() │ │ + eat() │ │ + sleep() │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑ ↑
│ ┌─────┼─────┐ │
│ │ │ │
┌─────────────────────┐ │ ┌─────────┐
│ Human │───────┘ │ Robot │
├─────────────────────┤ ├─────────┤
│ 🔧 work() │ │ 🔧 work()│
│ 🍽️ eat() │ └─────────┘
│ 😴 sleep() │
└─────────────────────┘
각 클래스가 필요한 인터페이스만 구현 ✅
ISP 위반 예제
// ISP 위반 예제
public interface Worker {
void work();
void eat();
void sleep();
}
public class Human implements Worker {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void eat() {
// 로봇은 먹을 수 없음! ISP 위반
throw new UnsupportedOperationException();
}
@Override
public void sleep() {
// 로봇은 잠을 잘 수 없음! ISP 위반
throw new UnsupportedOperationException();
}
}
ISP 개선된 예제
// ISP 준수 예제
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
}
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot working");
}
}
5. DIP (Dependency Inversion Principle) - 의존성 역전 원칙
DIP 정의와 디자인 패턴 연관성
의존성 역전 원칙은 상위 모듈은 하위 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙입니다.
이는 의존성 주입(Dependency Injection) 패턴과 밀접한 관련이 있습니다.
DIP 다이어그램 - 의존성 역전 구조
❌ 잘못된 설계 (DIP 위반)
┌────────────────────────────────────┐
│ NotificationService │ ← 상위 모듈
├────────────────────────────────────┤
│ - EmailService emailService │
│ + sendNotification(String msg) │
└────────────────────────────────────┘
↓ 직접 의존 ⚠️
┌────────────────────────────────────┐
│ EmailService │ ← 하위 모듈 (구체 클래스)
├────────────────────────────────────┤
│ + sendEmail(String msg) │
└────────────────────────────────────┘
문제점: 상위 모듈이 하위 모듈에 직접 의존
✅ 올바른 설계 (DIP 준수)
┌────────────────────────────────────┐
│ NotificationService │ ← 상위 모듈
├────────────────────────────────────┤
│ - MessageService messageService │
│ + sendNotification(String msg) │
└────────────────────────────────────┘
↓ 추상화에 의존 ✅
┌────────────────────────────────────┐
│ MessageService │ ← 🔸 추상 인터페이스
│ <<interface>> │
├────────────────────────────────────┤
│ + sendMessage(String msg) │
└────────────────────────────────────┘
↑ 구현
┌────────┼────────┐
│ │
┌─────────────────┐ ┌─────────────────┐
│ EmailService │ │ SMSService │ ← 하위 모듈들
├─────────────────┤ ├─────────────────┤
│ 📧 이메일 발송 │ │ 📱 SMS 발송 │
│ + sendMessage() │ │ + sendMessage() │
└─────────────────┘ └─────────────────┘
모든 모듈이 추상화에 의존하여 유연한 구조 ✨
DIP 위반 예제
// DIP 위반 예제
public class EmailService {
public void sendEmail(String message) {
System.out.println("Sending email: " + message);
}
}
public class NotificationService {
private EmailService emailService; // 구체 클래스에 의존! DIP 위반
public NotificationService() {
this.emailService = new EmailService();
}
public void sendNotification(String message) {
emailService.sendEmail(message);
}
}
DIP 개선된 예제
// DIP 준수 예제
public interface MessageService {
void sendMessage(String message);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
public class SMSService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
public class NotificationService {
private MessageService messageService; // 추상화에 의존! DIP 준수
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void sendNotification(String message) {
messageService.sendMessage(message);
}
}
SOLID 원칙과 DRY 원칙의 관계
DRY 원칙이란?
DRY(Don't Repeat Yourself) 원칙은 "동일한 코드를 반복하지 말라"는 소프트웨어 개발 원칙입니다.
모든 지식은 시스템 내에서 단일하고 명확하며 권위 있는 표현을 가져야 한다는 것이 핵심입니다.
SOLID와 DRY 원칙 비교표
구분 | SOLID 원칙 | DRY 원칙 |
---|---|---|
목적 | 객체지향 설계의 구조적 품질 | 코드 중복 제거 |
적용 범위 | 클래스, 인터페이스, 모듈 설계 | 코드, 로직, 데이터 전반 |
주요 관점 | 책임 분리, 확장성, 의존성 관리 | 중복 제거, 유지보수성 |
적용 시점 | 설계 단계부터 적용 | 구현 및 리팩토링 단계 |
SOLID와 DRY의 공통점
- 유지보수성 향상: 두 원칙 모두 코드의 유지보수를 쉽게 만듭니다.
- 변경에 대한 유연성: 요구사항 변경 시 최소한의 코드 수정으로 대응 가능합니다.
- 코드 품질 개선: 읽기 쉽고 이해하기 쉬운 코드 작성을 지향합니다.
SOLID와 DRY의 차이점
SOLID 원칙은 구조적 설계에 중점을 두어 클래스와 모듈 간의 관계를 정의합니다.
DRY 원칙은 중복 제거에 중점을 두어 같은 로직이나 데이터의 반복을 방지합니다.
// DRY 원칙 적용 예제
public class UserValidator {
// DRY 위반: 이메일 검증 로직 중복
public boolean validateUserRegistration(String email) {
return email.contains("@") && email.contains(".");
}
public boolean validateUserLogin(String email) {
return email.contains("@") && email.contains("."); // 중복!
}
// DRY 준수: 공통 로직 추출
private boolean isValidEmail(String email) {
return email.contains("@") && email.contains(".");
}
public boolean validateUserRegistration(String email) {
return isValidEmail(email);
}
public boolean validateUserLogin(String email) {
return isValidEmail(email);
}
}
SOLID + DRY 조합 적용법
실무에서는 SOLID 원칙으로 구조를 설계하고, DRY 원칙으로 중복을 제거하는 것이 효과적입니다.
// SOLID + DRY 조합 예제
public interface ValidationService { // ISP 적용
boolean validateEmail(String email);
boolean validatePassword(String password);
}
public class EmailValidator implements ValidationService { // SRP + DRY 적용
private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@(.+)$";
@Override
public boolean validateEmail(String email) {
return isValidFormat(email, EMAIL_PATTERN); // DRY: 공통 검증 로직
}
private boolean isValidFormat(String input, String pattern) {
return input != null && input.matches(pattern);
}
}
SOLID 원칙 실무 적용 팁
코딩 인터뷰에서 SOLID 원칙 활용법
코딩 인터뷰에서 SOLID 원칙을 적용한 코드를 작성하면 높은 평가를 받을 수 있습니다.
특히 시스템 설계 면접에서는 이러한 원칙들을 바탕으로 한 확장 가능한 아키텍처를 제시하는 것이 중요합니다.
팀 프로젝트에서 SOLID 원칙 적용 방법
- 코드 리뷰 시 SOLID 원칙 체크리스트 활용
- 각 클래스가 단일 책임을 가지는지 확인
- 확장성을 고려한 설계인지 점검
- 인터페이스 분리가 적절한지 검토
- 리팩토링 시 우선순위 결정
- SRP 위반 클래스부터 분리 작업 시작
- 공통 기능은 추상화하여 DIP 적용
- 인터페이스 설계를 통한 ISP 준수
개발 원칙 적용 시 주의사항
SOLID 원칙을 과도하게 적용하면 오히려 코드의 복잡성이 증가할 수 있습니다.
프로젝트의 규모와 요구사항을 고려하여 적절한 수준에서 적용하는 것이 중요합니다.
결론
SOLID 원칙은 객체지향 프로그래밍에서 유지보수 가능하고 확장 가능한 코드를 작성하기 위한 핵심 가이드라인입니다.
이 다섯 가지 원칙을 통해 코드의 품질을 향상시키고, 변경에 유연하게 대응할 수 있는 소프트웨어 아키텍처를 구축할 수 있습니다.
특히 DRY 원칙과 함께 적용할 때 더욱 강력한 효과를 발휘하며, 구조적 설계와 중복 제거를 동시에 달성할 수 있습니다.
초보개발자부터 시니어 개발자까지 모든 개발자가 이 원칙들을 이해하고 실무에 적용한다면, 더 나은 코드를 작성할 수 있을 것입니다.
지속적인 학습과 실습을 통해 SOLID 원칙을 자연스럽게 적용할 수 있도록 노력하시기 바랍니다.
다음 글 예고: DRY 원칙 완벽 가이드
다음 글에서는 DRY(Don't Repeat Yourself) 원칙을 심도 있게 다룰 예정입니다.
SOLID 원칙과 DRY 원칙을 함께 적용하는 실전 전략, 중복 코드를 효과적으로 제거하는 리팩토링 기법, 그리고 과도한 DRY 적용 시 주의사항까지 포함한 완전한 가이드를 제공할 예정입니다.
코드 중복 제거를 통한 유지보수성 향상과 개발 생산성 증대에 관심이 있으시다면 기대해 주세요!
DRY 원칙이란? 중복 없는 코드를 위한 DRY(Don't Repeat Yourself) 원칙과 실전 예제
DRY 원칙이란? 중복 없는 코드를 위한 DRY(Don't Repeat Yourself) 원칙과 실전 예제
DRY 원칙은 소프트웨어 개발에서 코드 중복을 방지하고 유지보수성을 높이는 핵심 개발원칙으로,같은 로직을 반복하지 않고 재사용 가능한 형태로 구현하는 클린코드 작성 방법론입니다.DRY 원
notavoid.tistory.com
참고 자료
- Oracle Java Documentation
- Python Official Documentation
- Clean Code: A Handbook of Agile Software Craftsmanship
- Design Patterns: Elements of Reusable Object-Oriented Software
- The Pragmatic Programmer: Your Journey to Mastery
리팩토링이란? 코드 품질을 높이는 리팩토링 실전 가이드와 예제
리팩토링이란? 코드 품질을 높이는 리팩토링 실전 가이드와 예제
리팩토링의 정의와 중요성리팩토링은 소프트웨어 개발에서 코드의 외부 동작을 변경하지 않으면서 내부 구조를 개선하는 과정입니다.코드개선의 핵심은 가독성, 유지보수성, 확장성을 높이는
notavoid.tistory.com
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
리팩토링이란? 코드 품질을 높이는 리팩토링 실전 가이드와 예제 (0) | 2025.07.16 |
---|---|
DRY 원칙이란? 중복 없는 코드를 위한 DRY(Don't Repeat Yourself) 원칙과 실전 예제 (0) | 2025.07.16 |
[Rust입문 #6] Rust 모듈, 패키지, 크레이트, 트레잇, 제네릭, 표준 라이브러리 한방에 끝내기 (0) | 2025.07.14 |
[Rust입문 #5] Rust 구조체, 열거형, 컬렉션 완전 정리 (0) | 2025.07.11 |
[Rust입문 #4] Rust 소유권과 참조, 에러 처리 완벽 이해하기 (0) | 2025.07.08 |