작은 Java 프로그램에서는 new로 필요한 객체를 만들어도 충분하다. 문제는 서비스가 커졌을 때 객체의 생성·연결·교체 책임이 여러 클래스에 흩어지는 것이다. IoC(Inversion of Control, 제어의 역전)는 이러한 제어 책임을 외부로 옮기는 설계 원리다. Spring은 이를 구현하는 IoC 컨테이너를 제공한다.
public class PointService {
private final MemoryPointRepository repository =
new MemoryPointRepository();
}
이렇게 작성하면 저장소를 DB 기반으로 변경할 때 서비스도 수정해야 한다. 테스트에서 가짜 저장소를 사용하려 해도 쉽지 않다. 구현체 생성 책임을 서비스 밖으로 분리하면 변경에 유리해진다.
public class PointService {
private final PointRepository repository;
public PointService(PointRepository repository) {
this.repository = repository;
}
}
이제 서비스는 구현체가 아닌 PointRepository 인터페이스에 의존한다. 실제 객체는 외부에서 전달된다. 이를 의존성 주입(DI)이라 부른다. DI는 IoC를 구현하는 대표적인 방법이지만, IoC와 완전히 같은 말은 아니다.
프로그램 시작
↓
빈 정의 수집 (애노테이션, @Bean 등)
↓
객체 생성 및 의존 관계 해결
↓
애플리케이션에서 빈 사용
Spring이 관리하는 객체를 Bean(빈)이라고 한다. 평범한 Java 객체가 모두 Spring Bean인 것은 아니다. 직접 new로 만든 객체가 컨테이너에 등록되지 않았다면 자동 주입이나 Spring의 빈 생명주기 관리가 적용되지 않는다.
ApplicationContext는 Spring IoC 컨테이너의 대표적인 인터페이스다. 객체를 만들고 연결할 뿐 아니라 이벤트, 메시지, 리소스 등 애플리케이션 기능도 제공한다. Spring Boot를 사용하면 컨테이너를 일일이 생성할 필요가 거의 없다.
아래 코드는 각각 별도의 Java 파일로, 같은 패키지 com.example.shop.point에 둔다.
package com.example.shop.point;
public interface PointRepository {
int findPoints(long memberId);
}
package com.example.shop.point;
import org.springframework.stereotype.Repository;
@Repository
public class MemoryPointRepository implements PointRepository {
@Override
public int findPoints(long memberId) {
return 1500;
}
}
package com.example.shop.point;
import org.springframework.stereotype.Service;
@Service
public class PointService {
private final PointRepository repository;
public PointService(PointRepository repository) {
this.repository = repository;
}
public boolean canRedeem(long memberId, int requiredPoints) {
return repository.findPoints(memberId) >= requiredPoints;
}
}
@Repository와 @Service는 Spring이 컴포넌트 스캔으로 찾을 수 있는 애노테이션이다. 메인 클래스가 com.example.shop에 있으면 그 하위 패키지를 기본 탐색 대상으로 삼는다. PointService에 생성자가 하나이므로 @Autowired를 생략해도 Spring이 필요한 저장소 빈을 주입한다.
동일한 PointRepository 인터페이스를 구현하는 Bean이 여러 개면 선택이 모호할 수 있다. 이런 경우 @Primary나 @Qualifier를 통해 어느 구현체를 선택할지 명시한다.
@Bean으로 등록외부 라이브러리 객체처럼 클래스에 @Component를 붙일 수 없거나 생성 과정을 직접 지정하려면 @Bean을 사용한다.
package com.example.shop.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TimeConfig {
@Bean
public Clock clock() {
return Clock.systemUTC();
}
}
Clock 객체는 컨테이너에 등록되어 다른 Bean의 생성자 매개변수로 주입될 수 있다. 빈 이름은 기본적으로 clock이다. @Bean은 @Configuration 외에도 @Component 클래스에 사용할 수 있지만, 명시적인 설정 코드를 모을 때는 @Configuration이 이해하기 쉽다.
Spring Bean의 기본 스코프는 singleton이다. 하나의 컨테이너에서 같은 빈 정의를 요청할 때 일반적으로 동일한 인스턴스를 공유한다. 이것은 JVM 전체에서 객체가 오직 하나라는 뜻은 아니다.
prototype 스코프에서는 빈을 요청할 때마다 새로운 인스턴스를 만들 수 있다. 웹 환경에서는 request, session과 같은 범위도 사용한다. 기본 singleton 서비스에 요청별 사용자 ID 같은 변경 가능한 상태를 필드로 보관하면 동시성 문제가 생길 수 있다.
NoSuchBeanDefinitionException: 필요한 타입의 Bean이 등록되지 않았다. 패키지 위치, 컴포넌트 스캔 범위, @Bean 등록 여부를 살펴본다.
NoUniqueBeanDefinitionException: 후보가 둘 이상이다. 역할에 따라 구현체를 정리하거나 @Qualifier를 사용한다.
순환 의존성: 서비스 A가 B를 요구하고 B가 다시 A를 요구하는 구조는 생성자 주입으로 해결하기 어렵다. 설정을 우회하기보다는 책임 분리를 검토한다.
ApplicationContext는 대표적인 IoC 컨테이너 인터페이스다.@Bean은 서로 다른 빈 등록 방법이다.