[Spring] Spring DI & AppConfig

윤여송·2026년 9월 24일

Spring

목록 보기
7/12

Spring DI와 AppConfig

1. 핵심 용어

Spring 컨테이너

Spring이 객체를 생성하고 보관하며, 객체 사이의 의존관계를 연결해 주는 공간이다.

ApplicationContext context =
        new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext가 Spring 컨테이너의 대표적인 인터페이스다.

Spring Bean

Spring 컨테이너가 생성하고 관리하는 객체를 Spring Bean이라고 한다.

다음 두 방법 모두 결과적으로 Spring Bean을 등록한다.

  1. @Service, @Repository, @Component 등을 이용한 컴포넌트 스캔
  2. @Configuration과 @Bean을 이용한 명시적 등록

AppConfig

어떤 구현 객체를 생성하고, 어느 객체에 연결할지 정의하는 설정 클래스다.

AppConfig와 Spring Bean은 서로 반대되는 개념이 아니다. @Configuration과 @Bean이 붙은 AppConfig는 Spring Bean을 등록하는 방법 중 하나다.


2. 생성자 주입

현재 MemberServiceImpl은 MemberRepository 인터페이스에 의존한다.

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

이 생성자는 "MemberServiceImpl을 만들려면 MemberRepository가 필요하다"는 사실을 표현한다.

생성자 주입 자체는 Spring 전용 기능이 아니다. 누가 생성자를 호출하느냐에 따라 수동 DI와 Spring DI로 나뉜다.

수동 DI

MemberRepository repository = new MemoryMemberRepository();
MemberService memberService = new MemberServiceImpl(repository);

개발자가 직접 객체를 생성하고 생성자에 전달한다.

Spring DI

@Service
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}

Spring 컨테이너가 실행되면 다음 순서로 처리한다.

  1. MemoryMemberRepository를 Bean으로 생성한다.
  2. MemberServiceImpl을 Bean으로 생성하려고 생성자를 확인한다.
  3. MemberRepository 타입의 Bean을 찾는다.
  4. 구현체가 하나라면 해당 객체를 생성자에 전달한다.

생성자가 하나라면 @Autowired는 생략할 수 있다.

중요한 점은 직접 new로 생성한 객체에는 Spring이 자동 주입하지 않는다는 것이다.

new MemberServiceImpl(); // 컴파일 오류: MemberRepository 인자가 필요함
new MemberServiceImpl(new MemoryMemberRepository()); // 수동 DI

3. Spring 컨테이너가 실행되는 경우

Spring Boot 애플리케이션 실행

SpringApplication.run(CoreApplication.class, args);

이 코드를 실행하면 Spring 컨테이너가 시작되고 컴포넌트 탐색, Bean 생성, 의존성 주입이 이루어진다.

Spring 테스트 실행

@SpringBootTest
class CoreApplicationTests {
}

@SpringBootTest도 Spring 컨테이너를 시작한다.

일반 main 또는 일반 단위 테스트

Spring 컨테이너를 시작하지 않으면 @Service, @Repository가 붙어 있어도 자동 주입은 일어나지 않는다. 이 경우 객체를 직접 생성해야 한다.

판단 기준은 "현재 코드가 Spring 컨테이너 안에서 실행되는가?"이다.


4. 일반 AppConfig와 Spring AppConfig의 차이

일반 Java AppConfig

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}

@Configuration과 @Bean이 없으면 Spring 설정이 아니라 일반 Java 클래스다. 여기서 반환한 객체도 Spring Bean이 아니다.

AppConfig config = new AppConfig();
MemberService memberService = config.memberService();

개발자가 AppConfig를 직접 생성하고 객체 조립을 요청하는 수동 DI 방식이다.

Spring AppConfig

현재 프로젝트의 AppConfig는 다음 구조다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy()
        );
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

@Bean 메서드가 반환한 객체들은 Spring Bean으로 등록된다.

ApplicationContext context =
        new AnnotationConfigApplicationContext(AppConfig.class);

MemberService memberService =
        context.getBean("memberService", MemberService.class);

Spring 설정으로 변경하면서 생기는 차이

일반 AppConfig에서는 메서드를 호출할 때마다 새로운 객체가 만들어질 수 있다.

AppConfig config = new AppConfig();

MemberService first = config.memberService();
MemberService second = config.memberService();

// 일반 Java AppConfig라면 서로 다른 객체가 될 수 있음
System.out.println(first == second);

Spring Bean의 기본 스코프는 싱글톤이다. 같은 컨테이너에서 같은 Bean을 여러 번 조회하면 같은 객체를 반환한다.

MemberService first = context.getBean(MemberService.class);
MemberService second = context.getBean(MemberService.class);

System.out.println(first == second); // true

또한 memberService()와 orderService()에서 호출하는 memberRepository()도 Spring이 관리하는 동일한 Bean을 사용한다.

MemberServiceImpl ─┐
                   ├─ 같은 MemoryMemberRepository Bean
OrderServiceImpl ──┘

5. 컴포넌트 스캔과 AppConfig 비교

컴포넌트 스캔

@Service
public class MemberServiceImpl implements MemberService {
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}

Spring이 클래스를 탐색해 자동으로 Bean을 등록한다.

AppConfig의 명시적 등록

@Bean
public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
}

개발자가 어떤 구현체를 어떻게 생성할지 명확하게 작성한다.

두 방식 모두 등록이 끝난 뒤에는 동일한 Spring Bean이다. 차이는 Bean 등록 정보를 어디에 작성하느냐이다.

구분컴포넌트 스캔AppConfig
등록 방법@Service, @Repository, @Component@Configuration, @Bean
장점작성이 간단함객체 생성과 구현체 선택이 명확함
주 사용처애플리케이션 서비스, 저장소, 컨트롤러외부 라이브러리, 복잡한 생성 설정, 정책 선택

6. 여러 구현체가 있을 때

public interface DiscountPolicy {
    int discount(Member member, int price);
}

구현체가 두 개 모두 Bean으로 등록되어 있다고 가정한다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
}

다음 생성자에 주입할 때 Spring은 둘 중 어느 구현체를 선택해야 할지 알 수 없다.

public OrderServiceImpl(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

이 문제는 컴파일 오류가 아니라 Spring 컨테이너 실행 시 발생하는 런타임 오류다.

NoUniqueBeanDefinitionException

해결 방법은 다음과 같다.

  • @Primary로 기본 구현체 지정
  • @Qualifier로 특정 구현체 지정
  • @Profile이나 조건 설정으로 환경에 맞는 구현체 등록
  • AppConfig에서 사용할 구현체 하나만 @Bean으로 등록

현재 AppConfig에서는 다음 한 줄로 할인 정책을 선택한다.

@Bean
public DiscountPolicy discountPolicy() {
    return new RateDiscountPolicy();
}

고정 금액 할인으로 변경할 때는 OrderServiceImpl을 수정하지 않고 설정만 바꿀 수 있다.

@Bean
public DiscountPolicy discountPolicy() {
    return new FixDiscountPolicy();
}

7. 현재 프로젝트에서 주의할 점

현재 프로젝트는 다음 두 등록 방식을 함께 사용하고 있다.

  • AppConfig에서 MemberService, MemberRepository를 @Bean으로 등록
  • MemberServiceImpl에 @Service 사용
  • MemoryMemberRepository에 @Repository 사용

new AnnotationConfigApplicationContext(AppConfig.class)처럼 AppConfig만 넘기면 별도의 컴포넌트 스캔 설정이 없으므로 AppConfig의 @Bean들이 중심이 된다.

하지만 CoreApplication의 @SpringBootApplication으로 실행하면 hello.core 아래를 컴포넌트 스캔한다. 이때 AppConfig의 Bean과 @Service, @Repository Bean이 함께 등록되어 같은 역할의 객체가 여러 개 생길 수 있다.

학습 단계에서는 다음 중 한 가지 방식으로 통일하는 편이 명확하다.

AppConfig 중심

  • MemberServiceImpl의 @Service 제거
  • MemoryMemberRepository의 @Repository 제거
  • AppConfig에서 @Bean으로만 등록

컴포넌트 스캔 중심

  • @Service, @Repository, @Component 사용
  • AppConfig에서 동일한 객체를 등록하는 @Bean 제거
  • 여러 구현체 선택이 필요한 부분만 별도 Config로 관리

8. 실무에서는 어떻게 사용하는가

실무에서는 두 방식을 함께 사용하는 경우가 많다.

직접 작성한 일반 애플리케이션 코드는 컴포넌트 스캔으로 등록하는 경우가 많다.

@RestController
class OrderController {
}

@Service
class OrderServiceImpl {
}

@Repository
class JpaOrderRepository {
}

다음과 같은 객체는 설정 클래스로 등록하는 경우가 많다.

  • 외부 라이브러리 객체
  • 생성 과정에 별도 설정이 필요한 객체
  • 환경별로 교체해야 하는 객체
  • 여러 구현체 중 하나를 선택해야 하는 정책 객체
  • 데이터베이스, 보안, 메시징, 직렬화 관련 객체

실무에서는 하나의 거대한 AppConfig보다 목적별 설정 클래스를 많이 사용한다.

SecurityConfig
DatabaseConfig
RedisConfig
JacksonConfig
PaymentConfig
DiscountConfig

9. 최종 정리

  1. 생성자 주입은 객체가 필요한 의존성을 생성자로 받는 설계 방법이다.
  2. 개발자가 new로 연결하면 수동 DI다.
  3. Spring이 Bean을 생성하면서 연결하면 Spring DI다.
  4. @Service와 @Repository는 컴포넌트 스캔을 통한 Bean 등록 방법이다.
  5. @Configuration과 @Bean은 설정 클래스를 통한 명시적 Bean 등록 방법이다.
  6. AppConfig와 Spring Bean은 반대 개념이 아니다. AppConfig는 Spring Bean을 등록하는 방법 중 하나다.
  7. Spring Bean으로 등록하면 기본적으로 객체 동일성, 생명주기, 의존성 주입 등을 컨테이너가 관리한다.
  8. 구현체가 여러 개면 자동 주입 시 선택 기준이 필요하다.
  9. 실제 애플리케이션에서는 컴포넌트 스캔과 설정 클래스를 목적에 따라 함께 사용한다.

가장 중요한 질문은 다음 두 가지다.

이 객체는 누가 생성하는가?
이 객체의 의존관계는 누가 연결하는가?

둘 다 Spring 컨테이너가 담당한다면 Spring Bean과 Spring DI를 사용하는 것이다.

profile
y_some__velog

0개의 댓글