본문 바로가기

Spring

SOLID 원칙과 다형성의 한계(OCP, DIP)

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

  • 하나의 클래스는 한 가지의 책임만 가져야 함
  • 클래스가 변경될 때 파급 효과가 작아야 함

예시

  • 단일책임 원칙 위반
    • User 클래스는 사용자 정보, 로그인기능, 사용자정보를 데이터베이스에 저장하는 내용 3가지의 기능을 모두 수행하고 있음
public class User {
		private String name; // 사용자 정보
    	public void login() { /* 로그인 기능 */ }
    	public void saveUser() { /* 데이터베이스 저장 기능 */ }
}

 

  • 단일 책임 원칙 적용
    • User는 사용자의 정보만 관리하고, 서비스는 로그인 기능을 담당하고, 리포지토리는 데이터베이스에 사용자 정보를 저장하는 기능 담당하는 책임을 가짐
public class User { /* 사용자 정보 관리 */ }

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

public class UserRepository {
    public void saveUser(User user) { /* 데이터베이스 저장 */ }
}

 

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

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 함 = 새로운 기능을 추가할 때 기존 코드의 수정 없이 추가할 수 있어야 함

예시

  • 개방 폐쇄 원칙 위반
    • 새로운 도형이 추가 될 때마다, AreaCalculator 클래스의 calculate 메서드 내부를 if문을 사용하여 수정해야 함
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 /* 사각형의 넓이 계산 */;
        }
    }
}

 

  • 개방 폐쇄 원칙 적용
    • 새로운 도형이 추가되어도, Shape인터페이스를 상속하여 이를 구현하면 되기 때문에 AreaCalculator를 수정할 필요가 없음
    • 다형성을 사용 = 역할(도형)과 구현(원,삼각형)을 분리한 것
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();
    }
}

 

 

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

  • 자식 클래스는 부모 클래스를 대체 할 수 있어야 함
  • 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램 동작에 문제가 없어야 함

예시

  • 리스코프 치환 원칙 위반
    • ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없음
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(); // "전기차가 배터리로 가속합니다."
    }
}

 

인터페이스 분리 원칙 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() { /* 수영 */ }
}

 

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

  • 구체적인 클래스에 의존하지 말고 인터페이스나 추상 클래스에 의존해야 함

 

예시

  • 의존관계 역전 원칙 위반
    • SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 함
// 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("안녕하세요! 이메일 알림입니다.");
    }
}

 

  • 적용
// 알림 인터페이스(추상화)
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 알림입니다.");
    }
}

 

다형성의 한계 : OCP, DIP의 위반과 Spring

OCP 위반 (수정없이 기능을 확장)

  • 구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 함
// 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);
			
		}
}

 

DIP 위반 (구체화 말고 추상화에 의존)

  • 클라리언트가 OrderService(=인터페이스)를 의존하고 있지만, RateDiscountPolicy라는 구현클래스에도 의존하고 있으므로 위반
 public class OrderServiceImpl implements OrderService {
 //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
     private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 }

 

⭐️ 결론 : Spring => OCP, DIP를 IOC, DI를 통해 가능하도록 만들어준다. ⭐️