객체지향 프로그래밍(Object-Oriented Programming, OOP)은 유지보수성과 확장성이 뛰어난 소프트웨어를 개발하는 데 중요한 패러다임입니다. 이를 효과적으로 활용하기 위해서는 객체지향 설계 원칙(SOLID) 을 이해하고 적용하는 것이 필수적입니다.
이번 글에서는 객체지향 설계 원칙의 핵심 개념과 실제 프로젝트에서 이를 적용하는 방법을 알아보겠습니다.
1. 객체지향 설계 원칙이란?
객체지향 설계 원칙은 소프트웨어를 설계할 때 결합도(Coupling)를 낮추고 응집도(Cohesion)를 높여 유지보수성과 확장성을 개선하는 것을 목표로 합니다. 대표적인 원칙으로 SOLID 원칙이 있으며, 이는 다섯 가지의 핵심 원칙으로 구성됩니다.
SOLID 원칙
SOLID는 다섯 가지 객체지향 설계 원칙의 약어입니다.
- S : 단일 책임 원칙 (Single Responsibility Principle, SRP)
- O : 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- L : 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- I : 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- D : 의존 역전 원칙 (Dependency Inversion Principle, DIP)
이제 각 원칙을 자세히 살펴보겠습니다.
2. SOLID 원칙 상세 설명
1) 단일 책임 원칙 (SRP: Single Responsibility Principle)
"클래스는 단 하나의 책임만 가져야 한다."
하나의 클래스가 여러 가지 역할을 수행하면 유지보수성이 떨어지고, 변경 시 영향을 받는 범위가 커집니다. 따라서, 클래스는 한 가지 책임(변경 이유)을 가져야 합니다.
✅ 적용 방법
- 하나의 클래스는 하나의 역할만 수행하도록 분리합니다.
- 기능이 많아지는 경우 별도의 클래스로 분리하여 관심사를 분리합니다.
❌ 잘못된 코드 (SRP 위반)
class Report {
public String generateReport() {
return "Report Data";
}
public void saveToFile(String filename) {
System.out.println("Saving report to " + filename);
}
}
➡️ Report 클래스가 리포트 생성과 파일 저장 두 가지 책임을 가지고 있습니다.
✅ 개선된 코드 (SRP 적용)
class Report {
public String generateReport() {
return "Report Data";
}
}
class FileSaver {
public void saveToFile(String data, String filename) {
System.out.println("Saving report to " + filename);
}
}
➡️ 리포트 생성과 파일 저장을 각각의 클래스로 분리하여 단일 책임 원칙(SRP) 을 준수했습니다.
2) 개방-폐쇄 원칙 (OCP: Open/Closed Principle)
"코드는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다."
❌ 잘못된 코드 (OCP 위반)
class Payment {
public void pay(String method, double amount) {
if ("credit".equals(method)) {
System.out.println("Paid with Credit Card");
} else if ("paypal".equals(method)) {
System.out.println("Paid with PayPal");
}
}
}
➡️ 새로운 결제 방법이 추가될 때마다 pay() 메서드를 수정해야 합니다.
✅ 개선된 코드 (OCP 적용)
interface PaymentMethod {
void pay(double amount);
}
class CreditCard implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paid with Credit Card");
}
}
class PayPal implements PaymentMethod {
public void pay(double amount) {
System.out.println("Paid with PayPal");
}
}
class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
paymentMethod.pay(amount);
}
}
➡️ 인터페이스를 활용하여 결제 방식을 확장 가능하게 만들었습니다. 새로운 결제 수단이 추가되어도 기존 코드를 변경할 필요가 없습니다.
3) 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)
"자식 클래스는 부모 클래스를 대체할 수 있어야 한다."
❌ 잘못된 코드 (LSP 위반)
class Bird {
public void fly() {
System.out.println("Flying...");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
➡️ 펭귄은 날 수 없기 때문에 fly() 메서드를 호출하면 예외가 발생합니다. 이는 LSP를 위반한 것입니다.
✅ 개선된 코드 (LSP 적용)
interface Bird {
void move();
}
class Sparrow implements Bird {
public void move() {
System.out.println("Flying...");
}
}
class Penguin implements Bird {
public void move() {
System.out.println("Swimming...");
}
}
➡️ Bird 인터페이스를 활용하여 모든 새가 이동할 수 있도록 일반화했습니다.
4) 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)
"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."
❌ 잘못된 코드 (ISP 위반)
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot working...");
}
@Override
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}
➡️ Robot은 eat() 메서드가 필요 없지만, 인터페이스를 구현해야 합니다.
✅ 개선된 코드 (ISP 적용)
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class HumanWorker implements Workable, Eatable {
public void work() {
System.out.println("Human working...");
}
public void eat() {
System.out.println("Human eating...");
}
}
class RobotWorker implements Workable {
public void work() {
System.out.println("Robot working...");
}
}
➡️ 필요한 기능만 인터페이스로 분리하여 불필요한 의존성을 제거했습니다.
5) 의존 역전 원칙 (DIP: Dependency Inversion Principle)
"상위 모듈은 하위 모듈에 의존하면 안 되며, 둘 다 추상화에 의존해야 한다."
❌ 잘못된 코드 (DIP 위반)
class EmailSender {
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
class Notification {
private EmailSender emailSender = new EmailSender();
public void notifyUser(String message) {
emailSender.send(message);
}
}
➡️ Notification 클래스가 EmailSender에 직접 의존하고 있어 변경이 어렵습니다.
✅ 개선된 코드 (DIP 적용)
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
class Notification {
private final MessageSender messageSender;
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void notifyUser(String message) {
messageSender.send(message);
}
}
➡️ Notification이 구체적인 구현이 아닌 인터페이스(MessageSender)에 의존하도록 변경하여 유연성을 높였습니다.
결론
SOLID 원칙을 준수하면 유지보수성이 높고 확장성이 뛰어난 객체지향 설계가 가능합니다.
원칙 | 핵심 개념 |
SRP | 하나의 클래스는 하나의 책임만 가져야 한다. |
OCP | 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다. |
LSP | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. |
ISP | 클라이언트가 필요하지 않은 메서드에 의존하지 않아야 한다. |
DIP | 구체적인 구현이 아니라 추상화에 의존해야 한다. |
이 원칙들을 적용하면 클린 코드와 유지보수성이 뛰어난 소프트웨어를 만들 수 있습니다! 🚀
'백엔드' 카테고리의 다른 글
메모리 누수: Memory Leak (1) | 2025.02.23 |
---|---|
Java의 Error와 Exception 차이점 (1) | 2025.02.15 |
Java 메모리 영역과 동작 원리 (1) | 2025.02.14 |
[자바/JAVA] 객체는 뭐로 만들어? Class, Interface, Record? (1) | 2025.02.12 |
[자바/JAVA] DAO, DTO, VO, Entity의 차이점과 역할 (1) | 2025.02.11 |