스프링 컨텍스트: 프레임워크가 관리하는 객체를 유지하는 데 사용하는 앱 메모리 공간이다. 프레임워크가 제공하는 기능으로 스프링 컨텍스트에서 보강해야 하는 모든 객체를 추가해야 한다.
2장에서는 컨텍스트 인스턴스의 getBean() 메서드를 사용하여 빈에 직접적으로 액세스했다. 하지만 앱에서는 필요한 곳에 스프링 컨텍스트의 인스턴스 참조를 제공하도록 스프링에 지시함으로써 하나의 빈에서 다른 빈으로 직관적으로 참조할 수 있게 해야 한다. 이 방법으로 빈들 사이의 관계를 설정한다(하나의 빈을 필요할 때 호출을 위임하고자 다른 빈에 대한 참조를 갖게 된다).
구성 파일에서 정의된 빈 간 관계 구현
이번에는 @Bean 애너테이션으로 메서드를 지정하는 구성 클래스에서 정의된 두 빈 간 관계를 구현하는 방법을 배운다.
스프링 컨텍스트에 앵무새(parrot)와 사람(person)이라는 두 인스턴스가 있다고 가정해 보자. 우리는 이 인스턴스를 생성하고 컨텍스트에 추가하고 사람이 앵무새를 소유하도록 만들고 싶다. 이를 위해서는 두 인스턴스를 연결해야 한다.
불필요한 복잡성을 추가하지 않고도 스프링 컨텍스트에서 빈을 연결하는 두 가지 접근 방식을 논의할 수 있으며 스프링 구성에만 집중할 수 있다. 두 방식(와이어링 및 오토와이어링) 모두 다음과 같은 두 단계가 있다.
- 2장에서 배운 대로 스프링 컨텍스트에 사람과 앵무새 빈 추가
- 사람과 앵무새 사이의 관계를 설정(사람이 앵무새 소유)
Parrot 객체와 Person 객체 정의
public class Parrot {
private String name;
// getters와 setters 생략
@Override
public String toString() {
return "Parrot : " + name;
}
}
public class Person {
private String name;
private Parrot parrot;
// getters와 setters 생략
}
구성 클래스의 @Bean 애너테이션 사용하여 빈 정의
@Configuration
public class ProjectConfig {
@Bean
public Parrot parrot() {
Parrot p = new Parrot();
p.setName("Koko");
return p;
}
@Bean
public Person person() {
Person p = new Person();
p.setName("Ella");
return p;
}
}
- 아직 인스턴스 사이에 아무런 관계가 설정되지 않았다.
1. 두 @Bean 메서드 간 직접 메서드를 호출하는 빈 작성 - 와이어링
두 클래스 인스턴스 간 관계 설정을 위한 첫 번째 방법(와이어링)은 구성 클래스에서 한 메서드에서 다른 메서드를 호출하는 것이다. 이 방법은 직관적이므로 자주 사용된다.
직접 메서드 호출을 사용하는 빈 간 링크 설정
@Configuration
public class ProjectConfig {
@Bean
public Parrot parrot() {
Parrot p = new Parrot();
p.setName("Koko");
return p;
}
@Bean
public Person person() {
Person p = new Person();
p.setName("Ella");
p.setParrot(parrot()); // 사람의 앵무새 속성에 앵무새 빈의 참조 설정
return p;
}
}
이렇게 하면 Parrot 타입의 인스턴스 두 개가 생성될까?
- 앵무새 빈이 컨텍스트에 이미 있을 때 : 스프링은 parrot() 메서드를 호출하는 대신 해당 컨텍스트에서 직접 인스턴스를 가져온다.
- 앵무새 빈이 컨텍스트에 아직 없을 때 : 스프링은 parrot() 메서드를 호출하고 빈을 반환한다.
빈이 컨텍스트에 이미 있는 경우 스프링은 호출을 @Bean 메서드로 전달하지 않고 기존 빈을 반환한다. 빈이 존재하지 않는 경우 스프링은 빈을 생성하고 해당 참조를 반환한다.
2. @Bean 메서드의 매개변수로 빈 와이어링 하기
참조하려는 빈을 정의하는 메서드를 호출하는 대신 해당 객체 타입의 메서드에 매개변수를 추가하고 스프링이 해당 매개변수를 이용하여 값을 제공하는 것에 의존하는 방식이다. 이 방식은 이전 방식보다 좀 더 유연하다.
이 방식을 사용하면 참조하려는 빈이 @Bean으로 애너테이션된 메서드로 정의되든 @Component 같은 스테레오타입 애너테이션으로 정의되든 상관없다.
@Configuration
public class ProjectConfig {
@Bean
public Parrot parrot() {
Parrot p = new Parrot();
p.setName("Koko");
return p;
}
@Bean
public Person person(Parrot parrot) { // 스프링은 매개변수에 앵무새 빈 주입
Person p = new Person();
p.setName("Ella");
p.setParrot(parrot); // 스프링이 전달한 참조로 사람의 속성 값을 설정
return p;
}
}
person 메서드를 호출할 때 스프링은 컨텍스트에서 앵무새 빈을 찾아 그 값을 메서드의 매개변수에 주입한다.
DI(의존성 주입)
- 프레임워크가 특정 필드 또는 매개변수에 값을 설정하는 기법
- IoC 원리를 응용한 것으로, IoC는 프레임워크가 실행될 때 애플리케이션을 제어하는 것을 의미한다.
- 생성된 객체 인스턴스를 관리하고 앱을 개발할 때 작성하는 코드를 최소화하는 데 도움이 되는 매우 편리한 방법이다.
@Autowired 애너테이션을 사용한 빈 주입
스프링 컨텍스트에서 빈 간 링크를 만드는 데 사용되는 또 다른 접근 방식이다. 빈을 정의하는 클래스를 변경할 수 있을 때(그 클래스가 의존성 일부가 아닐 때) @Autowired 애너테이션을 참조하는 기법을 자주 접하게 된다.
@Autowired 애너테이션을 사용하면 스프링이 컨텍스트에서 값을 주입하길 원하는 객체 속성을 표시하고, 의존성이 필요한 객체를 정의하는 클래스에 이 의도를 직접 표시할 수 있다. 1의 두 방식보다 두 객체 간 관계를 더 쉽게 확인할 수 있게 해준다.
@Autowired 애너테이션을 사용하는 방법에는 세 가지가 있다.
1. @Autowired로 클래스 필드를 이용한 값 주입
매우 간단한데 단점도 있어 프로덕션 코드를 작성할 때는 사용하지 않는다. 하지만 예제, 개념 증명(Poc), 테스트 작성에서 자주 쓰므로 사용 방법을 알아야 한다.
Person 클래스 정의
// 스테레오타입 애너테이션인 @Component는 스프링이 이 클래스(Person) 타입의 빈을 생성하고 추가하도록 지시
@Component
public class Person {
private String name = "Ella";
// 스프링이 스프링 컨텍스트에서 빈을 가져와 @Autowired 애너테이션된 필드 값에 직접 설정하도록 지시
// 이렇게 하면 두 빈 사이에 관계가 설정된다.
@Autowired
private Parrot parrot;
// getters and setters
}
Parrot 클래스 정의
@Component
public class Parrot {
private String name = "Koko";
// getters and setters
@Override
public String toString() {
return "Parrot : " + name;
}
}
이 예제에서는 스테레오타입 애너테이션을 사용하여 스프링 컨텍스트에 빈을 추가하였다. @Bean을 사용하여 빈을 정의할 수도 있지만, 실제 시나리오에서는 대부분 스테레오타입 애너테이션과 함께 @Autowired를 쓸 때가 많다.
@Configuration
@ComponentScan(basePackages = "beans")
public class ProjectConfig {
}
@ComponentScan 애너테이션으로 @Component 애너테이션을 사용하여 애너테이션을 지정한 클래스를 어디에서 찾을 수 있는지 그 위치를 스프링에 알려준다.
필드에 직접 값을 주입하면 다음과 같은 이유로 프로덕션 코드에 바람직하지 않다.
- 필드를 final로 만들 수 있는 방법은 없으며, final로 만들면 초기화한 후 아무도 값을 변경하지 못하게 할 수 있다.
@Autowired
private final Parrot parrot; // 초기값 없이는 final 필드를 정의할 수 없기 때문에 컴파일이 실패한다.
- 초기화할 때 값을 직접 관리하는 것이 더 어렵다.
하지만 때로는 객체의 인스턴스를 생성하고 단위 테스트 의존성을 쉽게 관리할 필요가 있다.
2. @Autowired를 사용하여 생성자로 값 주입
프로덕션 코드에서 가장 자주 이용되는 방식이며 권장하는 방식이다. 이 방법을 이용하면 필드를 final로 정의할 수 있어 스프링이 필드를 초기화한 후에는 아무도 필드 값을 변경할 수 없다.
생성자를 호출할 때 값을 설정할 수 있다는 점은 스프링이 필드를 주입하는 방식에 의존하지 않는 특정 단위 테스트를 작성할 때 도움이 된다.
@Component
public class Person {
private String name = "Ella";
private final Parrot parrot;
// 스프링이 Person 타입의 빈을 생성할 때 @Autowired 애너테이션이 달린 생성자를 호출
// 스프링은 매개변수 값으로 컨텍스트에서 Parrot타입의 빈을 전달
@Autowired
public Person(Parrot parrot) {
this.parrot = parrot;
}
// getters and setters
}
스프링 4.3버전 부터 클래스에 생성자가 하나만 있다면 @Autowired 애너테이션을 생략할 수 있다.
3. setter를 이용한 의존성 주입
개발자가 의존성 주입을 위해 setter를 사용하는 방식을 적용할 때는 많지 않다. 이 방식은 가독성이 떨어지고, final 필드를 만들 수 없으며, 테스트를 더 쉽게 만드는 데 도움이 되지 않는 등 장점보다 단점이 더 많다.
@Component
public class Person {
private String name = "Ella";
private Parrot parrot;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parrot getParrot() {
return parrot;
}
@Autowired
public void setParrot(Parrot parrot) {
this.parrot = parrot;
}
}
순환 의존성 다루기
스프링이 애플리케이션의 객체들에 대한 의존성을 빌드하고 설정하게 위임하는 것은 편리하다. 이 작업을 스프링에 맡기면 많은 코드를 작성하지 않고도 앱을 더 쉽게 읽고 이해할 수 있다. 하지만 스프링은 때때로 혼동될 수 있는데, 실제로 자주 발생하는 시나리오는 실수로 순환 의존성(circular dependency)을 생성하는 것이다.
순환 의존성은 빈(빈 A)을 생성하기 위해 스프링이 아직 없는 다른 빈(빈 B)을 주입해야 하는 상황을 의미한다. 하지만 빈 B도 빈 A에 대한 의존성이 필요하다. 따라서 빈 B를 생성하려면 먼저 빈 A가 있어야 한다. 이제 스프링은 교착 상태(deadlock)에 빠졌다. 빈 B가 필요하기 때문에 빈 A를 생성할 수 없고, 빈 A가 필요하기 때문에 빈 B를 생성할 수 없다.
@Component
public class Parrot { // 1
private String name = "Koko";
private final Person person;
@Autowired
public Parrot(Person person) { // 2
this.person = person;
}
}
@Component
public class Person { // 3
private String name = "Ella";
private final Parrot parrot;
@Autowired
public Person(Parrot parrot) { // 4
this.parrot = parrot;
}
}
- 스프링은 Parrot 타입의 빈을 생성해야 한다.
- Parrot 클래스 생성자를 호출하려면 스프링은 Person 타입의 빈이 필요하다.
- 스프링은 Person 타입의 빈을 생성하려고 한다.
- Person 타입의 빈을 생성하기 위해 스프링은 Parrot 타입의 빈이 필요하다. 스프링은 이제 교착 상태에 빠졌다!
순환 의존성은 피하기 쉽다. 생성을 위해 다른 객체에 의존해야 하는 객체를 정의하지 않는지 확인하면 된다.
스프링 컨텍스트에서 여러 빈 중 선택하기
이 절에서는 스프링에서 매개변수 또는 클래스 필드에 값을 주입해야 하지만 선택할 수 있는 동일한 타입의 빈이 여러 개 있는 경우에 대해 설명한다.
스프링 컨텍스트에 Parrot 빈이 세 개 있다고 가정해보자. Parrot 타입 값을 매개변수에 주입하도록 스프링을 구성하면 스프링은 어떻게 동작할까? 이런 시나리오에서 프레임워크는 동일한 타입의 빈 중 어떤 빈을 주입하도록 선택할까?
구현에 따라 2가지 경우가 존재할 수 있다.
1. 매개변수의 식별자가 컨텍스트의 빈 중 하나의 빈 이름과 일치하는 경우
값을 반환하는 @Bean으로 애너테이션도 메서드 이름과 동일하다. 이때 스프링은 매개변수와 이름이 동일한 빈을 선택한다.
@Configuration
public class ProjectConfig {
@Bean
public Parrot parrot1() {
Parrot p = new Parrot();
p.setName("Koko");
return p;
}
@Bean
public Parrot parrot2() {
Parrot p = new Parrot();
p.setName("Miki");
return p;
}
@Bean
// 매개변수 이름(parrot2)이 Miki 앵무새의 빈 이름(parrot2)과 일치한다.
public Person person(Parrot parrot2) {
Person p = new Person();
p.setName("Ella");
p.setParrot(parrot2);
return p;
}
}
실제 시나리오에서는 매개변수 이름에 의존하는 것을 피하고 싶을 것이다. 다른 개발자가 실수로 쉽게 리팩터링하고 변경할 수 있기 때문이다.
좀 더 안심할 수 있는 방법으로 특정 빈을 주입하려는 의도를 표현하고자 @Qualifier 애너테이션을 사용하는 방식이 가독성이 더 좋다. 의도를 명확하게 정의하고 싶을 때 사용하는 편이 더 좋다고 보지만, 다른 개발자들은 이 애너테이션을 추가하면 불필요한 (상용구) 코드가 생성된다고 생각할 수도 있다.
2. 매개변수의 식별자가 컨텍스트의 빈 이름과 일치하지 않는 경우
다음과 같은 선택지가 있다.
- 빈 중 하나를 기본으로 표시한 경우(@Primary 애너테이션 사용) 스프링은 기본 빈을 선택한다.
- @Qualifier 애너테이션을 사용하여 특정 빈을 명시적으로 선택할 수 있다.
- 어떤 빈도 기본 빈이 아니며, @Qualifier를 사용하지 않았다면 앱은 예외를 발생시켜 실패한다. 컨텍스트에 타입이 동일한 빈이 여러 개 있어 어떤 빈을 선택할지 모르겠다는 메시지를 출력한다.
Parrot 클래스
public class Parrot {
private String name;
// getters, setters and toString()
}
구성 클래스에 @Bean 애너테이션으로 Parrot 타입의 빈 두 개를 정의하려고 하므로 Parrot 클래스에 @Component 애너테이션을 추가하지 않는다.
Person 클래스
@Component
public class Person {
private String name = "Ella";
private final Parrot parrot;
public Person(@Qualifier("parrot2") Parrot parrot) {
this.parrot = parrot;
}
// getters and setters
}
@Qualifier 애너테이션으로 '컨텍스트에서 특정 빈을 주입한다'는 의도를 명확하게 표현하는 것이 좋다. 이렇게 하면 누군가가 변수 이름 리팩토링해서 앱 작동 방식에 영향을 미칠 가능성을 최소화할 수 있다.
구성 클래스
@Configuration
@ComponentScan(basePackages = "beans")
public class ProjectConfig {
@Bean
public Parrot parrot1() {
Parrot p = new Parrot();
p.setName("Koko");
return p;
}
@Bean
public Parrot parrot2() {
Parrot p = new Parrot();
p.setName("Miki");
return p;
}
}
'Spring > 스프링 교과서' 카테고리의 다른 글
[스프링 교과서] 4장 스프링 컨텍스트: 추상화 (0) | 2024.08.08 |
---|---|
[스프링 교과서] 2장 스프링 컨텍스트: 빈 정의 (0) | 2024.07.10 |
[스프링 교과서] 1장 현실 세계의 스프링 (0) | 2024.07.08 |