코딩 이래요래
SOLID 원칙 본문
객체지향 설계의 SOLID 원칙
객체지향 설계에서 유지보수성과 확장성을 높이기 위해 지켜야 하는 5가지 원칙
🔸 1. SRP (단일 책임의 원칙, Single Responsibility Principle)
- 한 클래스는 오직 하나의 책임만 가져야 한다.
- 클래스가 하나 이상의 책임을 가지면 변경에 취약해지고 유지보수가 어렵다.
❌ 잘못된 예시 (책임 혼합):
class Product {
String name;
int price;
void payment(); // 결제 기능
void printReceipt(); // 영수증 출력 기능
}
✅ SRP를 적용한 예시 (책임 분리):
class Product {
String name;
int price;
}
class PaymentService {
void payment(Product product); // 결제 책임만 수행
}
class ReceiptPrinter {
void printReceipt(Product product); // 영수증 출력 책임만 수행
}
- 클래스별 하나의 책임만 존재하므로 변경사항이 있을 때 유연한 대처가 가능하다.
- 유지보수 및 확장성이 높아진다.
* SRP를 적용하면 유지보수 및 확장성을 높이고, 기본적인 객체지향(OOP) 설계 원칙 중 하나임
🔸 2. OCP (개방-폐쇄 원칙, Open-Close Principle)
- 소프트웨어 요소는 확장에는 열려 있고(Open), 수정에는 닫혀(Close) 있어야 한다.
- 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있도록 설계한다.
- 핵심 메커니즘은 추상화와 다형성을 사용하는 것이다.
❌ 잘못된 예시 (확장 시 기존 코드 변경):
class Payment {
void pay(String type) {
if (type.equals("cash")) {
// 현금 결제
} else if (type.equals("card")) {
// 카드 결제
}
}
}
✅ OCP를 적용한 예시 (확장 가능 구조):
interface PaymentService {
void pay();
}
class CashPayment implements PaymentService {
@Override
public void pay() {
// 현금 결제
}
}
class CardPayment implements PaymentService {
@Override
public void pay() {
// 카드 결제
}
}
class PaymentProcessor {
private PaymentService paymentService;
public PaymentProcessor(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void process() {
paymentService.pay(); // 구현체에 따라 동작 다름
}
}
- 새로운 결제 방법 추가 시 PaymentService 인터페이스 구현 클래스만 추가하면됨
- 기존 코드는 변경하지 않아도됨
- 기존 PaymentsService는 수정하지 않고(close), 새로운 클래스를 만들어 확장(open)
* OCP를 적용하면 새로운 요구사항에도 기존 클래스를 매번 수정하는 위험과 유지보수 비용을 낮출 수 있음
🔸 3. LSP (리스코프 치환 원칙, Liskov Substitution Principle)
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야함
- 상위 클래스 위치에 하위 클래스로 바꿔도 역할을 하는데 문제가 없어야 한다는 의미
- 하위 클래스는 상위 클래스의 행동을 변경하거나 제한해선 안 됨
- 하위 클래스가 상위 클래스의 메소드를 오버라이딩 하더라도 기존의 상위 클래스의 메소드의 정의를 위반해서는 안됨
⚠️ 주의할 두 가지 조건:
- 사전조건 강화 금지: 자식 클래스에서 부모의 조건보다 더 엄격한 조건을 추가하면 안 됨
// 잘못된 예시
class TopClass {
void up(int count) {
// 처리
}
}
class BottomClass extends TopClass {
@Override
void up(int count) {
if (count > 50) return; // 조건 강화(잘못된 사례)
}
}
- 사후조건 약화 금지: 자식 클래스에서 부모보다 약한 결과값(예: null)을 반환하면 안 됨
// 잘못된 예시
class TopClass {
String print() {
return "10";
}
}
class BottomClass extends TopClass {
@Override
String print() {
return null; // 조건 약화(잘못된 사례)
}
}
✅ 올바른 상속 관계의 예시 (LSP 준수):
interface Animal {
void makeSound();
}
class Dog implements Animal {
public void makeSound() {
System.out.println("멍멍!");
}
}
class Cat implements Animal {
public void makeSound() {
System.out.println("야옹!");
}
}
// 모든 하위 클래스는 Animal 인터페이스의 규약을 지킨다.
💡 핵심:
- 상속은 반드시 is-a 관계일 때만 사용하자
- 그 외의 경우에는 앞에서 학습한 컴포지션(Compositon)을 이용해 기존의 메소드는 그대로 이용하면서 결합도를 낮추자
🔸 4. ISP (인터페이스 분리 원칙, Interface Segregation Principle)
- 하나의 큰 인터페이스보다는, 목적과 역할에 따라 인터페이스를 잘게 나누는 원칙
- 범용적인 인터페이스를 구현하고 설계하면 이 인터페이스를 구현하는 클래스는 사용하지 않는 기능들에 의존하게 되어 변경이 어려워 짐
- 클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않아야함
❌ 잘못된 예시 (너무 많은 기능):
interface Device {
void print();
void scan();
void fax();
}
✅ ISP를 적용한 예시 (역할별 인터페이스 분리):
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface Fax {
void fax();
}
class MultiFunctionPrinter implements Printer, Scanner, Fax {
public void print() { /* 출력 구현 */ }
public void scan() { /* 스캔 구현 */ }
public void fax() { /* 팩스 구현 */ }
}
class SimplePrinter implements Printer {
public void print() { /* 출력만 구현 */ }
}
- 필요한 기능만 선택적으로 구현 가능하여 불필요한 의존성 제거
- SRP와 연결되어 유지보수성도 향상됨
🔸 5. DIP (의존관계 역전 원칙, Dependency Inversion Principle)
- 고수준 모듈은 저수준 모듈에 직접 의존하지 않고, 추상화(인터페이스 또는 추상 클래스)에 의존해야 함
- 구현 클래스가 아닌 인터페이스나 추상 클래스에 의존하여 결합도를 낮춘다
❌ DIP 위반 예시 (강한 결합):
class EmailService {
public void sendEmail(String message) {
System.out.println("Email sent: " + message);
}
}
class Notification {
private EmailService emailService;
public Notification() {
emailService = new EmailService(); // 직접 의존
}
public void alert(String message) {
emailService.sendEmail(message);
}
}
✅ DIP를 적용한 예시 (인터페이스에 의존):
interface MessageService {
void sendMessage(String message);
}
class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Email sent: " + message);
}
}
class SMSService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("SMS sent: " + message);
}
}
class Notification {
private MessageService messageService;
public Notification(MessageService messageService) {
this.messageService = messageService; // 인터페이스에만 의존
}
public void alert(String message) {
messageService.sendMessage(message); // 구현체와는 결합하지 않음
}
}
- MessageService를 interface class로 변경
- Notification 클래스는 MessageService 인터페이스에 의존하므로 DIP 원칙 준수
- EmailService, SMSService 등 구현체를 쉽게 교체할 수 있음
- 확장성과 재사용성이 증가하고, 코드 수정 없이 새로운 구현체 추가 가능
💡 핵심:
- 구현체 변경 시 코드 수정 없이 자유롭게 추가 / 확장이 가능
- DIP는 결합도를 낮춰 유연한 설계를 가능하게 함
해당 게시글은 학습한 내용을 정리하고 복습하며 정리한 글이므로 틀린 부분이 있을 수 있음.
'JAVA' 카테고리의 다른 글
자료구조 - List (0) | 2025.04.13 |
---|---|
자료구조 (0) | 2025.04.13 |
객체지향 프로그래밍(OOP)의 4가지 (0) | 2025.04.01 |
JVM의 메모리 구조 (0) | 2025.03.31 |
Java의 클래스와 객체 (0) | 2025.03.31 |