
아래에는 글쓰기 컨트롤러 글조회 컨트롤러가 있다.
public class PostSearchController extends HttpServlet {
private final PostService postService;
public PostSearchController(PostService postService) {
this.postService = postService;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
Post post = postService.getPost(1L);
System.out.println(post.toString());
}
}
public class PostCreateController extends HttpServlet {
private final PostService postService;
public PostCreateController(PostService postService) {
this.postService = postService;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
PostDTO postDTO = new PostDTO(1L, "제목", "내용", "홍길동");
postService.createPost(postDTO);
}
}
두 컨트롤러 모두 같은 서비스를 의존하고 있다. 그런데 요청들이 들어오면 각 요청마다 새로운 서비스 객체를 배정해줘야해서 요청마다 새로운 객체가 생길 것이다.
스프링 컨테이너처럼 외부에 싱글톤 객체를 정의하고 같은 객체를 참조하도록 서블릿에 매핑해줘야한다.
컴포넌트 스캔을 만들기 위해 먼저 자바 어노테이션들을 만들어보자.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Repository {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RestController {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Service {
}
컴포넌트 어노테이션을 정의하고 서비스 레포지터리 레스트컨트롤러는 컴포넌트 어노테이션을 가지고 있다.
참고로 모든 어노테이션을 만들지는 않았다. 중심이 되는 부분만 만들어 보겠다.
이제 컴포넌트 스캔을 만들어 보자. 우선 모든 패키지를 스캔하여 클래스의 집합을 만들어보자
아래는 모든 패키지를 스캔하여 클래스 메타데이터를 반환하는 코드이다.
private static Set<Class<?>> scanPackage(String packageName) throws IOException, URISyntaxException, ClassNotFoundException {
Set<Class<?>> classes = new HashSet<>();
String path = packageName.replace(".", "/");
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File directory = new File(resource.toURI());
System.out.println("클래스로더 검사중 " + resource + ", " + directory);
for (File file : directory.listFiles()) {
if (file.isDirectory()) {
classes.addAll(scanPackage(packageName + "." + file.getName()));
} else if (file.getName().endsWith(".class")) {
String className = packageName + "." + file.getName().replace(".class", "");
classes.add(Class.forName(className));
}
}
}
return classes;
}
루트 경로를 입력값으로 받는다. 그리고 시스템 클래스로더를 호출하여 경로로 리소스를 가져온다.
클래스로더는 .class를 메타스페이스로 집어넣는 역할만 하지는 않고 경로의 리소스를 가져오는 역할도 한다.
그리고 와일문을 돌리며 경로에서 파일을 가져오는데 그 안에서 재귀적으로 컴포넌트 스캔을 다시 실행한다. 루트경로 이하 모든 클래스를 가져오기 위함이다. 그리고 클래스이름으로 메타데이터를 조회하여 Set안에 삽입한다.
자 이제 루트패스이하 모든 클래스의 메타데이터를 뽑아냈다. 그럼 이제 그 데이터중에서 컴포넌트 스캔을 해야한다.
아래는 컴포넌트를 포함하는 클래스를 확인하는 코드다
private static boolean hasComponent(Class<?> clazz) {
if (clazz.isAnnotationPresent(Component.class)) {
return true;
}
for (Annotation annotation : clazz.getAnnotations()) {
if (annotation.annotationType().isAnnotationPresent(Component.class)) {
return true;
}
}
return false;
}
리플랙션을 이용하여 클래스에 컴포넌트가 포함되어있는지 확인한다. 그리고 서비스 레포지터리등은 내부에 컴포넌트어노테이션을 가지고 있으므로 클래스의 어노테이션중에서 컴포넌트가 포함되어있는지 확인한다.
아래는 컴포넌트 스캔클래스의 오케스트레이션 메서드이다.
public static Set<Class<?>> scanComponent(String basePackage) throws IOException, URISyntaxException, ClassNotFoundException {
Set<Class<?>> classes = scanPackage(basePackage);
Set<Class<?>> beanDefinition = new HashSet<>();
for (Class<?> clazz : classes) {
if (!clazz.isAnnotation() && hasComponent(clazz)) { // 어노테이션 클래스는 빈으로 만들지 않는다.
beanDefinition.add(clazz);
System.out.println(clazz + "빈 정의 삽입");
}
}
return beanDefinition;
}
모든 패키지를 재귀적으로 내려가서 모든 클래스를 가져오고 어노테이션의 자체클래스 (ex: 서비스 어노테이션은 내부에 컴포넌트를 가지고 있으니 어노테이션클래스 자체가 빈으로 등록될 수 있다) 를 제외하는것과 동시에 컴포넌트를 가지고 있는 클래스를 빈정의라는 Set에 삽입하고 반환한다.
아래는 빈정의 클래스이다. 빈정의는 아직 빈이 만들어지지 않은 정의뿐인 데이터이다.
/**
* 빈 정의
* 빈이 되어야할 클래스 메타데이터를 관리한다.
*/
public class BeanDefinition {
private static Set<Class<?>> beanDefinition = new HashSet<>();
/**
* 빈 팩토리의 초기화시 발생한다. 컴포넌트 스캔으로 부터 얻은 빈정의를 저장하고 빈 팩토리에 전달한다.
*
*/
public static Set<Class<?>> initBeanDefinition(String basePackage) throws IOException, URISyntaxException, ClassNotFoundException {
beanDefinition = ComponentScan.scanComponent(basePackage);
return beanDefinition;
}
}
여기서 컴포넌트 스캔을 실시하여 빈 팩토리에 전달한다.
아래는 빈 팩토리 클래스다.
public class BeanFactory {
private static Map<Class<?>, Object> beans = new HashMap<>();
/**
* 빈 정의리스트를 가지고 와서 객체 생성과 의존관계 주입을 실행한다.
* 완료되면 빈 후처리기에 빈을 전달한다.
*/
public static void initialize(String basePackage) throws IOException, URISyntaxException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
Set<Class<?>> beanDefinition = BeanDefinition.initBeanDefinition(basePackage);
for (Class<?> clazz : beanDefinition) {
dependencyInject(clazz);
}
}
톰캣이 실행되기 전에 미리 빈을 초기화 시켜놓는다. 실제 스프링부트에서는 톰캣을 내장하여 @Condition 어노테이션으로 톰캣 라이브러리의 클래스까지 빈으로 등록한다. 하지만 아직 필수기능을 많이 구현해야하기에 우선 빈을 초기화하고 난 이후에 톰캣을 실행시키고 차후에 리팩토링하겠다.
Main클래스는 현재 이러한 형태다.
/**
* 톰캣 시작전에 스프링의 빈 팩토리를 초기화하고 서블릿에는 빈의 객체를 전달한다.
* 모든 객체는 싱글톤이다.
*/
public static void main(String[] args) throws LifecycleException, IOException, URISyntaxException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
BeanFactory.initialize("com.createspring");
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector();
사실 빈 생성과 의존관계 주입, 트랜잭셔널 구현등 어느정도 진행을하고 글을 작성하는 상태다.
그래서 현 단계에서 앞서나가는 부분은 코드로 보여주지 않았다.
2편은 빈 생성과 의존관계 주입을 구현하겠다.
'Spring > 스프링을 만들어보자' 카테고리의 다른 글
| 순수 자바로 스프링 구현 (2) - 빈 생성, 의존성 주입 (0) | 2026.04.01 |
|---|