Back-end/Spring

[Spring] 의존성 주입 (Dependency Injection)

woojeans7 2025. 6. 15. 22:20

📚 의존성 주입 (Dependency Injection)이란?

DI는 객체 지향 프로그래밍에서 객체가 필요로 하는 의존성을 외부에서 주입해주는 설계 패턴이다.

→ 즉, 객체를 직접 생성하지 않고, 외부에서 주입받아서 사용함

 

스프링에서 의존성 주입이 필요한 이유는 결합도를 낮추고, 테스트 용이성을 높이기 위해서이다.


Autowired

@Autowired : 스프링이 해당 타입의 Bean을 찾아서 자동 주입

스프링에서 의존성 주입을 수행하기 위해서 사용하는 어노테이션이다. 필요한 객체를 선언하면 스프링이 자동으로 찾아서 의존관계를 주입한다.

@Autowired는 기본적으로 타입(Type) 기준으로 스프링 컨테이너에 있는 Bean일치하는 객체를 찾아서 주입한다.

 

@Autowired 는 사용하는 위치에 따라서 유연성, 테스트 용이성, 유지보수성이 달라질 수 있다.

필드, 생성자, Set메서드에서 사용할 수 있지만, 일반적으로 생성자에 사용하는 것을 권장한다.

1. 필드 주입

@Component
public class Zoo {
    @Autowired
    private Parrot parrot;
}

간단해서 에전에는 많이 사용하던 방식이지만, final 을 사용할 수 없어 객체 불변성을 보장할 수 없다는 단점이 있다.

또한, 테스트에서 직접 parrot을 넣어줄 수 없고, 의존성이 숨어있어 의존 관계를 쉽게 파악하기가 어려워 테스트에 어려움이 존재한다.

 

2. 생성자 주입

@Component
public class Zoo {
    private final Parrot parrot;

    @Autowired
    public Zoo(Parrot parrot) {
        this.parrot = parrot;
    }
}

 

가장 권장하는 방식으로 객체가 생성되는 동시에 의존성을 주입 받기 때문에 final을 사용할 수 있고, 객체 불변성을 보장할 수 있다.

또한, 의존 관계 명확하기 때문에 테스트 역시 쉽게 진행할 수 있다는 장점이 있다. 주입이 안 되면 컴파일러 에러/런타임 에러로 바로 확인할 수 있음.

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

 

Set메서드 주입

@Component
public class Zoo {
    private Parrot parrot;

    @Autowired
    public void setParrot(Parrot parrot) {
        this.parrot = parrot;
    }
}

선택적 의존성 또는 동적으로 바꿀 필요 있을 때 사용한다. 그래서 가끔 사용하는 주입 방식이다.

객체 생성 후에도 의존성을 변경할 수 있는 유연성을 가졌지만, 변경될 수 있어 불변성을 확보하기는 어렵다.

 

요약

항목 필드 주입 생성자 주입 Setter 주입
사용 위치 private 필드 위 생성자 파라미터 위 Setter 메서드 위
코드 간결성 ★★★★★ ★★★ ★★★★
테스트 용이성 ★ (리플렉션 써야함) ★★★★★ (생성자로 직접 주입 가능) ★★★★ (Setter로 쉽게 변경 가능)
불변성 보장 ❌ final 사용 불가 ✅ final 가능 → 불변 객체 ❌ 변경 가능성 있음
선택적 주입 ❌ 불가능 (null 방지 어려움) ❌ 불가능 (생성자 인자는 필수) ✅ nullable 대응 가능
의존성 명시성 ❌ 숨어 있음 ✅ 명확히 드러남 ⭕ 반쯤 드러남
권장 여부 ❌ 지양 ✅ Spring 공식 권장 ⭕ 조건부 사용 가능

 


PostConstruct

@PostConstruct : 의존성 주입이 완료된 뒤 자동으로 호출되는 초기화 메서드에 붙이는 어노테이션

스프링이 @Autowired 등으로 필요한 의존성을 주입한 후 초기화 작업이 필요할 때 자동으로 호출하여 작동한다.

예) 초기 설정, 리소스 준비, 초기화

 

@Component
public class Parrot {
    private String name;

    @PostConstruct
    public void init() {
        this.name = "Kiki";
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

사용 예시

상황 설명
초기 데이터 로딩 DB에서 기본 설정값 불러오기, 캐시 세팅 등
외부 API 연동 시작 시점에 API 키 인증이나 연결 시도
로깅 및 검증 빈 초기화 로그 찍기, 환경 검증 등

 

주의할 점

항목 내용
리턴값 없음 void만 가능
예외 처리 예외 발생 시 애플리케이션 시작 자체가 실패할 수 있음
비동기 초기화 불가 블로킹 작업이나 오래 걸리는 작업은 피하는 게 좋음
스프링 외부에서는 동작 X 스프링 컨테이너가 관리할 때만 호출됨

Autowired와 Bean, Component

@Autowired는 스프링이 스프링 컨테이너 안에 있는 Bean 중에서" 해당 타입과 맞는 걸 찾아서 자동으로 주입해주는 어노테이션이다.

그래서 @Bean 으로 등록한 객체거나, @Component로 자동 등록을 해서 생성된 객체든 등록되어 있기만 하면 사용이 가능하다!

다만, 서로 의존 관계를 주입하는 클래스끼리 모두 스프링에 등록이 되어있어야 한다!

빈으로 등록을 했지만 주입 받는 클래스가 등록이 안되어있을 경우에는 @Autowired 로 주입이 되지 않는다는 것이다!

1. 둘 다 Bean으로 수동 등록 후 주입

a. ParrotZoo 클래스를 생성

    public class Parrot {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public void speak() {
            System.out.println("짹짹! " + name);
        }
    }
    public class Zoo {
        private Parrot parrot;

        public Zoo(Parrot parrot) {
            this.parrot = parrot;
        }

        public void open() {
            parrot.speak();
        }
    }

 

b. config에서 @Bean 으로 등록 후 의존성 주입

@Configuration
public class AppConfig {

    @Bean
    public Parrot parrot() {
        Parrot p = new Parrot();
        p.setName("Koko");
        return p;
    }

    @Bean
    public Zoo zoo(Parrot parrot) {
        return new Zoo(parrot);  // 의존성 주입됨!
    }
}

 

2. 둘 다 Component로 자동 등록 후 주입

 

a. @Component로 자동 등록

    @Component
    public class Parrot {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public void speak() {
            System.out.println("짹짹! " + name);
        }
    }

 

b. @Component + @Autowired로 주입

    @Component
    public class Zoo {

        private final Parrot parrot;

        @Autowired
        public Zoo(Parrot parrot) {
            this.parrot = parrot;
        }
    }

 

3. 하나는 수동, 하나는 자동 등록 후 주입

 

a. 빈으로 등록 후, 주입 받는 객체를 컴포넌트로 등록해서 자동 주입

    @Configuration
    public class AppConfig {

        @Bean
        public Parrot parrot() {
            Parrot p = new Parrot();
            p.setName("Koko");
            return p;
        }
    }
    @Component
    public class Zoo {

        @Autowired
        private Parrot parrot;  // 자동 주입

        public void open() {
            parrot.speak();
        }
    }

 

b. 주입할 객체를 컴포넌트로 등록하고, 주입 받는 객체를 빈으로 수동 주입

    @Component
    public class Parrot {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public void speak() {
            System.out.println("짹짹! " + name);
        }
    }
    @Configuration
    public class AppConfig {

        @Bean
        public Zoo zoo(Parrot parrot) {
            return new Zoo(parrot);  // 여기서도 자동 주입 가능!
        }
    }

 

@Autowired 를 쓰냐 안쓰냐의 차이인데, 결국 의존성을 주입하기 위해서는 객체가 스프링에 등록이 되어있어야 한다는 점이다. 주입 받는 객체가 @Component 로 자동 등록되어 있을 경우에만 @Autowired를 사용할 수 있다!