SOLID 원칙

김현정·2025년 3월 25일
0

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개의 댓글