Spring에서 의존성 주입은 객체 간 결합도를 낮추고 테스트와 유지보수를 쉽게 하기 위한 핵심 원칙입니다.
대표적으로 생성자 주입, 필드 주입, 세터 주입이 있으며, 각 방식은 코드 구조, 장단점이 분명히 다릅니다.
@Component
public class RamenCook {
private final Ingredient ingredient;
private final CookingTool tool;
@Autowired
public RamenCook(Ingredient ingredient, CookingTool tool) {
this.ingredient = ingredient;
this.tool = tool;
}
public void cook() {
ingredient.prepare();
tool.use();
}
}
final 필드 사용 가능 → 불변성 보장
컴파일 타임에 주입이 안될 시, 감지 할 수 있다.
@Test
void testCook() {
Ingredient mockIngredient = mock(Ingredient.class);
CookingTool mockTool = mock(CookingTool.class);
RamenCook ramenCook = new RamenCook(mockIngredient, mockTool); // ✅ 직접 주입
ramenCook.cook(); // ✅ 정상 동작
}
@Component
public class RamenCook {
@Autowired
private Ingredient ingredient;
@Autowired
private CookingTool tool;
}
public class RamenCookTest {
@Test
void testCook() {
RamenCook ramenCook = new RamenCook(); // 직접 생성
ramenCook.cook(); // ❌ NullPointerException 발생!
}
}
Spring 컨테이너 없이는 동작 불가
예시 Spring이 없기 때문에:
@Autowired는 Spring IoC 컨테이너가 작동할 때만 의미가 있음
=> 위 예제에서는 new RamenCook()으로 직접 객체를 생성했기 때문에 Spring은 관여하지 않음
ingredient, tool은 여전히 null 상태
결과적으로 cook() 메서드 실행 시 NullPointerException 발생!
=> 결국 NullPointerException 발생 가능
@Component
public class RamenCook {
private Ingredient ingredient;
private CookingTool tool;
@Autowired
public void setIngredient(Ingredient ingredient) {
this.ingredient = ingredient;
}
@Autowired
public void setTool(CookingTool tool) {
this.tool = tool;
}
public String cook() {
return ingredient.prepare() + " + " + tool.use();
}
}
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class RamenCookTest {
@Test
void testCook_withSetterInjection() {
// 1. Mock 객체 생성
Ingredient mockIngredient = mock(Ingredient.class);
CookingTool mockTool = mock(CookingTool.class);
// 2. Mock 동작 정의
when(mockIngredient.prepare()).thenReturn("면 준비 완료");
when(mockTool.use()).thenReturn("냄비 가열 완료");
// 3. 테스트 대상 객체 생성
RamenCook ramenCook = new RamenCook();
// 4. Setter로 Mock 객체 주입
ramenCook.setIngredient(mockIngredient);
ramenCook.setTool(mockTool);
// 5. 테스트 실행 및 결과 검증
String result = ramenCook.cook();
assertEquals("면 준비 완료 + 냄비 가열 완료", result);
}
}
초기화 이후 변경 가능 (유연하지만 위험성 존재)
객체 생성 시점에는 완전한 상태가 아닐 수 있음
Setter 주입 방식에서 객체를 만들자마자 사용할 경우, 아직 의존성이 주입되지 않았을 수도 있어서 문제가 생길 수 있다는 뜻
즉, 객체가 생성됐다고 해서 바로 사용할 수 있는 상태는 아닐 수도 있다는 의미
But, 유연성 높음!
👉 사용 예: 의존성을 나중에 바꿔치기 가능 (Setter 주입의 유연성)
RamenCook cook = new RamenCook();
cook.setIngredient(new Ingredient("라면")); // 처음엔 라면 재료
cook.setTool(new CookingTool("냄비")); // 처음엔 냄비 사용
cook.cook(); // 🍜 라면 요리
cook.setIngredient(new Ingredient("우동")); // 나중에 우동 재료로 변경
cook.setTool(new CookingTool("전기포트")); // 도구도 전기포트로 교체
cook.cook(); // 🍜 우동 요리
객체를 만든 다음에도 필요에 따라 설정을 바꿀 수 있습니다.
어떤 의존성은 "필수는 아니고, 있으면 좋음" => 선택적 주입에 적합합니다.
설정성 객체, 조건부 기능 컴포넌트 등에 많이 활용됩니다.
| 구분 | 생성자 주입 | 필드 주입 | 세터 주입 |
|---|---|---|---|
| 코드 구조 | 생성자에서 주입 | 필드에 직접 주입 | Setter 메서드를 통해 주입 |
| 불변성 | ✅ final 사용 가능 | ❌ 불가 | ❌ 불가 |
| 테스트 용이성 | ✅ 매우 좋음 | ❌ 어려움 (리플렉션 필요) | ⚠️ 중간 정도 |
| 필수 의존성 누락 | 컴파일 타임에 감지 가능 | 런타임까지 감지 어려움 | 주입 누락 시 NPE 가능성 있음 |
| 유연성 | 제한적 (불변 구조, final) | 낮음 | 높음 (선택적 의존성에 적합) |
| 주로 사용되는 곳 | 일반적인 비즈니스 로직 | 예외적 상황, 프레임워크 내부 | 설정용 Bean, 조건부 구성 요소 |