오늘은 객체지향 설계에서 빠질 수 없는 핵심 개념인 SOLID 원칙에 대해 살펴보려고 합니다. SOLID 원칙은 로버트 C. 마틴이 제안한 5가지 객체지향 설계 원칙으로, 유지보수성과 확장성을 높이기 위한 가이드라인입니다.
SOLID란 무엇인가?
SOLID는 다음 다섯 가지 원칙의 머리글자를 따 만든 약어입니다.
약어 | 원칙명 | 설명 |
S | Single Responsibility Principle (단일 책임 원칙) | 클래스는 단 하나의 책임만 가져야 한다. |
O | Open/Closed Principle (개방-폐쇄 원칙) | 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다. |
L | Liskov Substitution Principle (리스코프 치환 원칙) | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. |
I | Interface Segregation Principle (인터페이스 분리 원칙) | 자신이 사용하지 않는 메서드에 의존하지 않아야 한다. |
D | Dependency Inversion Principle (의존성 역전 원칙) | 구체화가 아닌 추상화에 의존해야 한다. |
각 원칙 상세 설명
S – 단일 책임 원칙 (Single Responsibility Principle)
하나의 클래스는 오직 하나의 책임만 가져야 합니다.
여기서 ‘책임’이란 하나의 역할만 가지고 있어야 하며, 변경의 이유도 한가지 이유로만 변경해야 한다는 뜻입니다.
안좋은 설계
class UserService {
public void createUser(String name) {
// 사용자 생성 로직
}
public void sendWelcomeEmail(String email) {
// 이메일 발송 로직
}
}
위 예시는 사용자 생성과 이메일 발송이라는 두 가지 책임을 한 클래스가 가지고 있습니다.
개선된 설계
class UserService {
public void createUser(String name) {
// 사용자 생성 로직
}
}
class EmailService {
public void sendWelcomeEmail(String email) {
// 이메일 발송 로직
}
}
이렇게 책임을 분리하면 유지보수가 쉬워지고, 한 기능의 변경이 다른 기능에 영향을 미치지 않습니다.
O – 개방-폐쇄 원칙 (Open/Closed Principle)
소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려(Open) 있어야 하고, 수정에는 닫혀(Closed) 있어야 합니다.
안좋은 설계
class DiscountService {
public double getDiscount(String type) {
if (type.equals("VIP")) {
return 0.2;
} else if (type.equals("Normal")) {
return 0.1;
}
return 0.0;
}
}
위의 코드에서는 새로운 할인 정책이 생길 때마다 if-else를 수정해야 합니다.
개선된 설계
interface DiscountPolicy {
double getDiscount();
}
class VipDiscount implements DiscountPolicy {
public double getDiscount() {
return 0.2;
}
}
class NormalDiscount implements DiscountPolicy {
public double getDiscount() {
return 0.1;
}
}
class DiscountService {
public double getDiscount(DiscountPolicy policy) {
return policy.getDiscount();
}
}
위의 코드에서는 새로운 할인 정책 클래스를 추가하기만 하면 됩니다. 기존 코드는 수정하지 않아도 됩니다.
L – 리스코프 치환 원칙 (Liskov Substitution Principle)
서브타입(자식 클래스)은 언제나 기반타입(부모 클래스)으로 교체할 수 있어야 합니다. 즉, 부모 클래스의 객체가 사용되는 곳에 자식 클래스의 객체를 넣어도 프로그램의 기능이 깨지지 않아야 합니다.
안좋은 설계
class Bird {
public void fly() {
System.out.println("날다");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없음");
}
}
위의 예시에서는 Penguin은 Bird로 대체할 수 없으므로 LSP 위반입니다.
개선된 설계
interface Bird { }
interface Flyable {
void fly();
}
class Sparrow implements Bird, Flyable {
public void fly() {
System.out.println("참새 날다");
}
}
class Penguin implements Bird {
public void swim() {
System.out.println("펭귄 수영");
}
}
위의 예시에서는 날 수 있는 새와 날 수 없는 새를 구분하여 LSP를 지킵니다.
I – 인터페이스 분리 원칙 (Interface Segregation Principle)
하나의 일반적인 인터페이스보다는 여러 개의 구체적인 인터페이스가 낫습니다. 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
안좋은 설계
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 Human implements Workable, Eatable {
public void work() {}
public void eat() {}
}
class Robot implements Workable {
public void work() {}
}
이렇게 필요한 기능만 구현하도록 인터페이스를 분리합니다.
D – 의존성 역전 원칙 (Dependency Inversion Principle)
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다. 구체적인 구현 클래스가 아니라 인터페이스나 추상 클래스에 의존하라는 뜻입니다.
안좋은 설계
class MySQLDatabase {
public void save(String data) {}
}
class UserService {
private MySQLDatabase database = new MySQLDatabase();
public void saveUser(String data) {
database.save(data);
}
}
위의 예시에서는 DB가 변경되면 UserService도 수정해야 합니다.
개선된 설계
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) {}
}
class MongoDatabase implements Database {
public void save(String data) {}
}
class UserService {
private Database database;
public UserService(Database database) {
this.database = database;
}
public void saveUser(String data) {
database.save(data);
}
}
위의 예시에서는 어떤 DB를 쓰든 UserService는 수정할 필요 없습니다.
SOLID 원칙의 장점
- 유지보수성 향상: 변경이 필요한 부분이 최소화됩니다.
- 확장성 강화: 새로운 기능 추가가 쉽습니다.
- 코드 재사용성 증가: 모듈 간 결합도가 낮아집니다.
- 테스트 용이성: 단위 테스트와 Mocking이 쉬워집니다.
SOLID 원칙은 단순한 이론이 아니라, 실제 프로젝트에서 코드 품질을 좌우하는 핵심 기준입니다. 특히 S, O, D는 업무상에서 많이 적용되는 만큼 알고 있으면 좋을 것 가아요. 모든 코드를 처음부터 SOLID하게 작성하기는 어렵지만, 리팩터링 시 하나씩 적용해 나가면 변화에 강하고 확장에 유연한 코드를 만들 수 있을 것 같습니다.
'Languague > Java' 카테고리의 다른 글
[Java] 추상클래스와 인터페이스의 공통점과 차이점 (0) | 2022.10.25 |
---|---|
[Java] 자바 인터페이스(Interface) 사용법 & 예제 (1) | 2022.10.24 |
[Java] 자바 추상 클래스(Abstract Class) 사용법 & 예제 (0) | 2022.10.23 |
[Java] 자바 클래스(Class)의 상속(Extends) 사용법 & 예제 (1) | 2022.10.11 |