SOLID 원칙은 객체 지향 설계의 5대 핵심 원칙으로, 유지보수성과 확장성이 뛰어난 소프트웨어를 설계하는 데 도움을 줍니다. 이번 포스팅에서는 SOLID의 각 원칙을 예제 코드와 함께 정리해 보겠습니다. 단순히 설명글만 읽는 것보다, 예제 코드를 함께 살펴보는 방식이 이해를 돕고 기억에도 오래 남을 것이라 기대합니다.
그럼, 각 원칙의 핵심 개념과 이를 구현한 예제 코드를 통해 SOLID 원칙을의존 관계를 맺을 때 자주 변화하는 차근차근 알아보겠습니라다변화가 거의 없는 .
< SOLID 요
인터페이스나 추상 클래스에 의존약 >
| 원칙 | 설명 | 구현 방법 |
|---|---|---|
| SRP | 하나의 클래스는 하나의 책임만 가져야 한다 | 기능을 분리하여 클래스 설계 |
| OCP | 확장에는 열려 있고, 수정에는 닫혀 있어야 한다 | 추상화와 다형성 활용 |
| LSP | 서브타입은 기반 타입으로 대체 가능해야 한다 | 부모 클래스의 규약을 따르는 설계 |
| ISP | 인터페이스는 클라이언트에 맞게 분리해야 한다 | 작은 단위의 인터페이스 설계 |
| DIP | 고수준 모듈과 저수준 모듈 모두 추상화에 의존해야 한다 | 인터페이스나 추상 클래스 활용 |
"한 클래스는 단 하나의 책임만 가져야 한다."
하나의 클래스가 하나의 역할만 수행할 때 코드의 유지보수성이 높아집니다.
<위반 사례>
class Report {
public String generateReport() {
return "보고 내용";
}
public void saveToFile(String content) {
System.out.println("보고 내용을 파일로 저장합니다.");
}
}
<SRP 준수>
class Report {
public String generateReport() {
return "보고 내용";
}
}
class FileSaver {
public void saveToFile(String content) {
System.out.println("보고 내용을 파일로 저장합니다.");
}
}
"확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다."
이 원칙의 핵심은 기존 코드를 변경하지 않고도 기능을 확장할 수 있는 구조를 설계하는 것입니다.
<위반 사례>
class DiscountCalculator {
public double calculateDiscount(String type, double price) {
if (type.equals("Regular")) {
return price * 0.9;
} else if (type.equals("VIP")) {
return price * 0.8;
}
return price;
}
}
<OCP 준수>
interface Discount {
double apply(double price);
}
class RegularDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.9;
}
}
class VIPDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.8;
}
}
class DiscountCalculator {
public double calculateDiscount(Discount discount, double price) {
return discount.apply(price);
}
}
"자식 클래스는 부모 클래스의 행위를 대체할 수 있어야 한다."
부모 클래스 타입으로 자식 클래스를 참조했을 때, 프로그램이 정상적으로 작동해야 한다는 원칙입니다. 즉, 자식 클래스는 부모 클래스가 제공하는 기능과 일관성을 유지해야 하며, 부모 클래스의 동작을 깨뜨리면 안 됩니다.
<위반 사례>
class Animal {
public void walk() {
System.out.println("걷기");
}
}
class Bird extends Animal {
@Override
public void walk() {
throw new UnsupportedOperationException("새는 걷기보다 날아다닙니다.");
}
}
<LSP 준수>
// 부모 클래스
class Animal {
public void walk() {
System.out.println("걷기");
}
}
// 자식 클래스
class Bird extends Animal {
public void fly() {
System.out.println("날기");
}
}
"특정 클라이언트를 위한 인터페이스 여러 개가, 범용 인터페이스 하나보다 낫다."
클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리해야 합니다.
<위반 사례>
interface Worker {
void work();
void eat();
}
class Developer implements Worker {
@Override
public void work() {
System.out.println("코드를 작성하는 중입니다.");
}
@Override
public void eat() {
System.out.println("식사를 합니다.");
}
}
class Robot implements Worker {
@Override
public void work() {
System.out.println("부품을 조립하는 중입니다.");
}
@Override
public void eat() {
throw new UnsupportedOperationException("로봇은 음식을 먹지 않습니다.");
}
}
<ISP 준수>
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Developer implements Workable, Eatable {
@Override
public void work() {
System.out.println("코드를 작성하는 중입니다.");
}
@Override
public void eat() {
System.out.println("삭사를 합니다.");
}
}
class Robot implements Workable {
@Override
public void work() {
System.out.println("부품을 조립하는 중입니다.");
}
}
"고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다."
자주 변화하는 구체적인 구현체가 아니라 변화가 거의 없는
인터페이스나 추상 클래스에 의존하도록 설계해야 합니다.
<위반 사례>
class Keyboard {
}
class Computer {
private Keyboard keyboard;
public Computer() {
this.keyboard = new Keyboard();
}
}
<DIP 준수>
interface InputDevice {
}
class Keyboard implements InputDevice {
}
class Mouse implements InputDevice {
}
class Computer {
private InputDevice inputDevice;
public Computer(InputDevice inputDevice) {
this.inputDevice = inputDevice;
}
}