[Java] SOLID 원칙

배창민·2025년 9월 19일
post-thumbnail

SOLID 원칙


핵심 지표 미리 보기

  • 유지보수성: 수정이 쉬운가
  • 확장성: 기존 코드 변경 없이 기능 추가 가능한가
  • 응집도: 모듈이 한 가지 일에 집중하는가
  • 결합도: 모듈 간 의존이 낮은가
  • 유연성: 요구 변화에 잘 적응하는가

1) SRP — 단일 책임 원칙

한 클래스는 하나의 책임만 가져야 한다. (변경 이유는 단 하나)

위반

class ReportGenerator {
    public void generateReport() { System.out.println("Generating..."); }
    public void saveToFile(String fileName) { System.out.println("Saving: " + fileName); }
}

적용

class ReportGenerator {
    public void generateReport() { System.out.println("Generating..."); }
}
class ReportSaver {
    public void saveToFile(String fileName) { System.out.println("Saving: " + fileName); }
}

체크리스트

  • 변경 이유가 2개 이상인가? → 클래스 분리
  • I/O, 도메인 로직, 포맷팅 등을 역할별로 분리

2) OCP — 개방/폐쇄 원칙

확장에는 열려 있고, 변경에는 닫혀 있어야 한다.

위반

class DiscountCalculator {
    double calculateDiscount(String type, double price) {
        if (type.equals("Regular")) return price * 0.1;
        else if (type.equals("VIP")) return price * 0.2;
        return 0;
    }
}

적용 (전략 패턴)

interface DiscountStrategy { double applyDiscount(double price); }

class RegularDiscount implements DiscountStrategy {
    public double applyDiscount(double price) { return price * 0.1; }
}
class VIPDiscount implements DiscountStrategy {
    public double applyDiscount(double price) { return price * 0.2; }
}

class DiscountCalculator {
    double calculateDiscount(DiscountStrategy strategy, double price) {
        return strategy.applyDiscount(price);
    }
}

체크리스트

  • if/else타입 분기가 늘어나는가? → 다형성으로 치환
  • 인터페이스/추상화를 통해 새 기능을 추가만 하게 만들 것

3) LSP — 리스코프 치환 원칙

하위 타입은 상위 타입을 대체할 수 있어야 한다.

위반 (못 나는 펭귄)

class Bird { public void fly() { System.out.println("flying"); } }
class Penguin extends Bird {
    @Override public void fly() { throw new UnsupportedOperationException(); }
}

적용 (타입 분리)

interface FlyingBird { void fly(); }

class Sparrow implements FlyingBird {
    public void fly() { System.out.println("Sparrow flying"); }
}
class Penguin {
    public void swim() { System.out.println("Penguin swimming"); }
}

체크리스트

  • 상속 후 **계약(행동)**을 깨뜨리나? → 상속 말고 분리/합성 고려
  • “예외 던지기”, “아무 동작 안 함”은 치환성 위반 신호

4) ISP — 인터페이스 분리 원칙

클라이언트는 사용하지 않는 메서드에 의존하지 않아야 한다.

위반 (불필요한 메서드 강제)

interface Worker { void work(); void eat(); }
class Robot implements Worker {
    public void work() { System.out.println("Robot working"); }
    public void eat() { throw new UnsupportedOperationException(); }
}

적용 (인터페이스 분리)

interface Workable { void work(); }
interface Eatable { void eat(); }

class Robot implements Workable {
    public void work() { System.out.println("Robot working"); }
}
class Human implements Workable, Eatable {
    public void work() { System.out.println("Human working"); }
    public void eat() { System.out.println("Human eating"); }
}

체크리스트

  • 비대한 인터페이스? → 역할별 작은 인터페이스로 분리
  • 구현체가 불필요한 메서드를 덮어쓰고 있나?

5) DIP — 의존 역전 원칙

상위/하위 모듈 모두 구체가 아닌 추상에 의존해야 한다.
(구체 생성은 외부에서 주입)

위반

class MechanicalKeyboard { void type() { System.out.println("Mech..."); } }
class Computer {
    private MechanicalKeyboard keyboard = new MechanicalKeyboard();
    void useKeyboard() { keyboard.type(); }
}

적용 (인터페이스 + DI)

interface Keyboard { void type(); }

class MechanicalKeyboard implements Keyboard {
    public void type() { System.out.println("Mech..."); }
}
class WirelessKeyboard implements Keyboard {
    public void type() { System.out.println("Wireless..."); }
}

class Computer {
    private final Keyboard keyboard;
    public Computer(Keyboard keyboard) { this.keyboard = keyboard; } // 생성자 주입
    public void useKeyboard() { keyboard.type(); }
}

체크리스트

  • 클래스가 구현체를 직접 생성/의존? → 인터페이스 + DI로 전환
  • 프레임워크(스프링 DI/IoC) 사용 시 바인딩을 외부 구성으로 이동

핵심 정리

  • SRP: 변경 이유 1개만 → 역할별 클래스로 분리
  • OCP: 새 기능은 추가로, 기존 코드는 안 건드리기
  • LSP: 하위 타입은 상위 타입의 계약을 준수
  • ISP: 작은 인터페이스로 필요한 것만 의존
  • DIP: 인터페이스 + DI로 결합도 최소화
profile
개발자 희망자

0개의 댓글