이번 글은 “원리 설명”이 아니라, 실제 코드에서 반드시 부딪히는 상황들을 정리한다.
이 글의 목표
→ DI가 왜 필요하냐가 아니라, DI 쓰다 보면 생기는 문제를 어떻게 해결하냐
1. @Autowired는 핵심이 아니다
- DI의 핵심은 @Autowired가 아님
- 핵심은 “객체를 내가 만들지 않는다”
- @Autowired는 주입 방법 중 하나
1-1) 왜 요즘은 @Autowired를 잘 안 쓰나?
과거 코드:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}
요즘 권장 방식:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
차이의 핵심
- 생성자 주입
- 객체가 생성될 때 필수 의존성이 명확
- 테스트 코드 작성 쉬움
- 불변(final) 유지 가능
- 필드 주입(@Autowired)
- 테스트 어려움
- 의존성 숨김
- 런타임까지 오류가 안 보일 수 있음
그래서 요즘 정석은: 생성자 주입 기본, @Autowired는 거의 안 씀
(스프링 4.3 이후엔 생성자 하나면 @Autowired도 필요 없음)
2. 같은 타입 빈이 2개면 무조건 터진다
상황 예시: 구현체를 교체해야 할 때
- 개발 초반: 메모리
- 이후: JPA
- 또는 테스트/운영 분리
이 순간 같은 타입(UserRepository) 빈이 2개가 된다.
@Repository
class MemoryUserRepository implements UserRepository { }
@Repository
class JpaUserRepository implements UserRepository { }
그리고 서비스에서:
@Service
class UserService {
public UserService(UserRepository userRepository) { }
}
이 상태로 실행하면?
👉 실행 자체가 안 됨
NoUniqueBeanDefinitionException:
expected single matching bean but found 2
스프링 입장에선 당연하다. → “UserRepository 타입 빈이 2개인데 뭘 넣으라는 거지?”
3. 해결책 1: @Qualifier (이름으로 지정)
가장 직관적인 방법
@Service
class UserService {
public UserService(
@Qualifier("jpaUserRepository") UserRepository userRepository
) {
this.userRepository = userRepository;
}
}
- @Qualifier는 빈 이름으로 지정
- 빈 이름 기본값 = 클래스명 camelCase
장점 / 단점
- 장점: 명확함
- 단점: 이름 문자열에 의존 → 리팩토링에 약함
4. 해결책 2: @Primary (기본값 지정)
기본으로 선택될 빈을 하나 정함
@Repository
@Primary
class JpaUserRepository implements UserRepository { }
이제 서비스에서는:
@Service
class UserService {
public UserService(UserRepository userRepository) { }
}
👉 자동으로 @Primary 붙은 빈이 주입됨
언제 쓰나?
- “대부분 이 구현체를 쓰고, 가끔만 다른 걸 쓸 때”
- JPA 구현이 기본, 테스트용/대체 구현이 보조일 때
5. @Qualifier vs @Primary 정리
| 상황 | 추천 |
| 특정 빈을 정확히 지정 | @Qualifier |
| 기본 구현체 하나 정하기 | @Primary |
| 구조 단순 | @Primary |
| 전략이 명확 | @Qualifier |
6. 환경에 따라 빈을 바꾸고 싶을 때: @Profile
대표적인 요구
- 로컬: 메모리 저장소
- 운영: JPA + 실제 DB
- 테스트: Fake 구현체
예시
@Repository
@Profile("local")
class MemoryUserRepository implements UserRepository { }
@Repository
@Profile("prod")
class JpaUserRepository implements UserRepository { }
그리고 실행 시:
-Dspring.profiles.active=local
또는 application.yml:
spring:
profiles:
active: prod
결과
- local → 메모리 저장소 빈만 등록
- prod → JPA 저장소 빈만 등록
👉 같은 코드, 다른 환경
7. @Profile이 왜 중요한가?
- if/else로 환경 분기 ❌
- 코드 수정해서 환경 바꾸기 ❌
- 빈 조합 자체를 환경별로 다르게 구성 ⭕
즉, 환경 차이를 “설정”으로 흡수하는 게 스프링 방식
8. 자주 헷갈리는거 정리
Q. @Autowired 꼭 알아야 하나?
→ 이름은 알아야 함, 하지만 핵심은 생성자 주입
Q. @Qualifier 남발해도 되나?
→ 보통은 @Primary + 일부만 @Qualifier
Q. @Profile은 언제 쓰는 게 맞나?
→ DB, 외부 API, 저장소 구현체처럼
환경마다 달라지는 빈
9. 요약
스프링에서는 생성자 주입을 기본으로 DI를 구성하고, 같은 타입의 빈이 여러 개일 경우 @Primary나 @Qualifier로 주입 대상을 결정한다. 또한 @Profile을 사용해 실행 환경별로 서로 다른 빈 구성을 선택할 수 있다.
'백엔드' 카테고리의 다른 글
| [스프링 로드맵] C-2. 엔티티 설계 - @Entity와 기본 생성자의 의미 (0) | 2026.01.19 |
|---|---|
| [스프링 로드맵] C-1. JPA 큰 그림 - JPA vs Hibernate vs Spring Data JPA (0) | 2026.01.19 |
| [스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클 (1) | 2026.01.19 |
| [스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기 (1) | 2026.01.19 |
| [스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity (0) | 2026.01.19 |