: DI(Dependency Injection)는 의존관계 주입이라고 하며, 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입시켜주는 방식이다.
DI를 사용하면 객체 간의 결합도가 낮아지고 유연한 코드를 작성할 수 있다.
스프링 프레임워크에서는 DI를 지원하는 IoC(Inversion of Control) 컨테이너를 제공
요구사항 : BurgerQueen에서는 상시 또는 불특정하게 할인 이벤트를 진행
public class CozDiscountCondition {
private FixedRateDiscountPolicy fixedRateDiscountPolicy = new FixedRateDiscountPolicy(10);
private boolean isSatisfied;
public boolean isSatisfied() {
return isSatisfied;
}
private void setSatisfied(boolean satisfied) {
isSatisfied = satisfied;
}
public void checkDiscountCondition() {
Scanner scanner = new Scanner(System.in);
System.out.println("코드스테이츠 수강생이십니까? (1)_예 (2)_아니오");
String input = scanner.nextLine();
if (input.equals("1")) setSatisfied(true);
else if (input.equals("2")) setSatisfied(false);
}
public int applyDiscount(int price) {
return fixedRateDiscountPolicy.calculateDiscountedPrice(price);
}
만약 코드스테이츠 수강생에게 비율 할인이 아닌 고정 금액 할인을 적용시키고자 한다면 private FixedRateDiscountPolicy fixedRateDiscountPolicy = new FixedRateDiscountPolicy(10);
이부분과 applyDiscount
메서드를 수정해주어야 한다. 즉, CozDiscountCondition
은 할인 정책이라는 역할이 아니라, FixedRateDiscountPolicy
라는 구체적인 구현 클래스에 의존하고 있다.
이때, 의존성 주입을 통해 구현에 의존하도록 하는 것이 아니라, 역할에 의존하도록 해줘야 한다.
public interface DiscountPolicy {
int calculateDiscountedPrice(int price);
}
calculateDiscountedPrice
메서드를 추상화시킨다.public class CozDiscountCondition {
private DiscountPolicy discountPolicy;
public CozDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
...
}
public void makeOrder() {
CozDiscountCondition cozDiscountCondition = new CozDiscountCondition(new FixedRateDiscountPolicy(10));
...
}
CozDiscountCondition
은 스스로 FixedRateDiscountPolicy
의 인스턴스를 생성하지 않는다. 다시 말해, 자신이 사용할 객체를 스스로 결정하지 않고 생성자를 통해 DiscountPolicy
의 역할을 수행하는 객체를 외부로부터 주입받아 사용하게 된다.그런데 할인 조건을 변경하게 되면 Order
클래스를 변경해주어야 하는 문제가 여전히 남아있다. Order
또한, 의존성 주입을 통해 외부에서 객체를 주입 받도록 해줘야 한다.과
KidDiscountCondition`
public interface DiscountCondition {
boolean isSatisfied();
void checkDiscountCondition();
int applyDiscount(int price);
}
CozDiscountCondition
과 KidDiscountCondition
의 공통부분을 뽑아 인터페이스로 만든다.public class Discount {
DiscountCondition[] discountConditions;
public Discount(DiscountCondition[] discountConditions) {
this.discountConditions = discountConditions;
}
public int discount(int price) {
int discountPrice = price;
for (DiscountCondition discountCondition : discountConditions) {
discountCondition.checkDiscountCondition();
if (discountCondition.isSatisfied()) {
discountPrice = discountCondition.applyDiscount(discountPrice);
}
}
return price;
}
}
discount(int price)
메서드를 통해 할인 조건을 적용시킨다.public class Order {
private Cart cart;
private Discount discount;
public Order(Cart cart, Discount discount) {
this.cart = cart;
this.discount = discount;
}
Order
클래스에서도 할인 조건을 하나씩 직접 생성하고 정의하는게 아니라 Discount
인터페이스 타입을 정의하고 생성자를 통해 주입 받아 외부에서 의존 객체를 주입받게 된다.: 객체는 오직 하나의 책임만 맡아야 한다.
public class OrderApp {
ProductRepository productRepository = new ProductRepository();
Product[] products = productRepository.getAllProducts();
Menu menu = new Menu(products);
Cart cart = new Cart(productRepository, menu);
Order order = new Order(cart, new Discount(new DiscountCondition[] {
new CozDiscountCondition(new FixedRateDiscountPolicy(10)),
new KidDiscountCondition(new FixedAmountDiscountPolicy(500))
}));
지금 OrderApp
클래스에 프로그램 동작에 필요한 객체를 모두 생성하고 있다. 따라서, 프로그램 동작에 필요한 모든 객체를 생성하고, 의존 관계를 맺어주는 역할을 하는 클래스를 정의해준다.
public class AppConfigurer {
public ProductRepository productRepository() {
return new ProductRepository();
}
public Menu menu() {
return new Menu(productRepository().getAllProducts());
}
public Cart cart() {
return new Cart(productRepository(), menu());
}
public Discount discount() {
return new Discount(
new DiscountCondition[]{
new CozDiscountCondition(new FixedRateDiscountPolicy(10)),
new KidDiscountCondition(new FixedAmountDiscountPolicy(500))
});
}
public Order order() {
return new Order(cart(), discount());
}
}
ProductRepository productRepository;
Menu menu;
Cart cart;
Order order;
public OrderApp(ProductRepository productRepository, Menu menu, Cart cart, Order order) {
this.productRepository = productRepository;
this.menu = menu;
this.cart = cart;
this.order = order;
}
public class Main {
public static void main(String[] args) {
AppConfigurer appConfigurer = new AppConfigurer();
OrderApp orderApp = new OrderApp(
appConfigurer.productRepository(),
appConfigurer.menu(),
appConfigurer.cart(),
appConfigurer.order()
);
orderApp.start();
}
}
: 객체의 인스턴스가 오직 1개만 생성되는 패턴
public class Main {
public static void main(String[] args) {
...
OrderApp orderApp = new OrderApp(
appConfigurer.productRepository(),
appConfigurer.menu(),
appConfigurer.cart(), // 🚨 cart() 호출 1
appConfigurer.order()
);
...
}
}
public class AppConfigurer {
...
public Order order() {
return new Order(cart(), discount()); // 🚨 cart() 호출 2
}
}
public class AppConfigurer {
Cart cart = new Cart(productRepository(), menu());
...
public Cart cart() {
return cart;
}
...
}
Cart 인스턴스가 단 한번만 생성될 수 있도록 하기 위해, AppConfigurer
에 cart 필드를 정의한 다음, 바로 초기화를 진행
cart()
메서드는 이제 단순히 cart를 리턴해주기 때문에 몇 번이나 cart()를 호출해도 미리 초기화 시킨 cart 인스턴스 하나만 리턴해주게 된다.