DI(Dependency Injection) 맛보기
by mignon25실습 과제에서 적용했던 간단한 DI에 대해 익숙해지기 위해 정리해본다.
실습 과제는 간단한 주문 프로그램을 만드는 것이고, 여기서 DI가 적용된 부분은 할인 기능의 구현이다.
과제에서 요구하는 사항은 다음과 같다.
- BurgerQueen에서는 상시 또는 불특정하게 할인 이벤트를 진행합니다. 따라서, 마케팅 팀에서 할인 이벤트를 한다고 하면, 마케팅 팀이 요구하는 할인을 주문 시에 적용할 수 있어야 합니다.
- 기본적으로 아래의 할인 정책을 적용해야 합니다.
- 코드스테이츠 수강생에게 10% 할인
- 20세 미만 청소년에게 500원 할인
- 기본적으로 아래의 할인 정책을 적용해야 합니다.
할인 이벤트가 수시로 바뀔 수 있기 때문에 그 변화에 유연하게 대응할 수 있도록 프로그램을 설계해야 하는 것이다.
변화에 유연하게 대응할 수 있도록 한다는 것은,
프로그램의 로직이 있는 곳에서 변화의 요인을 직접 결정하지 않도록 하는 것이다.
그렇게 되면 변화가 발생할 때마다 내부의 로직을 매번 수정해주어야 한다.
최대한 외부의 외부에서 변화를 적용하도록 해주는 것이다. (AppConfigurer까지)
그러므로 프로그램의 내부 로직 클래스에서 할인 적용 관련 클래스의 인스턴스를 직접 생성하는 것이 아니라
할인 조건과 할인 정책을 주관하는 클래스가 생성될 때 그 조건을 외부에서 주입하도록 해야 한다.
그러기 위해서 필요한 것이 다형성과 추상화의 개념이다.
위의 조건에 대해 필요한 클래스는 다음과 같다.
- 할인 정책
- 고정 비율 할인 : FixedRateDiscountPolicy 클래스
- 고정 금액 할인 : FixedAmountDiscountPolicy 클래스
- 할인 조건
- 코드스테이츠 수강생 할인 : CozDiscountCondition 클래스
- 청소년 할인 : KidDiscountCondition 클래스
할인 정책 관련 클래스는 그 정책에 따라 할인된 금액을 계산해주는 메서드를 가지고, 할인 금액이나 할인율의 수치는 달라질 수 있으니 외부로부터 입력받아서 생성한다.
여러 구체적인 정책을 하나로 변수로 참조하기 위해 인터페이스를 통해 작성한다.
public interface DiscountPolicy {
int calculateDiscountedPrice(int price);
}
public class FixedRateDiscountPolicy implements DiscountPolicy {
private int discountRate;
public FixedRateDiscountPolicy(int discountRate) {
this.discountRate = discountRate;
}
public int calculateDiscountedPrice(int price) {
return price - (price * discountRate / 100);
}
}
public class FixedAmountDiscountPolicy implements DiscountPolicy {
private int discountAmount;
public FixedAmountDiscountPolicy(int discountAmount) {
this.discountAmount = discountAmount;
}
@Override
public int calculateDiscountedPrice(int price) {
return price - discountAmount;
}
}
할인 조건 클래스는 내부적으로 할인 정책을 갖지만, 이 또한 외부로부터 클래스가 생성될 때 입력받는 정책을 가지도록 한다.
그리고 할인 조건을 체크하는 메서드와, 그 결과를 저장하는 멤버변수, 그리고 할인금액을 적용하는 메서드(정책클래스의 할인금액 메서드 이용)를 가진다.
public interface DiscountCondition {
void checkDiscountCondition();
int applyDiscount(int price);
boolean isSatisfied();
}
public class CozDiscountCondition implements DiscountCondition {
boolean isSatisfied;
DiscountPolicy discountPolicy;
public CozDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Override
public boolean isSatisfied() {
return isSatisfied;
}
public void setSatisfied(boolean satisfied) {
isSatisfied = satisfied;
}
@Override
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);
}
@Override
public int applyDiscount(int price) {
return discountPolicy.calculateDiscountedPrice(price);
}
}
public class KidDiscountCondition implements DiscountCondition {
boolean isSatisfied;
DiscountPolicy discountPolicy;
public KidDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Override
public boolean isSatisfied() {
return isSatisfied;
}
public void setSatisfied(boolean satisfied) {
isSatisfied = satisfied;
}
@Override
public void checkDiscountCondition() {
Scanner scanner = new Scanner(System.in);
System.out.println("나이가 어떻게 되십니까?");
int input = Integer.parseInt(scanner.nextLine());
setSatisfied(input < 20);
}
@Override
public int applyDiscount(int price) {
return discountPolicy.calculateDiscountedPrice(price);
}
}
이후 이 정책과 조건 클래스를 가지고 실제로 할인을 적용하게 될 Discount 클래스는,
여러 개의 할인 조건을 중첩 적용할 수 있어야 하기 때문에 DiscountCondition을 배열로 확인한다.
모든 조건에 대해 checkDiscountCondition()을 통해 할인 조건을 체크하고
할인 조건에 해당한다면 할인을 적용한 금액을 반환하는 메서드를 가진다.
public class Discount {
DiscountCondition[] discountConditions;
public Discount(DiscountCondition[] discountConditions) {
this.discountConditions = discountConditions;
}
public int discount(int price) {
int discountedPrice = price;
for(DiscountCondition discountCondition : discountConditions) {
discountCondition.checkDiscountCondition();
if(discountCondition.isSatisfied()) discountedPrice = discountCondition.applyDiscount(discountedPrice);
}
return discountedPrice;
}
}
그리고 주문하는 로직에서는 Discount 클래스를 부른 뒤 discount() 함수만 호출하면 된다.
public class Order {
private Cart cart;
private Discount discount; // 추가
public Order(Cart cart, Discount discount) {
this.cart = cart;
this.discount = discount; // 추가
}
public void makeOrder() {
int totalPrice = cart.calculateTotalPrice();
int finalPrice = discount.discount(totalPrice); // 추가
System.out.println("[📣] 주문이 완료되었습니다. ");
System.out.println("[📣] 주문 내역은 다음과 같습니다.");
System.out.println("-".repeat(60));
// 상품 상세 출력
cart.printCartItemDetails();
System.out.println("-".repeat(60));
System.out.printf("금액 합계 : %d원\n", totalPrice);
System.out.printf("할인 적용 금액 : %d원\n", finalPrice); // 추가
}
}
Discount에 대한 조건은 어디에서 넣어주는 것일까?
주문에 대한 메인 로직은 OrderApp이다.
그렇지만 OrderApp에서 필요한 클래스들도 생성자를 통해 Main 클래스에서 주입해주고 있다.
public class OrderApp {
private ProductRepository productRepository;
private Menu menu;
private Cart cart;
private 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에서도 직접 생성하지 않는다.
설정 관련된 정보를 담고 있는 클래스인 AppConfigurer 클래스에서 모든 구체적인 클래스를 생성하고 있고,
AppConfigurer가 반환하는 결과물을 가지고 프로그램을 실행하는 것이다.
결과적으로 이제 모든 변경사항은 AppConfigurer에서만 지정해주면 되고,
변경사항이 생기더라도 프로그램 내부 로직은 변경되지 않아도 되는 것이다.
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();
}
}
public class AppConfigurer {
private ProductRepository productRepository = new ProductRepository();
private Menu menu = new Menu(productRepository().getAllProducts());
private Cart cart = new Cart(productRepository(), menu());
public ProductRepository productRepository() {
return productRepository;
}
public Menu menu() {
return menu;
}
public Cart cart() {
return cart;
}
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());
}
}
이렇듯, 객체가 자신이 의존할 객체를 스스로 만들도록 하는 것이 아니라 외부에서 주입해주도록 하는 것을 의존성 주입이라고 한다.
구현에 의존하도록 하지 않고 역할에 의존하도록 역할과 구현을 분리하여 변화와 확장에 유연하도록 프로그램을 설계하는 것이다.
'Java' 카테고리의 다른 글
| [Collection Framework] Map<K, V> (0) | 2023.03.06 |
|---|---|
| 열거형 (Enum) (0) | 2023.03.06 |
| 제어자 - static, final, abstract (0) | 2023.02.28 |
| 추상화 - 추상클래스와 인터페이스 (0) | 2023.02.28 |
| 다형성(polymorphism) (0) | 2023.02.26 |
블로그의 정보
Mignon'S Dev Log
mignon25