Spring을 사용하다 보면 자연스럽게 접하게 되는 용어들이 있습니다.
그중에서도 IoC(제어의 역전, Inversion of Control)와 DI(의존성 주입, Dependency Injection)는 매우 중요한 개념입니다.
이 용어들은 처음에는 다소 어렵게 느껴질 수 있지만, 이를 잘 이해하고 활용할 수 있다면 코드의 유지보수성, 확장성, 테스트 가능성을 크게 향상할 수 있습니다.
특히 IoC 컨테이너와 Bean은 Spring의 핵심 요소로서, 이들이 어떻게 작동하고 어떤 역할을 하는지 깊이 이해하는 것이 중요합니다. 이번 포스팅에서는 IoC 컨테이너와 Bean의 개념, 역할, 그리고 이들이 어떻게 작동하는지에 대해 알아보겠습니다 😁
IoC란❓
Ioc(Inversion of Control)란❓
먼저 Inversion of Control을 직역한다면 "제어의 반전"이라고 합니다.
IoC(Inversion of Control)는 객체의 제어 흐름을 개발자가 아닌 프레임워크가 담당하는 디자인 원칙을 말합니다.
이는 전통적인 프로그래밍 방식에서 개발자가 직접 객체를 생성하고 관리하는 것과는 달리, IoC에서는 프레임워크가 이러한 작업을 대신합니다.
IoC의 구현 방식
IoC는 다양한 방식으로 구현될 수 있으며, 대표적으로는 다음과 같은 방식이 있습니다
- Dependency Injection (DI)
- Service Locator Pattern
- Event-Driven Programming
- Aspect-Oriented Programming (AOP)
- Factory Pattern
- Template Method Pattern
IoC의 장단점
IoC의 장점
- 결합도 감소
- IoC를 사용하면 객체 간의 결합도가 낮아지고, 시스템이 더 모듈화됩니다.
- 테스트 용이성
- 의존성을 외부에서 주입받기 때문에, 테스트 시에 모의 객체(mock objects) 등을 사용하여 쉽게 테스트할 수 있습니다.
- 유연성
- 의존성이 중앙에서 관리되므로, 시스템의 구성과 변경이 더 유연해집니다.
- 코드의 재사용성
- 다양한 객체들이 의존성을 외부에서 주입받기 때문에, 코드의 재사용성이 증가합니다.
IoC의 단점
- 학습 곡선
- IoC를 이해하고 사용하는 데에는 다소 학습이 필요합니다.
- 디버깅 어려움
- 제어 흐름이 명시적이지 않기 때문에 디버깅이 어려울 수 있습니다.
- 프레임워크 의존성
- IoC를 구현하는 프레임워크나 컨테이너에 의존하게 되어, 특정 프레임워크에 종속될 수 있습니다.
Container란❓
Container는 소프트웨어에서 객체의 생성, 초기화 및 생명 주기를 관리하는 구조체입니다. 또한, 객체 간의 의존성을 해결하고, 설정 정보를 중앙에서 관리합니다.
DI(의존성 주입)란❓
IoC Container(Invesion of control Container)를 알아보기에 앞서 먼저 아래의 "DI"용어를 이해하고 가는 것이 중요합니다.
- DI(의존성 주입, Dependency Injection)
- 객체 지향 설계에서 객체 간의 의존성을 관리하는 기법으로 DI를 통해 객체가 필요로 하는 의존성(다른 객체)을 주입하는 것
public class Soccer {
private Exercise exercise;
public Soccer() {
this.exercise = new Exercise(); // 객체를 직접 생성
}
}
위 코드를 보면 Soccer 클래스는 Exercise 객체를 직접 생성하고 있고, Soccer 클래스는 Exercise 클래스의 구현에 강하게 결합되어 있습니다. 이는 Exercise 클래스가 변경되면 Soccer 클래스도 변경해야 할 가능성이 있음을 의미합니다.
따라서 Soccer는 Exercise에 의존하고 있습니다. 즉, Soccer 클래스의 동작이 Exercise 객체에 영향을 받습니다.
이와 같이 DI는 객체가 자신의 의존성을 직접 생성하거나 찾는 것이 아니라, 외부에서 제공받도록 하는 방식입니다.
DI(의존성 주입)의 종류
의존성 주입(Dependency Injection, DI)에는 여러 가지 주입 방식이 있으며, 각각의 방식은 객체에 의존성을 제공하는 방법이 다릅니다. 주요 주입 종류는 다음과 같습니다.
1. 생성자 주입 (Constructor Injection)
생성자 주입은 의존성을 객체가 생성될 때 생성자를 통해 주입하는 방식입니다. 이 방법은 객체가 생성될 때 모든 의존성이 주입되므로, 객체가 완전히 초기화된 상태에서 사용할 수 있습니다.
public class Consumer {
private Food food;
// 생성자 주입
public Consumer(Food food) {
this.food = food;
}
public void eat() {
this.food.eat();
}
public static void main(String[] args) {
// 의존성 주입: Chicken 객체를 Consumer에 주입
Consumer consumer = new Consumer(new Chicken());
consumer.eat(); // 출력: 치킨을 먹는다.
// 의존성 주입: Pizza 객체를 Consumer에 주입
consumer = new Consumer(new Pizza());
consumer.eat(); // 출력: 피자를 먹는다.
}
}
interface Food {
void eat();
}
class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
장점
- 불변성
- 객체가 생성될 때 모든 의존성이 주입되므로, 객체는 생성 후 불변 상태가 됩니다.
- 명확성
- 객체의 모든 필수 의존성이 명시적으로 생성자에서 주입되므로, 객체의 의존성을 명확히 알 수 있습니다.
단점
- 변경 불가능: 객체를 생성한 후에 의존성을 변경할 수 없습니다.
2. 필드 주입 (Field Injection)
필드 주입은 의존성이 객체의 필드에 직접 주입되는 방식입니다. 주로 프레임워크에서 지원하는 방식으로, 의존성 주입을 간편하게 처리할 수 있습니다.
public class Consumer {
Food food; // 의존성 필드
void eat() {
this.food.eat(); // 의존성의 메소드 호출
}
public static void main(String[] args) {
Consumer consumer = new Consumer(); // Consumer 객체 생성
consumer.food = new Chicken(); // 의존성 주입: Chicken 객체 설정
consumer.eat(); // 출력: 치킨을 먹는다.
consumer.food = new Pizza(); // 의존성 변경: Pizza 객체 설정
consumer.eat(); // 출력: 피자를 먹는다.
}
}
interface Food {
void eat(); // 공통 메소드 정의
}
class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
이처럼 Food를 Consumer에 포함시키고 Food에 필요한 객체를 주입받아 사용할 수 있습니다.
장점
- 간편함
- 코드가 간결하고, 직접 필드에 값을 할당하여 의존성을 주입할 수 있으므로 간단하게 사용할 수 있습니다.
- 유연성
- 객체 생성 후 의존성을 쉽게 변경할 수 있습니다.
단점
- 캡슐화 부족
- 필드에 직접 접근하여 값을 설정하는 것은 객체의 캡슐화 원칙을 약화시킬 수 있습니다. 즉 객체의 상태가 외부에서 직접 변경될 수 있습니다.
- 불완전 초기화
- 객체가 생성된 후에 의존성을 설정하기 때문에, 객체의 초기 상태가 불완전할 수 있습니다. 즉 객체가
초기화되지 않은 상태에서 메서드를 호출할 가능성이 있습니다.
- 객체가 생성된 후에 의존성을 설정하기 때문에, 객체의 초기 상태가 불완전할 수 있습니다. 즉 객체가
3. 세터 주입 (Setter Injection)
세터 주입은 객체 생성 후, 세터 메소드를 통해 의존성을 주입하는 방식입니다. 이 방법은 객체가 생성된 후에 의존성을 설정할 수 있습니다.
public class Consumer {
Food food; // 의존성 필드
void eat() {
this.food.eat(); // 의존성의 메소드 호출
}
// 세터 메소드
public void setFood(Food food) {
this.food = food;
}
public static void main(String[] args) {
Consumer consumer = new Consumer(); // Consumer 객체 생성
consumer.setFood(new Chicken()); // 의존성 주입: Chicken 객체 설정
consumer.eat(); // 출력: 치킨을 먹는다.
consumer.setFood(new Pizza()); // 의존성 변경: Pizza 객체 설정
consumer.eat(); // 출력: 피자를 먹는다.
}
}
interface Food {
void eat(); // 공통 메소드 정의
}
class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
장점
- 유연성
- 객체 생성 후 의존성을 설정할 수 있으므로, 다양한 상황에 맞게 의존성을 주입할 수 있습니다.
- 선택적 의존성
- 의존성을 필수가 아닌 선택적으로 설정할 수 있습니다.
단점
- 불완전 초기화
- 객체가 생성된 후
의존성이 설정되지 않으면, 객체가정상적으로 동작하지 않을 수 있습니다.
- 객체가 생성된 후
- 불변성 부족
- 객체가 생성 후 상태를 변경할 수 있습니다.
따라서 각 주입 방식은 상황과 요구 사항에 따라 적절하게 선택할 수 있습니다.
생성자 주입은 객체의 불변성을 보장하고, 세터 주입은 유연성을 제공하며, 필드 주입은 코드의 간결함을 유지합니다.
DI를 활용하면 객체 간의 결합도를 줄이고, 코드의 유연성과 유지보수성을 높일 수 있습니다.
IoC Container란❓
그렇다면 IoC Container란 무엇일까요?
IoC Container는 Inversion of Control (IoC) 원칙을 구현하고 관리하는 핵심 구성 요소로, 객체의 생성, 초기화, 의존성 주입을 자동으로 처리하는 프레임워크입니다.
또한 IoC 컨테이너는 객체의 생명 주기를 관리하고, 객체 간의 의존성을 해결하며, 설정 정보를 중앙에서 관리합니다.
위 사진은 Spring Docs에서 소개하는 내용입니다. "IoC는 DI로도 알려져 있다"라고 소개하고 있습니다.
‘DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다’
의역을 해보자면 위와 같이 말할 수 있으며, 이때 생성된 객체를 "Bean"이라고 합니다.
따라서 Spring IoC Container는 'Bean'을 모아둔 컨테이너라고도 할 수 있습니다.
Bean란❓
바로 위에서 알아보았듯이 Ioc Container에 의해서 생성된 객체를 "Bean"이라고 했습니다.
Bean은 Spring Framework에서 사용되는 용어로 빈(Bean)은 스프링 컨테이너에 의해 관리되는 재사용 가능한 소프트웨어 컴포넌트입니다.
Spring에서 Bean은 애플리케이션의 핵심 구성 요소이며, IoC Container가 객체의 생성, 초기화, 의존성 주입, 소멸을 관리합니다.
Bean 등록 방법
Bean등록 방법에는 크게 3가지 경우가 있습니다.
- XML 기반 설정
- @Bean 어노테이션 기반
- @Component, @Controller, @Service, @Repository 어노테이션 기반 설정
1.XML 기반 설정
<!-- spring-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 빈 정의 -->
<bean id="myBean" class="com.example.클래스명">
<property name="message" value="Hello from XML configuration!"/>
</bean>
</beans>
application-config.xml을 resource 아래에 만들어줍니다.
스프링 XML 설정 파일에서는 <beans> 엘리먼트를 사용하여 빈을 정의합니다.
2. @Bean 기반 설정
자바 클래스를 사용하여 Bean을 정의하고 생성할 수 있으며, @Configuration 애너테이션과 @Bean 메서드를 사용하여 빈을 설정합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
MyBean myBean = new MyBean();
myBean.setMessage("Hello from Java-based configuration!");
return myBean;
}
}
즉 빈 객체로 등록하고 싶은 메서드의 위에 @Bean 어노테이션을 추가하면 됩니다.
3. 어노테이션 기반 설정
어노테이션 기반 설정은 Spring Framework에서 빈의 정의와 의존성 주입을 자바 애너테이션을 통해 간편하게 처리하는 방법입니다.
XML 파일 대신 자바 코드에서 직접 빈을 정의하고 구성할 수 있으며, 이는 코드와 설정을 밀접하게 통합하여 가독성과 유지보수성을 향상합니다.
@Component
위 그림과 같이 클래스 옆에 Bean 모양이 뜨는 것은 그 클래스가 Spring의 관리 하에 있는 Bean으로 등록되었다는 것을 의미합니다.
import org.springframework.stereotype.Component;
@Component
public class MyBean {
public void doSomething() {
System.out.println("Doing something...");
}
}
- @Component
- 역할
- Spring 컨테이너에 의해 관리되는 일반 빈을 정의합니다.
- 사용법
- 클래스를 빈으로 등록하려면 해당 클래스에 @Component 어노테이션을 추가합니다.
- 역할
@Service
@Service 위에 @Service 위에 @Component가 추가되어 있는 것을 볼 수 있습니다. 따라서 IoC Container가 찾아서 등록이 가능한 것입니다.가 추가되어있는 것을 볼 수 있습니다. 따라서 IoC Container가 찾아서 등록이 가능한 것입니다.
import org.springframework.stereotype.Service;
@Service
public class MyService {
public void execute() {
System.out.println("Executing service...");
}
}
- @Service
- 역할
- 서비스 레이어의 빈을 정의합니다.
- @Component의 특수화된 형태로, 주로 비즈니스 로직을 처리하는 서비스 클래스에 사용됩니다.
- 사용법
- 서비스 클래스에 @Service 어노테이션을 추가합니다.
- 역할
@Repository
import org.springframework.stereotype.Repository;
@Repository
public class MyRepository {
public void save() {
System.out.println("Saving data...");
}
}
- @Repository
- 역할
- 데이터 액세스 레이어의 빈을 정의합니다.
- @Component의 특수화된 형태로, 주로 데이터베이스와의 상호작용을 처리하는 DAO 클래스에 사용됩니다.
- 사용법: DAO 클래스에 @Repository 애너테이션을 추가합니다.
- 역할
@Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class MyController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() {
return "hello";
}
}
- @Controller
- 역할
- 웹 애플리케이션에서 컨트롤러 클래스를 정의합니다.
- 주로 Spring MVC에서 요청을 처리하는 컨트롤러에 사용됩니다.
- 사용법
- 웹 요청을 처리하는 클래스에 @Controller 어노테이션을 추가합니다.
- 역할
@Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
}
- @Configuration
- 역할
- 설정 클래스를 정의합니다.
- Bean 설정을 자바 코드로 작성할 수 있으며, @Bean 메서드를 통해 빈을 정의합니다.
- 사용법
- 설정 클래스에 @Configuration 어노테이션을 추가합니다.
- 역할
@Autowired
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
public void performAction() {
myRepository.doSomething();
}
}
- @Autowired
- 역할
- 의존성 주입을 자동으로 처리합니다.
- Bean의 필드, 생성자, 세터 메서드에 사용되어 Spring 컨테이너가 의존성을 주입합니다.
- 사용법
- 빈의 의존성을 주입하려는 필드, 생성자, 세터 메서드에 @Autowired 어노테이션을 추가합니다.
- 역할
가장 일반적으로 사용되는 어노테이션입니다.
@Autowired 생략 조건
생성자가 1개일 때
@Component
public class MyService {
private final MyRepository myRepository;
// 생성자가 하나만 있는 경우 @Autowired 생략 가능
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
Spring 4.3부터는 생성자가 하나만 존재하는 경우 @Autowired 어노테이션을 생략해도 자동으로 의존성이 주입됩니다. 이 경우, 스프링이 해당 생성자를 자동으로 인식하고 의존성 주입을 수행합니다.
@RequiredArgsConstructor
@RequiredArgsConstructor 어노테이션은 final 필드와 @NonNull 어노테이션이 붙은 필드를 매개변수로 하는 생성자를 자동으로 생성합니다.
생성자가 하나만 있을 경우, @Autowired 어노테이션이 생략이 가능하기 때문에, 스프링에서는 해당 생성자를 자동으로 인식하여 의존성 주입을 수행합니다.
따라서 @RequiredArgsConstructor를 사용하면 생성자 주입을 간편하게 설정할 수 있으며, @Autowired 어노테이션이 자동으로 처리됩니다.
@ComponentScan
@ComponentScan 어노테이션은 스프링 애플리케이션에서 지정된 패키지 및 그 하위 패키지 내의 클래스를 스캔하여, 이들 클래스가 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙어 있는 경우 자동으로 빈으로 등록합니다.
이 과정을 통해 스프링 컨테이너는 자동으로 필요한 빈을 찾아내고 관리하게 됩니다.
@SpringBootApplication
@SpringBootConfiguration은@Configuration의 스프링 부트 버전으로, 스프링의 설정 클래스를 정의합니다.
또한 @SpringBootApplication이 붙은 클래스는 @ComponentScan을 포함하므로, 이 클래스가 위치한 패키지와 그 하위 패키지를 자동으로 스캔합니다.
위 코드의 MemoService는 service패키지에 존재합니다. 따라서 "MemoApplication" 하위 패키지 안에 들어있기 때문에 그리고 @Component를 달았기 때문에 스프링이 실행이 될 때 @ComponentScan에 의해서 전부 찾고, Bean으로 등록이 되는 것입니다.
DI(의존성 주입)의 조건
@Autowired는 Spring의 IoC (Inversion of Control) 컨테이너에 의해 관리되는 Bean 클래스에서만 사용할 수 있습니다. 그 이유와 관련된 핵심 개념을 정리하자면 다음과 같습니다.
1. Spring IoC 컨테이너 관리
앞서 알아보았듯이
Spring IoC 컨테이너 ➡️ Bean의 생명주기를 관리하고, 의존성 주입을 수행하는 역할을 합니다.
@Autowired ➡️ 컨테이너가 관리하는 Bean 사이의 의존성을 주입하기 위해 사용됩니다.
2. Bean으로 관리되는 클래스만 주입 가능
@Autowired를 사용하려면 해당 의존성을 주입받는 클래스와 주입될 Bean 클래스 모두 Spring IoC 컨테이너에 의해 관리되어야 합니다.
- 주입받는 클래스
- @Component, @Service, @Repository, @Controller 등으로 어노테이션이 붙어 있거나, @Configuration 클래스 나 @SpringbootConfiguration에서 @Bean 메서드를 통해 등록된 클래스
- 주입되는 Bean
- 위와 같은 방식으로 Spring IoC 컨테이너에 의해 관리되는 Bean이어야 합니다.
이 두 가지가 충족되어야 Spring이 의존성 주입(DI)을 정상적으로 수행할 수 있습니다.
3. 생성 권한
Spring IoC 컨테이너는 Bean의 생성과 초기화를 담당합니다. 따라서, @Autowired를 통해 주입을 받으려면
- 주입받는 클래스
- Spring이 관리할 수 있어야 하며, 이는 주로 @Component 또는 @Bean으로 등록된 클래스여야 합니다.
- 주입되는 Bean
- Spring 컨테이너가 생성하고 관리하는 Bean이어야 합니다. 따라서, @Autowired로 주입되는 객체도 Spring 컨테이너에 의해 관리되어야 합니다.
Spring 컨테이너가 Bean을 관리함으로써, 객체의 생성을 중앙에서 제어할 수 있게 되며, 이로 인해 객체 간의 의존성을 효율적으로 주입하고 관리할 수 있습니다.
정리하자면 주입을 위해서는 객체가 생성되어야 하며, 스프링 IoC 컨테이너가 관리하는 Bean으로 등록되면 생성과 주입 권한을 부여받게 됩니다.
ApplicationContext를 이용한 수동 주입 방법
ApplicationContext은 BeanFactory 등을 상속하여 기능을 확장한 Container입니다.
BeanFactory ❓
기본적인 IoC 컨테이너로, 빈의 생성 및 의존성 주입만을 담당합니다.
1. Bean이름으로 가져오기
ApplicationContext의 getBean(String name) 메서드를 사용하여 빈의 이름으로 빈을 가져오는 방법입니다. 이 방법은 빈의 이름을 문자열로 직접 제공하여 빈을 조회합니다.
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(ApplicationContext context) {
// 1. 'Bean' 이름으로 가져오기
this.memoRepository = (MemoRepository) context.getBean("memoRepository");
}
}
2. Bean 클래스 형식으로 가져오기
ApplicationContext의 getBean(Class <T> requiredType) 메서드를 사용하여 빈의 클래스 타입으로 빈을 가져오는 방법입니다. 이 방법은 클래스 타입을 통해 빈을 조회하며, 빈의 이름을 알 필요가 없습니다.
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(ApplicationContext context) {
// 2. 'Bean' 클래스 형식으로 가져오기
this.memoRepository = context.getBean(MemoRepository.class);
}
}
'Framework > Spring\Spring boot' 카테고리의 다른 글
[Spring] Spring에서 빈 이름 결정 방식 (0) | 2024.07.28 |
---|---|
[Spring] Spring IoC 컨테이너에 Bean 수동 등록하기 (@Configuration, @Bean) (0) | 2024.07.28 |
[Spring] Spring MVC: HTTP 요청 데이터의 객체 변환 방법 (@ModelAttribute vs @RequestBody) (2) | 2024.07.23 |
[Spring] Spring MVC : HTTP 요청으로 데이터 받기 (URL, Query String) (1) | 2024.07.23 |
[Spring] 로그(logger) 알아보기 (0) | 2024.03.17 |