[디자인패턴] SOLID
SOLID 원칙은 객체 지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위해 제안된 5가지 설계 원칙이다.
말 그래도 원칙일뿐이다. 원칙보다는 때에 맞는 유연한 설계와 코드 작성이 중요하다.
1. S : Single Responsibility Principle (SRP, 단일 책임 원칙)
하나의 클래스는 하나의 책임만을 가진다.
ex : Report 클래스가 보고서 출력과 저장까지 둘 다 한다면 → 출력과 저장을 분리하는 것이 SRP에 맞음.
하나의 UserService 클래스에서 유저를 저장하고 이메일발송하고 이런 코드들...
class ReportManager {
public String createReport() {
// 보고서 생성 로직
return "Report Content";
}
public void saveReport(String report) {
// 보고서를 파일로 저장하는 로직
System.out.println("Saving report to file: " + report);
}
}
//아래와 같이 service와 Repository로 나누어서 책임 분리 가능
class ReportService {
public String createReport() {
// 보고서 생성 로직
return "Report Content";
}
}
// 보고서 저장 책임
class ReportRepository {
public void saveReport(String report) {
// 보고서를 파일에 저장하는 로직
System.out.println("Saving report to file: " + report);
}
}
클래스에 여기저기 책임이 묶여있어서 필요한것만 가져다 쓰기도 힘들고 리팩터링 및 유지보수가 어렵다. (분류가 안됨)
2. Open-Closed Principle (OCP, 개방-폐쇄의 원칙)
각 클래스가 확장에는 열려있어야 하고 변경에는 닫혀있어야 한다.
public class ReportGenerator {
public void generateReport(String type) {
if(type.equals("PDF")) {
System.out.println("PDF 생성");
} else if(type.equals("HTML")) {
System.out.println("HTML 파일 생성");
}
}
}
위와 같이 작성된다면 다른 케이스가 추가될때마다 해당 메서드는 수정되어야 할것이다. ---> 위험하다. 어떤 영향을 끼칠지 모르고 요구사항이 어떻게 변화할지도 모른다. 나중에는 메서드를 갈아 엎어야 할 수 있다.
public interface Report {
void generate();
}
public class PDFReport implements Report {
@Override
public void generate() {
System.out.println("pdf 생성");
}
}
public class HTMLReport implements Report {
@Override
public void generate() {
System.out.println("HTML 생성");
}
}
이렇게 인터페이스를 통해 형태와 이름정도만을 정의해두고 각 인터페이스를 구현하는 구현체를 두어서 기존의 코드를 수정하지 않으며 추가하는 방식으로 요구사항에 대응 할 수 있게 된다.
3. Liskov Substitution Principle (LSP, 리스코프-치환 원칙)
자식 클래스는 언제나 부모클래스를 대체할 수 있어야 한다. (자식은 최대한 부모가 하는일은 수행해야 한다.)
class Bird {
public void fly() {
System.out.println("Flap flap, flying!");
}
}
class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying!");
}
}
class Penguin extends Bird {
@Override
public void fly() {
// 펭귄은 날 수 없음 → 부모의 계약을 위반
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
public class Main {
public static void main(String[] args) {
Bird bird1 = new Sparrow();
Bird bird2 = new Penguin();
bird1.fly(); // 정상 동작
bird2.fly(); //예외 발생
}
}
펭귄은 날 수 없어서 부모클래스를 대체 할 수가 없다. 클라이언트는 Bird가 날수 있을거라 기대했지만 그렇지 못하게된다.
interface Bird {
void eat();
}
interface Flyable {
void fly();
}
// 참새는 날 수 있음
class Sparrow implements Bird, Flyable {
@Override
public void eat() {
System.out.println("Sparrow is eating");
}
@Override
public void fly() {
System.out.println("Sparrow is flying!");
}
}
// 펭귄은 날 수 없음
class Penguin implements Bird {
@Override
public void eat() {
System.out.println("Penguin is eating");
}
// fly() 강제 구현 필요 없음 → 계약 위반 X
}
4. Interface Segregation Principle (인터페이스 분리 법칙, ISP)
클래스는 자신이 사용하지 않을 메서드를 구현하도록 강요받지 않아야 한다.
즉, 덩치 큰 인터페이스보다는 작은 인터페이스 여러 개로 나누는 게 좋습니다.
// 너무 큰 인터페이스
interface Machine {
void print(String content);
void scan(String content);
void fax(String content);
}
// 다기능 복합기 - 문제 없음
class MultiFunctionPrinter implements Machine {
@Override
public void print(String content) {
System.out.println("Printing: " + content);
}
@Override
public void scan(String content) {
System.out.println("Scanning: " + content);
}
@Override
public void fax(String content) {
System.out.println("Faxing: " + content);
}
}
// 단순 프린터 - 사용하지 않는 메서드도 억지로 구현해야 함
class SimplePrinter implements Machine {
@Override
public void print(String content) {
System.out.println("Printing: " + content);
}
@Override
public void scan(String content) {
throw new UnsupportedOperationException("This printer can't scan");
}
@Override
public void fax(String content) {
throw new UnsupportedOperationException("This printer can't fax");
}
}
위와 같은 코드를 아래와 같이 분리할 수 있다.
interface Printer {
void print(String content);
}
interface Scanner {
void scan(String content);
}
interface Fax {
void fax(String content);
}
// 다기능 복합기
class MultiFunctionPrinter implements Printer, Scanner, Fax {
@Override
public void print(String content) {
System.out.println("Printing: " + content);
}
@Override
public void scan(String content) {
System.out.println("Scanning: " + content);
}
@Override
public void fax(String content) {
System.out.println("Faxing: " + content);
}
}
// 단순 프린터
class SimplePrinter implements Printer {
@Override
public void print(String content) {
System.out.println("Printing: " + content);
}
}
5. Dependency Inversion Priciple (의존성 역전 법칙, DIP)
고수준 모듈이 저수준 모듈에 의존해서는 안된다.
//구체적 동작을 정의하는 저수준 모듈
public class Fan {
public void spin(){
System.out.println("선풍기 돌아감");
}
public void stop(){
System.out.println("선풍기 멈춤");
}
}
// 선풍기를 제어하는 추상화된 로직을 제공하는 고수준 모듈
public class Switch {
private Fan fan;
public Switch(Fan fan){
this.fan = fan;
}
public void turnOn(){
fan.spin();
}
public void turnOff(){
fan.spin();
}
}
선풍기에 의존적인 코드가 작성되었다.
만약 저수준모듈의 클래스가 변경된다면 고수준 모듈의 클래스에도 수정이 필요하게 된다.
또한 선풍기말고 다른 전자기기를 다룰 수 없게 된다.
public interface Switchable { // 추상화 (인터페이스)
void turnOn();
void turnOff();
}
public class Fan implements Switchable { // 저수준 모듈 (구현체)
@Override
public void turnOn() { System.out.println("선풍기돌아감"); }
@Override
public void turnOff() { System.out.println("선풍기 멈춤"); }
}
public class Switch { // 고수준 모듈
private Switchable device; // 추상화에 의존
public Switch(Switchable device) {
this.device = device;
}
public void switchOn() { device.turnOn(); }
public void switchOff() { device.turnOff(); }
}
이제는 저수준 모듈과 고수준 모듈 모두 추상화에 의존하는 구조로 변경되었음.