[Java] SOLID 원칙

김선형·2025년 9월 6일

Java

목록 보기
12/27

개요

유지보수성과 확장성을 높이기 위한 5가지 설계 원칙으로, Clean Architecture의 핵심이다.

Single Responsibility Principle (SRP)

단일 책임 원칙은 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 즉, 단일 모듈은 변경 이유가 오직 하나뿐이어야 한다.

// 나쁜 구조: 한 클래스에 여러 책임(계산, 출력, 저장)을 모두 담음
// 영수증 포맷 변경, 가격 계산 규칙 변경 등 변경 이유가 서로 다름
class OrderService {
    private final List<OrderLine> lines = new ArrayList<>();

    public void addLine(String name, int unitPrice, int qty) {
        lines.add(new OrderLine(name, unitPrice, qty));
    }

    public int totalPrice() { // 도메인 로직
        return lines.stream().mapToInt(l -> l.unitPrice * l.qty).sum();
    }

    public String toReceipt() { // 출력 로직
        return "TOTAL: " + totalPrice() + " KRW";
    }

    public void saveToDb() { // 영속화 로직
        // DB 저장 코드
    }
}

class OrderLine {
    final String name; final int unitPrice; final int qty;
    OrderLine(String name, int unitPrice, int qty) { this.name=name; this.unitPrice=unitPrice; this.qty=qty; }
}
// 좋은 구조: 책임을 역할별로 분리
class Order { // 계산 책임
    private final List<OrderLine> lines = new ArrayList<>();
    public void addLine(String name, int unitPrice, int qty) {
        lines.add(new OrderLine(name, unitPrice, qty));
    }
    public int totalPrice() {
        return lines.stream().mapToInt(l -> l.unitPrice * l.qty).sum();
    }
}
record OrderLine(String name, int unitPrice, int qty) {}

class OrderPrinter { // 출력 책임
    public String toReceipt(Order order) {
        return "TOTAL: " + order.totalPrice() + " KRW";
    }
}

class OrderRepository { // 저장 책임
    public void save(Order order) { /* DB 코드 */ }
}

Open/Closed Principle (OCP)

개방-폐쇄 원칙은 소프트웨어 요소 (클래스, 모듈, 함수, etc.)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙이다. 새로운 요구사항을 구현하는 데 있어 코드를 전반적으로 수정해야 한다면 좋은 설계라고 할 수 없다.

// 나쁜 구조: 새로운 도형이 추가될 때마다 if-else 수정 필요
// 새 도형을 추가할 때마다 기존 계산 코드를 수정해야 함
class AreaCalculator {
    public double area(Object shape) {
        if (shape instanceof Rectangle r) return r.w * r.h;
        else if (shape instanceof Circle c) return Math.PI * c.r * c.r;
        // 새로운 도형 추가 시 여기를 계속 수정해야 함
        throw new IllegalArgumentException("Unknown shape");
    }
}
class Rectangle { double w, h; }
class Circle { double r; }
// 좋은 구조: 다형성에 의존, 확장에는 열려 있고 수정에는 닫힘
interface Shape { double area(); }

class Rectangle implements Shape {
    private final double w, h;
    Rectangle(double w, double h) { this.w=w; this.h=h; }
    public double area() { return w * h; }
}

class Circle implements Shape {
    private final double r;
    Circle(double r) { this.r=r; }
    public double area() { return Math.PI * r * r; }
}

class AreaCalculator {
    public double sumArea(List<Shape> shapes) {
        return shapes.stream().mapToDouble(Shape::area).sum();
    }
}

Liskov Substitution Principle (LSP)

리스코프 치환 원칙은 자식 클래스는 부모 클래스를 대체할 수 있어야 한다는 원칙이다. 즉, 하위 클래스의 인스턴스는 상위 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.

// 나쁜 구조: 상위 타입 계약(LIFO)을 위반
// RandomPopStack.pop()이 무작위 요소를 반환해 LIFO를 위반함
interface Stack<E> {
    void push(E e);
    E pop();
    E peek();
    boolean isEmpty();
}

class RandomPopStack<E> implements Stack<E> {
    private final List<E> list = new ArrayList<>();
    private final Random rnd = new Random();
    public void push(E e) { list.add(e); }
    public E pop() { return list.remove(rnd.nextInt(list.size())); } // 계약 위반
    public E peek() { return list.get(list.size()-1); }
    public boolean isEmpty() { return list.isEmpty(); }
}
// 좋은 구조: 상위 계약을 유지하면서 기능 확장
class ArrayStack<E> implements Stack<E> {
    private final Deque<E> dq = new ArrayDeque<>();
    public void push(E e) { dq.push(e); }
    public E pop() { return dq.pop(); }
    public E peek() { return dq.peek(); }
    public boolean isEmpty() { return dq.isEmpty(); }
}

class BoundedStack<E> implements Stack<E> {
    private final Stack<E> delegate;
    private final int capacity;
    private int size = 0;
    BoundedStack(Stack<E> delegate, int capacity) { this.delegate=delegate; this.capacity=capacity; }
    public void push(E e) {
        if (size >= capacity) throw new IllegalStateException("Full");
        delegate.push(e); size++;
    }
    public E pop() { size--; return delegate.pop(); }
    public E peek() { return delegate.peek(); }
    public boolean isEmpty() { return size==0; }
}

Interface Segregation Principle (ISP)

인터페이스 분리 원칙은 하나의 범용적인 인터페이스보다는 여러 개의 구체적인 인터페이스를 만드는 것이 좋다는 원칙이다. 즉, 사용하지 않는 것에 의존하지 않아야 한다. 사용하지 않는 함수가 존재한다면, 인터페이스를 분리해야 한다.

// 나쁜 구조: 범용 인터페이스가 너무 커서 불필요한 메서드 구현 강제
// print/scan/fax를 모두 강제하여, print만 필요해도 scan과 sendFax를 억지로 구현해야 함
interface Machine {
    void print(String doc);
    String scan();
    void sendFax(String number, String doc);
}

class SimplePrinter implements Machine {
    public void print(String doc) { System.out.println(doc); }
    public String scan() { throw new UnsupportedOperationException(); }
    public void sendFax(String n, String d) { throw new UnsupportedOperationException(); }
}
// 좋은 구조: 인터페이스를 역할별로 분리
interface Printer { void print(String doc); }
interface Scanner { String scan(); }
interface Fax { void sendFax(String number, String doc); }

class SimplePrinter implements Printer {
    public void print(String doc) { System.out.println(doc); }
}

class AllInOneMachine implements Printer, Scanner, Fax {
    public void print(String doc) { /* 구현 */ }
    public String scan() { return "scanned"; }
    public void sendFax(String number, String doc) { /* 구현 */ }
}

Dependency Inversion Principle (DIP)

의존 역전 원칙은 고수준 모듈은 저수준 모듈에 의존하면 안 된다는 원칙이다. 즉, 자신보다 변하기 쉬운 것에 의존하면 안 되며, 구체화가 아닌 추상화에 의존해야 한다.

// 나쁜 구조: 고수준 모듈이 저수준 구현에 직접 의존
// SMS로 교체 등 구현 교체가 어려움
class NotificationService {
    private final EmailSender sender = new EmailSender(); // 구체 클래스 의존

    public void sendWelcome(String user) {
        sender.send(user, "Welcome!");
    }
}

class EmailSender {
    void send(String to, String msg) { /* SMTP 코드 */ }
}
// 좋은 구조: 추상에 의존, 구체는 외부에서 주입
// MessageSender라는 추상에 의존하고, 구체 (Email, SMS)는 조립 구간에서 주입
interface MessageSender { void send(String to, String message); }

class EmailSender implements MessageSender {
    public void send(String to, String message) { /* SMTP 코드 */ }
}

class SmsSender implements MessageSender {
    public void send(String to, String message) { /* SMS 코드 */ }
}

class NotificationService {
    private final MessageSender sender;
    NotificationService(MessageSender sender) { this.sender = sender; }
    public void sendWelcome(String user) { sender.send(user, "Welcome!"); }
}

class App {
    public static void main(String[] args) {
        MessageSender sender = new EmailSender(); // 구체 선택
        NotificationService svc = new NotificationService(sender);
        svc.sendWelcome("kim@example.com");
    }
}
profile
선형의 비선형적 기록 🐜

0개의 댓글