▼ Why ? What ?
"빈(Bean)" 을 다루기 위해선 "의존성 주입(DI)" 의 개념을 이해하는 것은 필수적이다. 최근에 GDSC - Web 커리큘럼에서 스프링 시큐리티 환경 설정 클래스를 다룰 때 빈(Bean)을 수동적으로 생성하는 코드를 작성해봤다. 이렇게 생성한 빈(Bean)을 사용하는 과정을 추가적으로 공부해봤는데, 의존관계가 있는 클래스 타입의 빈(Bean)을 "스프링 컨테이너"에서 탐색한 후에 찾은 빈(Bean), 즉 객체를 외부에서 주입해주는 과정이 필요하다. 따라서, DI에 대한 개념을 다잡아보고 스프링 컨테이너에 대해서 공부한 후에 정리하였다.
▼ DI (Dependency Injection)
DI ?
- "의존관계 주입 (DI)" 은 의존관계를 "스프링 컨테이너", 즉 외부에서 객체 간의 관계(의존성)를 결정해주는 것.
➜ 객체를 직접 생성하는 것이 아니라 외부에서 생성 후 주입시켜 주는 방식을 말한다 !
( "의존성" 은 객체를 생성하거나 사용함에 있어 의존관계가 있는 경우를 말하고, 의존 대상인 B가 변경되었을 때 그 영향이 A에도 미치게 되면 A는 B와 의존관계가 있다고 할 수 있다. )
DI가 필요한 이유 ?
- 아래의 코드를 보면 CakeBaker 객체와 CheeseCakeRecipe 객체에 의존관계가 있음을 알 수 있다.
➜ 두 클래스(CakeBaker, CheeseCakeRecipe)가 강하게 결합되어 있는 문제 때문에 레시피(ChocolateCakeRecipe 클래스)가 바뀔 때마다 CakeBaker 클래스의 생성자를 매번 변경해줘야 해서 유연성이 매우 떨어진다고 볼 수 있다 !
➜ 즉, 객체들 간이 아니라 클래스 간에 관계가 맺어진다고 볼 수 있고, 이는 객체 지향 5원칙(SOLID) 중 "구체화(Implementation; 구현 클래스)에 의존해선 안되고 추상화(Abstraction; 인터페이스)에 의존해야 한다." 라는 DIP 원칙에 위반하는 것이 된다.
➜ 이러한 문제를 해결하기 위해 "의존관계 주입(DI)" 가 필요한 것이다.
즉, DI를 통해 객체 간의 의존관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮추는 것이다 !
public class CakeBaker{
private CheeseCakeRecipe cheeseCakeRecipe;
public CakeBaker() {
this.cheeseCakeRecipe = new cheeseCakeRecipe();
}
}
의존관계 주입 (Dependency Injection) 예시
- CakeRecipe 인터페이스를 구현
public interface CakeRecipe { ... }
public class CheeseCakeRecipe implements CakeRecipe { ... }
- CakeBaker 클래스의 생성자에서 외부로부터 CakeRecipe 객체를 주입(Injection) 받도록 수정
public class CakeBaker {
/*
private CheeseCakeRecipe cheeseCakeRecipe;
public CakeBaker() {
this.cheeseCakeRecipe = new cheeseCakeRecipe();
}
*/
private CakeRecipe cakeRecipe;
public CakeBaker(CakeRecipe cakeRecipe) {
this.cakeRecipe = cakeRecipe;
}
}
- "스프링 컨테이너" 에서 애플리케이션 실행 시점에 필요한 객체, 즉 빈(Bean)을 생성하여 CakeBaker 클래스에 주입해준다 !
// Bean 생성
CakeRecipe cheeseCakeRecipe = new cheesCakeRecipe();
// DI 주입
CakeBaker cakeBaker = new CakeBaker(cheeseCakeRecipe);
▼ 스프링 컨테이너(Spring Container)
스프링 컨테이너 ?
- 스프링 프레임워크의 핵심 컴포넌트인 "스프링 컨테이너" 는 자바 객체, 즉 빈(Bean)의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공한다.
- 스프링 컨테이너의 기능
➜ 빈(Bean)의 인스턴스화, 구성, 전체 생명 주기 및 제거까지 관리한다.
스프링에서의 의존관계 주입(DI) - Constructor & Field & Setter Injection
- 스프링 컨테이너가 DI를 담당하고, "빈(Bean)" 에 의존성 주입을 하는 방법은 총 3가지가 있다.
➜ 스프링(Spring)에선 "생성자 주입 (Contructor Injection)" 을 지향하자 !

1. Constructor Injection - 생성자 주입
: 스프링(Spring)을 포함한 DI 프레임워크의 대부분이 이 "Constructor Injection" 을 권장하고 있다.
➜ 아래의 코드처럼 '@Autowired' 애너테이션을 사용하는 것이 생성자를 통해 의존관계가 있는 객체(Bean)를 주입하는 방식이다.
- @Autowired 애너테이션을 붙이면, 해당 생성자(Injection)가 자동으로 호출되고 의존 객체 'injectionService' 의 클래스 타입에 해당하는 빈(Bean)을 찾아서 주입해준다.
( 단, InjectionService 클래스가 빈(Bean)으로 등록되어 있어야 한다 ! )
public class Injection {
private InjectionService injectionService;
// 생성자 DI
@Autowired // Spring4.3부터는 @Autowired 생략 가능
public Injection(InjectionService injectionService) {
this.injectionService = injectionService;
}
}
- @Autowired는 해당 클래스 내에 생성자가 하나만 있을 땐 생략도 가능하다 !
➜ 게다가 '@RequiredArgsConstructor' 애너테이션을 함께 사용하게 되면 생성자를 생성하는 코드 없이 "생성자 주입" 이 가능하다 !
Why ?
➜ 롬복(Lombok)에서 제공하는 @RequiredArgsConstructor 애너테이션은 초기화 되지 않은 final 필드나 @NonNull 애너테이션이 붙은 필드에 대해 생성자를 자동으로 생성해주기 때문이다.
( final은 선언해준 동시에 초기화 시켜줘야 하는데, 꼭 생성자를 통해서만 초기화가 가능하다 ! )
@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final FirstQuestionService firstQuestionService;
private final SecondQuestionService SecondQuestionService;
/* 이렇게 하나의 생성자만 생성되기 때문에 @Autowired가 생략된 것으로 간주하여
생성자 주입이 가능한 것이다!
public QuestionController(FirstQuestionService firstQuestionService,
SecondQuestionService SecondQuestionService) {
this.firstQuestionService = firstQuestionService;
this.secondQuestionService = secondQuestionService;
}
*/
...
}
🔻 이처럼 final 키워드를 사용해서 값이 한 번 할당되면 변경할 수 없기에 자동적으로 "객체의 불변성(Immutability)" 이 보장되는 장점이 있다 !
➜ 초기에 값이 할당되기 때문에 NPE(Null Pointer Exception)도 절대 발생하지 않는다 !
2. Field Injection - 필드 주입
- 다음과 같이 빈(Bean)으로 등록된 객체를 주입하고자 하는 클래스에 간단하게 필드로 선언해준 후에 @Autowired 애너테이션만 달아주면 스프링에서 의존성을 주입해준다
➜ 단, 이렇게 하면 DI가 생성자 이후에 호출되므로, "생성자 주입" 처럼 필드를 final로 선언할 수 없다 !
➜ "객체의 불변성" 보장 X
public class Injection {
@Autowired
private InjectionService injectionService;
}
3. Setter Injection - 수정자 주입
- Setter 메서드에 @Autowired 애너테이션을 사용해서 주입하는 방식이다.
➜ 이 또한 "생성자 주입" 이 아니기 때문에, 필드에 final 키워드를 사용 X
➜ 따라서 "객체의 불변성" 도 보장되지 않으며 NPE도 발생할 수 있다.
public class Injection {
private InjectionService injectionService;
@Autowired
public void setInjectionService( InjectionService injectionService) {
this.injectionService = injectionService;
}
}
🔻 코드에서 보이다싶이 수정자 주입 방식을 이용하는 경우엔 Setter 메서드를 public으로 선언해두어야 하기 때문에 안정성이 떨어진다고 볼 수 있다. !
▼ Why ? What ?
"빈(Bean)" 을 다루기 위해선 "의존성 주입(DI)" 의 개념을 이해하는 것은 필수적이다. 최근에 GDSC - Web 커리큘럼에서 스프링 시큐리티 환경 설정 클래스를 다룰 때 빈(Bean)을 수동적으로 생성하는 코드를 작성해봤다. 이렇게 생성한 빈(Bean)을 사용하는 과정을 추가적으로 공부해봤는데, 의존관계가 있는 클래스 타입의 빈(Bean)을 "스프링 컨테이너"에서 탐색한 후에 찾은 빈(Bean), 즉 객체를 외부에서 주입해주는 과정이 필요하다. 따라서, DI에 대한 개념을 다잡아보고 스프링 컨테이너에 대해서 공부한 후에 정리하였다.
▼ DI (Dependency Injection)
DI ?
- "의존관계 주입 (DI)" 은 의존관계를 "스프링 컨테이너", 즉 외부에서 객체 간의 관계(의존성)를 결정해주는 것.
➜ 객체를 직접 생성하는 것이 아니라 외부에서 생성 후 주입시켜 주는 방식을 말한다 !
( "의존성" 은 객체를 생성하거나 사용함에 있어 의존관계가 있는 경우를 말하고, 의존 대상인 B가 변경되었을 때 그 영향이 A에도 미치게 되면 A는 B와 의존관계가 있다고 할 수 있다. )
DI가 필요한 이유 ?
- 아래의 코드를 보면 CakeBaker 객체와 CheeseCakeRecipe 객체에 의존관계가 있음을 알 수 있다.
➜ 두 클래스(CakeBaker, CheeseCakeRecipe)가 강하게 결합되어 있는 문제 때문에 레시피(ChocolateCakeRecipe 클래스)가 바뀔 때마다 CakeBaker 클래스의 생성자를 매번 변경해줘야 해서 유연성이 매우 떨어진다고 볼 수 있다 !
➜ 즉, 객체들 간이 아니라 클래스 간에 관계가 맺어진다고 볼 수 있고, 이는 객체 지향 5원칙(SOLID) 중 "구체화(Implementation; 구현 클래스)에 의존해선 안되고 추상화(Abstraction; 인터페이스)에 의존해야 한다." 라는 DIP 원칙에 위반하는 것이 된다.
➜ 이러한 문제를 해결하기 위해 "의존관계 주입(DI)" 가 필요한 것이다.
즉, DI를 통해 객체 간의 의존관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮추는 것이다 !
public class CakeBaker{
private CheeseCakeRecipe cheeseCakeRecipe;
public CakeBaker() {
this.cheeseCakeRecipe = new cheeseCakeRecipe();
}
}
의존관계 주입 (Dependency Injection) 예시
- CakeRecipe 인터페이스를 구현
public interface CakeRecipe { ... }
public class CheeseCakeRecipe implements CakeRecipe { ... }
- CakeBaker 클래스의 생성자에서 외부로부터 CakeRecipe 객체를 주입(Injection) 받도록 수정
public class CakeBaker {
/*
private CheeseCakeRecipe cheeseCakeRecipe;
public CakeBaker() {
this.cheeseCakeRecipe = new cheeseCakeRecipe();
}
*/
private CakeRecipe cakeRecipe;
public CakeBaker(CakeRecipe cakeRecipe) {
this.cakeRecipe = cakeRecipe;
}
}
- "스프링 컨테이너" 에서 애플리케이션 실행 시점에 필요한 객체, 즉 빈(Bean)을 생성하여 CakeBaker 클래스에 주입해준다 !
// Bean 생성
CakeRecipe cheeseCakeRecipe = new cheesCakeRecipe();
// DI 주입
CakeBaker cakeBaker = new CakeBaker(cheeseCakeRecipe);
▼ 스프링 컨테이너(Spring Container)
스프링 컨테이너 ?
- 스프링 프레임워크의 핵심 컴포넌트인 "스프링 컨테이너" 는 자바 객체, 즉 빈(Bean)의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공한다.
- 스프링 컨테이너의 기능
➜ 빈(Bean)의 인스턴스화, 구성, 전체 생명 주기 및 제거까지 관리한다.
스프링에서의 의존관계 주입(DI) - Constructor & Field & Setter Injection
- 스프링 컨테이너가 DI를 담당하고, "빈(Bean)" 에 의존성 주입을 하는 방법은 총 3가지가 있다.
➜ 스프링(Spring)에선 "생성자 주입 (Contructor Injection)" 을 지향하자 !

1. Constructor Injection - 생성자 주입
: 스프링(Spring)을 포함한 DI 프레임워크의 대부분이 이 "Constructor Injection" 을 권장하고 있다.
➜ 아래의 코드처럼 '@Autowired' 애너테이션을 사용하는 것이 생성자를 통해 의존관계가 있는 객체(Bean)를 주입하는 방식이다.
- @Autowired 애너테이션을 붙이면, 해당 생성자(Injection)가 자동으로 호출되고 의존 객체 'injectionService' 의 클래스 타입에 해당하는 빈(Bean)을 찾아서 주입해준다.
( 단, InjectionService 클래스가 빈(Bean)으로 등록되어 있어야 한다 ! )
public class Injection {
private InjectionService injectionService;
// 생성자 DI
@Autowired // Spring4.3부터는 @Autowired 생략 가능
public Injection(InjectionService injectionService) {
this.injectionService = injectionService;
}
}
- @Autowired는 해당 클래스 내에 생성자가 하나만 있을 땐 생략도 가능하다 !
➜ 게다가 '@RequiredArgsConstructor' 애너테이션을 함께 사용하게 되면 생성자를 생성하는 코드 없이 "생성자 주입" 이 가능하다 !
Why ?
➜ 롬복(Lombok)에서 제공하는 @RequiredArgsConstructor 애너테이션은 초기화 되지 않은 final 필드나 @NonNull 애너테이션이 붙은 필드에 대해 생성자를 자동으로 생성해주기 때문이다.
( final은 선언해준 동시에 초기화 시켜줘야 하는데, 꼭 생성자를 통해서만 초기화가 가능하다 ! )
@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final FirstQuestionService firstQuestionService;
private final SecondQuestionService SecondQuestionService;
/* 이렇게 하나의 생성자만 생성되기 때문에 @Autowired가 생략된 것으로 간주하여
생성자 주입이 가능한 것이다!
public QuestionController(FirstQuestionService firstQuestionService,
SecondQuestionService SecondQuestionService) {
this.firstQuestionService = firstQuestionService;
this.secondQuestionService = secondQuestionService;
}
*/
...
}
🔻 이처럼 final 키워드를 사용해서 값이 한 번 할당되면 변경할 수 없기에 자동적으로 "객체의 불변성(Immutability)" 이 보장되는 장점이 있다 !
➜ 초기에 값이 할당되기 때문에 NPE(Null Pointer Exception)도 절대 발생하지 않는다 !
2. Field Injection - 필드 주입
- 다음과 같이 빈(Bean)으로 등록된 객체를 주입하고자 하는 클래스에 간단하게 필드로 선언해준 후에 @Autowired 애너테이션만 달아주면 스프링에서 의존성을 주입해준다
➜ 단, 이렇게 하면 DI가 생성자 이후에 호출되므로, "생성자 주입" 처럼 필드를 final로 선언할 수 없다 !
➜ "객체의 불변성" 보장 X
public class Injection {
@Autowired
private InjectionService injectionService;
}
3. Setter Injection - 수정자 주입
- Setter 메서드에 @Autowired 애너테이션을 사용해서 주입하는 방식이다.
➜ 이 또한 "생성자 주입" 이 아니기 때문에, 필드에 final 키워드를 사용 X
➜ 따라서 "객체의 불변성" 도 보장되지 않으며 NPE도 발생할 수 있다.
public class Injection {
private InjectionService injectionService;
@Autowired
public void setInjectionService( InjectionService injectionService) {
this.injectionService = injectionService;
}
}
🔻 코드에서 보이다싶이 수정자 주입 방식을 이용하는 경우엔 Setter 메서드를 public으로 선언해두어야 하기 때문에 안정성이 떨어진다고 볼 수 있다. !