📌의존성 주입(D.I)

: DI(Dependency Injection)는 의존관계 주입이라고 하며, 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입시켜주는 방식이다.

  1. DI를 사용하면 객체 간의 결합도가 낮아지고 유연한 코드를 작성할 수 있다.

  2. 스프링 프레임워크에서는 DI를 지원하는 IoC(Inversion of Control) 컨테이너를 제공

요구사항 : BurgerQueen에서는 상시 또는 불특정하게 할인 이벤트를 진행

  • 코드스테이츠 수강생에게 10% 할인
  • 20세 미만 청소년에게 500원 할인
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라는 구체적인 구현 클래스에 의존하고 있다.

이때, 의존성 주입을 통해 구현에 의존하도록 하는 것이 아니라, 역할에 의존하도록 해줘야 한다.

DiscountPolicy 인터페이스 생성

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`

DiscountCondition 인터페이스 생성

public interface DiscountCondition {
    boolean isSatisfied();
    void checkDiscountCondition();
    int applyDiscount(int price);
}
  • CozDiscountConditionKidDiscountCondition의 공통부분을 뽑아 인터페이스로 만든다.

Discount 클래스 생성

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) 메서드를 통해 할인 조건을 적용시킨다.

Order 클래스

public class Order {
    private Cart cart;
    private Discount discount;

    public Order(Cart cart, Discount discount) {
        this.cart = cart;
        this.discount = discount;
    }
  • 이제 Order 클래스에서도 할인 조건을 하나씩 직접 생성하고 정의하는게 아니라 Discount 인터페이스 타입을 정의하고 생성자를 통해 주입 받아 외부에서 의존 객체를 주입받게 된다.

📌단일 책임 원칙(SRP)

: 객체는 오직 하나의 책임만 맡아야 한다.

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 클래스에 프로그램 동작에 필요한 객체를 모두 생성하고 있다. 따라서, 프로그램 동작에 필요한 모든 객체를 생성하고, 의존 관계를 맺어주는 역할을 하는 클래스를 정의해준다.

AppConfigurer 클래스 생성

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());
    }
}
  • 프로그램 동작에 필요한 모든 객체를 생성해준다.

OrderApp에 의존성 주입

    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;
    }
  • 필요한 객체들을 정의해주고 그 객체들을 생성자를 통해 주입받을 수 있도록 생성자도 만들어준다.

Main 클래스

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();
    }
}
  • AppCOnfigurer 객체를 생성하고 이 객체를 OrderApp이 생성자 주입을 통해 사용하도록 만들어준다.

📌싱글톤 패턴(Singleton Pattern)

: 객체의 인스턴스가 오직 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
    }
}
  • 현재 코드는 cart()를 2번 호출함으로써 2개의 cart 인스턴스가 만들어지게 된다. 이를 싱글톤 패턴을 통해서 해결하려고 한다.
public class AppConfigurer {
    Cart cart = new Cart(productRepository(), menu());
	...

    public Cart cart() {
        return cart;
    }
	...
}
  • Cart 인스턴스가 단 한번만 생성될 수 있도록 하기 위해, AppConfigurer에 cart 필드를 정의한 다음, 바로 초기화를 진행

  • cart() 메서드는 이제 단순히 cart를 리턴해주기 때문에 몇 번이나 cart()를 호출해도 미리 초기화 시킨 cart 인스턴스 하나만 리턴해주게 된다.

0개의 댓글