CH 01에서 큰 그림을 잡았다면, 이번 장은 실제 코드로 파헤칩니다. "제어권이 넘어간다", "의존 객체를 주입받는다"가 구체적으로 어떤 코드인지 눈으로 확인해요. 자바를 아는 태호님껜 술술 붙습니다.
new 하는 강한 결합과 주입받는 느슨한 결합의 차이를 코드로 구분해요.IoC(Inversion of Control, 제어의 역전)는 객체의 생성·연결·생명주기를 관리하는 제어권이 개발자에서 스프링(컨테이너)으로 넘어가는 것을 뜻해요. 원래 개발자가 하던 "언제 무엇을 만들고 연결할지"를 스프링이 대신 쥐어요. 이건 원칙(개념)이에요.
DI(Dependency Injection, 의존성 주입)는 그 IoC를 실제로 실현하는 방법이에요. 어떤 객체가 필요로 하는 다른 객체(의존성)를 스스로 new 하지 않고, 외부(스프링)가 만들어서 넣어주는(주입) 것이에요. 즉 DI는 IoC의 구체적인 구현 수단이에요.
OrderService가 저장을 위해 OrderRepository를 필요로 하면, 그 저장소가 곧 의존성이에요.new vs 주입)를 눈에 익혀두면, 다음 장의 빈·컨테이너가 "아 그래서 이렇게 굴러가는구나"로 자연스레 이어집니다.new 하지 않아도, 컨테이너가 객체(Bean)를 만들어 두고 필요한 곳에 넣어줘요(제어의 역전 + 의존성 주입).OrderService가 필요한 OrderRepository를 스스로 new로 생성해요. 그러면 OrderService는 특정 구현 클래스에 딱 붙어버려요. 나중에 다른 저장소로 바꾸려면 이 클래스 코드를 직접 고쳐야 해요. 이게 강한 결합(tight coupling)이에요.
public class OrderService {
// 의존 객체를 내가 직접 생성 → 강한 결합
private OrderRepository repo = new OrderRepository();
public void order() {
repo.save();
}
}
OrderService는 new를 하지 않아요. 필요한 객체를 생성자로 "받기만" 하고, 실제 객체는 스프링이 만들어 넣어줘요(주입). OrderService는 어떤 구현이 들어오는지 몰라도 되니 느슨한 결합(loose coupling)이 돼요.
@Service
public class OrderService {
private final OrderRepository repo;
// 의존 객체를 외부(스프링)가 주입해줌
public OrderService(OrderRepository repo) {
this.repo = repo;
}
public void order() {
repo.save();
}
}
차이의 핵심: 아래 코드에는 new OrderRepository()가 없어요. 만드는 책임을 스프링에 넘겼기 때문이에요. 이게 바로 IoC(제어의 역전)를 DI로 실현한 모습이에요.
new 하면, 구현을 바꾸거나 테스트할 때 가짜 객체로 갈아끼우기가 어려워요. 코드가 특정 클래스에 못 박혀 있으니까요. DI는 이 "갈아끼우기"를 주입 지점만 바꿔서 쉽게 만들어줘요.final로 둘 수 있어 한 번 주입되면 바뀌지 않고(불변), 필수 의존성이 명확해요. 생성자로 넣으니 테스트할 때 직접 객체를 넣어 만들기도 쉬워요.
@Service
public class OrderService {
private final OrderRepository repo; // 불변(final) 가능
// 생성자로 필수 의존성을 받음 → 명확 + 테스트 쉬움
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}
@Autowired를 붙여 바로 주입받아요. 코드가 짧고 간편하지만, 필드를 final로 못 만들고(불변에 불리), 스프링 없이 테스트할 때 가짜 객체를 넣기가 번거로워요.
@Service
public class OrderService {
@Autowired
private OrderRepository repo; // 간편하지만 테스트·불변에 불리
}
@Autowired를 붙여 주입받아요. 선택적(있어도 없어도 되는) 의존성이나 나중에 바꿀 수 있어야 하는 경우에 어울려요. 다만 주입 뒤에 값이 바뀔 수 있어(불변 아님) 필수 의존성엔 덜 적합해요.
@Service
public class OrderService {
private OrderRepository repo;
@Autowired
public void setRepo(OrderRepository repo) {
this.repo = repo;
}
}
final로 만들어 불변이 되고, ② 생성자에 매개변수가 있으니 필수 의존성이 한눈에 보이며, ③ 스프링 없이 순수 자바로 new OrderService(가짜repo) 하면 되니 단위 테스트가 쉬워요. 그래서 실무 표준은 생성자 주입이에요.@Autowired는 스프링에게 "여기에 맞는 빈을 찾아서 넣어줘"라고 표시하는 어노테이션이에요. 스프링은 컨테이너 안에서 타입(type)에 맞는 빈을 찾아 자동으로 주입해줘요. 개발자가 new로 만들거나 직접 연결하지 않아도 돼요.
new 하지 않아도 스프링이 대신 만들어 두고, 필요한 곳에 주입해줘요. (다음 장에서 더 깊이 다뤄요.)@로 시작하는 표식이에요. 코드에 붙여 "이건 이런 역할이야"라고 스프링에게 알려줘요. (예: @Service, @Autowired)@Autowired를 붙이지 않아도 자동으로 주입돼요. 그래서 요즘 생성자 주입 코드는 어노테이션 없이 아주 깔끔해요.
@Service
public class OrderService {
private final OrderRepository repo;
// 생성자가 하나뿐이므로 @Autowired 생략 가능!
public OrderService(OrderRepository repo) {
this.repo = repo;
}
}
참고: 롬복(Lombok)의 @RequiredArgsConstructor를 쓰면 final 필드용 생성자를 자동 생성해줘서, 생성자 코드조차 안 써도 돼요. (실무에서 자주 봐요.)
@Qualifier로 이름을 지정하거나 @Primary로 우선순위를 주는 식으로 골라줘요. (지금은 "타입으로 찾는다"만 기억하면 충분해요. 뒷 장에서 다시 나와요.)| 이점 | 설명 |
|---|---|
| 결합도 ↓ | 구현 클래스에 직접 new로 묶이지 않아, 구현을 교체하기 쉬워요. |
| 단위 테스트 쉬움 | 실제 객체 대신 가짜(Mock) 객체를 주입해서 로직만 콕 집어 테스트해요. |
| 유연성 | 연결(설정)을 한 곳에서 관리하니, 조합을 바꾸기가 쉬워요. |
| 관심사 분리 | "객체를 만드는 일"과 "객체를 쓰는 일"이 나뉘어 코드가 깔끔해져요. |
public interface OrderRepository {
void save();
}
@Service
public class OrderService {
private final OrderRepository repo; // 인터페이스에 의존!
public OrderService(OrderRepository repo) { // 어떤 구현이든 주입 가능
this.repo = repo;
}
}
테스트에서는 new OrderService(new FakeRepository())처럼 가짜 구현을 넣어 스프링 없이도 OrderService의 로직만 검증할 수 있어요. 이게 "테스트가 쉬워진다"의 실체예요.
new는 강한 결합, 주입받으면 느슨한 결합이에요.