반응형
의존성 주입은 무엇이고 스프링에서는 어떻게 관리할까?
스프링 공부를 하면서 의존성 주입이 무엇이고 여러 방법과 각각의 장단점이 궁금했습니다.
의존성 주입(Dependency Injection, DI)이란?
- 의존성(Dependency): 객체가 자신의 기능을 수행하기 위해 필요한 다른 객체나 서비스입니다.
예를 들어, Car 클래스가 Engine 클래스를 필요로 하는 경우, Engine은 Car의 의존성입니다. - 의존성 주입(Dependency Injection): 객체가 직접 의존 객체를 생성하는 대신, 외부에서 필요한 의존 객체를 제공(주입)받는 디자인 패턴입니다.
이를 통해 객체 간의 결합도를 낮추고, 코드의 유연성과 재사용성을 높일 수 있습니다.
Inversion of Control (IoC)
- IoC 개념: 전통적인 프로그래밍에서는 객체가 자신이 필요로 하는 의존 객체를 스스로 생성하지만, IoC에서는 제어 흐름(Control Flow)을 개발자가 아닌 프레임워크(또는 컨테이너)가 관리합니다.
- 의존성 주입과 IoC의 관계: 의존성 주입은 IoC의 한 형태로, 객체 생성과 의존성 관리의 제어를 애플리케이션 코드에서 분리하여 IoC 컨테이너에 맡기는 방식입니다.
- Ex
// 전통적인 방식 (강한 결합)
public class Car {
private Engine engine = new Engine();
public void start() {
engine.run();
}
}
// DI를 활용한 방식 (느슨한 결합)
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.run();
}
}
전통적인 방식에서는 Car 클래스 내부에서 Engine 객체를 직접 new Engine()으로 생성하지만, DI를 사용하면 외부에서 Engine 객체를 주입받아 사용합니다.
스프링에서의 의존성 주입 관리
스프링 IoC 컨테이너
- IoC 컨테이너 역할: 스프링은 애플리케이션이 시작될 때 IoC 컨테이너(주로 ApplicationContext 또는 BeanFactory)를 생성하여, 애플리케이션에 필요한 모든 객체(빈, Bean)를 관리합니다.
- 객체 생성 및 관리: 스프링은 빈 설정 정보를 바탕으로 각 빈을 생성하고, 서로의 의존성을 주입합니다.
이 과정은 리플렉션과 어노테이션, XML 또는 자바 기반 설정을 통해 이루어집니다. - 빈 라이프사이클: 스프링 컨테이너는 빈의 생성, 초기화, 소멸 등 생명주기 전반에 걸쳐 관리하며, 이를 통해 애플리케이션의 안정성과 일관성을 높입니다.
빈 등록과 의존성 설정
- 어노테이션 기반
- @Component, @Service, @Repository, @Controller 등의 어노테이션을 통해 클래스가 스프링 빈임을 선언합니다.
- @Autowired 어노테이션은 스프링에게 해당 필드, 생성자, 혹은 setter에 의존 객체를 주입하라고 지시합니다.
- 자바 기반
- @Configuration 클래스와 @Bean 메서드를 사용해 명시적으로 빈을 등록할 수 있습니다.
- XML 기반
- 이전 버전의 스프링에서는 XML 파일에 빈과 의존성을 정의했지만, 최근에는 어노테이션과 자바 설정이 주로 사용됩니다.
의존성 주입 방식
스프링은 주로 아래의 방식을 통해 의존성을 주입합니다.
1) 생성자 주입 (Constructor Injection)
생성자에 필요한 의존 객체를 인자로 받아 초기화합니다.
- 장점
- 불변성: 생성자에서 주입받은 의존성을 final로 선언해 객체 상태의 불변성을 보장합니다.
- 명시적 의존성: 어떤 의존성이 필요한지 생성자 시그니처를 통해 명확하게 파악할 수 있습니다.
- 테스트 용이성: 단위 테스트 시 원하는 의존 객체를 쉽게 주입할 수 있습니다.
- 순환 참조 예방: 순환 의존성을 애플리케이션 시작 시점에 발견할 수 있어 문제를 미리 방지할 수 있습니다.
- Ex
@Component
public class Car {
private final Engine engine;
// 단일 생성자의 경우 @Autowired 생략 가능 (Spring 4.3 이상)
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.run();
}
}
2) 필드 주입 (@Autowired 사용)
클래스 내부의 필드에 직접 @Autowired 어노테이션을 붙여 스프링이 자동으로 의존 객체를 주입합니다.
- 장점
- 코드가 간결하고, 어노테이션 한 줄로 의존성을 주입할 수 있습니다.
- 단점
- 테스트 어려움: 필드에 직접 의존성이 주입되므로, 단위 테스트에서 원하는 객체를 주입하기 어렵습니다.
- 불변성 부족: final 필드 선언이 어려워 객체 상태의 변경 가능성이 있습니다.
- 캡슐화 문제: 필드에 직접 접근하는 방식이므로, 캡슐화 원칙에 어긋날 수 있습니다.
- Ex
@Component
public class Car {
@Autowired
private Engine engine;
public void start() {
engine.run();
}
}
3) Setter(수정자) 주입
setter 메서드를 통해 의존 객체를 주입하는 방식입니다.
- 장점
- 유연성: 실행 중에 의존 객체를 변경할 수 있어 유연성이 높습니다.
- 선택적 의존성: 반드시 필요한 의존성이 아닌 경우, 선택적으로 주입할 수 있습니다.
- 단점
- 불변성 저해: setter를 사용하면 객체의 상태가 외부에서 변경될 수 있어 불변성을 보장하기 어렵습니다.
- Ex
@Component
public class Car {
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
public void start() {
engine.run();
}
}
4) new 연산자를 통한 직접 생성
스프링의 DI 컨테이너를 사용하지 않고, 객체 내부에서 직접 new 연산자를 통해 의존 객체를 생성합니다.
- 단점
- 테스트와 유지보수 어려움: 의존 객체를 외부에서 주입받지 않으므로, 유닛 테스트나 객체 확장이 어려워집니다.
- DI 컨테이너의 이점 상실: 스프링의 관리, 생명주기 관리, AOP 등의 기능을 활용할 수 없습니다.
- Ex
public class Car {
private final Engine engine = new Engine();
public void start() {
engine.run();
}
}
스프링의 DI 관리 원리
1) 빈 등록과 스캔
- 컴포넌트 스캔(Component Scan): 스프링 부트는 기본적으로 애플리케이션 시작 시 지정된 패키지를 스캔하여 @Component, @Service, @Repository, @Controller 등이 붙은 클래스를 자동으로 빈으로 등록합니다.
- 명시적 빈 등록: 자바 기반 설정(@Configuration)이나 XML 파일을 통해 직접 빈을 정의할 수 있습니다.
2) 의존성 해소
- 의존성 매칭: 스프링 컨테이너는 빈 등록 시점에 각 빈의 의존성을 확인합니다. 생성자, 필드, setter를 통해 어떤 의존성이 필요한지 파악하고, 동일한 타입(또는 이름)이 일치하는 빈을 찾아 주입합니다.
- 타입 기반 주입: 스프링은 주로 타입을 기준으로 의존성을 검색합니다. 동일한 타입의 빈이 여러 개 존재할 경우, @Qualifier 어노테이션 등을 사용하여 특정 빈을 지정할 수 있습니다.
- 순환 참조 처리: 만약 두 빈이 서로를 참조하는 순환 의존성이 있다면, 스프링은 이를 감지하고 에러를 발생시키거나 일부 경우 setter 주입 등을 통해 해결합니다.
3) 빈 라이프사이클과 초기화
- 생성 시점: 스프링 컨테이너는 애플리케이션 시작 시 모든 싱글톤 빈을 생성하고, 의존성 주입을 완료합니다.
- 초기화 콜백: 빈이 생성된 후 @PostConstruct 어노테이션이나 InitializingBean 인터페이스를 통해 초기화 작업을 수행할 수 있습니다.
- 소멸 단계: 애플리케이션 종료 시, @PreDestroy 어노테이션이나 DisposableBean 인터페이스를 통해 정리 작업을 수행합니다.
의존성 주입의 장점과 스프링에서의 활용
1) 코드 결합도 감소
- 느슨한 결합(Low Coupling): 객체들이 외부에서 주입받기 때문에, 서로의 내부 구현에 의존하지 않고 인터페이스나 계약만을 공유하게 됩니다.
- 유연성 향상: 한 객체의 구현을 다른 구현으로 쉽게 교체할 수 있어, 확장성과 유지보수성이 높아집니다.
2) 테스트 용이성
- 단위 테스트: 의존 객체를 주입받아 테스트 환경에서 모의(Mock) 객체나 스텁을 전달할 수 있으므로, 외부 시스템과의 연계 없이 단위 테스트를 수행할 수 있습니다.
- 독립적 실행: 스프링 컨테이너 없이도 생성자를 통해 객체를 직접 생성하고 테스트할 수 있어, 테스트 코드 작성이 용이해집니다.
3) 객체 생명주기 관리 및 AOP 연계
- 통합 관리: 스프링은 DI와 함께 객체의 전체 생명주기를 관리하여, 초기화, 소멸, 그리고 런타임 중의 부가기능(예: 트랜잭션 관리, 로깅 등)을 AOP(Aspect-Oriented Programming)와 연계해 제공합니다.
- 관심사의 분리: 애플리케이션 로직과 부가기능(예: 보안, 로깅, 트랜잭션 관리)을 분리할 수 있어, 코드의 가독성과 유지보수성이 향상됩니다.
정리
의존성 주입은 객체 간의 결합도를 낮추고, 재사용성과 테스트 용이성을 크게 향상시키는 중요한 디자인 패턴입니다.
스프링은 IoC 컨테이너를 통해 의존성 주입을 자동화합니다.
- 생성자 주입: 불변성과 명시적 의존성으로 인해 가장 많이 사용되고 권장됩니다.
- 필드 및 Setter 주입: 간편하지만 테스트와 유지보수 측면에서 한계가 있습니다.
- 직접 new 연산자 사용: DI의 장점을 상실하므로 스프링 기반 애플리케이션에서는 지양됩니다.
이러한 원리를 바탕으로 스프링은 애플리케이션의 복잡성을 줄이고, 확장 가능하고 안정적인 구조를 구축할 수 있도록 돕습니다.
반응형
'SPRING' 카테고리의 다른 글
[Q] DTO vs 엔티티: 왜 엔티티만으로 데이터를 주고받으면 안 될까? (1) | 2025.02.28 |
---|---|
[Q] Model과 ModelAndView의 차이 (3) | 2025.02.27 |
[SPRING]#85 도서 쇼핑몰 구현 (DB 연동10) (0) | 2024.03.04 |
[SPRING]#84 도서 쇼핑몰 구현 (DB 연동9) (0) | 2024.03.04 |
[SPRING]#83 도서 쇼핑몰 구현 (DB 연동8) (0) | 2024.03.04 |