IoC(Inversion of Control, 제어의 역전)와 DI(Dependency Injection, 의존성 주입)는 객체 지향 설계에서 객체 간의 결합도를 낮추고 유연한 구조를 만들기 위한 핵심 개념. Spring Framework 등 OOP 진영의 현대 프레임워크 기반 철학이기도 합니다.
IoC (Inversion of Control) - 제어의 역전
전통적 방식에서는 개발자가 필요한 객체를 생성하거나 조작했습니다. IoC 방식에서는 객체의 생성과 생명주기 관리 권한을 외부(컨테이너)에 위임합니다.
즉, “객체 제어권”이 개발자에서 프레임워크로 “역전”되는 것입니다.
IoC 예시
// 전통 방식 (제어권 있음, 개발자)
Service service = new Service();
Controller controller = new Controller(service);
// IoC 방식 (제어권 외부로, 프레임워크)
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Controller controller = context.getBean(Controller.class);
DI (Dependency Injection) - 의존성 주입
IoC를 구현하는 방법 중 하나로 객체가 직접 의존 객체를 생성하지 않고, 외부에서 주입(Injection) 받습니다.
주입 방식에는 생성자, setter, 필드 주입 등이 있습니다.
DI를 사용하는 목적은 의존 객체를 교체하거나 테스트하기 쉽도록 설계하고 유연하고 확장 가능한 구조를 만들기 위해서입니다.
DI 예시(생성자 주입)
@Component
class Service {}
@Component
class Controller {
private final Service service;
// 생성자 주입
@Autowired
public Controller(Service service) {
this.service = service;
}
}
IoC와 DI의 연관성을 비교
| 구분 | IoC | DI |
| 정의 | 제어의 주체가 프레임워크로 바뀜 | 객체의 의존성을 외부에서 주입 |
| 목적 | 객체 제어권의 이동 | 객체 간 결합도 최소화 |
| 관계 | 큰 개념 | IoC의 구현 기법 중 하나 |
| 방식 | 이벤트 기반, DI 등 | 생성자/세터/필드 주입 |
DI?, IoC의 구현 방법 4가지를 알아보자
객체 생성과 실행 흐름을 외부로 위임한다는 개념인데, 이 IoC를 실현하는 방법에는 여러가지 있고 DI는 구중 하나일 뿐입니다.
| 구분 | 설명 | 대표 예시 |
| 1. Dependency Injection (DI) | 객체가 필요로 하는 의존성을 외부에서 주입 | Spring Framework, Dagger, Guice |
| 2. Service Locator | 의존 객체를 저장하는 ’등록소(Locator)’에서 꺼내 사용 | Java EE JNDI, 일부 레거시 DI 컨테이너 |
| 3. Event Callback / Listener | 흐름을 직접 제어하지 않고, 이벤트에 응답하는 방식 | Java Swing, JavaScript 이벤트, Spring @EventListener |
| 4. Template Method Pattern | 상위 클래스가 전체 흐름 제어, 하위 클래스는 세부 구현 | JdbcTemplate, AbstractController 등 |
1) DI - 가장 일반적이고 대표적인 IoC 방식
위에서 설명했듯이 의존 객체를 외부에서 주입받는 구조이고 생성자, 세터, 필드를 통해 주입 받을 수 있습니다.
결합도가 낮아 테스트에 용이하지만 주입 설정이 필요합니다. 최근 스프링의 경우에는 신경 쓸 부분이 크게 없어서 간편하게 사용이 가능합니다.
// 생성자 주입
@Component
class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
2) Service Locater (서비스 등록 패턴)
객체가 직접 필요한 의존성을 등록소에서 이름으로 찾아 조회합니다. 가장 대표적인 기능이 Java EE의 JNDI 기능입니다.
JNDI는 자바 앱의 외부 리소스를 이름으로 찾아서 사용한 표준 API 입니다. 간단하게 생각한다면 해시맵같은 느낌입니다.
DI 보다 상대적으로 결합도와 테스트 설정이 높으나 Java EE 진영에서는 Service Locator 패턴 기반으로 동작합니다.
반면, Spring에서는 대부분 자원을 @Autowired 같은 DI 방식으로 주입하므로, JNDI 사용은 줄어들고 있는 추세입니다.
- WAS(Tomcat) 설정 예시 - context.xml
<Context>
<Resource name="jdbc/MyDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="20"
maxIdle="10"
username="root"
password="password"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb"/>
</Context>
- JNDI를 통한 조회(Service Locator 방식)
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
public class JndiExample {
public static void main(String[] args) throws Exception {
Context context = new InitialContext();
DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/MyDB");
try (Connection conn = ds.getConnection()) {
System.out.println("DB 연결 성공!");
}
}
}
- JND의 구조 요약(네이밍 규칙에 의해 저장되는 방법으로 여기서는 크게 다루지 않습니다.)
InitialContext (JNDI 루트)
└── java:comp/env/
└── jdbc/MyDB ← 이름 기반으로 리소스 참조
3) Event 기반 IoC(콜백 / 리스너)
프레임워크가 흐름을 제어하고, 내가 정의한 콜백을 필요한 때 호출해 주는 구조로 이벤트 기반 시스템, UI, 메시징, 비동기 프로그래밍 등에서 자주 사용됩니다.
- Java Swing(요새 쓰는지 모르겠지만)
JButton button = new JButton("Click Me");
button.addActionListener(e -> {
System.out.println("Button clicked!");
});
- 버튼 클릭은 프레임워크가 감지하고 개발자는 addActionListener()로 콜백만 등록합니다.
- JavaScript(웹 개발을 하다보면 한번씩 사용했을 기능입니다.)
document.getElementById("btn").addEventListener("click", function() {
alert("Clicked!");
});
- 사용자는 클릭만 하고 이벤트가 발생하면 등록된 함수가 실행되는 구조이죠.
- Spring의 @EventListener나 Kafka 등을 통한 IoC 방식 이벤트 처리
// Step 1: 이벤트 정의
public class OrderCreatedEvent {
private final Long orderId;
public OrderCreatedEvent(Long orderId) {
this.orderId = orderId;
}
public Long getOrderId() { return orderId; }
}
// Step 2: 이벤트 발행
@Component
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher publisher;
public void createOrder(Long id) {
// 주문 생성 로직...
publisher.publishEvent(new OrderCreatedEvent(id));
}
}
// Step 3: 이벤트 리스너 등록
@Component
public class OrderEventListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("주문 완료 알림: " + event.getOrderId());
}
}
4) Template Method Pattern
상위 클래스가 제어흐름을 제공하고, 하위 클래스가 구체 동작을 정의하는 구조입니다.
디자인 패턴을 공부하다보면 우선적으로 배우는 방식이기에 쉽게 와닿을 겁니다.
abstract class Task {
public final void execute() {
validate();
run();
cleanUp();
}
protected abstract void run();
}
위 내용을 정리한 표
| 방식 | 제어 주체 | 유연성 | 테스트 용이성 | 실사용 예 |
|---|---|---|---|---|
| DI | 프레임워크 | 높음 | 좋음 | Spring, Guice |
| Service Locator | 클라이언트 | 낮음 | 나쁨 | Java EE, 레거시 |
| Event Callback | 프레임워크 | 중간 | 중간 | UI 이벤트, Spring Event |
| Template Method | 추상 클래스 | 중간 | 좋음 | 템플릿 기반 API |
마지막으로 요약하자면
- IoC: “누가 객체를 만들고 실행 순서를 제어하느냐?” → 프레임워크가 제어
- DI: “누가 객체에 필요한 것을 넣어주느냐?” → 프레임워크가 주입
'SW 공학 & 프로그래밍 언어 > SW 방법론' 카테고리의 다른 글
| TDD 시나리오 OOP 적용 예시 (1) | 2025.03.13 |
|---|---|
| 테스트 주도 개발(TDD: Test Driven Development) (3) | 2025.03.06 |