본문 바로가기
프로그래밍/JAVA

Java SOLID 원칙 완벽 정리: SRP, OCP, LSP, ISP, DIP 개념과 예제

by 곰 옥수수 2025. 3. 1.
728x90
반응형

SOLID 원칙은 객체 지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위한 다섯 가지 설계 원칙을 의미합니다. 각각의 원칙을 자세히 설명하겠습니다.


1. SRP (Single Responsibility Principle) - 단일 책임 원칙

"클래스는 단 하나의 책임만 가져야 한다."

  • 하나의 클래스는 오직 하나의 기능(책임)만 수행해야 하며, 변경해야 하는 이유도 하나만 존재해야 한다.
  • 즉, 한 클래스가 여러 가지 기능을 가지면 수정할 때 예상치 못한 부작용이 발생할 가능성이 커진다.

예제

// ❌ 단일 책임 원칙 위반: 한 클래스가 여러 가지 역할을 수행함. 
class Report { 
	public void generateReport() { 
    	System.out.println("Report Generated"); 
    } 
    
    public void printReport() { 
    	System.out.println("Printing Report"); 
    } 
 } 
 
 // ✅ 단일 책임 원칙 준수: 클래스를 분리하여 각각의 역할을 수행하도록 개선. 
 class ReportGenerator { 
 	public void generateReport() { 
    	System.out.println("Report Generated"); 
    } 
} 

class ReportPrinter { 
	public void printReport() { 
    	System.out.println("Printing Report"); 
    } 
}
 

ReportGenerator는 보고서를 생성하는 역할, ReportPrinter는 보고서를 출력하는 역할을 수행하도록 분리.


2. OCP (Open/Closed Principle) - 개방-폐쇄 원칙

"확장에는 열려 있고, 수정에는 닫혀 있어야 한다."

  • 새로운 기능이 추가될 때 기존 코드를 수정하지 않고 확장 가능하도록 설계해야 한다.
  • 주로 인터페이스, 추상 클래스, 다형성(Polymorphism)을 이용하여 설계를 유연하게 만든다.

예제

// ❌ 개방-폐쇄 원칙 위반: 새로운 결제 방식이 추가될 때마다 기존 코드를 수정해야 함.
class Payment {
    public void processPayment(String type) {
        if (type.equals("CreditCard")) {
            System.out.println("Processing Credit Card Payment");
        } else if (type.equals("PayPal")) {
            System.out.println("Processing PayPal Payment");
        }
    }
}

// ✅ 개방-폐쇄 원칙 준수: 새로운 결제 방식을 추가할 때 기존 코드를 수정하지 않아도 됨.
interface PaymentMethod {
    void processPayment();
}

class CreditCardPayment implements PaymentMethod {
    public void processPayment() {
        System.out.println("Processing Credit Card Payment");
    }
}

class PayPalPayment implements PaymentMethod {
    public void processPayment() {
        System.out.println("Processing PayPal Payment");
    }
}

class PaymentProcessor {
    public void process(PaymentMethod method) {
        method.processPayment();
    }
}

새로운 결제 방식이 필요하면 PaymentMethod를 구현하는 클래스를 추가하기만 하면 된다.


3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙

"하위 클래스는 상위 클래스의 기능을 대체할 수 있어야 한다."

  • 즉, 상위 클래스 객체를 하위 클래스 객체로 교체해도 프로그램이 정상적으로 동작해야 한다.
  • 상속받은 하위 클래스는 상위 클래스의 계약(기능과 동작)을 반드시 유지해야 한다.

예제

// ❌ 리스코프 치환 원칙 위반: Rectangle(직사각형)을 상속한 Square(정사각형)가 예상치 못한 동작을 함.
class Rectangle {
    protected int width, height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 정사각형이므로 높이도 동일하게 설정
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}
 

Square(정사각형)는 Rectangle을 상속했지만, 높이와 너비가 같아야 하는 특성 때문에 Rectangle(직사각형)의 기능을 제대로 대체하지 못함.

 

개선 방법:

abstract class Shape {
    abstract int getArea();
}

class Rectangle extends Shape {
    protected int width, height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square extends Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

 

Rectangle(직사각형)과 Square(정사각형)를 Shape(모양) 인터페이스로 분리하여 상속의 문제를 해결.


4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙

"클라이언트가 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다."

  • 하나의 거대한 인터페이스를 여러 개의 작은 인터페이스로 나누어야 한다.
  • 필요 없는 기능을 구현하지 않도록 강제하는 구조를 방지한다.

예제

// ❌ 인터페이스 분리 원칙 위반: 한 인터페이스가 너무 많은 기능을 포함하고 있음.
interface Worker {
    void work();
    void eat();
}

class Developer implements Worker {
    public void work() {
        System.out.println("Coding...");
    }

    public void eat() {
        System.out.println("Eating...");
    }
}

Developer는 work()만 필요하지만 eat()까지 구현해야 함.

 

개선 방법:

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Developer implements Workable {
    public void work() {
        System.out.println("Coding...");
    }
}

class OfficeWorker implements Workable, Eatable {
    public void work() {
        System.out.println("Doing office work...");
    }

    public void eat() {
        System.out.println("Eating lunch...");
    }
}

Workable, Eatable 인터페이스를 분리하여 필요한 기능만 구현하도록 개선.


5. DIP (Dependency Inversion Principle) - 의존 역전 원칙

"고수준 모듈이 저수준 모듈에 의존하지 않고, 추상화에 의존해야 한다."

  • 구체적인 구현이 아니라 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
  • 이를 통해 결합도를 낮추고 유지보수를 쉽게 만들 수 있다.

예제

// X 의존 역전 원칙 위반: Switch(고수준 모듈)가 LightBulb(저수준 모듈)에 직접 의존
class LightBulb { //저수준 모듈
    public void turnOn() {
        System.out.println("Light Bulb is On");
    }

    public void turnOff() {
        System.out.println("Light Bulb is Off");
    }
}

class Switch { //고수준 모듈
    private LightBulb bulb;

    public Switch(LightBulb bulb) {  //Switch가  LightBulb 클래스를 직접 의존
        this.bulb = bulb;
    }

    public void operate() {
        bulb.turnOn();
    }
}

Switch 클래스는 LightBulb에 직접 의존하고 있어 확장성이 떨어짐.

 

개선 방법:

// 추상화된 인터페이스 (고수준 모듈과 저수준 모듈 간의 연결 역할)
interface Switchable {
    void turnOn();
    void turnOff();
}

// 저수준 모듈 1: 전구 (LightBulb)
class LightBulb implements Switchable {
    public void turnOn() {
        System.out.println("Light Bulb is On");
    }

    public void turnOff() {
        System.out.println("Light Bulb is Off");
    }
}

// 저수준 모듈 2: 선풍기 (Fan)
class Fan implements Switchable {
    public void turnOn() {
        System.out.println("Fan is Spinning");
    }

    public void turnOff() {
        System.out.println("Fan Stopped");
    }
}

// 고수준 모듈: Switch
class Switch {
    private final Switchable device; // 인터페이스에 의존 (추상화)

    public Switch(Switchable device) {  //  DIP 준수 - 인터페이스를 통해 의존성 주입
        this.device = device;
    }

    public void turnOn() {
        device.turnOn(); //  구현체가 아닌 인터페이스의 메서드를 호출
    }

    public void turnOff() {
        device.turnOff();
    }
}

// 실행 예제
public class Main {
    public static void main(String[] args) {
        Switchable bulb = new LightBulb();
        Switchable fan = new Fan();

        Switch switch1 = new Switch(bulb);
        Switch switch2 = new Switch(fan);

        switch1.turnOn();  // "Light Bulb is On"
        switch2.turnOn();  // "Fan is Spinning"
    }
}

Switchable 인터페이스를 도입하여 Switch가 LightBulb 구현체에 직접 의존하지 않도록 개선.


정리

  • SRP: 단일 책임을 유지하라.
  • OCP: 기존 코드를 변경하지 않고 확장 가능하게 하라.
  • LSP: 하위 클래스는 상위 클래스를 완전히 대체할 수 있어야 한다.
  • ISP: 인터페이스를 작은 단위로 분리하라.
  • DIP: 구체적인 구현이 아닌 추상화에 의존하라.

 

다음엔 SRP, OCP, LSP, ISP, DIP를 세부적으로 좀 더 정리해봐야겠다.

728x90
반응형

댓글