참고 자료
개념
-
의존성 주입
딸깍를 위한 애노테이션- 필요한 의존성이
Bean으로Spring Container에서 관리되고 있으면 자동으로 주입함.
- 필요한 의존성이
-
이를 통해 직접
new키워드를 통해 개발자가 관리하지 않아도 되어 편리함 제공.- 특히 테스트 시 매우 용이
@Component
public class OrderService {
@Autowired
private PaymentService paymentService;
// Spring이 자동으로 주입 → 낮은 결합도, 테스트 용이
}사용할 수 있는 위치
- 생성자 (Constructor)
- 필드 (Field)
- 세터 메서드 (Setter Method)
- 일반 설정 메서드 (Config Method)
1. 생성자 주입 (Constructor Injection)
기본 예제
@Component
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
@Autowired // 생성자가 하나만 있으면 생략 가능 (Spring 4.3+)
public OrderService(PaymentService paymentService,
InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public void createOrder(Order order) {
inventoryService.checkStock(order);
paymentService.process(order);
}
}- 스프링 4.3 이후로는 생성자가 하나일 때
Autowired생략 가능.
생성자 주입의 장점
- 장점 1: final 키워드 → 불변성 보장
- 장점 2: 테스트 시 Mock 객체 쉽게 주입
- 장점 3: 순환 참조를 컴파일 타임에 감지 가능
- 장점 4: 모든 의존성이 생성자에 명시 → 가독성 향상
@Service
public class ShoppingCartService {
private final UserService userService;
private final ProductService productService;
private final PriceCalculator priceCalculator;
public ShoppingCartService(UserService userService,
ProductService productService,
PriceCalculator priceCalculator) {
this.userService = userService;
this.productService = productService;
this.priceCalculator = priceCalculator;
}
}테스트 시의 이점
→ 굳이 Mock 를 안써도(즉, 테스트용 객체를 별도로 주입할 때에도) 편리함.
class ShoppingCartServiceTest {
@Test
void testCalculateTotal() {
// Mock 객체를 생성자로 쉽게 주입
UserService mockUserService = mock(UserService.class);
ProductService mockProductService = mock(ProductService.class);
PriceCalculator mockCalculator = mock(PriceCalculator.class);
ShoppingCartService service = new ShoppingCartService(
mockUserService,
mockProductService,
mockCalculator
);
// 테스트 수행...
}
}2. 필드 주입 (Field Injection)
기본 예제
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private NotificationService notificationService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.createOrder(request);
notificationService.sendConfirmation(order);
return ResponseEntity.ok(order);
}
}단점과 주의사항
final키워드 사용 불가 → 가변 상태- 테스트 시
reflection사용하여야 함. - 순환 참조를 컴파일 단계에서 찾을 수 없음 → 런타임에서 발견 가능
- 의존성이 숨겨져 있음 → 가독성 저하
언제 사용하나?
@Configuration
public class AppConfig {
// 설정 클래스의 간단한 유틸리티 빈에는 괜찮음
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(env.getProperty("db.url"))
.username(env.getProperty("db.username"))
.password(env.getProperty("db.password"))
.build();
}
}3. 세터 주입 (Setter Injection)
기본 예제
@Component
public class ReportGenerator {
private TemplateEngine templateEngine;
private EmailService emailService;
@Autowired
public void setTemplateEngine(TemplateEngine templateEngine) {
this.templateEngine = templateEngine;
}
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
public void generateAndSend(Report report) {
String content = templateEngine.generate(report);
emailService.send(report.getRecipient(), content);
}
}적절한 사용 예시
선택적 의존성에 사용
@Service
public class ProductService {
private ProductRepository productRepository;
private CacheService cacheService; // 선택적
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Autowired(required = false)
public void setCacheService(CacheService cacheService) {
this.cacheService = cacheService;
}
public Product findById(Long id) {
// 캐시가 있으면 사용, 없으면 DB 직접 조회
if (cacheService != null) {
Product cached = cacheService.get("product:" + id);
if (cached != null) return cached;
}
return productRepository.findById(id).orElse(null);
}
}재설정 가능한 의존성
@Component
public class DynamicRoutingService {
private RoutingStrategy strategy;
@Autowired
public void setStrategy(RoutingStrategy strategy) {
this.strategy = strategy;
}
// 런타임에 전략 변경 가능
public void changeStrategy(RoutingStrategy newStrategy) {
this.strategy = newStrategy;
}
public Route calculateRoute(Location from, Location to) {
return strategy.calculate(from, to);
}
}4. 일반 메서드 주입 (Method Injection)
적합한 사용 시기
복잡한 초기화가 필요한 경우
@Component
public class DataProcessor {
private DatabaseService databaseService;
private ValidationService validationService;
private LoggingService loggingService;
@Autowired
public void initializeDependencies(DatabaseService databaseService,
ValidationService validationService,
LoggingService loggingService) {
this.databaseService = databaseService;
this.validationService = validationService;
this.loggingService = loggingService;
// 복잡한 초기화 로직 수행
loggingService.info("DataProcessor initialized");
databaseService.warmUpConnections();
}
public void process(Data data) {
if (validationService.isValid(data)) {
databaseService.save(data);
}
}
}여러 메서드에 주입
@Service
public class OrderProcessor {
private InventoryService inventoryService;
private PaymentService paymentService;
private ShippingService shippingService;
@Autowired
public void setupInventory(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Autowired
public void setupPayment(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Autowired
public void setupShipping(ShippingService shippingService) {
this.shippingService = shippingService;
}
}@Autowired 의 속성
1. required 속성
- 기본값 :
true- 반드시 기본값이
true인@Autowired는 최대 한 개만 존재하여야 함. - 아래 예제에서
emailSender가 없으면 실행 불가 →NoSuchBeanDefinitionException발생
- 반드시 기본값이
required 옵션 false 사용 예제
- 필요한
Bean이 없어도 구동 가능. - 의존성 관련 작업 전
Null를 명시적으로 확인하여야 함.Optional타입으로 의존성을 주입하면 확인을 강제함.Spring5 이상에서부터는@Nullable를 사용할 수도 있음.
@Service
public class UserService {
@Autowired(required = false)
private AuditLogger auditLogger; // 없어도 null로 설정되고 진행
public void createUser(User user) {
// null 체크 필수!
if (auditLogger != null) {
auditLogger.log("User created: " + user.getId());
}
// 핵심 로직 수행
}
}Optional 타입 사용
@Service
public class ReportService {
@Autowired
private Optional<CacheService> cacheService;
public Report generateReport(Long id) {
// Optional API로 안전하게 처리
cacheService.ifPresent(cache ->
cache.evict("report:" + id)
);
return reportRepository.findById(id)
.orElseThrow(() -> new ReportNotFoundException(id));
}
}@Nullable 사용 (Spring 5.0+)
@Service
public class AnalyticsService {
private MetricsCollector metricsCollector;
@Autowired
public AnalyticsService(@Nullable MetricsCollector metricsCollector) {
this.metricsCollector = metricsCollector;
}
public void trackEvent(String event) {
if (metricsCollector != null) {
metricsCollector.track(event);
}
}
}생성자가 여러 개일 때
@Component
public class ComplexService {
private final DatabaseService databaseService;
private final CacheService cacheService;
private final LoggingService loggingService;
// required = true 생성자는 1개만 허용
@Autowired
public ComplexService(DatabaseService databaseService,
CacheService cacheService,
LoggingService loggingService) {
this.databaseService = databaseService;
this.cacheService = cacheService;
this.loggingService = loggingService;
}
// required = false 생성자는 여러 개 가능
@Autowired(required = false)
public ComplexService(DatabaseService databaseService,
CacheService cacheService) {
this(databaseService, cacheService, null);
}
@Autowired(required = false)
public ComplexService(DatabaseService databaseService) {
this(databaseService, null, null);
}
// Spring이 가장 많은 의존성을 충족할 수 있는 생성자 선택
}2. 컬렉션 주입
List 주입
@Service
public class PaymentProcessor {
@Autowired
private List<PaymentHandler> handlers;
public void processPayment(Payment payment) {
for (PaymentHandler handler : handlers) {
if (handler.supports(payment.getType())) {
handler.process(payment);
break;
}
}
}
}
// 구현체들
@Component
public class CardPaymentHandler implements PaymentHandler {
@Override
public boolean supports(PaymentType type) {
return type == PaymentType.CARD;
}
@Override
public void process(Payment payment) {
// 카드 결제 처리
}
}
@Component
public class BankTransferHandler implements PaymentHandler {
@Override
public boolean supports(PaymentType type) {
return type == PaymentType.BANK_TRANSFER;
}
@Override
public void process(Payment payment) {
// 계좌 이체 처리
}
}Map 주입
@Service
public class NotificationService {
@Autowired
private Map<String, NotificationChannel> channels;
// key = 빈 이름, value = 빈 객체
public void send(String channelName, Notification notification) {
NotificationChannel channel = channels.get(channelName);
if (channel != null) {
channel.send(notification);
}
}
public void sendToAll(Notification notification) {
channels.values().forEach(channel ->
channel.send(notification)
);
}
}
@Component("email")
public class EmailChannel implements NotificationChannel {
@Override
public void send(Notification notification) {
// 이메일 전송
}
}
@Component("sms")
public class SmsChannel implements NotificationChannel {
@Override
public void send(Notification notification) {
// SMS 전송
}
}배열 주입
@Configuration
public class FilterConfig {
@Autowired
private Filter[] filters;
@Bean
public FilterChain filterChain() {
FilterChain chain = new FilterChain();
for (Filter filter : filters) {
chain.addFilter(filter);
}
return chain;
}
}3. 순서 지정 (@Order)
@Component
@Order(1)
public class AuthenticationFilter implements Filter {
// 첫 번째로 실행
}
@Component
@Order(2)
public class LoggingFilter implements Filter {
// 두 번째로 실행
}
@Component
@Order(3)
public class ValidationFilter implements Filter {
// 세 번째로 실행
}
@Service
public class FilterService {
@Autowired
private List<Filter> filters; // Order 순서대로 정렬됨
public void applyFilters(Request request) {
for (Filter filter : filters) {
filter.doFilter(request);
}
}
}@Qualifier와 함께 사용
Case1. 같은 타입의 빈이 여러 개일 때
// 두 개의 DataSource 빈이 있는 경우
@Configuration
public class DataSourceConfig {
@Bean
@Qualifier("primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://primary-db:3306/mydb")
.build();
}
@Bean
@Qualifier("secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://secondary-db:3306/mydb")
.build();
}
}
@Service
public class UserService {
@Autowired
@Qualifier("primary")
private DataSource primaryDataSource;
@Autowired
@Qualifier("secondary")
private DataSource secondaryDataSource;
public void syncData() {
// primary에서 읽고 secondary에 쓰기
}
}Case2. 생성자에서 @Qualifier 사용
@Service
public class ReportService {
private final DataSource reportDataSource;
@Autowired
public ReportService(@Qualifier("reportDB") DataSource dataSource) {
this.reportDataSource = dataSource;
}
}Case3. 커스텀 Qualifier 어노테이션
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Database {
DatabaseType value();
}
public enum DatabaseType {
MASTER, SLAVE, ANALYTICS
}
@Configuration
public class DatabaseConfig {
@Bean
@Database(DatabaseType.MASTER)
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Database(DatabaseType.SLAVE)
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
@Service
public class DataService {
@Autowired
@Database(DatabaseType.MASTER)
private DataSource masterDb;
@Autowired
@Database(DatabaseType.SLAVE)
private DataSource slaveDb;
}주의사항
1. BeanPostProcessor / BeanFactoryPostProcessor에는 사용 불가
// ❌ 이렇게 하면 안 됨
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Autowired
private SomeService someService; // 주입되지 않음!
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// someService는 null
return bean;
}
}
// ✅ 대신 이렇게
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) {
this.applicationContext = context;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 필요할 때 직접 조회
SomeService service = applicationContext.getBean(SomeService.class);
return bean;
}
}⚠️ 순환 참조 문제
// ❌ 순환 참조 발생
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
// → BeanCurrentlyInCreationException 발생
// ✅ 해결 방법 1: 생성자 + @Lazy
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
// ✅ 해결 방법 2: 설계 개선
// ServiceA와 ServiceB가 서로 의존한다면 설계를 다시 검토
// 공통 기능을 별도 서비스로 분리하는 것이 좋음⚠️ 필드 주입 테스트의 어려움
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // private 필드
}
// 테스트 시 리플렉션 필요
class OrderServiceTest {
@Test
void test() throws Exception {
OrderService service = new OrderService();
PaymentService mock = mock(PaymentService.class);
// 리플렉션으로 private 필드에 접근
Field field = OrderService.class.getDeclaredField("paymentService");
field.setAccessible(true);
field.set(service, mock);
// 테스트...
}
}
// ✅ 생성자 주입이라면
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
class OrderServiceTest {
@Test
void test() {
PaymentService mock = mock(PaymentService.class);
OrderService service = new OrderService(mock); // 간단!
// 테스트...
}
}