해당 포스트는 백기선 님의 강의를 바탕으로 작성되었습니다.
일반적인 의존성 설정과 관리는
class Controller {
private Repository repo = new Repository();
}
와 같은 형태로 의존성을 클래스가 만들어 사용합니다.
하지만 IoC 는 Inversion of Control (제어의 역전)이라고 하며
직접 의존성을 관리하는 것이 아닌
class Controller {
private Repository repo;
public Controller(Repository repo) {
this.repo;
}
}
위의 코드와 같이 Controller 클래스는 Repository를 사용을 하지만
직접 객체를 생성하지는 않고, 누군가가 Controller 밖에서 생성자를 통해 받아옵니다.
그렇다면 의존성을 생성하는 일은 더 이상 Controller의 일이 아니므로 제어권이 역전되었다고 합니다.
이렇게 의존성을 주입하는것을 DI (Dependency Injection)라고 하는데
DI 또한 일종의 IoC 라고 할 수 있습니다.
IoC 컨테이너는 주로 Application Cotext를 사용하는데 그 이유는 BeanFactory를 사용해도 되지만
Application Context가 BeanFactory를 상속받고 있으며, 더 다양한 기능을 가지고 있기 때문
IoC Container는 Bean을 만들고 그들 사이의 의존성을 엮어주며,
Container가 가지고 있는 Bean들을 제공해주는 역할을 합니다.
여기서 주의할 점은
기본적으로 DI (Dependency Injection)는
IoC Container 에 생성되어 있는 Bean 들만 서로 의존성 주입이 가능하고
IoC Container 에 있는 Bean들을 가져오는 방법을 제공합니다.
IoC Container 밖에 있는 객체에는 의존성 주입이 가능하지만 지양하는 편입니다.
@Autowired
ApplicationContext applicationContext;
public OwnerController(OwnerRepository clinicService,
VisitRepository visits,
ApplicationContext applicationContext) {
this.owners = clinicService;
this.visits = visits;
this.applicationContext = applicationContext;
}
@GetMapping("/bean")
@ResponseBody
public String bean() {
return "bean : " + applicationContext.getBean(OwnerRepository.class)
+ "
" + "owners : " + this.owners;
}
위와 같은 코드로 IoC Container에서 OwnerRepository를 가져온 것과
ApplicationContext가 주입해준 owners 가 같은지 비교해 볼 수 있습니다.
해당 코드를 동작시키면 아래와 같은 결과값을 볼 수 있습니다.
bean : org.springframework.data.jpa.repository.support.SimpleJpaRepository@29031da1
owners : org.springframework.data.jpa.repository.support.SimpleJpaRepository@29031da1
두 객체는 같은 해쉬 코드를 가지고 있으며 두 객체는 같은 객체라고 볼 수 있습니다.
이러한 형태를 Singleton Scope의 객체라고 합니다.
Spring은 Bean을 생성할 때 기본적으로 모든 Bean을 Singleton으로 생성하여 관리하는데
SIngleton Bean은 Spring 컨테이너에서 한 번 생성되며, 컨테이너가 사라질 때 같이 제거되며,
객체 하나를 Application 전반에서 계속 재사용됩니다.
특히, Multi Thread 상황에서 Singleton Scope을 구현하는 일은 굉장히 번거롭고 조심스러운 일인데,
IoC Container를 사용하면 특별한 코드를 작성하지 않아도 편하게 Singleton Scope을 달성할 수 있으며,
IoC Container를 사용하는 이유 중 하나입니다.
객체 중에서 Spring IoC Container가 관리하는 객체를 Bean이라고 합니다.
간단하게 비교를 하자면
OwnerController ownerController = new OwnerController();
OwnerController bean = applicationContext.getBean(OwnerController.class);
해당 코드에서 위의 임의로 생성한 ownerController는 Bean이 아닙니다.
하지만 아래의 bean은 같은 ownerController 지만,
이 객체는 ApplicationContext에서 가져온 객체이기 때문에 Bean입니다.
이처럼 Bean은 ApplicationContext가 담고 있는 객체 즉, IoC Container에 담겨있는 객체를 말합니다.
Bean을 등록하는 방법은 크게 2가지가 있습니다.
@Component
@Repository
@Service
@Controller
@Configuration 등
우리가 흔히 보는 여러 가지 Annotation은 사실상 @Component라는 meta Annotation을 사용한 Annotation으로 Component Annotation 이라고 볼 수 있습니다.
Spring IoC Container가 사용하는 여러 가지 Interface 를 LifeCycleCallback 이라고 부릅니다
여러가지 LifeCycleCallback 중에는 Component라는 Annotation이 붙어있는 모든 클래스를 찾아서
그 클래스의 인스턴스를 Bean으로 등록하는 Annotation 처리가 등록되어있습니다.
Spring Boot Project 같은 경우 @SpringBootApplication 이러한 Annotation이 적용되어 있는데
그 Annotation 안에 @ComponentScan이라는 Annotation 이 적용되어 있습니다.
이 Annotation 자체가 행위를 처리하는 것은 아니고
주석과 비슷한 것이지만 Component 범주를 알려줍니다.
즉, @ComponentScan 어노테이션의 설정값에 따라 ComponentScan 이 동작합니다.
그 외에도 Spring Data JPA가 제공하는 기능에 의해서 Repository를 Bean으로 등록할 수 있습니다.
방법은 특정 Annotation 없이도 특정 Interface를 상속받은 경우에
그 Interface를 상속받은 객체를 찾아서
그 객체의 구현체를 IoC Container 내부적으로 만들어서 Bean으로 등록해주는 복잡한 과정을 처리합니다.
해당 객체의 빈 생성 여부를 Test 코드를 작성하여 알아볼 수 있습니다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleControllerTest {
@Autowired
ApplicationContext applicationContext;
@Test
public void testDI() {
SampleController bean =
applicationContext.getBean(SampleController.class);
assertThat(bean).isNotNull();
}
}
(최근 추세는 Java 설정 파일을 더 많이 사용합니다.)
Java 설정 파일 예시
@Configuration
public class SampleConfig {
@Bean
public SampleController sampleController() {
return new SampleController();
}
}
설정 파일을 이용하여 직접 Bean을 등록하면
@Controller Annotation을 생성하지 않고도 Bean으로 등록하여
위의 Test 코드 실행 시 성공하는 모습을 볼 수 있습니다.
Spring이 제공하는 DI 의존성 주입은 다양한 방법이 있습니다.
Autowired Annotation은 Field, Setter, Constructor 등 다양한 곳에서 사용할 수 있습니다.
예제 코드를 보면
class Controller {
private Repository repo;
public Controller(Repository repo) {
this.repo;
}
}
이처럼 생성자를 사용해서 주입을 받고 있는데
class Controller {
private Repository repo;
@Autowired
public Controller(Repository repo) {
this.repo;
}
}
원래는 이처럼 @Autowired Annotation을 사용할 수 있습니다.
하지만 Spring version 4.3부터 어떠한 클래스에 생성자가 하나이고,
그 생성자로 주입받는 reference 변수들이 Bean으로 등록되어 있다면
그 Bean을 자동으로 주입해주도록 기능이 추가되었습니다.
-> @Autowired Annotation을 생략 가능합니다
생성자가 아닌 Field에 주입받고 싶다면
class Controller {
@Autowired
private Repository repo;
}
이러한 방식으로 작성이 가능합니다
Setter로 주입한다면
class Controller {
private Repository repo;
@Autowired
public void setRepo(Repository repo) {
this.repo = repo;
}
}
Spring IoC Container가 해당 인스턴스를 생성하고 Setter를 통해서
IoC Container의 해당 Bean의 의존성을 주입합니다.
만약 @Autowired Annotation이 작성된 객체가 Bean으로 등록이 되어있지 않다면 Error가 발생합니다. (Error message : No qualifying bean)
이 중 Spring Framework Reference에서 권장하는 방법은 생성자를 통한 주입입니다.
생성자를 사용하는 주입이 좋은 이유는 필수적으로 사용해야 하는
Reference 없이는 해당 인스턴스를 생성하지 못하도록 강제할 수 있습니다.
Field Injection이나 Setter Injection 은 Reference 없이도
인스턴스가 생성이 가능하기 때문에 문제가 발생할 수 있습니다.
하지만 만약 순환 참조가 발생한다면 ( A -> B 참조 , B -> A 참조 )
이러한 경우는 두 인스턴스를 둘 다 생성할 수 없기 때문에
생성자 대신에 필드나 세터에 주입하는 방식으로 인스턴스를 만든 후
그다음 서로의 인스턴스를 주입하는 방식으로 해당 문제를 해결할 수 있습니다.
순환 참조가 안 생기도록 작성하는 것이 Best
생성자로 주입 시 예제 코드에 Final이 붙은 이유
@Controller
class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM =
"owners/createOrUpdateOwnerForm";
// final을 굳이 붙이지 않아도 상관은 없지만 한번 주입받은
// Reference가 다른 Reference로 바뀌지 않도록 보장하기 위함
private final OwnerRepository owners;
private VisitRepository visits;
public OwnerController(OwnerRepository clinicService,
VisitRepository visits) {
this.owners = clinicService;
this.visits = visits;
}
}