Spring IoC 컨테이너를 통한 의존성 주입 이해하기
Dependency Injection through IoC Container in Spring
Spring IoC(Inversion of Control) 컨테이너는 객체의 생성과 의존성 관리를 자동화하여 개발자가 코드 유지 관리에 집중할 수 있도록 합니다. 의존성 주입(DI, Dependency Injection)은 객체 간의 결합을 줄이며, 설정을 외부에서 조정할 수 있게 함으로써 유연성과 테스트 용이성을 향상시킵니다. 이러한 방법으로 Spring은 보다 명료하고 재사용 가능한 코드 구조를 제공하여 애플리케이션의 개발과 유지보수를 간소화합니다.
Spring IoC Fundamentals
소프트웨어는 변화하는 요구 사항에 빠르게 대응할 수 있어야 하며, 이를 위해 유연하고 유지보수하기 쉬운 설계가 중요합니다. 객체 지향 프로그래밍에서는 이러한 목표를 달성하기 위해 다양한 원칙과 패턴이 사용됩니다. 특히, 객체 간의 의존성을 효과적으로 관리하고 코드의 결합도를 낮추는 것이 중요한 과제입니다. 이를 위해 제어의 역전(IoC)과 의존성 주입(DI) 같은 개념이 도입되었습니다.
Inversion of Control
IoC(Inversion of Control, 제어의 역전)은 소프트웨어 설계의 중요한 원칙으로, 컴포넌트 간의 결합을 느슨하게 하여 시스템을 더 모듈화하고 유연하며 유지보수하기 쉽게 만드는 것을 목표로 합니다. 전통적인 객체 지향 설계에서는 객체들이 서로 강하게 결합되어 있어, 하나의 객체를 수정하면 관련된 여러 객체들을 수정해야 하는 문제가 발생했습니다. 이를 해결하기 위해 IoC 원칙이 등장하였고, 이를 통해 객체 간의 결합을 느슨하게 하여 유연성과 재사용성을 높이고자 했습니다.
IoC의 주요 개념은 할리우드 원칙이라고도 불리는 "우리가 호출할 테니, 당신은 대기하세요(Don't call us, we'll call you)"라는 개념에 기반합니다. 이는 제어의 흐름을 역전시켜, 객체가 자신의 의존성을 직접 관리하는 것이 아니라 외부에서 관리하고 주입하는 방식입니다. IoC는 소프트웨어의 다양한 컴포넌트들이 독립적으로 설계되고, 실행 시점에 필요한 의존성을 외부에서 주입받는 방식을 의미합니다. 전통적인 방법에서는 객체가 직접 다른 객체를 생성하고 제어합니다. 하지만 IoC를 적용하면 이러한 제어의 흐름이 역전되어, 객체가 필요한 의존성을 외부에서 주입받게 됩니다. 이는 주로 프레임워크가 객체의 생명 주기를 관리하고 의존성을 주입하는 형태로 나타납니다.
Frameworks vs. Libraries
프레임워크와 라이브러리를 구분하는 한 가지 방법은 제어의 주체입니다. 프레임워크는 제어권을 가지고 있어, 개발자가 작성한 코드가 프레임워크에 의해 호출되고 애플리케이션의 구조와 흐름을 관리합니다. 반면에 라이브러리는 제어권이 개발자에게 있어, 필요할 때 개발자가 직접 호출하여 사용하는 도구입니다. 이로 인해 프레임워크는 통합된 환경을 제공하는 반면, 라이브러리는 특정 기능을 수행하는 데 집중합니다.
IoC를 구현하는 다양한 방법들은 다음과 같습니다:
- Dependency Injection (DI): 객체가 필요로 하는 의존성을 외부에서 주입받는 방식입니다. 이를 통해 객체 간의 결합도를 낮추고, 유연성과 테스트 용이성을 높일 수 있습니다.
- Service Locator Pattern: 중앙 서비스 레지스트리를 통해 의존성을 찾고 제공받는 방식입니다. 코드의 의존성을 관리하는 데 유용하지만, DI보다는 결합도가 높습니다.
- Factory Pattern: 객체 생성 로직을 별도의 팩토리 클래스로 분리하여 관리하는 방식입니다. 클라이언트 코드가 객체 생성 방법을 알 필요가 없게 합니다.
- Template Method Pattern: 상위 클래스가 알고리즘의 구조를 정의하고, 하위 클래스가 세부 구현을 제공하는 방식입니다. 알고리즘의 구조는 유지하면서 세부 구현을 변경할 수 있습니다.
- Aspect-Oriented Programming (AOP): 횡단 관심사를 분리하여 코드의 모듈화를 돕는 방식입니다. 로깅, 트랜잭션 관리 등 공통 기능을 분리하여 관리할 수 있습니다.
- Contextual Lookup: 특정 컨텍스트에서 필요한 의존성을 조회하여 사용하는 방식입니다. 예를 들어, 웹 애플리케이션에서 요청 스코프에 따라 의존성을 주입하는 방식입니다.
- Event-based Injection: 이벤트 발생 시 필요한 의존성을 주입하는 방식입니다. 이벤트 중심의 시스템에서 유용하게 사용할 수 있습니다.
제어의 역전을 통해 오는 주요 이점은 다음과 같습니다:
- 유연성: IoC는 시스템의 유연성을 크게 증가시킵니다. 의존성이 외부에서 주입되므로, 컴포넌트를 쉽게 교체하거나 수정할 수 있습니다.
- 모듈화: 각 컴포넌트가 자신의 책임만을 가지게 되므로, 시스템의 모듈화가 촉진됩니다.
- 테스트 용이성: 외부에서 의존성을 주입받기 때문에, 테스트 시에 모의 객체(Mock Object)를 사용하여 쉽게 테스트할 수 있습니다.
그리고 제어의 역전을 구현함으로써 발생할 수 있는 단점도 있습니다:
- 복잡성 증가: IoC를 도입하면 시스템의 설계가 더 복잡해질 수 있습니다. 특히, 의존성이 많아질수록 관리가 어려워질 수 있습니다.
- 성능 저하: IoC 컨테이너를 사용하면 런타임에서 의존성을 해결하는 과정에서 성능이 약간 저하될 수 있습니다. 하지만 일반적으로 이 성능 저하는 현대 시스템에서는 큰 문제가 되지 않습니다.
- 디버깅 어려움: 외부에서 의존성을 주입받기 때문에, 문제 발생 시 디버깅이 어려울 수 있습니다. 의존성의 경로를 추적하는 것이 복잡해질 수 있습니다.
IoC는 소프트웨어 개발에서 중요한 개념으로, 이를 통해 시스템의 유연성과 모듈성을 높이고, 유지보수와 테스트를 용이하게 할 수 있습니다. 다양한 IoC 구현 방법을 통해 개발자는 시스템의 복잡성을 줄이고, 변경에 유연하게 대응할 수 있습니다.
Dependency Injection
DI(Dependency Injection, 의존관계 주입)는 IoC를 구현하는 가장 일반적이고 효과적인 방법 중 하나입니다. DI는 객체가 필요한 의존관계를 직접 생성하지 않고, 외부에서 주입받는 방식입니다. 이를 통해 객체 간의 결합도를 낮추고, 코드의 유연성과 테스트 용이성을 크게 향상시킬 수 있습니다.
DI는 객체 지향 설계의 주요 원칙인 단일 책임 원칙(Single Responsibility Principle)과 개방-폐쇄 원칙(Open-closed Principle)을 지원합니다. DI를 통해 각 객체는 자신의 책임만을 집중적으로 수행할 수 있으며, 변경이 필요할 때는 기존 코드의 수정 없이 새로운 기능을 추가할 수 있습니다.
DI는 주로 다음과 같은 구성 요소로 이루어집니다:
- Dependency: 주입될 객체나 서비스입니다.
- Injector: 의존성을 주입하는 역할을 합니다. 이는 프레임워크나 컨테이너가 될 수 있습니다.
- Client: 의존성을 필요로 하는 객체입니다. 클라이언트는 주입자를 통해 의존성을 받습니다.
DI 컨테이너는 객체의 생성, 생명 주기 관리, 의존관계 주입 등의 작업을 자동으로 처리합니다. 대표적인 DI 컨테이너로는 Spring Framework, Unity 등이 있습니다.
Dependency Inversion Principle
DIP(Dependency Inversion Principle, 의존관계 역전 원칙)는 객체 지향 설계의 다섯 가지 SOLID 원칙 중 하나로, 모듈 간의 의존성을 관리하는 중요한 원칙입니다.
DIP의 주요 목적은 코드의 의존성을 줄이고, 변경에 대한 영향을 최소화하는 것입니다. 이를 통해 시스템의 유연성과 확장성을 높일 수 있습니다. DIP를 통해 고수준 모듈과 저수준 모듈이 서로 독립적으로 변경될 수 있으므로, 코드의 재사용성과 유지보수성을 크게 향상시킬 수 있습니다.
DIP는 두 가지 주요 규칙을 따릅니다:
- 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다.
비즈니스 로직을 담당하는 고수준 모듈은 세부 구현을 담당하는 저수준 모듈에 직접 의존해서는 안 됩니다. 대신, 두 모듈 모두 추상화된 인터페이스에 의존해야 합니다. 이렇게 하면 고수준 모듈의 변경 없이 저수준 모듈을 교체할 수 있어 유연성이 높아집니다.
또한, 인터페이스나 추상 클래스와 같은 추상화는 구체적인 구현에 의존해서는 안 됩니다. 오히려, 구체적인 구현이 추상화에 의존해야 합니다. 이는 시스템의 구조를 더 이해하기 쉽고 유지보수하기 쉽게 만듭니다.
DI(Dependency Injection, 의존관계 주입)은 DIP를 구현하는 방법 중 하나입니다. DI를 통해 의존성을 외부에서 주입받음으로써, 고수준 모듈과 저수준 모듈이 추상화에 의존하도록 만들 수 있습니다. 이렇게 하면 모듈 간의 결합도가 낮아지고, 변경에 대한 영향을 최소화할 수 있습니다.
IoC vs. DI vs. DIP
지금까지 IoC, DI, DIP에 대해 살펴봤습니다. 이들은 소프트웨어 설계와 개발에서 매우 중요한 개념들이며, 서로 밀접하게 관련되어 있습니다. 표현이나 개념이 비슷해서 헷갈리기 쉬워, 이들의 특징과 관계를 다시 한 번 간략하게 살펴보겠습니다.
Concept | Description |
---|---|
IoC | IoC(Inversion of Controler, 제어의 역전)는프로그램의 제어 흐름을 역전시켜 객체 간의 결합을 느슨하게 하는 원칙입니다. 이는 시스템의 모듈성, 유연성 및 유지보수성을 향상시키며, 프레임워크나 컨테이너를 사용하여 객체의 생성과 의존성을 관리합니다. DI와 DIP를 포함한 상위 개념입니다. |
DI | DI(Dependency Injection, 의존관계 주입)객체가 필요한 의존성을 직접 생성하지 않고 외부에서 주입받는 방식입니다. 이를 통해 객체 간의 결합도가 감소하고, 코드의 유연성 및 테스트 용이성이 향상됩니다. 생성자 주입, 세터 주입, 인터페이스 주입 방식이 있으며, IoC를 구현하는 방법 중 하나입니다. |
DIP | DIP(Dependency Inversion Principle, 의존관계 역전 원칙)는 SOLID 원칙 중 하나로, 고수준 모듈이 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존하도록 하는 원칙입니다. 이를 통해 코드의 유연성과 재사용성이 증가하고, 변경의 영향을 최소화할 수 있습니다. 인터페이스와 추상 클래스를 사용하며, DI와 서비스 로케이터 패턴을 포함하여 다양한 방법을 통해 구현될 수 있는 원칙입니다. |
이 세 가지 개념은 상호 보완적이며, 함께 사용될 때 소프트웨어의 설계와 유지보수성을 크게 향상시킬 수 있습니다.
국내 대다수의 백엔드 서버의 기반이 되는 Spring Framework는 바로 이 DI를 통해 IoC를 구현하며, 이를 통해 DIP를 준수하는 대표적인 사례입니다. 이제, Spring Framework가 어떻게 IoC 컨테이너로 작동하는지 살펴보겠습니다.
Spring IoC Implementations
현대 소프트웨어 개발에서는 유연하고 유지보수가 쉬운 애플리케이션을 만드는 것이 중요합니다. 이를 위해 흔히 사용하는 방법 중 하나는 IoC 컨테이너를 구현하는 것입니다. 간단한 서비스를 구현하는 과정을 통해 개발자가 직면할 수 있는 문제를 살펴보고, 의존성 주입을 활용한 IoC가 이러한 문제를 어떻게 해결할 수 있는지 알아보겠습니다.
Confronting Challenges
먼저, Spring Initializr를 사용하여 Web 의존성을 포함한 Spring Boot 프로젝트를 생성합니다. 지금 당장 필요한 것은 아니지만, 이 글에서 전반적으로 활용할 예정이기 때문에 Web 의존성을 추가하였습니다:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
- Java 21
- Spring Boot 3.3
- Gradle 8
현재 요구사항에 따르면 신용카드를 통해 결제가 가능하도록 기능을 만들어야 한다고 합니다. 이를 위해 비즈니스 로직을 작성해보았습니다:
public class PaymentServiceImpl implements PaymentService {
private final CreditCardProcessor processor = new CreditCardProcessor();
@Override
public PaymentResponse processPayment(double amount) {
ProcessedPaymentResult result = processor.process(amount);
return PaymentResponse.bind(result);
}
}
여기에서 PaymentServiceImpl
클래스는 신용카드 결제를 처리하는 CreditCardProcessor
를 사용하고 있습니다. 실제라면 결제 처리가 매우 복잡하겠지만, 지금은 간단한 형태로만 작성하여 로그를 출력하고 적절한 응답 객체를 반환하도록 하였습니다:
@Slf4j
public class CreditCardProcessor implements PaymentProcessor {
@Override
public ProcessedPaymentResult process(double amount) {
log.info("process: Processing payment of amount {} via {}", amount, CREDIT_CARD);
return ProcessedPaymentResult.create(CREDIT_CARD, amount);
}
}
이제 이 비즈니스 로직을 사용하는 클라이언트를 구현해보겠습니다:
@Slf4j
public class PaymentClient {
public PaymentResponse pay(double amount) {
PaymentService service = new PaymentServiceImpl();
PaymentResponse response = service.processPayment(amount);
log.info("pay: {}", response);
return response;
}
}
PaymentClient
클래스는 PaymentServiceImpl
객체를 직접 생성하고 사용하여 결제를 처리합니다. 구현한 기능이 정상적으로 동작하는지 확인하기 위해 테스트 코드를 작성해보겠습니다:
public class PaymentClientTest {
private PaymentClient client;
@BeforeEach
void setup() {
client = new PaymentClient();
}
@Test
@DisplayName("지불이 성공했을 때 테스트")
void should() throws Exception {
// Given
double amount = 100.0;
// When
PaymentResponse response = client.pay(amount);
// Then
assertThat(response.getAmount()).isEqualTo(amount);
System.out.println("response = " + response);
}
}
테스트까지 모두 마치고 서비스를 런칭하여 정상적으로 운영하게 되었지만, 일정 기간이 지나 회사 정책이 변경되면서 이제부터는 무조건 애플 페이만 지원해야 한다는 새로운 요구사항이 전달되었습니다. 이에 대응하기 위해 새로운 결제 프로세서를 추가하려고 하니 PaymentServiceImpl
클래스가 CreditCardProcessor
를 직접 생성하고 사용하기 때문에, 새로운 결제 프로세서를 사용하려면 다음과 같이 기존 코드를 반드시 수정해야만 합니다:
public class PaymentServiceImpl implements PaymentService {
- private final CreditCardProcessor processor = new CreditCardProcessor();
+ private final ApplePayProcessor processor = new ApplePayProcessor();
@Override
public PaymentResponse processPayment(double amount) {
ProcessedPaymentResult result = processor.process(amount);
return PaymentResponse.bind(result);
}
}
이는 클래스가 특정 결제 프로세서에 강하게 결합되어 있음을 의미합니다. 비록 프로세서를 PaymentProcessor
인터페이스 기반으로 구성하였지만, 이 객체의 클라이언트인 PaymentServiceImpl
클래스가 CreditCardProcessor
의 구체적인 구현체를 직접 생성하고 사용합니다. 따라서 다른 결제 프로세서를 추가하려면 이 클래스를 수정해야 합니다. 이러한 결합도는 유지보수와 확장을 어렵게 만들며, 코드의 유연성을 저해합니다. 지금은 코드가 간단해서 큰 문제가 없어 보이지만, 실제 서비스에서는 변경 사항이 상당히 복잡해질 수 있습니다.
여기서 아래 코드와 같이 프로세서를 파라미터를 통해 외부에서 전달받으면 되지 않을까 하고 생각할 수 있습니다:
public class PaymentServiceImpl implements PaymentService {
@Override
public PaymentResponse processPayment(
PaymentProcessor processor) {
...
}
}
그러나 이는 processPayment()
를 호출하는 클라이언트 입장에서 결제 프로세서를 관리해야 하는 책임을 떠맡게 되는 것일 뿐이며, 이는 또한 클라이언트의 관심사가 아니기 때문에 객체 지향 설계 원칙을 위반하게 됩니다. 이로 인해 문제가 더 악화될 수 있습니다.
이 문제를 해결하기 위해 다음 단계에서는 의존성 주입을 도입하여 결제 프로세서를 외부에서 주입받도록 변경할 것입니다.
Implementing Dependency Injection
의존성 주입(DI, Dependency Injection)을 통해 결제 프로세서를 외부에서 주입받도록 변경하면 코드의 결합도를 낮추고, 유연성과 확장성을 높일 수 있습니다.
먼저, PaymentServiceImpl
클래스를 수정하여 생성자를 통해 결제 프로세서 인터페이스를 주입받도록 변경합니다. 이로써, PaymentServiceImpl
클래스가 직접 프로세서 구현체 생성을 담당하는 대신, 외부에서 주입받은 결제 프로세서를 사용하게 됩니다:
public class PaymentServiceImpl implements PaymentService {
- private final CreditCardProcessor processor = new CreditCardProcessor();
+ private final PaymentProcessor processor;
+
+ public PaymentServiceImpl(PaymentProcessor processor) {
+ this.processor = processor;
+ }
@Override
public PaymentResponse processPayment(double amount) {
ProcessedPaymentResult result = processor.process(amount);
return PaymentResponse.bind(result);
}
}
다음으로, 필요한 결제 프로세서 객체를 주입하여 PaymentService
인스턴스를 구성하는 역할을 담당하는 PaymentConfiguration
클래스를 정의합니다. 이 객체가 바로 의존성을 관리하게 됩니다:
public class PaymentConfiguration {
public PaymentService paymentService() {
CreditCardProcessor processor = new CreditCardProcessor();
return new PaymentServiceImpl(processor);
}
}
이제 PaymentClient
클래스를 수정합니다. 기존 PaymentServiceImpl
구현체를 직접 생성하는 대신 PaymentConfiguration
를 통해 주입받도록 변경합니다:
@Slf4j
public class PaymentClient {
public PaymentResponse pay(double amount) {
- PaymentService service = new PaymentServiceImpl();
+ PaymentConfiguration config = new PaymentConfiguration();
+ PaymentService service = config.paymentService();
PaymentResponse response = service.processPayment(amount);
log.info("pay: {}", response);
return response;
}
}
이제 클라이언트는 구현체를 직접 생성하지 않고, 추상화된 인터페이스에만 의존하게 됩니다.
PaymentServiceImpl
클래스는 객체를 생성하고 구성하는 역할을 PaymentConfiguration
에 위임함으로써 생성자 주입을 통해 결제 프로세서를 받게 되어, 특정 결제 프로세서에 강하게 결합되지 않게 됩니다. 이를 통해 코드의 결합도를 낮추고 유연성을 높일 수 있습니다. 새로운 결제 프로세서를 추가할 때마다 기존 코드를 수정할 필요 없이, PaymentConfiguration
클래스만 변경하면 됩니다. 이는 코드의 유지보수성과 확장성을 크게 향상시킵니다.
그럼 새로운 결제 프로세서 구현체인 ApplePayProcessor
클래스를 추가하고, 기존 프로세서를 어떻게 교체할 수 있는지 살펴보겠습니다.
먼저, ApplePayProcessor
클래스를 정의합니다. 이 클래스는 PaymentProcessor
인터페이스를 구현합니다:
@Slf4j
public class ApplePayProcessor implements PaymentProcessor {
@Override
public ProcessedPaymentResult process(double amount) {
log.info("process: Processing payment of amount {} via {}", amount, APPLE_PAY);
return ProcessedPaymentResult.create(APPLE_PAY, amount);
}
}
이제 기존 CreditCardProcessor
프로세서를 ApplePayProcessor
로 변경하려면, PaymentServiceImpl
을 수정할 필요 없이 구성을 담당하는 PaymentConfiguration
클래스만 변경하면 됩니다:
public class PaymentConfiguration {
public PaymentService paymentService() {
- CreditCardProcessor processor = new CreditCardProcessor();
+ ApplePayProcessor processor = new ApplePayProcessor();
return new PaymentServiceImpl(processor);
}
}
이처럼 의존성 주입을 통해 결제 프로세서를 유연하게 변경할 수 있으며, 이는 코드의 유지보수성과 확장성을 높이는 데 큰 도움이 됩니다.
Integrating Spring IoC
이제 Spring IoC 컨테이너를 사용하여 의존성을 주입하는 방법을 살펴보겠습니다. Spring IoC의 세부적인 아키텍처와 작동 방식에 대해서는, 현재까지 개선한 프로젝트를 Spring으로 통합하는 과정을 살펴본 다음에 자세히 알아보겠습니다.
기존 PaymentConfiguration
를 Spring 구성 클래스로 변경하는 것은 매우 간단합니다. @Configuration
과 @Bean
어노테이션만 추가하면 됩니다:
@Configuration
public class PaymentConfiguration {
@Bean
public PaymentService paymentService() {
CreditCardProcessor processor = new CreditCardProcessor();
return new PaymentServiceImpl(processor);
}
}
여기서 @Configuration
어노테이션은 이 클래스가 Spring 구성 클래스임을 나타내며, @Bean
어노테이션은 해당 메서드가 반환하는 객체를 Spring 컨테이너가 관리하는 빈(Bean)으로 등록하도록 합니다. Spring Bean은 Spring IoC 컨테이너에 의해 관리되는 객체를 의미합니다.
다음으로, PaymentClient
클래스를 수정하여 Spring IoC 컨테이너를 통해 의존성을 주입받도록 수정합니다. 모든 Spring Bean은 애플리케이션 컨텍스트에서 관리되므로, 이를 통해 등록된 PaymentService
Bean을 가져올 수 있습니다:
@Slf4j
public class PaymentClient {
public PaymentResponse pay(double amount) {
- PaymentConfiguration config = new PaymentConfiguration();
- PaymentService service = config.paymentService();
+ ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PaymentConfig.class);
+ PaymentService service = applicationContext.getBean(PaymentService.class);
PaymentResponse response = service.processPayment(amount);
log.info("pay: {}", response);
return response;
}
}
ApplicationContext
: Spring IoC 컨테이너의 중앙 인터페이스로, 애플리케이션에 대한 구성 정보를 제공하고, Spring Bean을 생성 및 관리합니다.AnnotationConfigApplicationContext
:ApplicationContext
구현체 중 하나로, Java 클래스에서@Configuration
과@Bean
어노테이션이 붙은 메서드를 찾아서 이들을 Spring IoC 컨테이너에 빈으로 등록하는 역할을 합니다.
이렇게 구성 클래스에 어노테이션을 추가하고 이를 애플리케이션 컨텍스트에 제공하기만 하면, Spring Bean이 자동으로 등록되어 비즈니스 로직에서 활용할 수 있습니다. 이렇게 Bean으로 등록되면 단순히 인스턴스 초기화를 넘어서, 재사용성, 테스트 용이성, 의존성 주입을 통한 느슨한 결합 등 여러 이점을 얻을 수 있습니다. 지금은 이 과정을 이해하기 위해 직접 애플리케이션 컨텍스트에 구성 클래스를 등록하고, 해당 Bean을 조회하여 활용했지만, Spring Boot는 이러한 작업을 자동으로 처리해주어 의존성 관리가 더욱 편리해집니다.
애플리케이션의 로깅 레벨을 디버그(Debug)로 설정하고 이전에 작성한 테스트 코드를 실행해 보면, Spring IoC 컨테이너가 초기화되고 객체가 생성되는 과정을 로그를 통해 확인할 수 있습니다. 다음은 그 디버그 로그입니다:
AnnotationConfigApplicationContext -- Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3bcbb589
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'PaymentConfiguration'
DefaultListableBeanFactory -- Creating shared instance of singleton bean 'paymentService'
> Task :test
BUILD SUCCESSFUL in 4s
이제, Spring IoC의 주요 아키텍처에 대해 더 자세히 알아보겠습니다.
Spring IoC Architecture
Spring IoC 아키텍처는 애플리케이션의 의존성을 중앙에서 효과적으로 관리하고, Spring Bean의 생명 주기를 주관하는 Spring Framework의 핵심 구성 요소입니다. Bean의 생성부터 초기화, 구성, 그리고 소멸에 이르는 전체 과정을 이 아키텍처가 관리함으로써, 시스템의 복잡성이 줄어들고 유연성과 확장성이 증가합니다. 이러한 방식은 결합도를 최소화하고, 애플리케이션의 변경사항에 더 유연하게 대응할 수 있게 합니다. 프레임워크를 사용하는 개발자 입장에서 모든 세부사항을 알아야 할 필요는 없겠지만, 전체적인 흐름을 이해한다면 프레임워크를 더욱 효과적으로 활용할 수 있을 것입니다.
Spring Beans Basic
Spring Bean은 Spring IoC 컨테이너에 의해 생성되고 관리되는 객체입니다. 기본적으로 Java 인스턴스로서, Spring 애플리케이션이 시작될 때 IoC 컨테이너에 의해 자동으로 의존성이 주입되어 초기화됩니다. 처음에는 다소 어렵고 복잡하게 느껴졌지만, 단순하게 접근해서 Spring이 직접 인스턴스화하여 관리하는 객체라고 보면 될 것 같습니다.
이러한 Spring Bean은 애플리케이션의 비즈니스 로직을 구현하며, IoC 컨테이너는 Bean의 생명 주기와 의존성 관리를 자동으로 처리합니다. 일반적으로 Spring IoC 컨테이너에서 Bean은 싱글톤 인스턴스로 관리되어 동일한 인스턴스를 재사용함으로써 성능을 최적화합니다.
Spring Bean은 크게 세 가지 역할로 구분할 수 있습니다:
APPLICATION
: 개발자가 직접 정의하고 관리하는 애플리케이션의 주요 비즈니스 로직을 구현합니다.SUPPORT
: 인프라 관련 주요 구성 요소를 지원하고 재정의하며, 개발자가 애플리케이션의 특정 요구사항에 맞게 커스터마이즈할 수 있습니다.INFRASTRUCTURE
: 백그라운드에서 작동하며, 애플리케이션의 내부 기능을 지원하는 역할을 합니다. 최종 사용자에게는 직접적으로 드러나지 않습니다.
APPLICATION
역할은 주로 애플리케이션의 비즈니스 로직, 데이터 접근 및 웹 요청 처리를 담당합니다. 이 Bean들은 애플리케이션의 주요 기능을 구현하며, 사용자가 직접 정의하고 관리합니다.
@Service
public class UserService {
// business logic
}
@Repository
public class UserRepository extends JpaRepository<User, Long> {
// repository logic
}
@Controller
public class UserController {
// request handler logic
}
다음으로, SUPPORT
역할은 애플리케이션의 주요 로직을 직접 수행하지 않지만, 사용자가 인프라 관련 주요 구성 요소를 보완하거나 재정의할 수 있는 Bean입니다. 이러한 Bean은 개발자가 애플리케이션의 특정 요구사항에 맞게 커스터마이즈할 수 있습니다. 이들은 핵심 애플리케이션 로직과 직접적인 관련이 없지만, 애플리케이션이 원활하게 작동하도록 지원하는 중요한 역할을 합니다.
예를 들어, Spring Boot가 자동으로 구성하는 데이터베이스 연결 설정인 DataSource
를 개발자가 직접 커스터마이즈할 수 있습니다.
@Configuration
public class CustomDataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}
}
다음은 SUPPORT
역할을 하는 Bean에 해당하는 몇 가지 사례입니다:
DataSource
: 데이터베이스 연결 설정을 처리합니다.MessageConverter
: 메시지 형식 변환을 처리하여 JSON, XML 등의 형식을 지원합니다.TransactionManager
: 트랜잭션 관리를 처리하여 데이터의 일관성을 유지합니다.CacheManager
: 캐싱 설정을 처리하여 애플리케이션 성능을 최적화합니다.TaskExecutor
: 비동기 작업 실행을 처리하여 병렬 처리를 지원합니다.
이 Bean들은 주로 기본적으로 제공되는 인프라 Bean을 재정의하거나 확장하여 애플리케이션의 특정 요구에 맞게 조정하는 데 사용됩니다. 이는 애플리케이션의 확장성과 유지보수성을 높이는 데 중요한 역할을 합니다.
마지막으로, INFRASTRUCTURE
역할은 전적으로 백그라운드에서 동작하며, 최종 사용자에게 직접적으로 노출되지 않는 Bean을 나타냅니다. 이 역할은 주로 애플리케이션의 내부 작동을 지원하는 데 사용됩니다. 예를 들어, Spring의 내부 동작을 지원하는 Bean들이 해당됩니다. Spring Framework에서 애플리케이션 컨텍스트의 초기화와 구성을 담당하는 중요한 인프라 Bean 중 하나는 ConfigurationClassPostProcessor
입니다. 이 클래스는 @Configuration
이 선언된 클래스를 처리하고, BeanDefinition
을 등록하여 Spring IoC 컨테이너를 설정합니다.
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
BeanRegistrationAotProcessor, BeanFactoryInitializationAotProcessor, PriorityOrdered,
ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware {...}
INFRASTRUCTURE
역할의 Bean들은 ApplicationContext
, Environment
, BeanPostProcessor
, BeanFactoryPostProcessor
, Advisor
, AopProxy
, ConfigurationClassPostProcessor
등이 있습니다. 이들은 애플리케이션의 내부 구조를 지원하며, 최종 사용자와 직접적으로 상호작용하지 않습니다. INFRASTRUCTURE
역할은 프레임워크의 내부 동작을 처리하고 관리하는 데 중요한 역할을 합니다.
Spring Beans in Action
Spring Boot는 복잡한 설정 없이 간단한 어노테이션과 기본 설정만으로 웹 애플리케이션을 빠르게 개발할 수 있도록 지원합니다. Spring IoC 컨테이너의 역할을 이해하는 데 가장 적합한 예로는 Spring MVC 서블릿 컨테이너가 아닐까 합니다. REST API 요청을 처리하기 위해 Spring MVC 서블릿 컨테이너가 자동으로 구성되고 동작하는 과정을 통해 Spring IoC 컨테이너의 역할을 자세히 살펴보겠습니다.
먼저, 위에서 생성한 프로젝트를 사용하여 간단한 REST API 컨트롤러를 구현해보겠습니다:
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/payments")
public class PaymentController {
@PostMapping
public ResponseEntity<?> pay(@RequestBody PaymentRequest request) {
String responseMessageFormat = "Payment of %.2f %s processed successfully.";
String response = String.format(responseMessageFormat, request.getAmount(), request.getCurrency());
return ResponseEntity.ok(response);
}
}
@RestController
: 이 어노테이션은@Controller
와@ResponseBody
를 결합한 것입니다.@Controller
어노테이션이 선언된 클래스는 디스패처 서블릿에 의해 HTTP 요청을 처리하는 핸들러로 등록됩니다.@ResponseBody
어노테이션은 메서드의 반환 값을 JSON 또는 XML 형식으로 변환하여 HTTP 응답 본문에 포함시킵니다. 이를 사용하면 해당 클래스가 RESTful 웹 서비스의 요청을 처리하고, 응답을 JSON 또는 XML 형식으로 반환하도록 지정됩니다.@RequestMapping
: 클래스나 메서드에 URL 경로를 매핑하는 어노테이션입니다. 여기서는/payments
경로에 매핑되어, 해당 URL로 들어오는 요청을 처리합니다. 이 어노테이션은 클래스와 메서드 수준에서 사용되어 요청을 처리할 특정 경로를 지정할 수 있습니다.@PostMapping
: HTTP POST 요청을 특정 메서드에 매핑하는 어노테이션으로,@RequestMapping
의 확장 버전입니다. 이와 유사하게 HTTP 메서드를 확장한 어노테이션으로는@GetMapping
,@PutMapping
,@DeleteMapping
,@PatchMapping
등이 있습니다. 이러한 어노테이션들은 각 HTTP 메서드별로 구체적인 매핑을 간편하게 설정할 수 있어, 코드 가독성과 유지 보수성을 높이기 위해 도입되었습니다. 여기서는pay()
메서드가 POST 요청을 처리하도록 지정됩니다. 이를 통해 디스패처 서블릿은POST /payments
요청이 들어올 때 해당 메서드를 핸들러로 호출합니다.
이렇게 컨트롤러를 추가하고 Spring Boot 애플리케이션을 구동하기만 하면 REST API 서버가 구현됩니다. 서버를 이렇게 간단하게 구현할 수 있는 이유는 Spring Boot가 서버 초기화 시 많은 자동 구성을 진행하기 때문입니다. 이제, 애플리케이션이 초기화된 후 API 요청을 처리하는 과정을 살펴보겠습니다. 여기서 사용된 코드는 생략된 부분이 많으며, 버전마다 구현 방식이 다를 수 있습니다.
Spring Boot 애플리케이션은 내장된 서블릿 컨테이너(Servlet Container)를 사용할 수 있습니다. 별도의 설정이 없다면 기본적으로 Tomcat 서블릿 컨테이너를 구성합니다.
Servlet & Servlet Container
서블릿(Servlet)은 HTTP 프로토콜의 요청/응답 모델을 Java 객체로 추상화한 웹 컴포넌트(Web Component)입니다. 이를 통해, 클라이언트의 요청을 Java로 처리한 후 그 결과를 다시 HTTP 프로토콜의 스펙에 맞춰 응답으로 반환하는 기능을 구현할 수 있게 됩니다. 서블릿 컨테이너(Servlet Container)는 이런 서블릿을 실행하고 관리하는 환경을 제공합니다. 클라이언트로부터의 HTTP 요청을 받아 적절한 서블릿으로 전달하며, 서블릿으로부터 생성한 응답을 클라이언트에게 반환합니다. 서블릿의 생명주기를 관리하여 서블릿의 생성, 초기화, 서비스 제공, 종료 등을 처리하고, 클라이언트와 서버 간의 세션을 관리하여 상태를 유지합니다. 대표적인 서블릿 컨테이너로는 Apache Tomcat, Jetty, Undertow 등이 있습니다.
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(
ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
ObjectProvider<TomcatContextCustomizer> contextCustomizers,
ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers().addAll(connectorCustomizers.orderedStream().toList());
factory.getTomcatContextCustomizers().addAll(contextCustomizers.orderedStream().toList());
factory.getTomcatProtocolHandlerCustomizers().addAll(protocolHandlerCustomizers.orderedStream().toList());
return factory;
}
}
}
ServletWebServerFactoryConfiguration
: Tomcat 서블릿 컨테이너를 구성하고 초기화합니다.TomcatServletWebServerFactory
: Tomcat의 설정을 제공하는 Spring Bean을 생성합니다.
Spring Boot에서는 컨테이너리스 아키텍처(Containerless Architecture), 즉 독립 실행형 애플리케이션(Standalone Application)을 구현하기 위해 서블릿 컨테이너도 Spring Bean으로 관리합니다. 이를 통해 개발자는 필요한 설정을 쉽게 주입할 수 있으며, 다른 서블릿 컨테이너로 교체하거나 Spring의 생명 주기 관리 및 의존성 주입 기능을 활용할 수 있습니다.
Spring Boot에서는 컨테이너리스 아키텍처(Containerless Architecture), 즉 독립 실행형 애플리케이션(Standalone Application)을 구현하기 위해 서블릿 컨테이너를 Spring Bean으로 관리합니다. 이를 통해 개발자는 서블릿 컨테이너의 설정을 쉽게 주입할 수 있으며, 필요에 따라 다른 서블릿 컨테이너로 교체할 수 있습니다. 또한, Spring의 생명 주기 관리 및 의존성 주입 기능을 활용할 수 있어 보다 유연하고 효율적인 애플리케이션 구성이 가능합니다.
또한, 서블릿 컨테이너와 Spring IoC 컨테이너 간의 통합을 개선하기 위해 프론트 컨트롤러인 디스패처 서블릿을 도입했습니다.
DispatcherServlet
은 중앙에서 모든 HTTP 요청을 처리하고 이를 적절한 핸들러로 라우팅하는 역할을 합니다. 이를 지원하기 위해, 웹 애플리케이션 컨텍스트와 핸들러들은 애플리케이션이 실행될 때 자동으로 구성됩니다.
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
}
...
}
DispatcherServletAutoConfiguration
: Spring Boot 애플리케이션이 구동될 때DispatcherServlet
을 Spring Bean으로 등록합니다.DispatcherServlet
: 모든 HTTP 요청을 받아들이고, 이를 적절한 핸들러로 전달합니다.
DispatcherServlet
에서 요청 처리를 위임하는 핸들러는 @Controller
어노테이션이 선언된 클래스, 즉 개발자가 작성한 실제로 요청을 처리할 수 있는 메서드를 가진 컨트롤러 객체를 의미합니다. 이러한 컨트롤러 객체들은 Spring MVC의 자동 구성을 담당하는 WebMvcAutoConfiguration
클래스에 의해 자동으로 Spring Bean으로 구성되고 관리됩니다.
@AutoConfiguration(after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@ImportRuntimeHints({WebResourcesRuntimeHints.class})
public class WebMvcAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager,
conversionService, validator);
setIgnoreDefaultModelOnRedirect(adapter);
return adapter;
}
@Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
if (this.mvcRegistrations != null) {
RequestMappingHandlerAdapter adapter = this.mvcRegistrations.getRequestMappingHandlerAdapter();
if (adapter != null) {
return adapter;
}
}
return super.createRequestMappingHandlerAdapter();
}
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null) {
RequestMappingHandlerMapping mapping = this.mvcRegistrations.getRequestMappingHandlerMapping();
if (mapping != null) {
return mapping;
}
}
return super.createRequestMappingHandlerMapping();
}
...
}
...
}
WebMvcAutoConfiguration
는 요청 핸들링을 위한 주요 컴포넌트들을 Spring Bean으로 등록하여 HTTP 요청을 처리하는 핵심 컴포넌트들을 구성합니다. 이 과정에서 RequestMappingHandlerMapping
클래스는 @Controller
와 @RequestMapping
어노테이션이 선언된 모든 클래스와 메서드들을 핸들러로 등록하고, 이를 매핑 테이블 형식으로 관리합니다. 결과적으로, 서블릿 컨테이너로 전달된 클라이언트의 HTTP 요청은 Spring IoC 컨테이너에 의해 적절한 컨트롤러 메서드로 전달됩니다. 디스패처 서블릿은 이 매핑 정보를 활용하여 HTTP 요청을 처리하기 위한 적합한 핸들러를 찾을 수 있습니다.
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null)
{
info = typeInfo.combine(info);
}
if (info.isEmptyMapping()) {
info = info.mutate().paths("", "/").options(this.config).build();
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
updateConsumesCondition(mapping, method);
}
private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) {
MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
}
}
}
}
isHandler()
: 주어진 클래스가 핸들러 역할을 할 수 있는지 확인합니다. 기본적으로 클래스가@Controller
어노테이션이 붙어 있는지 확인합니다.getMappingForMethod()
: 핸들러에 대한RequestMappingInfo
를 생성합니다. 이 정보는 요청을 해당 메서드에 매핑하는 데 사용됩니다.createRequestMappingInfo()
:@RequestMapping
어노테이션에서 정보를 추출하여RequestMappingInfo
객체를 생성합니다.registerHandlerMethod()
: 주어진 메서드를 핸들러로 등록합니다.
서버가 구동된 후, HTTP 클라이언트 도구를 통해 서버로 다음과 같이 API를 호출해보겠습니다:
POST /payments HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: localhost:8080
Connection: close
User-Agent: RapidAPI/4.2.2 (Macintosh; OS X/14.5.0) GCDHTTPRequest
Content-Length: 35
{"amount":100.0,"currency":"USD"}
HTTP 요청이 오면, 서블릿 컨테이너인 톰캣이 해당 요청을 인수하고 중앙에서 라우터 역할을 하는 디스패처 서블릿에 전달합니다. 디스패처 서블릿은 초기화 과정에서 생성된 매핑 테이블을 활용해 적절한 핸들러를 판별하고, 해당 요청의 처리를 핸들러에 위임합니다. 핸들러는 요청 처리 후, 결과를 디스패처 서블릿에 반환하게 됩니다.
@Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
doDispatch()
:DispatcherServlet
이 HTTP 요청을 받아들이고 이를 처리하는 과정입니다.DispatcherServlet
은 모든 HTTP 요청을 받아들이고 이를 적절한 핸들러에게 요청 처리를 위임합니다.getHandler()
: 요청을 처리할 핸들러를HandlerMapping
을 통해 찾습니다.HandlerMapping
은 요청 URL과 매핑된 컨트롤러 메서드를 찾아주는 역할을 합니다.getHandlerAdapter()
: 찾은 핸들러를 기반으로HandlerAdapter
를 조회합니다. 핸들러 어댑터는 실제로 컨트롤러의 메서드를 호출하고, 요청을 처리하는 역할을 합니다.
이 과정에서 디스패처 서블릿은 매핑 테이블에서 찾은 핸들러의 어댑터 RequestMappingHandlerAdapter
를 통해 InvocableHandlerMethod
를 사용하여 실제로 컨트롤러 메서드를 호출하게 됩니다. InvocableHandlerMethod
는 메서드 호출을 캡슐화하여 인수를 바인딩하고, 반환 값을 처리합니다.
@Override
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
InvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
}
mav = new ModelAndView(mavContainer.getViewName(), mavContainer.getModel());
mav.setStatus(mavContainer.getStatus());
return mav;
} finally {
webRequest.requestCompleted();
}
}
요청 처리의 결과로 생성된 응답 메시지는 InvocableHandlerMethod
에서 만들어집니다. REST API의 경우 반환 값은 ModelAndView
형식이 아니라 JSON 형식이므로, 이를 적절히 변환하기 위해 HTTP 메시지 컨버터가 내부적으로 사용됩니다.
public class InvocableHandlerMethod extends HandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(this.responseReason)) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
if (returnValue instanceof CharSequence) {
mavContainer.setViewName(returnValue.toString());
} else {
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
}
}
모든 요청 처리가 완료되면 서버는 처리 결과를 포함한 HTTP 응답을 클라이언트에 전달합니다.
{
"success": true,
"amount": 100.0,
"message": "Payment of 100.00 USD processed successfully."
}
이렇게 Spring Boot는 복잡한 설정 없이도 HTTP 요청을 처리하고, 적절한 응답을 생성할 수 있는 환경을 자동으로 구성합니다. 이를 통해 개발자는 비즈니스 로직에 집중할 수 있으며, Spring IoC 컨테이너에 의해 많은 부분이 편리하게 구성됩니다.
위 과정을 요약해보면:
- 서버를 구동하면 Spring Boot 애플리케이션이 자동으로 초기화를 진행하여 필요한 설정과 구성을 완료합니다.
- 클라이언트가 HTTP 요청을 작성하여 서버에 보냅니다. 이 요청은 웹서버, 즉 Tomcat과 같은 서블릿 컨테이너에 도달합니다.
- 서블릿 컨테이너는 클라이언트로부터 받은 일반 HTTP 요청을 Java 서버 사이드 객체, 즉
HttpServletRequest
객체로 변환합니다. 이 객체는 요청 헤더, 요청 파라미터, 쿠키 정보, 요청 URL 등의 HTTP 요청 정보를 담고 있습니다. HttpServletRequest
객체를 기반으로 디스패처 서블릿은 요청을 적절한 컨트롤러와 메서드로 라우팅합니다. 이때 디스패처 서블릿은 핸들러 매핑 전략을 사용하여 요청 URL이나 타입 등에 따라 적절한 컨트롤러를 찾습니다.- 선택된 컨트롤러의 메서드는 요청을 처리하고, 결과를 표현하는 데이터를 반환합니다. 이 반환 값은 Spring MVC 설정과 메서드의 반환 타입에 따라 다양한 형태가 될 수 있습니다. 결과 데이터는 JSP 뷰 이름, Model 객체, 직접적인 HTTP 응답을 나타내는
ResponseEntity
등이 될 수 있습니다. - 메서드에서 반환된 결과 데이터는 디스패처 서블릿에 의해
HttpServletResponse
객체에 설정됩니다. 만약 뷰 이름이나 Model 객체가 반환되면, 해당 뷰가 렌더링되어 HTML과 같은 형태의 응답 본문이 생성됩니다. 이 응답 본문은HttpServletResponse
의 바디에 설정됩니다. - 디스패처 서블릿의 작업이 모두 끝나면,
HttpServletResponse
객체는 다시 서블릿 컨테이너에 전달됩니다. 서블릿 컨테이너는HttpServletResponse
객체를 읽어서 해당 정보를 기반으로 HTTP 응답 메시지를 생성합니다. 이 과정에서HttpServletResponse
의 상태 코드, 헤더, 바디 등이 HTTP 프로토콜로 변환됩니다. - 마지막으로, 서블릿 컨테이너는 생성한 HTTP 응답을 클라이언트에게 전송합니다. 이렇게 해서 클라이언트는 서버로부터 HTTP 응답을 받게 됩니다.
Spring Beans Registration Process
Spring에서 Bean을 생성하고 등록하는 과정은 여러 단계를 거칩니다. 이 과정에서 자동 구성, 조건부 등록, 사용자 정의 설정이 적절히 이루어져 최종적으로 애플리케이션이 실행됩니다. 이 과정을 이해하면 프레임워크를 더 효과적으로 활용하는 데 도움이 될 것입니다. 그럼 Spring Bean이 등록되는 전반적인 과정을 살펴보도록 하겠습니다.
- Spring Initializr를 사용하여 프로젝트를 생성할 때, 언어, 빌드 도구, 필요한 의존성 등을 선택할 수 있습니다. Gradle을 빌드 도구로 선택하면
build.gradle
이라는 빌드 스크립트가 생성됩니다. 이 스크립트는 프로젝트의 빌드 구성, 의존성 설정 및 플러그인 적용 등을 정의합니다. Gradle은build.gradle
파일을 읽어 필요한 라이브러리들을 다운로드하고 프로젝트를 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
- Spring Boot는
@AutoConfiguration
어노테이션을 통해 미리 정의된 자동 구성 Bean 후보들을 로딩합니다. 이 후보들은spring.factories
파일이나.imports
파일에 정의되어 있으며, 각기 다른 Spring 모듈에서 제공하는 다양한 자동 구성 클래스들을 포함합니다.
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
...
@EnableAutoConfiguration
어노테이션을 통해 자동 구성 Bean이 로딩됩니다. 이 어노테이션은 Spring Boot가 애플리케이션 실행 시 필요한 Bean들을 자동으로 구성하게 합니다. 이 과정에서, 위에서 불러온 후보들 중 조건에 맞는 Bean들이 실제로 로딩됩니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
@Conditional
어노테이션을 사용하여 특정 조건이 만족될 때 로딩한 후보에서 Bean을 등록합니다. 이 어노테이션은 특정 조건이 만족될 때만 Bean을 등록하는 데 사용됩니다. 예를 들어, 아래와 같이 작성하면 특정 프로퍼티가 설정되어 있는 경우에만 해당 Bean이 등록됩니다:
@Configuration
@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true")
public class ConditionalConfiguration {
@Bean
public ConditionalService conditionalService() {
return new ConditionalService();
}
}
- 인프라 관련 자동 구성 Bean들의 초기화가 진행됩니다. 추가한 의존성에 따라 다르겠지만, 일반적으로 웹 서버를 개발한다고 하면 웹 서버, 데이터 소스 및 JPA 관련 인프라 Bean들이 기본 값으로 초기화됩니다.
@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}
application.yml
과 같은 외부 설정 파일을 로드하여 속성 값을 적용합니다. Spring Boot는application.yml
또는application.properties
파일을 로드하여 설정 값을 적용합니다. 이 파일들은 애플리케이션의 환경 설정을 정의하는 데 사용됩니다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/catsriding
username: ongs
password: mongs
@SpringBootApplication
어노테이션은 지정된 패키지에서 스테레오타입 어노테이션이 선언된 클래스를 스캔합니다. 이 어노테이션은@ComponentScan
을 포함하고 있어, 지정된 패키지를 스캔한 후@Component
,@Service
,@Repository
,@Controller
어노테이션이 붙은 클래스들을 자동으로 Spring Bean으로 등록합니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
- 컴포넌트 스캐닝으로 발견한 스테레오타입 어노테이션이 선언된 클래스들을 Bean으로 등록합니다. 스캔된 클래스들은 Spring IoC 컨테이너에 Bean으로 등록되어 애플리케이션에서 사용됩니다.
@Service
public class UserService {
// business logic
}
@Repository
public class UserRepository extends JpaRepository<User, Long> {
// repository logic
}
@Controller
public class UserController {
// request handler logic
}
@Component
public class UserValidator {
// validation logic
}
- 커스텀 인프라 Bean을 등록하기 위해, 개발자가
@Configuration
과@Bean
어노테이션을 선언한 클래스들이 스캔되어 Bean으로 등록됩니다.
@Configuration
public class CustomInfraConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
- 최종적으로 모든 사용자 정의 애플리케이션 Bean이 애플리케이션 컨텍스트에 등록되고 초기화됩니다.
@Component public class UserService {...}
@Service public class UserRepository {...}
@Controller public class UserController {...}
@Component public class UserValidator {...}
@Configuration public class CustomInfraConfiguration {...}
@Bean public JPAQueryFactory jpaQueryFactory() {...}
BeanPostProcessor
를 통해 초기화된 Bean에 대한 추가 처리가 수행됩니다.BeanPostProcessor
를 사용하여 초기화된 Bean에 대해 추가적인 처리를 수행합니다. 일반적으로 이 포스트 프로세서를 통해 Bean의 프로퍼티 값을 검증하거나 수정하고, 프록시 객체로 교체하여 AOP 기능을 추가하거나, 초기화 후 로깅 및 모니터링 등의 로직을 삽입합니다. 이러한 작업은 Bean의 생성 및 초기화 이후에 추가적인 설정이나 변형을 적용하여 더 복잡한 비즈니스 로직을 처리하는 데 유용합니다.
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// before initialization logic
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// after initialization logic
return bean;
}
}
CommandLineRunner
또는ApplicationRunner
를 통해 애플리케이션 초기화 시 실행할 추가 작업들을 정의합니다. 이 인터페이스들은 애플리케이션 컨텍스트가 로드된 후 특정 로직을 실행할 수 있게 합니다. 이들은 애플리케이션 시작 시 환경 설정, 추가 프로세스 시작, 디버그 정보 로깅, 데이터베이스 초기화, 외부 서비스 설정 등의 작업을 수행하는 데 유용합니다. 두 인터페이스 모두 애플리케이션의 마지막 단계에서 실행되며, 여러 러너가 필요한 경우@Order
어노테이션이나Ordered
인터페이스를 통해 실행 순서를 지정할 수 있습니다.
@Component
public class StartupRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// initialization logic
}
}
- 모든 Spring Bean이 초기화된 후, 애플리케이션 컨텍스트가 준비되어 애플리케이션이 실행됩니다. 로그 레벨을 디버그로 설정하면 엄청나게 많은 Bean이 자동으로 추가되는 것을 확인할 수 있습니다.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.0)
12:10:21.258 [main] INFO app.catsriding.ioc.Application -- Starting Application using Java 21.0.3 with PID 54750 (/Users/catsriding/Workspace/ideas/ioc/build/classes/java/main started by catsriding in /Users/catsriding/Workspace/ideas/ioc)
12:10:21.259 [main] DEBUG app.catsriding.ioc.Application -- Running with Spring Boot v3.3.0, Spring v6.1.8
12:10:21.259 [main] INFO app.catsriding.ioc.Application -- No active profile set, falling back to 1 default profile: "default"
12:10:21.259 [main] DEBUG o.s.boot.SpringApplication -- Loading source class app.catsriding.ioc.Application
12:10:21.336 [main] DEBUG o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext -- Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@40499e4f
12:10:21.355 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
12:10:21.363 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory'
12:10:21.636 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'propertySourcesPlaceholderConfigurer'
12:10:21.639 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor'
12:10:21.643 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
12:10:21.643 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'preserveErrorControllerTargetClassPostProcessor'
12:10:21.643 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -- Creating shared instance of singleton bean 'forceAutoProxyCreatorToUseClassProxying'
...
이와 같이, Spring Boot 애플리케이션에서 Spring Bean이 등록되고 초기화되는 과정을 단계별로 살펴봤습니다. Spring Boot의 자동 구성 덕분에 수많은 Bean들이 등록되어 기본적인 환경이 세팅됩니다. 또한, 개발자가 어노테이션만 선언하면 의존성이 적절하게 주입되어 해당 객체가 Spring Bean으로 등록됩니다. 이로 인해 개발자는 비즈니스 로직에 집중할 수 있으며, 애플리케이션의 효율적이고 일관된 구성이 보장됩니다.
Spring Beans Lifecycle
Spring Bean 스코프는 Bean이 존재할 수 있는 범위를 정의합니다. Spring은 다양한 스코프를 제공하며, 주요 스코프와 그 특징은 다음과 같습니다:
- Singleton: 기본 스코프로, 애플리케이션 컨텍스트당 하나의 인스턴스만 생성됩니다.
- Prototype: 요청할 때마다 새로운 인스턴스가 생성됩니다.
- Request: 웹 애플리케이션에서 HTTP 요청마다 하나의 인스턴스가 생성됩니다.
- Session: 웹 애플리케이션에서 HTTP 세션마다 하나의 인스턴스가 생성됩니다.
- Application: 서블릿 컨텍스트 범위 내에서 하나의 인스턴스가 생성됩니다.
각 스코프는 다양한 애플리케이션 요구 사항을 충족할 수 있도록 Bean의 생명 주기와 사용 범위를 결정합니다.
Singleton Scope
싱글톤 스코프(Singleton Scope)는 기본 스코프로, 별도의 설정을 하지 않는 경우 싱글톤으로 Bean이 등록됩니다. Spring 컨테이너의 시작과 종료까지 유지되며, 애플리케이션 내에서 Bean의 단일 인스턴스를 제공합니다.
@Scope("singleton")
@Component
public class SingletonBean {...}
싱글톤 스코프는 메모리 사용을 절약하고 애플리케이션의 성능을 향상시키지만, 상태를 가지는 Bean의 경우 동시성 문제가 발생할 수 있으므로 주의가 필요합니다.
Prototype Scope
프로토타입 스코프(Prototype Scope)는 Bean 요청 시마다 새로운 인스턴스를 생성합니다. Spring 컨테이너는 Bean의 생성과 의존성 주입까지만 관여하며, 이후 Bean의 생명 주기를 관리하지 않습니다.
@Scope("prototype")
@Component
public class PrototypeBean {...}
프로토타입 스코프는 매번 새로운 인스턴스를 제공하여 상태가 공유되지 않도록 합니다. 하지만, 메모리 사용량이 증가할 수 있으며, 사용 후 명시적으로 자원을 해제해야 합니다.
Prototype Beans in a Singleton Bean Context
싱글톤 스코프의 Bean에서 프로토타입 스코프의 Bean을 주입해서 사용할 때, 프로토타입 Bean은 매번 새로 생성되지 않고 초기 주입시의 인스턴스가 계속해서 사용됩니다. 이러한 문제를 해결하기 위해, 프로토타입 Bean을 직접 요청하거나,ObjectProvider
또는 JSR-330의Provider
를 이용할 수 있습니다.
Web Scopes
웹 관련 스코프(Web Scopes)는 웹 환경에서만 동작합니다. 웹 스코프는 프로토타입과 달리 Spring이 해당 스코프의 종료 시점까지 관리하므로 종료 메서드가 호출됩니다. 웹 스코프에는 다음과 같은 종류가 있습니다:
- request 스코프: HTTP 요청 동안 Bean을 유지합니다. 요청이 완료되면 Bean이 소멸됩니다.
- session 스코프: HTTP 세션 동안 Bean을 유지합니다. 세션이 종료되면 Bean이 소멸됩니다.
- application 스코프: 웹 애플리케이션의 서블릿 컨텍스트와 같은 범위로 유지됩니다.
- websocket 스코프: 웹 소켓의 생명 주기 동안 Bean을 유지합니다.
Spring은 다양한 Bean 스코프를 통해 Bean의 생명 주기와 사용 범위를 관리하며, 적절한 스코프를 선택하여 효율적으로 애플리케이션을 구성할 수 있습니다. 그러나 실무에서는 대부분의 요구사항이 싱글톤 스코프로 처리가 가능하여, 다른 스코프를 사용하는 상황은 비교적 적습니다.
Spring Beans Injection Mechanisms
DI(Dependency Injection, 의존관계 주입)은 Spring IoC 아키텍처의 핵심 메커니즘 중 하나로, 애플리케이션 구성 요소 간의 의존성을 설정하고 관리합니다. Spring IoC의 의존관계 주입은 주로 다음 세 가지 방식으로 이루어집니다:
- 생성자 주입(Constructor Injection) ⭐
- 세터 주입(Setter Injection)
- 필드 주입(Field Injection)
Constructor Injection
생성자 주입은 객체 생성 시 필요한 의존성을 생성자를 통해 주입하는 방식입니다. 이는 불변성을 보장하며 모든 의존성을 명확하게 표시할 수 있다는 장점이 있습니다. 실제로 이 방식은 가장 권장되고 널리 사용됩니다. 생성자 주입을 통해 의존성을 명확하게 드러내고, 테스트 용이성을 높이며, 의존성 누락을 방지할 수 있습니다.
특히, Spring 4.3 이상에서는 단일 생성자를 가진 private final
필드의 경우 @Autowired
어노테이션 없이도 의존성이 자동으로 주입됩니다. 이는 개발자가 더욱 명확하고 안전하게 의존성을 관리할 수 있도록 도와줍니다.
public class PaymentService {
private final PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
public void processPayment(double amount) {
processor.process(amount);
}
}
@Configuration
public class AppConfig {
@Bean
public PaymentProcessor paymentProcessor() {
return new CreditCardProcessor();
}
@Bean
public PaymentService paymentService() {
return new PaymentService(paymentProcessor());
}
}
Setter Injection
세터 주입(Setter Injection)은 객체 생성 후 세터 메서드를 통해 의존성을 주입하는 방식입니다. 이는 선택적 의존성을 처리할 수 있다는 장점이 있지만, 객체가 완전히 초기화되지 않은 상태에서 사용될 가능성이 있습니다.
public class PaymentService {
private PaymentProcessor processor;
public void setPaymentProcessor(PaymentProcessor processor) {
this.processor = processor;
}
public void processPayment(double amount) {
processor.process(amount);
}
}
@Configuration
public class AppConfig {
@Bean
public PaymentProcessor paymentProcessor() {
return new CreditCardProcessor();
}
@Bean
public PaymentService paymentService() {
PaymentService service = new PaymentService();
service.setPaymentProcessor(paymentProcessor());
return service;
}
}
Field Injection
필드 주입(Field Injection)은 필드에 직접 의존성을 주입하는 방식입니다. 이는 코드가 간결해지는 장점이 있지만, 테스트가 어려워지고 클래스 외부에서 의존성을 변경할 수 있는 수단이 없어지는 단점이 있습니다.
public class PaymentService {
@Autowired
private PaymentProcessor processor;
public void processPayment(double amount) {
processor.process(amount);
}
}
@Configuration
public class AppConfig {
@Bean
public PaymentProcessor paymentProcessor() {
return new CreditCardProcessor();
}
@Bean
public PaymentService paymentService() {
return new PaymentService();
}
}
Secret of Getting Ahead is Getting Started
여기까지, Spring Framework의 IoC에 대해 살펴보았습니다. 이 프레임워크는 20년 동안 지속적으로 발전해 왔으며, 많은 문제들을 해결하고 개선하면서 현재의 모습을 갖추게 되었습니다. 어떤 문제에 직면해서 이를 해결하기 위해 어떠한 개념들이 도입되었는지를 이해하면, 기술을 활용할 때 더 잘 사용할 수 있는 것 같습니다. 이제, 이 편리함을 활용해 열심히 서비스를 구현하면 될 것 같습니다. ⛏️
- Java
- Spring
- Architecture