프로그램을 개발하다 보면, 여러 클래스나 객체들은 서로 필요한 기능을 사용하기 위해 관계를 맺습니다. 예를 들어, OrderService가 PaymentService를 이용해 결제 로직을 수행하는 경우, OrderService는 PaymentService에 의존하고 있다고 표현합니다. 이때 PaymentService는 OrderService가 필요로 하는 의존성이 됩니다.
의존성이 많아질수록 코드 구조가 복잡해지기 마련입니다. 예전에 흔히 사용하던 전통적인 방식은, 개발자가 직접 필요한 객체를 생성(new 키워드 사용)하거나 설정함으로써 의존성을 연결했습니다. 하지만 이렇게 하면 클래스 간 결합도가 매우 높아져서, 유지보수나 테스트가 어려워지고 코드의 유연성이 떨어지는 문제점이 발생합니다.
의존성 주입은 객체가 사용할 의존성을 외부에서 주입해주는 개념으로, 스프링 프레임워크의 핵심 원리이자, 보다 넓은 의미로는 IoC(Inversion of Control, 제어의 역전) 컨테이너가 제공하는 가장 대표적인 기능입니다.
스프링 프레임워크에서 의존성을 주입받기 위해서는, 주로 어노테이션을 이용하여 IoC 컨테이너(스프링 컨테이너)에게 필요한 객체를 등록(Bean 등록)하고 그 객체를 주입받게 됩니다.
스프링 프레임워크에서는 크게 다음과 같은 방식으로 의존성을 주입할 수 있습니다.
스프링 공식 문서나 다양한 베스트 프랙티스에서는 생성자 주입을 가장 권장합니다. 그 이유를 포함해 각 방식의 특징을 자세히 살펴보겠습니다.
@Component
public class OrderService {
private final PaymentService paymentService;
// 생성자를 통해 의존성을 주입
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void order() {
// paymentService를 이용해 결제 로직 처리
paymentService.pay();
}
}
Java@Component
public class OrderService {
private PaymentService paymentService;
// Setter 메서드를 통해 의존성을 주입
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void order() {
paymentService.pay();
}
}
Java@Component
public class OrderService {
@Autowired
private PaymentService paymentService;
public void order() {
paymentService.pay();
}
}
Java스프링 공식 문서나 많은 커뮤니티에서 필드 주입은 지양하고, 생성자 주입을 최우선적으로 사용하도록 가이드를 제시하고 있습니다.
스프링의 DI를 활용하기 위해서는 @Autowired, @Inject, @Resource 등의 어노테이션을 사용할 수 있습니다.
스프링 컨테이너(IoC 컨테이너)는 다음 단계를 통해 빈을 스캔하고 의존성을 주입합니다.
스프링에서는 개발/테스트/운영 환경에 따라 서로 다른 빈을 주입하고 싶을 때 프로파일 기능을 사용할 수 있습니다.
@Configuration
@Profile("dev")
public class DevDataSourceConfig {
@Bean
public DataSource devDataSource() {
// 개발 환경에서 사용할 DataSource 설정
return new HikariDataSource(...);
}
}
@Configuration
@Profile("prod")
public class ProdDataSourceConfig {
@Bean
public DataSource prodDataSource() {
// 운영 환경에서 사용할 DataSource 설정
return new HikariDataSource(...);
}
}
Java스프링 4부터 지원하는 @Conditional 애노테이션을 사용하면 특정 조건을 만족할 때만 빈을 등록할 수 있습니다. 예를 들어, 특정 라이브러리가 클래스패스에 있을 경우에만 빈을 생성하도록 할 수 있습니다.
@Configuration
@ConditionalOnClass(name = "com.example.ExternalLibrary")
public class ConditionalConfig {
@Bean
public MyService myService() {
// com.example.ExternalLibrary 클래스가 있으면 빈이 등록됨
return new MyService();
}
}
Java이처럼 스프링 DI는 단순 의존성 주입을 넘어, 다양한 조건과 환경 설정에 따라 유연하게 빈을 구성하고 주입하는 데까지 범위가 확장됩니다.
아래는 간단한 Spring Boot 프로젝트에서 DI를 적용한 예시 구조입니다.
└─ src
├─ main
│ ├─ java
│ │ └─ com.example.demo
│ │ ├─ DemoApplication.java
│ │ ├─ service
│ │ │ ├─ OrderService.java
│ │ │ └─ PaymentService.java
│ │ ├─ repository
│ │ │ └─ OrderRepository.java
│ │ └─ controller
│ │ └─ OrderController.java
│ └─ resources
│ └─ application.properties
└─ test
└─ java
└─ com.example.demo
└─ OrderServiceTest.java
Markdownpackage com.example.demo.service;
public interface PaymentService {
void pay();
}
Javapackage com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class CreditCardPaymentService implements PaymentService {
@Override
public void pay() {
System.out.println("신용카드 결제 처리");
}
}
Javapackage com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final PaymentService paymentService;
// 생성자 주입
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void processOrder() {
System.out.println("주문 로직 처리");
paymentService.pay();
System.out.println("주문 완료");
}
}
Javapackage com.example.demo.controller;
import com.example.demo.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderService orderService;
// 생성자 주입
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/order")
public String order() {
orderService.processOrder();
return "Order Processed!";
}
}
Java위 구조는 아주 간단하지만, DI의 기본 흐름을 잘 보여줍니다.
스프링 의존성 주입(DI)은 스프링 프레임워크가 제공하는 가장 핵심적인 기능 중 하나로, IoC 원리를 바탕으로 객체의 생성과 의존 관계 설정을 프레임워크(스프링 컨테이너)가 담당합니다. 이를 통해 결합도를 낮추고, 테스트와 유지보수가 쉬운 유연한 애플리케이션 아키텍처를 구성할 수 있습니다.
결론적으로, 스프링을 활용한 DI는 객체지향적 설계 원칙을 실제 프로젝트에서 구현하기에 매우 적합한 패턴이며, 유지보수성과 확장성을 모두 만족하는 강력한 도구입니다.
컴포넌트 스캔이란? 컴포넌트 스캔(Component Scan)은 스프링 프레임워크가 특정 패키지를 탐색하면서, 스캔 대상에 해당하는 클래스를 찾아…
스프링 빈이란? 스프링 빈(Spring Bean)은 스프링 IoC(Inversion of Control) 컨테이너가 관리하는 자바 객체를 의미합니다. 간단히…
스프링 컨테이너(Spring Container)란? 스프링 컨테이너는 스프링 프레임워크에서 가장 핵심적인 부분으로, IoC(Inversion of Control) 개념을 기반으로…
Queue란 무엇인가? Java에서 Queue는 데이터 구조의 일종으로, 데이터를 선입선출(FIFO, First-In-First-Out) 방식으로 처리합니다. 이 글에서는 Queue의…
Stack이란 무엇인가? Java에서 Stack은 자료구조의 한 종류로, 데이터를 순서대로 쌓아 올리는 형태로 운영됩니다. 컴퓨터 과학에서…
소개 자바에서 Map 인터페이스는 키(Key)와 값(Value)의 쌍을 저장하는 자료구조입니다. 이는 연관 배열이라고도 불리며, 각 키는…