카테고리 없음

[Spring] 스프링에서 DIP(의존성 역전 원칙) 위반과 해결 방법

장일규 2025. 2. 23. 21:34

DIP(Dependency Inversion Principle, 의존성 역전 원칙)란?

스프링을 사용하다 보면 DIP(Dependency Inversion Principle, 의존성 역전 원칙)라는 개념을 자주 접하게 된다.

DIP는 SOLID 원칙 중 하나로, "상위(고수준) 모듈이 하위(저수준) 모듈에 의존하지 않고, 추상화(인터페이스)에 의존해야 한다"는 개념이다.

DIP를 적용하면?

  • 확장성이 좋아진다 → 새로운 기능을 추가할 때 기존 코드를 수정할 필요가 없음.
  • 테스트가 쉬워진다 → Mock 객체를 활용할 수 있음.
  • 유지보수성이 향상된다 → 코드 수정 범위를 최소화할 수 있음.

반대로 DIP를 위반하면 코드 변경이 어렵고 유지보수성이 떨어지는 문제가 발생할 수 있다. 그럼 DIP를 위반한 코드와 이를 해결하는 방법을 살펴보자.


1️⃣ DIP를 위반한 코드 (문제점 분석)

❌ 결제 서비스가 특정 결제 방식에 직접 의존하는 경우

public class PaymentService {
    private final CardPayment cardPayment = new CardPayment();

    public void processPayment(int amount) {
        cardPayment.pay(amount);
    }
}

public class CardPayment {
    public void pay(int amount) {
        System.out.println("💳 신용카드로 " + amount + "원 결제 완료!");
    }
}

✅ 문제점 분석

  • PaymentServiceCardPayment 구현체를 직접 사용 → DIP 위반!
  • 다른 결제 방식(카카오페이, 네이버페이 등)을 추가하려면 PaymentService 코드도 변경해야 함.
  • 결제 방식이 변경될 때마다 PaymentService를 수정해야 해서 유지보수성이 떨어짐.

📌 해결 방법?

  • PaymentService가 특정 결제 방식이 아니라 인터페이스(Payment)에 의존하도록 변경
  • 스프링의 DI(의존성 주입) 을 활용하여 결제 방식이 변경되더라도 PaymentService를 수정하지 않도록 개선

2️⃣ DIP를 준수한 코드 (개선 방법)

✅ 1) 결제 방식에 대한 인터페이스 정의

public interface Payment {
    void pay(int amount);
}

이제 CardPayment, KakaoPay, NaverPay 같은 개별 결제 수단 클래스는 Payment 인터페이스만 구현하면 된다.

✅ 2) 다양한 결제 수단 구현

public class CardPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("💳 신용카드로 " + amount + "원 결제 완료!");
    }
}

public class KakaoPay implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("📱 카카오페이로 " + amount + "원 결제 완료!");
    }
}

public class NaverPay implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("🔵 네이버페이로 " + amount + "원 결제 완료!");
    }
}

✅ 3) 결제 서비스가 특정 결제 방식이 아니라 인터페이스(Payment)에만 의존

public class PaymentService {
    private final Payment payment;

    public PaymentService(Payment payment) {
        this.payment = payment;
    }

    public void processPayment(int amount) {
        payment.pay(amount);
    }
}

이제 결제 방식이 바뀌어도 PaymentService 코드는 수정할 필요가 없다! 🎉

  • 새로운 결제 수단 추가가 간편해짐.
  • 유지보수성이 향상됨.


 

3️⃣ 스프링을 활용한 DIP 준수 방법 (DI 적용)

DIP를 적용하려면 생성자 주입을 활용하는 것이 좋다. 이를 스프링과 함께 사용하면 더욱 유연한 설계를 만들 수 있다.

🔹 1) @Component와 @Autowired 활용

@Component
public class CardPayment implements Payment { ... }

@Component
public class KakaoPay implements Payment { ... }

@Component
public class PaymentService {
    private final Payment payment;

    @Autowired
    public PaymentService(Payment payment) {
        this.payment = payment;
    }
}

이점

  • @Component를 사용하면 스프링이 자동으로 객체를 생성하고 관리함.
  • @Autowired를 사용하면 PaymentService에 적절한 Payment 구현체가 자동으로 주입됨.

🔹 2) @Qualifier로 특정 결제 수단 선택

스프링에서는 @Qualifier를 사용하여 특정 결제 수단을 지정할 수도 있다.

@Component
public class PaymentService {
    private final Payment payment;

    @Autowired
    public PaymentService(@Qualifier("cardPayment") Payment payment) {
        this.payment = payment;
    }
}

@Qualifier를 활용하면 특정 결제 수단을 지정하여 사용할 수 있음.



 

4️⃣ DIP를 적용한 설계의 장점

새로운 결제 수단 추가가 간편

  • KakaoPay, NaverPay 등 새로운 결제 수단을 추가할 때 기존 코드를 수정할 필요 없음.

코드 유지보수성이 향상

  • PaymentService를 수정하지 않아도 결제 방식만 변경 가능.

스프링과 함께 사용하면 더욱 강력한 확장성

  • @Autowired@Qualifier를 활용하여 동적으로 결제 방식을 변경할 수 있음.


 

🔥 결론: DIP를 적용하면 유지보수성이 높아진다!

DIP를 위반하면?

  • 새로운 결제 수단이 추가될 때마다 PaymentService를 수정해야 함.
  • 유지보수가 어렵고 확장성이 떨어짐.

DIP를 준수하면?

  • 결제 서비스가 특정 결제 수단이 아니라 인터페이스만 의존하므로 확장성이 높아짐.
  • 유지보수가 쉬워지고, 새로운 결제 수단 추가가 간편해짐.
  • 스프링의 DI를 활용하면 더욱 유연한 결제 서비스 설계 가능!