개발을 하다 보면 여러 클래스들이 서로 얽혀있는 복잡한 구조를 만나게 된다. 이때 Spring의 의존성 주입은 이런 복잡함을 깔끔하게 정리해주는 핵심 기술이다.
❓ 정의
의존성 주입(DI)은 클래스가 필요로 하는 의존 객체를 직접 생성하지 않고, 외부에서 주입받아 사용하는 설계 패턴이다. Spring Framework에서는 IoC 컨테이너가 이 역할을 담당하여 객체의 생성과 의존성 관리를 자동으로 처리해준다.
쉽게 말해, 클래스 A가 클래스 B를 사용해야 할 때 A가 직접 B를 new로 생성하는 것이 아니라, Spring이 알아서 B 객체를 만들어서 A에게 전달해주는 방식이다. 예를 들면 음식점에서 요리사가 직접 재료를 구하러 다니는 것이 아니라, 누군가가 필요한 재료를 미리 준비해서 가져다주는 것과 같다.
▶ 기본 사용 예시
강한 결합의 문제점을 먼저 살펴보자.
public class OrderService {
private final PaymentService paymentService;
public OrderService() {
this.paymentService = new KakaoPayService();
}
}
이렇게 작성하면 OrderService는 KakaoPayService에 강하게 의존하게 되어, 다른 결제 서비스로 변경하려면 코드를 직접 수정해야 한다. Spring DI를 사용하면 아래와 같이 개선할 수 있다.
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
❗ 사용이유
✅ 결합도 감소
의존성 주입을 사용하면 클래스 간의 결합도가 상당히 낮아진다. 예를 들어, A 클래스가 B 클래스의 구체적인 구현이 아닌 인터페이스에만 의존하게 되면, B의 구현이 변경되어도 A는 영향을 받지 않는다. 이는 전기 제품이 특정 브랜드의 콘센트가 아닌 표준 규격의 콘센트만 알면 되는 것과 같은 원리다.
✅ 테스트 용이
DI를 사용하면 테스트할 때 실제 의존 객체 대신 Mock 객체를 주입할 수 있어 단위 테스트가 훨씬 쉬워진다. 실제 데이터베이스 대신 가짜 데이터베이스를 사용해서 테스트하는 것처럼, 외부 의존성 없이 독립적인 테스트가 가능하다.
✅ 유지보수성 증가
코드의 변경이 다른 부분에 미치는 영향을 최소화할 수 있어 유지보수가 훨씬 수월하다. 새로운 기능을 추가하거나 기존 기능을 수정할 때도 관련된 클래스만 수정하면 되므로 편리하다.
💥 주의
1️⃣ 순환 참조 문제
A가 B를 참조하고 B가 다시 A를 참조하는 순환 참조가 발생할 수 있다.
-> 생성자 주입을 사용하면 컴파일 시점에 이런 문제를 발견할 수 있다.
2️⃣ 동일 타입 빈 충돌
같은 타입의 빈이 여러 개 등록되어 있으면 어떤 것을 주입할지 Spring이 판단하지 못한다.
-> @Qualifier나 @Primary 어노테이션을 사용해서 명시적으로 지정해야 한다.
💠 예시
1️⃣ 생성자 주입
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
2️⃣ 세터 주입
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
3️⃣ 필드 주입
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
▶ 사용 예시
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
앞으로 Spring을 사용할 때는 항상 의존성 주입을 염두에 두고 설계하는 습관을 기르는 것이 중요할 것 같다. 그러면 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있을 것이다!
[참고]
'Java' 카테고리의 다른 글
Spring Security와 ThreadLocal (0) | 2025.08.29 |
---|