본문 바로가기
  • 시 쓰는 개발자
Spring

Inversion of Control (Bean내용 포함)

by poetDeveloper 2024. 3. 3.

백기선님의 스프링 강의를 듣고 작성하였습니다. 내용이 어려워서 종종 내용 확인해보고 틀린 부분은 보완하겠습니다.

 

스프링 핵심 3요소(Spring Triangle) 中  "제어의 역전", IoC(Inversion of Control)에 대해서 알아보자.

IoC : 의존성 관리를 외부에서 해준다.

한마디로 메소드나 객체의 호출을 개발자가 결정하는 것이 아니라 외부에서 결정하는 것이다. 예를 들어 다음과 같은 코드를 보자.

class Owner{
	private OwnerRepository repo;
    
	public Owner(OwnerRepository repo) {
	this.repo = repo;
    }
}

 

Owner 클래스에서는 분명히 OwnerRepository를 사용하는데, 객체를 직접 생성하고 있진 않다. 생성자를 통해 Owner 클래스 밖에서 받아오는 구조이다. OwnerRepository에 대한 의존성을 만드는 일을 더이상 Owner에서 하는 게 아니라 밖에서 하는 것이기 때문에 제어권이 밖에 있는 것이다. 그래서 IoC와 함께 자주 언급되는 의존성 주입 DI(Dependency Injection)도 IoC의 일종이다.

무엇이 좋은가?

이렇게 IoC를 고려해 코드를 작성하면 무엇이 좋을까??

  • 먼저, 코드 자체만 보면 Owner를 사용하기 위해서는 반드시 OwnerRepository를 만든 후에 사용해야한다. 따라서, OwnerRepository가 필요한 시점에서 안전하게 코드를 작성할 수 있게 된다.
  • 또한 유지보수, 가독성 등에서 이점을 얻게 된다.

IoC 컨테이너

IoC컨테이너는 빈을 만들고 제공하며, 빈들 사이의 의존성을 엮어주는 역할을 한다. IoC컨테이너는 스프링 컨테이너, 또는 DI 컨테이너라고도 불린다. IoC 컨테이너의 종류로는 BeanFactory와 ApplicationContext가 있는데, ApplicationContext가 BeanFactory를 상속받고 있기 때문에 같은 일을 한다고도 본다. 하지만 우리가 직접 ApplicationContext를 써가며 코드를 짤 일은 거의 없다. IoC 컨테이너가 자동으로 관리해주기 때문이다. 여기서 간단히 빈과 컨테이너를 알아보자.

  • : 스프링 컨테이너(IoC컨테이너)가 관리하는 객체
  • 빈 팩토리 : 빈들을 관리하는 컨테이너
  • 컨테이너 : 객체들이 담긴 공간을 의미한다. 객체의 생명주기를 관리하여 개발자는 로직에만 집중할 수 있게 도와준다.

참고로 위 사진같은 녹색 콩 모양이 있다면 빈으로 등록되어 있는 것이다. 빈에 대해서는 아래에서 더 자세히 다뤄보자. 이때 Ioc 컨테이너가 하는 의존성 주입은 Bean끼리만 가능하다. 즉, IoC컨테이너 내에 있는 빈들끼리만 서로간의 의존성 주입이 가능하다.

Bean을 등록방법

빈 등록 방법에는 2가지가 있다.

  1. 컴포넌트 스캔 방식: 말이 어려워서 그렇지, 그냥 어노테이션을 사용하는 방식이다. 다만 이름이 컴포넌트 스캔 방식인 이유는, @Repository, @Controller, @Service같은 어노테이션이 컴포넌트 스캔을 포함하고 있기 때문이다. 참고로 빈으로 등록된 객체끼리 연결은 Autowired를 이용해서 가능하다.
  2. 빈으로 직접 등록 : 자바 코드로 직접 빈등록 하는 방법이다. @Configuration이 붙은 클래스에서, 어떤 메소드에서 빈으로 등록할 클래스를 return시켜주고 이 메소드에 @Bean을 붙여준다. 아래 코드를 참고한다.
@Configuration
public class TestConfig{

	@Bean
	public TestController testController() {
		return new TestController();
	}
}

위 코드를 봐도 알 수 있듯이, 아무래도 컴포넌트 스캔 방식이 편하기 때문에 정형화된 controller, service, repository같은 부분에서는 어노테이션을 활용해 빈 등록을 해주고, 상황에 따라 상황에 따라 구현 클래스를 변경해야한다면 설정을 통해 스프링 빈으로 등록해준다. 예를들어, X라는 DB를 쓰다가 나중에 Y라는 DB로 바꿀 계획이라면 아래와 같이 쉽게 수정할 수 있다.

@Configuration
public class TestConfig{

	@Bean
	public MemberRepository memberrepository() {
		return new YRepository();
        // return new XRepository(); (원래 쓰던 DB)
	}
}

Bean 꺼내서 쓰기

Bean을 꺼내 쓰는 방법은 2가지이다.

 

1. ApplicationContext를 직접 사용해서 쓸 수 있다.

applicationContext.getBean(TestController.class);

 

2. @Autowired를 활용한다. 꺼내 쓸 코드 위에 붙여준다. 아래는 필드 자체에 의존성을 주입하는 방법이다.

@Autowired
private OwnerRepository owners;

 

ApplicationContext 자체를 직접 코드로 쓸 일이 많지 않으므로 2번 Autowired 방법을 권장한다. 여기서 나오는 Autowired에 대해 아래에서 더 자세히 다뤄보자.

의존성 주입 방식 3가지

의존성 주입은 3가지 방법이 있다. 이중에서 권장하는 방법은 생성자 방식이다.

 

1. 생성자 주입 (권장)

생성자로 의존성 주입을 사용하게 되면, 필수적으로 사용해야하는 레퍼런스 없이는 인스턴스를 만들지 못하도록 "강제"할 수 있다. 이것은 위에서 IoC의 이점에서 언급했듯이 안전하게 코딩할 수 있는 방법 중 하나이다. 필드와 setter로 의존성 주입을 하게 되면 의존성 없이도 일단은 인스턴스 자체를 만들 수가 있다. 그렇기에 원하는 결과가 안나올 수 있다.

물론 생성자 방식에서도 단점은 있는데, 바로 순환 참조가 발생할 수 있다는 것이다. A를 만들려고 할 때 B를 참조, 근데 B를 만들 때 A를 참조 ... 그래서 둘 다 만들 수 없는 것이다. 이런 상황에선 오히려 필드/Setter 주입방식이 더 나을 수도 있긴 하지만 애당초 그런 상황을 만들지 않게 하면서 생성자 주입 방식을 사용하는 것이 바람직하다.

@Controller
class OwnerController{

	private final OwnerRepository owners;
    
	public OwnerController(OwnerRepository ownerrepository){
		this.owners = ownerrepository;
	}
}

 

2. 필드 주입 (권장안함)

필드 주입을 사용할 땐 인스턴스를 반드시 만들어야 하는 상황이기에 final을 붙이면 안된다. 다만, 테스트 코드 작성시 편리함을 위해 필드 주입을 사용하기도 한다. 테스트만 할 때 사용될 것이기 때문에 편리하게 필드 주입으로 할 수 있다.

@Autowired
private OwnerRepository owners;


3. Setter 주입

Setter 주입도 마찬가지로 final을 붙이지 않는다.

private OwnerRepository owner;

@Autowired
public void setOwner(OwnerRepository owner){
	this.owner = owner;
}