SOLID 원칙에 대한 확실한 설명
I’m happy to translate the text for you, but I don’t see the content you’d like translated—only the source line. Could you please provide the full text you’d like me to translate into Korean? Once I have it, I’ll keep the source link at the top unchanged and translate the rest while preserving all formatting.
소개
소프트웨어가 **“알고리즘이 잘못되었다”**는 이유로 실패하는 경우는 드뭅니다.
대부분은 서서히 퇴화합니다: 새로운 기능을 추가하기 어려워지고, 버그가 다시 나타나며, 아주 작은 변경조차 위험하게 느껴집니다.
SOLID 원칙은 코드를 더 오래 이해하기 쉽고 적응 가능하게 유지할 수 있는 다섯 가지 실용적인 방법을 제공합니다.
SOLID는 세 가지 물질 상태 중 하나가 아닙니다.
이는 유지보수가 매우 쉬운 코드를 작성하도록 돕는 다섯 가지 설계 원칙의 약어입니다.
원칙은 다음과 같습니다:
- 단일 책임 원칙 (Single Responsibility Principle, SRP)
- 개방‑폐쇄 원칙 (Open‑Closed Principle, OCP)
- 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
이 원칙들을 함께 적용하면 느슨한 결합, 명확한 책임, 그리고 안정적인 추상화를 촉진하여 장기간에 걸쳐 살아남을 수 있는 코드를 만들 수 있습니다.
1. 단일 책임 원칙 (SRP)
클래스는 변경 이유가 하나만 있어야 합니다.
나쁜 예
class UserService {
public void validate(User user) { /* … */ }
public void saveToDatabase(User user) { /* … */ }
public void sendEmail(User user) { /* … */ }
public void log(User user) { /* … */ }
}
문제점
- 네 가지 변경 이유 (
validate,saveToDatabase,sendEmail,log). - 재사용이 어려움 – 호출자는 검증만 필요할 때 이메일 전송이나 DB 쓰기를 원하지 않음.
- 테스트가 악몽 – 로거, 이메일 클라이언트, DB 클라이언트를 모두 모킹해야 함.
더 나은 설계
class UserValidator {
public void validate(User user) { /* … */ }
}
class UserRepository {
public void save(User user) { /* … */ }
}
class EmailNotifier {
public void send(User user) { /* … */ }
}
class Logger {
public void log(User user) { /* … */ }
}
각 클래스는 이제 변경 이유가 하나이며 독립적으로 사용할 수 있습니다 (예: 검증만 수행하고 이메일은 보내지 않음).
좌우명: Ek class – ek kaam (한 클래스 – 한 작업).
2. Open‑Closed Principle (OCP)
소프트웨어 엔티티는 확장에 열려 있어야 하지만 수정에는 닫혀 있어야 합니다.
Violating OCP
class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
}
// Adding a Square would require modifying this method → OCP violation
}
}
OCP‑compliant design
abstract class Shape {
abstract double area();
}
class Rectangle extends Shape {
double width, height;
@Override double area() { return width * height; }
}
class Circle extends Shape {
double radius;
@Override double area() { return Math.PI * radius * radius; }
}
// New shapes (e.g., Square) just extend Shape – no change to AreaCalculator
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
이제 Square(또는 새로운 도형)를 추가해도 AreaCalculator를 수정할 필요가 없습니다.
Mantra: Extend karo, edit nahi (확장하라, 수정하지 마라).
3. 리스코프 치환 원칙 (LSP)
상위 클래스의 객체는 하위 클래스의 객체로 프로그램의 바람직한 속성을 변경하지 않고 교체할 수 있어야 합니다.
문제 예시
interface Bird {
void fly();
void walk();
}
class Sparrow implements Bird {
public void fly() { /* flying logic */ }
public void walk() { /* walking logic */ }
}
class Ostrich implements Bird {
public void fly() { throw new UnsupportedOperationException(); }
public void walk() { /* walking logic */ }
}
Ostrich는 fly() 계약을 위반합니다 – 어떤 Bird든 날 수 있다고 기대하는 코드는 실패하게 됩니다.
더 나은 추상화
interface Walkable {
void walk();
}
interface Flyable {
void fly();
}
abstract class Bird implements Walkable { /* common walk logic */ }
class Sparrow extends Bird implements Flyable {
public void fly() { /* flying logic */ }
}
class Ostrich extends Bird {
// No fly() method – respects LSP
}
이제 makeWalk(Bird b)는 Sparrow와 Ostrich 모두에서 예상치 못한 동작 없이 정상적으로 동작합니다.
Memory aid (Hindi): Agar bachche ka behavior parents ke jaise hai, to wo party mein ja sakta hai (하위 클래스가 상위 클래스와 같은 행동을 보이면, 서로 교체해서 사용할 수 있다).
4. 인터페이스 분리 원칙 (ISP)
클라이언트는 사용하지 않는 인터페이스에 의존하도록 강요받아서는 안 된다.
ISP 위반
interface IWorker {
void work();
void eat(); // 로봇은 먹지 않는다!
}
Robot 클래스는 의미 없는 eat() 메서드를 구현해야 하므로, 더미 코드와 강한 결합도가 생깁니다.
ISP 준수 설계
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() { /* … */ }
}
이제 각 클래스는 실제로 필요한 인터페이스만 구현합니다.
좌우명: 분할 정복 – 큰 인터페이스를 집중된 작은 인터페이스로 나눕니다.
5. 의존성 역전 원칙 (DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 두 모듈 모두 추상화에 의존해야 합니다.
결합도가 높은 예시
class MasalaTeaMaker {
public String makeTea() { return "Masala Tea ready!"; }
}
class TeaShop {
private MasalaTeaMaker maker = new MasalaTeaMaker(); // 하드코딩된 의존성!
public String orderTea(String type) {
if ("masala".equals(type)) {
return maker.makeTea();
}
return "Only Masala available"; // 경직됨!
}
}
TeaShop은 MasalaTeaMaker에 직접 묶여 있습니다. 새로운 차 종류를 추가하려면 TeaShop을 수정해야 합니다.
DIP‑준수 설계
interface TeaMaker {
String makeTea();
}
class MasalaTeaMaker implements TeaMaker {
public String makeTea() { return "Masala Tea ready!"; }
}
class GreenTeaMaker implements TeaMaker {
public String makeTea() { return "Green Tea ready!"; }
}
class TeaShop {
private final Map<String, TeaMaker> makers = new HashMap<>();
public TeaShop(Map<String, TeaMaker> makers) {
this.makers.putAll(makers);
}
public String orderTea(String type) {
TeaMaker maker = makers.get(type);
return (maker != null) ? maker.makeTea() : "Tea not available";
}
}
TeaShop은 추상화인TeaMaker에 의존합니다.- 새로운 차 메이커를 추가하더라도
TeaShop을 수정할 필요가 없으며, 맵에 등록하기만 하면 됩니다.
요약
| 원칙 | 핵심 아이디어 | 전형적인 만트라 |
|---|---|---|
| SRP | 클래스당 하나의 변경 이유 | 한 클래스 – 한 일 |
| OCP | 기존 코드를 수정하지 않고 행동을 확장 | 확장하라, 수정은 하지 마라 |
| LSP | 하위 타입은 기본 타입을 대체할 수 있어야 함 | 자식의 행동은 부모와 같아야 함 |
| ISP | 비대한 인터페이스를 클라이언트‑특화 인터페이스로 분리 | 분할 정복 |
| DIP | 구체가 아닌 추상에 의존 | 고수준 ↔ 저수준을 인터페이스로 연결 |
이 다섯 가지 원칙을 적용하면 느슨하게 결합되고, 높은 응집력을 가지며, 쉽게 유지보수 가능한 소프트웨어를 구축할 수 있어 시간이 지나도 우아하게 진화할 수 있습니다.
Source: …
추상화를 활용한 TeaShop 예제 업데이트
아래는 원본 스니펫을 정리한 버전입니다.
코드는 이제 의존성 역전 원칙(SOLID의 “D”)을 따르며, 구체 클래스 대신 추상화(TeaMaker)에 의존합니다.
1️⃣ 도메인 모델 (Java)
// 1. Abstraction -------------------------------------------------
public interface TeaMaker {
String makeTea();
}
// 2. Concrete implementations --------------------------------------
public class MasalaTeaMaker implements TeaMaker {
@Override
public String makeTea() {
return "Masala Tea ready!";
}
}
public class GingerTeaMaker implements TeaMaker {
@Override
public String makeTea() {
return "Ginger Tea ready!";
}
}
// 3. High‑level module (depends on the abstraction) -------------
public class TeaShop {
// Constructor injection (setter injection is also possible)
private final TeaMaker teaMaker;
public TeaShop(TeaMaker teaMaker) {
this.teaMaker = teaMaker;
}
public String orderTea(String type) {
// The shop no longer knows about concrete makers.
// It simply asks the injected TeaMaker to prepare the drink.
return teaMaker.makeTea();
}
}
2️⃣ 작동 방식
| 구성 요소 | 역할 | 의존성 |
|---|---|---|
TeaMaker | 모든 차 제조 클래스가 따라야 할 계약을 정의합니다. | – |
MasalaTeaMaker / GingerTeaMaker | TeaMaker 계약을 구현하는 구체 클래스입니다. | TeaMaker 구현 |
TeaShop | 차를 주문하는 고수준 클래스입니다. | 오직 TeaMaker(추상화)만 의존 |
TeaShop은 이제 MasalaTeaMaker나 GingerTeaMaker를 직접 생성하거나 알지 못합니다. 대신 실행 시점에 특정 TeaMaker가 주입됩니다.
3️⃣ 의존성 주입 옵션
| 주입 방식 | 예시 |
|---|---|
| 생성자 주입 (위 예시) | new TeaShop(new MasalaTeaMaker()) |
| 세터 주입 | java\npublic void setTeaMaker(TeaMaker teaMaker) { this.teaMaker = teaMaker; }\n |
| 필드 주입 (프레임워크 전용) | @Inject private TeaMaker teaMaker; |
프로젝트 아키텍처에 가장 적합한 방식을 선택하세요.
4️⃣ 빠른 기억법
“추상화에 의존하고, 클래스에 의존하지 마라.”
인터페이스에 의존하고 구체 클래스에 의존하지 않는다는 의미입니다. 이 원칙을 기억하면 SOLID의 의존성 역전 원칙을 잘 지킬 수 있습니다.
5️⃣ 마무리
위와 같이 리팩터링하면 SOLID 원칙, 특히 D(Dependency Inversion)를 만족하게 됩니다.
궁금한 점이 있으면 아래에 댓글을 남겨 주세요.
읽어 주셔서 감사합니다 – 앞으로도 기대해 주세요!