스프링

스프링 빈 간 관계 구현

yoon4360 2025. 3. 31. 22:00

스프링을 사용하다 보면 어떤 클래스가 다른 클래스를 사용하는 경우가 많다.
예를 들어 OrderService가 PaymentService를 사용하거나, UserController가 UserService를 사용한다.

이처럼 한 객체가 다른 객체를 사용하는 관계를 스프링에서는 빈 간 관계라고 하고, 이 관계를 자동으로 연결해주는 걸 의존성 주입(Dependency Injection)이라고 한다.

이번 글에서는 스프링의 IOC 개념부터 의존성 주입 방식, 그리고 빈이 여러 개일 때 어떤 걸 주입할지 선택하는 방법까지 정리해 보려 한다.

 


IOC 제어의 역전

IOC 사용전에는 개발자가 new로 직접 객체 생성하고 의존성 주입을 했다.

IOC 사용후에는 스프링이 빈을 대신 생성하고 필요한 의존성을 주입 한다.

 

즉 제어의 역전이란, 객체 생성과 연결 제어 권한을 스프링에 넘기는 것을 말한다.

 


 

빈 간 관계 연결 방법 (와이어링)

1. @Bean  메서드 직접 호출

@Bean
public Parrot parrot() {
    return new Parrot("Coco");
}

@Bean
public Owner owner() {
    return new Owner(parrot()); // 메서드 직접 호출
}
특징
  • 컨텍스트에 해당 빈이 있으면 가져오고, 없으면 새로 만든다.
  • 명시적으로 어떤 빈을 사용할지 제어 할 수 있다.

 

2. @Bean  메서드 매개변수 방식 (권장)

@Bean
public Owner owner(Parrot parrot) {
    return new Owner(parrot); // 스프링이 자동으로 주입
}
특징
  • 스프링 컨텍스트에서 자동으로 Parrot 빈을 찾아서 주입
  • 코드 가독성 좋고, 명시적인 타입 기반 주입을 할 수 있다.

 

3. @Autowired  자동 의존성 주입

스프링은 클래스 내부에서 @Autowired를 통해 빈을 자동 주입할 수 있다.

 

1. 필드 주입 (지양)

클래스의 필드에 직접 의존성 주입하는 방식이다. 

코드가 간결하다는 장점이 있지만 아래와 같은 단점이 크다.

@Component
public class Car {
    @Autowired
    private Engine engine;
}
단점
  • final 사용 불가 → 불변 객체 만들기 어렵다.
  • 테스트 어려움 → mock 객체 주입 어렵다.
  • 순환 참조가 되어있을때 메서드 실행전까지 알기 어렵다.

 

2. 생성자 주입 (권장)

클래스의 생성자를 통해 의존성을 주입하는 방식이다.

객체가 생성될 때 의존성을 한번에 주입받기 때문에 의존성 주입후 변경이 불가하다.

@Component
public class Car {
    private final Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}
장점
  • final 사용 가능 → 불변 객체
  • 테스트 시 주입 용이하다.
  • 생성자가 1개일 경우 @Autowired 생략 가능하다.

 

3. Setter 주입

객체가 먼저 만들고 나서 설정자 메서드를 호출해 의존성을 주입하는 방식이다.

객체가 만들어진 후에도 의존성을 바꿀 수 있으며, 꼭 필요한게 아닌 경우 없어도 일단 객체가 생성 가능하다.

@Component
public class Car {
    private Engine engine;

    @Autowired
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}
단점
  • 객체가 완전히 초기화 되기전에 사용될 수 있다.
  • 의존성이 변경될 가능성이 있다.
  • 순환참조 발생시 오류를 늦게 발견한다.

 

생성자 주입은 순환 참조가 되어있다면 서버자체가 구동되지 않아 바로 알 수 있지만

필드 주입과 세터 주입은 해당 메서드를 실행하기 전까지는 알지 못한다.

그 이유는,

필드 주입과 세터 주입은 먼저 빈을 먼저 만들어 놓고 나중에 필요한 의존성을 주입한다.

반면 생성자 주입은 빈을 만들기 전에 필요한 의존성을 먼저 확인하기에 서버 자체가 구동되지 않는다.

 

생성자 주입을 스프링 공식에서 가장 권장하고 특히, 필수 의존성이 있고 불변성을 유지해야 할 때 사용하도록 하자.

세터 주입은 선택적 의존성이거나, 초기화 이후 바뀔 수 있는 값일 때 사용하도록 하자.

필드 주입은 테스트 용도나 간단한 예제 코드에서만 사용하고 실무에서는 거리두기하자.

 

 


순환 의존성 주의

@Component
class A {
    @Autowired
    B b;
}

@Component
class B {
    @Autowired
    A a;
}

 

이런 구조는 A → B → A 형태로 무한 루프가 되어 스프링 애플리케이션 시작을 실패한다.

해결방법은 설계를 변경하거나, 중간에 인터페이스를 분리하거나 Setter 주입 등으로 순환을 끊어야 한다.

 


 

같은 타입의 빈이 둘 이상일때 어떤 것을 주입?

예를 들어 아래 예시처럼 Parrot 빈이 두개라면,

@Bean
public Parrot parrot1() {
    return new Parrot("Coco");
}

@Bean
public Parrot parrot2() {
    return new Parrot("Kiki");
}

 

스프링이 어떤것을 주입할지 모호해져 오류가 발생한다.

 

해결방법1 : @Qualifer (권장)

@Component
public class Owner {
    private final Parrot parrot;

    @Autowired
    public Owner(@Qualifier("parrot1") Parrot parrot) {
        this.parrot = parrot;
    }
}

빈 이름을 명확하게 지정하는 방법이다. 가장 명시적이고 오류 없는 방식이다.

 

해결방법2 : @Primary

@Bean
@Primary
public Parrot parrot1() {
    return new Parrot("Coco");
}

 

여러 빈 중 하나를 기본값으로 설정하는 방법이다.

다른 설정 없으면 이 빈이 주입되고 한 종류의 빈이 대부분인 경우 유용하다.

 

하지만 여러 종류 빈을 명확하게 구분해야 할 땐 @Qualifier를 사용하자.

 

 


마무리

스프링에서 빈 간 관계를 잘 설계하고 주입하는 것은

안정적이고 유지보수 쉬운 애플리케이션을 만드는 핵심이다.

 

  • 언제나 명확하고 의존성에 강한 구조를 만들기 위해 생성자 주입을 사용하자.
  • 빈이 여러 개일 땐 @Qualifier로 명시하거나, 기본값으로 @Primary를 설정하자.
  • 순환 의존성은 설계상 문제이므로, 구조 자체를 바꾸는 것이 바람직하다.