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 + "원 결제 완료!");
}
}
✅ 문제점 분석
- PaymentService가 CardPayment 구현체를 직접 사용 → 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를 활용하면 더욱 유연한 결제 서비스 설계 가능!