스프링을 학습한 개발자라면 IoC(Inversion of Control, 제어 역전) 개념에 익숙할 것이다. IoC는 객체 간의 결합도를 낮추고 유지보수성을 높이기 위한 중요한 객체지향 프로그래밍 원칙으로, 스프링 프레임워크의 핵심 기반이다. 일반적으로 IoC는 의존성 탐색(Dependency Lookup)과 의존성 주입(Dependency Injection, DI) 두 가지 형태로 나뉘며, 특히 DI가 가장 널리 사용된다. 본 문서에서는 직접 간단한 예제를 통해 IoC의 동작 원리를 이해하고, 스프링이 이를 어떻게 추상화하는지 살펴본다.
예제 시나리오는 사용자 정보를 데이터베이스에 저장하는 작업이다. 계층 구조를 명확히 하여 유지보수성과 확장성을 고려하며, 인터페이스 기반 설계를 통해 구현체의 변경에 유연하게 대응할 수 있도록 한다. 먼저 사용자 데이터를 담을 도메인 클래스부터 정의한다.
public class UserDO {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
다음은 클라이언트 요청을 처리하는 진입점 역할을 할 애플리케이션 클래스다. 실제 웹 환경에서는 서블릿이 담당하겠지만, 여기서는 `main` 메서드를 통해 흐름을 시뮬레이션한다.
public class UserApplication {
public static void main(String[] args) {
UserDO user = new UserDO();
user.setName("김개발");
user.setPassword("1234");
BeanFactory factory = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) factory.getBean("userService");
userService.registerUser(user);
}
}
비즈니스 로직은 `UserService`에서 처리하며, 이는 데이터 접근 계층인 DAO를 활용한다. 하지만 직접적인 생성을 하지 않고, 외부에서 주입받도록 설계한다.
public class UserService {
private UserDao userRepository;
public void setUserRepository(UserDao userRepository) {
this.userRepository = userRepository;
}
public void registerUser(UserDO user) {
userRepository.save(user);
}
}
데이터 영속성은 인터페이스를 통해 추상화한다. 이를 통해 다양한 저장소(MySQL, MongoDB 등)로의 교체가 용이해진다.
public interface UserDao {
void save(UserDO user);
}
실제 구현체는 콘솔 출력을 통해 "저장" 동작을 시뮬레이션한다.
public class JdbcUserDaoImpl implements UserDao {
@Override
public void save(UserDO user) {
System.out.println("DB에 사용자 저장: " + user.getName());
}
}
이제 구성 정보를 XML 파일로 관리한다. `src/main/resources` 경로에 `spring.xml`을 생성한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userRepository" class="com.example.dao.JdbcUserDaoImpl"/>
<bean id="userService" class="com.example.service.UserService">
<property name="userRepository" ref="userRepository"/>
</bean>
</beans>
XML 설정을 기반으로 빈(Bean)을 생성하고 의존성을 주입하는 컨테이너를 직접 구현해보자. 우선 팩토리 인터페이스를 정의한다.
public interface BeanFactory {
Object getBean(String beanId);
}
구현체는 JDOM 라이브러리를 사용해 XML을 파싱하고, 리플렉션을 통해 객체를 생성 및 연결한다.
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map container = new HashMap<>();
public ClassPathXmlApplicationContext(String configLocation) {
try {
SAXBuilder builder = new SAXBuilder();
Document document = builder.build(
this.getClass().getClassLoader().getResourceAsStream(configLocation)
);
Element root = document.getRootElement();
List<Element> beans = root.getChildren("bean");
// 1단계: 모든 빈 인스턴스 생성
for (Element bean : beans) {
String id = bean.getAttributeValue("id");
String className = bean.getAttributeValue("class");
Object instance = Class.forName(className).newInstance();
container.put(id, instance);
}
// 2단계: 의존성 주입 (setter 기반)
for (Element bean : beans) {
String id = bean.getAttributeValue("id");
Object target = container.get(id);
List<Element> properties = bean.getChildren("property");
for (Element prop : properties) {
String propertyName = prop.getAttributeValue("name");
String refId = prop.getAttributeValue("ref");
Object dependency = container.get(refId);
String setterName = "set" +
propertyName.substring(0, 1).toUpperCase() +
propertyName.substring(1);
Class<?> depInterface = dependency.getClass().getInterfaces()[0];
Method setter = target.getClass().getMethod(setterName, depInterface);
setter.invoke(target, dependency);
}
}
} catch (Exception e) {
throw new RuntimeException("컨테이너 초기화 실패", e);
}
}
@Override
public Object getBean(String beanId) {
return container.get(beanId);
}
}
이렇게 구현된 컨테이너는 스프링의 `ClassPathXmlApplicationContext`와 유사한 방식으로 작동한다. 실제로 스프링을 사용하면 다음과 같이 간결하게 작성할 수 있다.
// Maven 또는 Gradle로 spring-context 추가 필요
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService service = context.getBean("userService", UserService.class);
service.registerUser(new UserDO());
스프링 프레임워크는 이러한 IoC 컨테이너를 통해 객체의 생명주기와 의존관계를 완전히 관리함으로써, 개발자는 비즈니스 로직에 집중할 수 있도록 해준다.