- construction-injectiondependency-injectioniocjavajava-reflectionspring-framework
Spring IoC와 DI 개념을 Java 코드로 이해하기
이전 글에서는 Spring Framework가 등장하기 전 자바 애플리케이션에서 발생했던 코드 수정 문제를 살펴보았다.
객체를 코드에서 직접 생성하면 로그 시스템, 데이터베이스, 결제 모듈, 암호화 알고리즘 등이 변경될 때마다 소스 코드를 수정하고 다시 빌드해야 하는 문제가 발생한다.
이러한 문제를 해결하기 위해 등장한 개념이 IoC(Inversion of Control)와 DI(Dependency Injection)이다.
Spring Framework의 동작 원리를 이해하는 가장 좋은 방법 중 하나는 Java 코드로 직접 간단한 DI 구조를 구현해 보는 것이다.
인터페이스(Interface)와 생성자 주입(Constructor Injection)을 이용하면 Spring의 핵심 개념을 비교적 간단한 코드로 확인할 수 있다.
메시지 전송 서비스 코드 예제
아래 코드는 의존성 주입(Dependency Injection)의 기본 구조를 보여주는 간단한 예제이다.
DI 개념을 설명하기 위해 최소한의 클래스만 사용해 구현하였다.
인터페이스 정의 (Dependency Injection 대상 타입)
EmailService는 이메일을 전송하고 SMSService는 SMS를 전송한다.
두 클래스는 MessageService 인터페이스를 구현하며 sendMessage() 메서드를 제공한다.
이처럼 인터페이스를 기반으로 설계하면 구현체를 쉽게 교체할 수 있다.
인터페이스 구현 클래스
MessageApp 클래스는 메시지를 전송하는 기능을 담당한다.
이 클래스는 MessageService 객체를 사용하지만 직접 new 키워드로 객체를 생성하지 않는다.
대신 MessageApp 객체가 생성될 때 외부에서 이미 생성된 MessageService 타입의 객체를 전달받는다.
이처럼 객체의 의존성을 외부에서 전달받는 방식을 의존성 주입(DI)이라고 한다.
즉
- MessageApp은 MessageService에 의존한다.
- 하지만 해당 객체를 직접 생성하지 않는다.
- 필요한 객체는 외부에서 주입된다.
컨테이너 역할 클래스 (Dependency Injection 실행)
이 예제에서는 MainApp 클래스가 의존성 주입을 수행한다.
즉 MainApp이 객체를 생성하고 어떤 구현체를 사용할지 결정한다.
이 역할은 실제 Spring Framework에서는 IoC 컨테이너가 담당한다.
// 1. 서비스 인터페이스 (추상화)
interface MessageService {
void sendMessage(String msg);
}
// 2. 실제 구현체 1 (Email)
class EmailService implements MessageService {
public void sendMessage(String msg) {
System.out.println("이메일 발송: " + msg);
}
}
// 3. 실제 구현체 2 (SMS)
class SMSService implements MessageService {
public void sendMessage(String msg) {
System.out.println("SMS 발송: " + msg);
}
}
// 4. DI를 적용한 클라이언트 클래스
class MessageApp {
private final MessageService service;
// 생성자를 통해 외부에서 의존성을 주입받음
public MessageApp(MessageService service) {
this.service = service;
}
public void process(String msg) {
this.service.sendMessage(msg);
}
}
// 5. IoC 컨테이너 역할을 하는 메인 클래스
public class MainApp {
public static void main(String[] args) {
MessageService emailService = new EmailService();
MessageApp app = new MessageApp(emailService);
app.process("안녕하세요! DI 예제입니다.");
MessageService smsService = new SMSService();
MessageApp app2 = new MessageApp(smsService);
app2.process("문자로도 보낼 수 있습니다.");
}
}코드에서의 DI와 IoC
위 코드에서 DI와 IoC는 다음과 같다.
의존성 주입 (DI)
MessageApp은 스스로 EmailService나 SMSService를 생성하지 않는다.
대신 생성자를 통해 외부에서 생성된 객체를 전달받는다.
이를 통해 클래스 간 결합도를 낮출 수 있다.
제어의 역전 (IoC)
어떤 구현체를 사용할지에 대한 결정권은 MessageApp 내부가 아니라 외부(MainApp)에 있다.
즉 객체 생성과 의존성 관리의 제어권이 클래스 내부에서 외부로 이동한다.
이것이 제어의 역전(IoC)이다.
Spring Framework에서는 이 역할을 Spring IoC 컨테이너가 담당한다.
생성자를 통해 의존성을 주입하는 방식을 생성자 주입(Constructor Injection)이라고 한다.
final 키워드를 사용하면 객체의 불변성을 보장할 수 있기 때문에 최근 가장 권장되는 DI 방식이다.
설정 파일과 리플렉션을 이용한 IoC와 DI
앞선 예제에서는 어떤 MessageService 구현체를 사용할지 코드에서 직접 결정했다. 따라서 구현체를 변경하려면 코드를 수정하고 다시 빌드해야 한다. 이 문제를 해결하기 위해 외부 설정 파일과 자바 리플렉션(Reflection)을 이용할 수 있다.
설정 파일 기반 구현
예를 들어 사용할 서비스 클래스를 설정 파일에서 정의하면 소스 코드 수정 없이 기능을 변경할 수 있다.
# config.properties
service.class=com.myapp.SMSService이 방식의 장점은 다음과 같다.
- 코드 수정 없이 기능 교체 가능
- 재빌드 없이 설정 변경 가능
- 환경별 설정 분리 가능
- 코드 수정으로 인한 Side Effect 감소
자바 리플렉션 (Reflection)
리플렉션은 런타임에 클래스 정보를 분석하고 객체를 동적으로 생성할 수 있는 기능이다. 설정 파일에서 읽은 클래스 이름을 기반으로 객체를 생성하면 다음과 같은 코드가 된다.
public class MainApp {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.load(new FileInputStream("config.properties"));
String className = props.getProperty("service.class");
Class<?> clazz = Class.forName(className);
MessageService service =
(MessageService) clazz.getDeclaredConstructor().newInstance();
MessageApp app = new MessageApp(service);
app.process("설정 파일에서 선택한 방식으로 메시지를 보냅니다.");
}
}이 방식의 핵심 특징은 컴파일 시점이 아니라 실행 시점에 사용할 객체가 결정된다는 것이다.
주요 리플렉션 기능
-
Class.forName()
문자열로 전달된 클래스 이름을 기반으로 런타임에 클래스를 로드한다. -
getDeclaredConstructor().newInstance()
컴파일 시점의 new 대신 런타임에 객체를 생성한다. -
setAccessible(true)
접근 제어자(private 등)를 무시하고 필드에 접근할 수 있도록 한다. Spring이 필드 주입(@Autowired)을 구현할 때 사용되는 기술 중 하나이다.
Spring Framework는 내부적으로 이러한 리플렉션 기술을 적극적으로 활용하여 IoC 컨테이너를 구현한다. 개발자는 직접 리플렉션 코드를 작성할 필요 없이 설정과 비즈니스 로직만 작성하면 된다.
IoC와 DI의 장점
Spring의 IoC와 DI를 사용하면 다음과 같은 장점이 있다.
- 객체 생성 코드 제거
- 모듈 교체 용이
- 테스트 코드 작성 편의성 증가
- 코드 결합도 감소
- 유지보수성 향상
즉 애플리케이션 구조가 더 유연하고 확장 가능한 구조로 발전하게 된다.
(끝)