📢 공지합니다
이 게시글은 메인 페이지에 항상 고정되어 표시됩니다.
스프링 컨테이너는 자바 객체(빈, Bean)를 생성하고 관리하며, 의존관계를 자동으로 연결해주는 역할을 한다. 단순한 객체 생성만 담당하는 것이 아니라, 생명주기 관리, 의존성 주입(DI), AOP 지원 등 다양한 기능을 제공한다.
스프링에서 이 객체들을 "빈(Bean)"이라고 부른다.
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean(MyService.class);
즉, 우리가 평소에 사용하는 스프링 컨테이너는 ApplicationContext다.
스프링 컨테이너가 생성하고 관리하는 객체를 빈이라고 부른다. 일반적으로 개발자가 직접 new 하지 않고, 스프링이 대신 생성해주며, 필요시 생성자나 필드 등에 주입해준다.
@Component
public class Board {}
@Service
@RequiredArgsConstructor
public class MemberService {
private final Board board;
}
위 예제에서 Board는 스프링이 먼저 생성하고, MemberService의 생성자에 주입된다. 개발자는 new 하지 않아도 된다.
다음 구조처럼 DI가 구성되어 있다고 해보자:
Controller → Service → Repository
즉, 컨트롤러가 서비스를 가지고, 서비스는 리포지토리를 가진다. 그럼 생성 순서는?
1. Repository 생성 (맨 아래부터)
2. Service 생성 (Repository 주입)
3. Controller 생성 (Service 주입)
이 순서를 따라 스프링이 내부적으로 의존성을 파악하고, 가장 아래쪽(leaf)에 있는 빈부터 순차적으로 생성 후, 상위 빈의 생성자에 주입해 나간다.
즉, 생성자 주입 방식에서는 반드시 "안쪽 객체부터 먼저 생성되어야" 위쪽에서 주입이 가능하다.
스프링이 자동 주입을 위해 사용하는 기술 중 하나가 바로 리플렉션이다.
리플렉션이란? → 자바의 클래스, 메서드, 필드 정보를 런타임에 동적으로 조회하거나 조작할 수 있는 기능
예를 들어, 스프링은 아래처럼 동작한다:
Class<?> clazz = MemberService.class;
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
Parameter[] params = constructor.getParameters();
for (Parameter param : params) {
Object bean = applicationContext.getBean(param.getType());
// 생성자 호출 시 필요한 파라미터를 준비함
}
MemberService service = (MemberService) constructor.newInstance(...);
즉, 생성자 파라미터의 타입을 런타임에 조사해서, 그에 맞는 빈을 스프링 컨테이너에서 꺼내서 자동으로 newInstance로 주입해주는 방식이다.
리플렉션은 강력하지만:
그래서 우리가 직접 쓸 일은 거의 없지만, 스프링 내부에선 리플렉션 없이는 DI 자체가 불가능하다.
@Configuration
public class AppConfig {
@Bean
public Board board() {
return new Board();
}
}
Board b1 = appConfig.board();
Board b2 = appConfig.board();
System.out.println(b1 == b2); // true
프록시 방식은 바이트코드를 조작하는 방식으로 복잡하게 구현되어 있다. 따라서 임의의 다른 클래스가 싱글톤을 보장할 수 있는 것은 아니며, Configuration 클래스만 해당된다.
@Component
public class OrderService {}
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {}
| 항목 | @Configuration (with @Bean) | @Component |
| 빈 등록 방식 | 메서드 단위로 등록 | 클래스 자체 등록 |
| 내부 제어력 | 높음 | 낮음 |
| 대표 예시 | 설정 전용 클래스, 빈 팩토리 역할 | 서비스 클래스, DAO 클래스 등 |
| 언제 쓰나? | - 여러 @Bean 메서드를 가지는 설정 클래스 - 외부 라이브러리 Bean 구성 시 - 커스터마이징한 설정을 명시적으로 분리할 때 |
- @Service, @Repository, @Controller로 분류되지 않는 일반적인 컴포넌트 - 비즈니스 로직을 처리하거나, 단일 기능 클래스를 빈으로 등록하고 싶을 때 |
생성자 주입에 기반하여 명확하고 불변한 객체 설계를 하고 싶다면, Configuration + @Bean 구조가 좀 더 유연하다.
@RequiredArgsConstructor
@Component
public class MyService {
private final MyRepository repository;
}
@Component
public class MyService {
private MyRepository repository;
@Autowired(required = false)
public void setRepository(MyRepository repository) {
this.repository = repository;
}
}
@Component
public class MyService {
@Autowired
private MyRepository repository;
}
의존성이 "있으면 좋지만, 없어도 돌아갈 수 있는" 경우를 의미한다.
예를 들어 로컬 환경에서는 Logger가 없어도 되지만, 운영 환경에서는 반드시 Logger가 필요한 경우:
@Component
public class NotificationService {
private Logger logger;
@Autowired(required = false)
public void setLogger(Logger logger) {
this.logger = logger;
}
public void send(String msg) {
if (logger != null) logger.log(msg);
}
}
이처럼 "선택적으로 있어도 되고 없어도 되는" 기능에 대해서만 setter 주입을 쓴다. 무조건 필요한 의존성은 생성자 주입을 써야 한다. 그래서 setter는 굳이?
@Autowired
@Qualifier("mainService")
private MyService service;
@Primary
@Component
public class DefaultService implements MyService {}
둘 다 존재할 경우: @Qualifier가 우선된다 (명시적 지정 > 암시적 우선순위)
| 어노테이션 | 설명 및 역할 |
| @Component | 가장 기본적인 컴포넌트. 스프링 빈으로 등록됨 |
| @Service | 비즈니스 로직 담당. 의미 전달용 (기능 차이 없음) |
| @Repository | DB 접근 계층. 예외 변환(AOP 기반) 기능 추가됨 |
| @Controller | 웹 계층 (요청/응답). 내부에 @ResponseBody 없이도 View 처리 가능 |
결국 "객체를 직접 만들지 말고 맡겨라" — 이것이 스프링 DI의 핵심 철학이다.
| [12편] 스프링 AOP 이란? (0) | 2025.06.06 |
|---|---|
| [11편] 빈 스코프 (0) | 2023.04.10 |
| [10편] 조회 대상 빈이 2개 이상일때 (0) | 2023.04.04 |
| [9편] 롬복과 최신트랜드 (0) | 2023.04.04 |
| [8편] 의존관계 주입 (0) | 2023.04.03 |