[Java] SOLID 원칙

한동우·2025년 4월 7일

Java

목록 보기
2/4
post-thumbnail

1. 객체 지향 설계의 5원칙 SOLID


SOLID 원칙이란 객체지향 설계에서 유지보수 가능하고, 확장 가능한 코드를 위한 5가지 설계 원칙의 앞글자를 따서 만든 원칙입니다.

  • SRP(Single Responsibility Principle): 단일 책임 원칙
  • OCP(Open Closed Principle): 개방 폐쇄 원칙
  • LSP(Liskov’s Substitution Principle): 리스코프 치환 원칙
  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle): 의존성 역전 원칙

객체 지향 프로그래밍에서 SOLID 원칙을 적용하면 유지보수가 쉽고 코드 확장에 유연해지고 테스트하기 좋고 협업 시 충돌 최소화가 가능해져 개발의 생산성을 높일 수 있습니다.

SOLID 원칙은 특정 문제를 해결하기 위한 지침일 뿐이기 때문에 꼭 프로젝트에 전부 적용할 필요가 없으며 상황에 따라 선택하면 된다.


1.1 SRP(Single Responsibility Principle): 단일 책임 원칙

  • 하나의 클래스는 하나의 책임만 가져야한다는 원칙
  • 책임기능이라고 보면 된다.
  • 하나의 클래스가 여러 이유로 변경된다면 그 클래스는 책임이 둘 이상이다.

🪄 적용 사례

class Product {
    private String name;
    private int price;
    
    public void save() {/* 상품 정보 저장 */}
    public void print() {/* 상품 정보 출력 */}
}
  • 데이터 관리 + 저장 + 출력 책임이 Product 클래스에 모두 섞여 있음

✅ SRP 적용

class Product {
    private String name;
    private int price;    
}
class ProductSaver {
    public void save(Product product) {...}    
}
class ProductPrinter {
    public void print(Product product) {...}    
}
  • 데이터 관리, 저장, 출력을 각각의 클래스에서 가지고 있음
  • 변경 이유가 명확하게 분리됨

1.2 OCP(Open Closed Principle): 개방 폐쇄 원칙

  • 클래스는 확장에는 열려있고 수정에는 닫혀있어야 한다는 원칙
  • 기존 코드를 변경하지 않고 새로운 기능은 확장을 통해 추가할 수 있도록 프로그램을 설계하는 원칙
  • 새로운 요구사항이 있을 때마다 클래스를 수정하면 유지보수 비용이 증가하고 오류 위험이 증가

🪄 적용 사례

class ShapePainter {
   public void draw(String shape) {
       if (shape.equals("triangle")) {
           System.out.println("삼각형");
       } else if (shape.equals("square")) {
           System.out.println("사각형");
       }
   }
}
  • 새로운 도형이 추가될 때 마다 draw() 메소드를 변경해야 한다.

✅ OCP 적용

interface Shape {
	void draw();
}

class Triangle implements Shape {
	public void draw() {
    		System.out.println("삼각형");
    }
}

class Square implements Shape {
	public void draw() {
    		System.out.println("사각형");
    }
}

class ShapePainter {
	
    private Shape shape;
    
    public ShapePainter(Shape shape) {
    		this.shape = shape;
    }
    
    public void drawShape() {
    		shape.draw();
    }
}
  • 도형이 추가될 때 마다 클래스를 만들어 확장 가능
  • 기존 ShapePainter는 전혀 수정하지 않아도 됨

1.3 LSP(Liskov’s Substitution Principle): 리스코프 치환 원칙

  • 자식 클래스는 언제나 부모 클래스 타입으로 교체할 수 있어야 한다는 원칙
  • 메소드의 사전조건, 사후 조건, 불변 조건 등을 명확하게 정해놓고 지키는 설계 방식
  • 자식 클래스가 부모의 메소드를 재정의하더라도, 기존 계약을 위반해서는 안 됨
  • 다형성 원리를 이용하기 위한 원칙

위반 사례

class Bird {
	public void fly() {...}
}

class Penguin extends Bird {
	public void fly() { throw new UnsupportedOperationException();}
}
  • PenguinBird지만 날지 못함 -> Bird로서 날아야한다는 계약 위반

✍️ 사전 조건(Pre-condition)

  • 메소드가 실행되기 전에 만족해야 할 조건
class BankAccount {
	public void withdraw(int amount) {...}
}

class SavingsAccount {
	public void withdraw(int amount) {
    		if (amount < 10000) { throw new IllegalArgumentException(); }
    }
}
  • BankAccount는 모든 금액에 대해 인출이 가능한데 SavingsAccount에 제약 조건이 추가됨
    -> 사전 조건이 강화됨
    -> LSP 위반

✍️ 사후 조건(Post-condition)

  • 메소드가 실행된 후에 만족해야하는 결과
class Shape {
	private int area;
	public int getArea() { return area; }
}

class FailingShape {
	public int getArea() { return null; }
}
  • 하위 클래스는 사후조건을 약화하면 안 됨

❗즉, LSP는 안정적인 다형성을 위해 필요하다.

1.4 ISP(Interface Segregation Principle): 인터페이스 분리 원칙

  • 하나의 인터페이스를 쪼개서 각각 사용에 맞게 분리해야한다는 원칙
  • 클라이언트가 사용하지 않는 메소드에 의존하지 않게 하는 원칙
  • 작고 응집도 높은 인터페이스를 만드는 것이 더 좋은 설계

🪄 적용 사례

// 복합기
interface MultiFunctionDevice {
    void print();
    void scan();
    void fax();
}

class SimplePrinter implements MultiFunctionDevice {
	@Override
	public void print() {...}
	
 	@Override
	public void scan() { throw new UnsupportedOperationException(); }
	
 	@Override
	public void fax() { throw new UnsupportedOperationException(); }
}
  • SimplePrinter는 print 기능만 사용하는데 scan과 fax 기능의 구현을 강제 당함
    -> 불필요한 의존에 의해 ISP 위반

✅ ISP 적용

interface Printer {
	void print();
}

interface Scanner {
	void scan();
}

interface Fax() {
	void fax();
}

class SimplePrinter implements Printer {
  	public void print() { ... }
}

class SimpleScanner implements Scanner {
   	public void scan() { ... }
}

class HomeMultiFunctionalPrinter implements Printer, Scanner {
	public void print() { ... }
	public void scan() { ... }
}
  • 필요한 기능을 작은 인터페이스로 분리
  • 클라이언트는 필요한 기능만 구현하면 됨
  • 응집도 높은 설계

Q. '응집도가 높다'는 것은?
A. 인터페이스에 포함된 메소드들이 하나의 목적을 공유하고 있다는 뜻

1.5 DIP(Dependency Inversion Principle): 의존성 역전 원칙

  • 구체적인 클래스에 직접 의존하지 말고 인터페이스(추상화)에 의존하라는 설계 원칙

🪄 적용 사례

class LightBulb {
    public void turnOn() {
        System.out.println("LightBulb is turned on.");
    }
    
    public void turnOff() {
        System.out.println("LightBulb is turned off.");
    }
}

class Switch {
    private LightBulb lightBulb;
    
    public Switch() {
        // DIP 위반: 구체적인 구현(LightBulb)에 직접 의존
        lightBulb = new LightBulb();
    }
    
    public void operate(String command) {
        if (command.equals("on")) {
            lightBulb.turnOn();
        } else if (command.equals("off")) {
            lightBulb.turnOff();
        }
    }
}
  • SwitchLightBulb에 강하게 결합됨
  • LightBulbNeonBulb등으로 바꾸기 위해서는 직접 수정해야한다.

✅ DIP 적용

interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb is turned on.");
    }
    
    @Override
    public void turnOff() {
        System.out.println("LightBulb is turned off.");
    }
}

class NeonBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("NeonBulb is turned on.");
    }
    
    @Override
    public void turnOff() {
        System.out.println("NeonBulb is turned off.");
    }
}

class Switch {
    private Switchable device;
    
    // DIP 준수: 생성자 주입을 통해 추상화(Switchable)에 의존
    public Switch(Switchable device) {
        this.device = device;
    }
    
    public void operate(String command) {
        if (command.equals("on")) {
            device.turnOn();
        } else if (command.equals("off")) {
            device.turnOff();
        }
    }
}
  • SwitchSwitchable라는 추상화에만 의존
  • 다양한 Switchable이 와도 유연하게 대체 가능

0개의 댓글