하위 태스크 1

전통적인 방식 코드 분석

강한 결합 코드의 문제점 파악 및 분석

한 클래스가 변경되면 다른 클래스도 바뀌어야 하는 관계를 강한 결합이라고 한다. 강한 결합은 연쇄적인 코드 수정을 동반한다.

CoffeeMaker 클래스는 EspressoMachine 클래스에 의존한다. EspressoMachine 클래스의 인스턴스는 CoffeeMaker 클래스의 생성자에서 생성된다.

public class CoffeeMaker {
	private EspressoMachine espressoMachine;
	
	public CoffeeMaker() {
		this.espressoMachine = new EspressoMachine();
	}
	
	public void makeCoffee() {
		System.out.println(espressoMachine.brew());
	}
}

CoffeeMaker 클래스가 의존하는 클래스를 DripCoffeeMachine 클래스로 바꾸려면, CoffeeMaker 클래스의 내용을 수정해야 한다.

public class CoffeeMaker {
	private DripCoffeeMachine dripCoffeeMachine;
	
	public CoffeeMaker() {
		this.dripCoffeeMachine = new DripCoffeeMachine();
	}
	
	public void makeCoffee() {
		System.out.println(dripCoffeeMachine.brew());
	}
}

CoffeeMaker 클래스의 의존성이 변경될 때마다 CoffeeMaker 클래스가 수정되어야 한다. 따라서 두 클래스는 강한 결합을 가진다.

하위 태스크 2

인터페이스 설계

CoffeeMachine 인터페이스 구조 이해

느슨한 결합을 구현하기 위해 인터페이스를 사용한다. brew 메서드를 가진 CoffeeMachine 인터페이스를 선언한다.

public interface CoffeeMachine {
    String brew();
}

하위 태스크 3

Setter Injection 구현

Setter 메서드를 통한 의존성 주입 구현

CoffeeMaker 클래스가 CoffeeMachine 인터페이스에 의존하도록 수정한다. 의존성 주입을 위한 setCoffeeMachine 클래스를 추가한다.

public class CoffeeMaker {
	private CoffeeMachine coffeeMachine;
	
	setCoffeeMachine(CoffeeMachine coffeeMachine) {
		this.coffeeMachine = coffeeMachine;
	}
	
	public void makeCoffee() {
		System.out.println(coffeeMachine.brew());
	}
}

하위 태스크 4

스프링 프로젝트 설정

Spring Boot 프로젝트에 필요한 의존성 추가

Main 클래스에서 CoffeeMaker 인스턴스를 생성하고, 의존성 주입한다.

public class Main {  
    public static void main(String[] args) {  
        CoffeeMaker coffeeMaker = new CoffeeMaker();  
        coffeeMaker.setCoffeeMachine(new DripCoffeeMachine());  
        // coffeeMaker.setCoffeeMachine(new EspressoMachine());  
        coffeeMaker.makeCoffee();  
    }  
}

하위 태스크 5

@Component 어노테이션 적용

클래스에 @Component 추가하여 Bean 등록

스프링 부트는 자동으로 @Component가 사용된 클래스를 스캔해 객체로 만든다. 만들어진 객체는 스프링 컨테이너에서 관리된다. 스프링 부트가 생성하고 스프링 컨테이너에서 관리되는 객체는 스프링 빈 객체라고 한다.

CoffeeMaker 클래스에 @Component 애노테이션을 추가한다.

@Component
public class CoffeeMaker {
	private CoffeeMachine coffeeMachine;
	
	setCoffeeMachine(CoffeeMachine coffeeMachine) {
		this.coffeeMachine = coffeeMachine;
	}
	
	public void makeCoffee() {
		System.out.println(coffeeMachine.brew());
	}
}

EspressoMachine 클래스와 DripCoffeeMachine 클래스에도 @Component 애노테이션을 추가한다.

@Component
public class EspressoMachine implements CoffeeMachine {
    @Override
    public String brew() {
        return "Brewing coffee with Espresso Machine";
    }
}
@Component
public class DripCoffeeMachine implements CoffeeMachine {
    @Override
    public String brew() {
        return "Brewing coffee with Drip Coffee Machine";
    }
}

하위 태스크 6

@Autowired 자동 주입

@Autowired를 통한 자동 의존성 주입 구현

CoffeeMaker 클래스의 coffeeMachine 멤버에 @Autowired 애노테이션을 추가한다. 스프링 부트가 의존성을 주입하는 제어의 역전으로 인해 setCoffeeMachine 메서드는 필요가 없다. 따라서 제거한다.

@Component
public class CoffeeMaker {
	@Autowired
	private CoffeeMachine coffeeMachine;
	
	public void makeCoffee() {
		System.out.println(coffeeMachine.brew());
	}
}

스프링 부트를 시작하면 다음과 같은 오류가 발생한다. 주입될 수 있는 의존성이 여럿이기 때문이다. 의존성의 우선 순위를 정의하거나, 특정 의존성을 주입하도록 명시하여 해결할 수 있다. 하위 태스크 8에서 해결한다.

Field coffeeMachine in com.example.demo.CoffeeMaker required a single bean, but 2 were found:
	- dripCoffeeMachine: defined in file [C:\Users\admin\Downloads\hanbit-springboot\ioc\build\classes\java\main\com\example\demo\DripCoffeeMachine.class]
	- espressoMachine: defined in file [C:\Users\admin\Downloads\hanbit-springboot\ioc\build\classes\java\main\com\example\demo\EspressoMachine.class]

하위 태스크 7

Constructor Injection 구현

생성자를 통한 의존성 주입 구현

하위 태스크 8

@Primary/@Qualifier 실습

Bean 선택 방법 학습 및 적용

클래스에 @Primary 애너테이션을 적용하여 의존성 주입 시 우선 순위를 높일 수 있다. 다음 예제는 EspressoMachine 클래스에 @Primary 애너테이션을 적용한다.

@Component  
@Primary  
public class EspressoMachine implements CoffeeMachine {
	// ...
}

스프링 부트를 시작하면 Brewing coffee with Espresso Machine이 출력된다.

클래스에 @Component 애너테이션을 적용할 때 식별자를 함께 전달할 수 있다. 의존성이 주입될 멤버에 @Qualifier 애너테이션을 적용하여 특정 스프링 빈을 주입할 수 있다. 다음 예제는 DripCoffeeMachine 클래스의 @Component에 스프링 빈 식별자를 함께 전달한다. 그리고 CoffeeMaker 클래스의 coffeeMachine 멤버에 @Qualifier 애노테이션을 적용한다.

@Component("dripCoffeeMachine")  
public class DripCoffeeMachine implements CoffeeMachine {  
    // ...
}
@Component
public class CoffeeMaker {
    @Autowired
    @Qualifier("dripCoffeeMachine")
    private CoffeeMachine coffeeMachine;
 
    // ...
}

스프링 부트를 시작하면 Brewing coffee with Drip Coffee Machine이 출력된다.

하위 태스크 9

주입 방식 비교 분석

Setter Injection vs Constructor Injection 비교 문서 작성

CoffeeMaker 클래스를 Constructor Injection 방식으로 변경한다.

public class CoffeeMaker {
	private final CoffeeMachine coffeeMachine;
	
	public CoffeeMaker(CoffeeMachine coffeeMachine) {
		this.coffeeMachine = coffeeMachine;
	}
	
	public void makeCoffee() {
		System.out.println(coffeeMachine.brew());
	}
}

Setter Injection 방식과 Constructor Injection 방식의 비교 표는 다음과 같다.

Setter InjectionConsturctor Injection
불변성 보장어려움final 키워드에 의한 보장 가능
필수 의존성 확인어려움컴파일 타임에 검사 가능
순환 참조 방지런타임에 탐지 가능컴파일 타입에 탐지 가능
선택적 의존성 처리가능불가능 (모든 의존성이 필수)