객체지향 설계 시 유지보수성과 확장성을 높이기 위해 지켜야 할 5가지 원칙이다.
각 원칙은 클래스, 인터페이스, 모듈 설계 시 발생할 수 있는 문제를 방지해준다.
| 원칙 | 이름 | 설명 |
|---|---|---|
| S | SRP (단일 책임 원칙) | 하나의 클래스는 하나의 책임만 가져야 한다. |
| O | OCP (개방-폐쇄 원칙) | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다. |
| L | LSP (리스코프 치환 원칙) | 하위 클래스는 상위 클래스를 완벽히 대체할 수 있어야 한다. |
| I | ISP (인터페이스 분리 원칙) | 클라이언트는 사용하지 않는 메서드에 의존하면 안 된다. |
| D | DIP (의존성 역전 원칙) | 추상화에 의존해야 하며, 구현체에 의존하지 않아야 한다. |
SRP 에서 책임이란, 기능정도로 생각하면 된다. 만약 한 클래스가 수행할 수 있는 기능 (책임) 이 여러 개라면, 클래스 내부의 함수끼리 강한 결합을 발생할 가능성이 높아져 유지보수가 복잡해질 것이다.
class UserManager {
void createUser(User user) {
// 사용자 생성 로직
}
void sendEmail(User user, String message) {
// 이메일 전송 로직
}
}
class UserManager {
void createUser(User user) {
// 사용자 생성 로직
}
}
class EmailService {
void sendEmail(User user, String message) {
// 이메일 전송 로직
}
}
어떤 모듈의 기능을 하나 수정할 때, 그 모듈을 이용하는 다른 모듈들 역시 줄줄이 고쳐야 한다면 유지보수가 복잡할 것이다.
class PaymentProcessor {
void pay(String method) {
if (method.equals("card")) {
// 카드 결제
} else if (method.equals("cash")) {
// 현금 결제
}
}
}
interface PaymentMethod {
void pay();
}
class CardPayment implements PaymentMethod {
@Overriding
public void pay() {
// 카드 결제 처리
}
}
class CashPayment implements PaymentMethod {
@Overriding
public void pay() {
// 현금 결제 처리
}
}
class PaymentProcessor {
void pay(PaymentMethod method) {
method.pay();
}
}
상속관계에서는 꼭 일반화 관계 (IS-A) 가 성립해야 한다. 이를 위배하게 된다면 기능 확장을 위해 기존의 코드를 여러 번 수정해야 할 것이다.
class Rectangle {
protected int width, height;
void setWidth(int width) { this.width = width; }
void setHeight(int height) { this.height = height; }
int area() { return width * height; }
}
class Square extends Rectangle {
void setWidth(int side) {
width = height = side;
}
void setHeight(int side) {
width = height = side;
}
}
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.area()); // 기대값: 50, 실제값: 100
기대값과 실제값이 다름
abstract class Shape {
abstract int area();
}
class Rectangle extends Shape {
private int width, height;
void setWidth(int width) { this.width = width; }
void setHeight(int height) { this.height = height; }
int area() { return width * height; }
}
class Square extends Shape {
private int side;
void setSide(int side) { this.side = side; }
int area() { return side * side; }
}
하나의 통상적인 인터페이스보다는 차라리 여러 개의 세부적인 (구체적인) 인터페이스가 낫다.
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() { /* 로봇 일 처리 */ }
public void eat() { /* 로봇은 먹지 않음 */ }
}
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Robot implements Workable {
public void work() { /* 로봇 일 처리 */ }
}
class Human implements Workable, Eatable {
public void work() { /* 인간 일 처리 */ }
public void eat() { /* 식사 처리 */ }
}
저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이기 때문이다.
class MySQLDatabase {
void saveData(String data) {
// MySQL 저장
}
}
class DataService {
private MySQLDatabase db = new MySQLDatabase();
void save(String data) {
db.saveData(data);
}
}
interface Database {
void saveData(String data);
}
// 구체 구현 A
class MySQLDatabase implements Database {
public void saveData(String data) {
System.out.println("MySQL에 저장: " + data);
}
}
// 구체 구현 B
class InMemoryDatabase implements Database {
public void saveData(String data) {
System.out.println("메모리에 저장: " + data);
}
}
// 상위 모듈: 추상화에만 의존
class DataService {
private Database db;
public DataService(Database db) {
this.db = db; // 추상화에 의존 → 유연성 확보
}
void save(String data) {
db.saveData(data);
}
}
| 항목 | 설명 |
|---|---|
| 유연성 | MySQL ↔ InMemory 자유롭게 교체 가능 |
| 테스트 용이 | 테스트 시 FakeDatabase 주입 가능 |
| OCP와 궁합 | 구현체 추가 시 DataService 수정 불필요 |
| 확장성 | 다양한 저장 방식에 쉽게 대응 가능 |
| 의존성 방향 역전 | 고수준 모듈이 하위 구현에 의존하지 않음 |
SOLID 원칙은 단순한 코딩 스타일이 아니라 좋은 소프트웨어 아키텍처의 기반이 된다.
처음엔 부담스러울 수 있지만, 점차 익숙해지면 유지보수하기 쉽고 확장 가능한 코드를 작성할 수 있을 것이다.