SOLID 원칙

김현정·2025년 3월 25일

SOLID 원칙

객체 지향 설계의 5가지 기본 원칙, 소프트웨어 설계에서 유지보수성, 확장성, 유연성을 높이기 위한 지침을 제공한다.

SOLID 원칙의 종류

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

  • 하나의 클래스는 하나의 책임만 가져야 한다.
    • 클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야한다.
  • 예시
    • User 클래스는 사용자 정보 관리, 로그인 및 데이터베이스 저장 책임을 동시에 가지고 있다.
public class User {
		private String name; // 사용자 정보
    public void login() { /* 로그인 기능 */ }
    public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
  • 단일 책임 원칙 적용
public class User { /* 사용자 정보 관리 */ }

public class AuthService {
    public void login(User user) { /* 로그인 기능 */ }
}

public class UserRepository {
    public void saveUser(User user) { /* 데이터베이스 저장 */ }
}
  • 실제로는 상황에 따라 책임의 크기가 달라진다.
  • 클래스가 변경될 때 파급 효과가 작으면 된다.

2. 개방 패쇄 원칙 OCP(Open Closed Principle)

  • 소프트웨어 요소는 확장에는 열려있어야 하고, 수정에는 닫혀있어야 한다.
    • 새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장 할 수 있도록 설계해야 한다.
    • 새로운 도형이 추가될 때 마다 AreaCalculator 클래스를 수정해야 한다.
public class Shape {
    public String type;
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        if (shape.type.equals("circle")) {
            return /* 원의 넓이 계산 */;
        } else if (shape.type.equals("square")) {
            return /* 사각형의 넓이 계산 */;
        }
    }
}
  • 개방 폐쇄 원칙 적용
public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    public double calculateArea() { return /* 원의 넓이 계산 */; }
}

public class Square implements Shape {
    public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.calculateArea();
    }
}
  • 새로운 도형이 추가되더라도 Shape 인터페이스만 구현하면 된다.
  • AreaCalculator는 수정할 필요가 없다.
  • 다형성을 활용하여 해결한다.
    • 인터페이스를 implements하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
    • 역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
  • 문제점
// Circle을 계산하는 경우
public class Main {
		public static void main(String[]) {
		
				AreaCalculator areaCalculator = new AreaCalculator();
				Circle circle = new Circle();
				
				areaCalculator.calculate(circle);
			
		}
}

// Square를 계산하는 경우
public class Main {
		public static void main(String[]) {
		
				AreaCalculator areaCalculator = new AreaCalculator();
				// Circle circle = new Circle();
				Square square = new Square();
				
				areaCalculator.calculate(square);
			
		}
}
  • 구현객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트 측의 코드를 변경해야 한다.
  • 객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다
    • Spring Container의 역할

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

  • 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
    • 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
  • 예시
    • ElectricCar는 Car 클래스를 상속 받았지만, accelerate()를 사용할 수 없다. (LSP위반)
class Car {
    public void accelerate() {
        System.out.println("자동차가 휘발유로 가속합니다.");
    }
}

class ElectricCar extends Car {
    @Override
    public void accelerate() {
        throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate(); // "자동차가 가속합니다."

        Car electricCar = new ElectricCar();
        electricCar.accelerate(); // UnsupportedOperationException 발생
    }
}
  • 리스코프 치환 원칙 적용
// 가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
    void accelerate();
}

class Car implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println("내연기관 자동차가 가속합니다.");
    }
}

class ElectricCar implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println("전기차가 배터리로 가속합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Acceleratable car = new Car();
        car.accelerate(); // "내연기관 자동차가 가속합니다."

        Acceleratable electricCar = new ElectricCar();
        electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
    }
}
  • 인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
  • 엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.

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

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
    • 즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
  • 예시
  • Dog 클래스는 fly()메서드를 사용하지 않지만 구현해야 한다.
public interface Animal {
    void fly();
    void run();
    void swim();
}

public class Dog implements Animal {
    public void fly() { /* 사용하지 않음 */ }
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}
  • 인터페이스 분리 원칙 적용
public interface Runnable {
    void run();
}

public interface Swimmable {
    void swim();
}

public class Dog implements Runnable, Swimmable {
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}
  • 인터페이스가 명확해진다.
  • Spring의 기능들은 대부분 인터페이스로 분리되어 있다.

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

  • 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
  • 예시
    • NotificationService는 EmailNotifier 클래스를 의존한다.
// Email 알림 클래스
class EmailNotifier {
    public void sendEmail(String message) {
        System.out.println("Email 알림: " + message);
    }
}

// 알림 시스템
class NotificationService {
    private EmailNotifier emailNotifier;

    public NotificationService() {
		    // 구체적인 클래스인 EmailNotifier에 의존
        this.emailNotifier = new EmailNotifier();
    }

    public void sendNotification(String message) {
        emailNotifier.sendEmail(message);
    }
}

public class Main {
    public static void main(String[] args) {
        NotificationService service = new NotificationService();
        service.sendNotification("안녕하세요! 이메일 알림입니다.");
    }
}
  • 이메일 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 한다. DIP 위반
  • 의존관계 역전 원칙 적용
// 알림 인터페이스(추상화)
interface Notifier {
    void send(String message);
}

// Email 알림 클래스
class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("Email 알림: " + message);
    }
}

// SMS 알림 클래스
class SMSNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("SMS 알림: " + message);
    }
}

// 알림 서비스 (높은 수준 모듈)
class NotificationService {
		// 추상화된 인터페이스에 의존
    private Notifier notifier;

    // 의존성 주입 (생성자를 통해 주입)
    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }

    public void sendNotification(String message) {
		    // notifier가 어떤 구현체인지 상관하지 않음
        notifier.send(message);
    }
}

public class Main {
    public static void main(String[] args) {
        // Email 알림을 사용
        Notifier emailNotifier = new EmailNotifier();
        NotificationService emailService = new NotificationService(emailNotifier);
        emailService.sendNotification("안녕하세요! 이메일 알림입니다.");

        // SMS 알림을 사용
        Notifier smsNotifier = new SMSNotifier();
        NotificationService smsService = new NotificationService(smsNotifier);
        smsService.sendNotification("안녕하세요! SMS 알림입니다.");
    }
}
  • 추상화된 Notifier 인터페이스에만 의존한다.
    • 새로운 알림 방식이 추가되어도 NotificationService 는 변경되지 않아도 된다.
  • 필요한 Notifier 객체를 외부에서 주입받는다.
    • NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
  • 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
  • 서로의 변경 사항에 독립적이어서 변경에 유연하다.

0개의 댓글