Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
560f9e8
init
jude-loopers Apr 12, 2026
5a4f362
1주차 자바 웹 개발 기초
jude-loopers Apr 22, 2026
3cd3989
2주차 자바 웹 요청 추상화
jude-loopers Apr 29, 2026
87473be
Merge pull request #65 from Loopers-play-dev-lab/template-main
jude-loopers Apr 29, 2026
a15d543
2주차 자바 웹 요청 추상화
jude-loopers May 3, 2026
1a7081a
3주차 빈생성
jude-loopers May 6, 2026
183266d
4주차 Bean 이름 관리 & @Bean & Controller 빈 등록
May 13, 2026
c086f99
feat: @Controller, @RequestMapping, RequestMethod 애너테이션 추가
May 20, 2026
8c3fd73
feat: HandlerKey equals/hashCode 및 HandlerMethod invoke() 구현
May 20, 2026
ffb5a81
feat: HandlerMapping, HandlerAdapter 인터페이스 추가
May 20, 2026
d421427
feat: SimpleUrlHandlerMapping, AnnotationHandlerMapping 구현
May 20, 2026
b9c50dd
feat: SimpleControllerHandlerAdapter, AnnotationHandlerAdapter 구현
May 20, 2026
a486628
refactor: DispatcherServlet HandlerMapping/Adapter 기반으로 변경
May 20, 2026
da13ea2
refactor: ApplicationContext @Controller 스캔 및 getBeans() 추가
May 20, 2026
47405fc
feat: LectureController 애너테이션 기반으로 마이그레이션
May 20, 2026
1712293
refactor: LectureApplication HandlerMapping/Adapter 초기화 방식 변경
May 20, 2026
c7d80e6
chore: application 플러그인 추가
May 20, 2026
401c7f8
refactor: getBeans() Map 반환으로 변경 및 getControllerMapping() 제거
May 27, 2026
cd7c182
refactor: SimpleUrlHandlerMapping 빈 이름 기반 자체 초기화로 변경
May 27, 2026
98375a5
refactor: AnnotationHandlerMapping initialize() 파라미터 Map으로 통일
May 27, 2026
7b25bbc
feat: 인터페이스 기반 Controller 등록 및 초기화 정리
May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ bin/

### Mac OS ###
.DS_Store\ntomcat.8080/

## tomcat
**/tomcat.8080
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
plugins {
id("java")
id("application")
kotlin("jvm")
}

application {
mainClass.set("com.diy.app.LectureApplication")
}

group = "com.diy"
version = "1.0-SNAPSHOT"

Expand All @@ -26,6 +31,9 @@ dependencies {
implementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.42")
implementation("org.apache.tomcat.embed:tomcat-embed-jasper:8.5.42")

// 리플렉션 의존성 주입
implementation("org.reflections:reflections:0.10.2")

implementation(kotlin("stdlib-jdk8"))
}

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/diy/app/Lecture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.diy.app;

import java.math.BigDecimal;

public class Lecture {

private Long id;
private String name;
private BigDecimal price;

public Lecture() {
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public BigDecimal getPrice() {
return price;
}

public void setId(Long id) {
this.id = id;
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/diy/app/LectureApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.diy.app;

import com.diy.framework.context.ApplicationContext;
import com.diy.framework.web.mvc.HandlerAdapter;
import com.diy.framework.web.mvc.HandlerMapping;
import com.diy.framework.web.mvc.adapter.AnnotationHandlerAdapter;
import com.diy.framework.web.mvc.adapter.SimpleControllerHandlerAdapter;
import com.diy.framework.web.mvc.handler.AnnotationHandlerMapping;
import com.diy.framework.web.mvc.handler.SimpleUrlHandlerMapping;
import com.diy.framework.web.mvc.view.JspViewResolver;
import com.diy.framework.web.mvc.view.UrlBasedViewResolver;
import com.diy.framework.web.mvc.view.ViewResolver;
import com.diy.framework.web.server.TomcatWebServer;
import com.diy.framework.web.servlet.DispatcherServlet;

import java.util.List;
import java.util.Map;

public class LectureApplication {
public static void main(String[] args) {
final ApplicationContext ac = new ApplicationContext(LectureApplication.class.getPackageName());
ac.initialize();

final Map<String, Object> beans = ac.getBeans();

final AnnotationHandlerMapping annotationHM = new AnnotationHandlerMapping();
annotationHM.initialize(beans);

final SimpleUrlHandlerMapping simpleHM = new SimpleUrlHandlerMapping();
simpleHM.initialize(beans);

final List<HandlerMapping> handlerMappings = List.of(annotationHM, simpleHM);
final List<HandlerAdapter> handlerAdapters = List.of(new AnnotationHandlerAdapter(), new SimpleControllerHandlerAdapter());
final List<ViewResolver> viewResolvers = List.of(new UrlBasedViewResolver(), new JspViewResolver());

final DispatcherServlet servlet = new DispatcherServlet(handlerMappings, handlerAdapters, viewResolvers);
new TomcatWebServer(servlet).start();
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/diy/app/LectureController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.diy.app;

import com.diy.framework.context.annotation.Controller;
import com.diy.framework.context.annotation.RequestMapping;
import com.diy.framework.web.mvc.RequestMethod;
import com.diy.framework.web.mvc.view.ModelAndView;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/lectures")
public class LectureController {

private final Map<Long, Lecture> lectureRepository = new HashMap<>();

@RequestMapping(methods = RequestMethod.GET)
public ModelAndView list(final HttpServletRequest req, final HttpServletResponse resp) {
final Collection<Lecture> lectures = lectureRepository.values();
final Object lectureModels = lectures.stream()
.map(lecture -> Map.of("id", lecture.getId(), "name", lecture.getName(), "price", lecture.getPrice()))
.toList();

final Map<String, Object> model = new HashMap<>();
model.put("lectures", lectureModels);

return new ModelAndView("lecture-list", model);
}

@RequestMapping(methods = RequestMethod.POST)
public ModelAndView create(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
final byte[] bodyBytes = req.getInputStream().readAllBytes();
final String body = new String(bodyBytes, StandardCharsets.UTF_8);

final Lecture lecture = new ObjectMapper().readValue(body, Lecture.class);

final long id = lectureRepository.size();
lectureRepository.put(id, lecture);
lecture.setId(id);

return new ModelAndView("redirect:/lectures");
}
}
6 changes: 0 additions & 6 deletions src/main/java/com/diy/app/Main.java

This file was deleted.

15 changes: 15 additions & 0 deletions src/main/java/com/diy/app/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.diy.app;

import com.diy.framework.context.annotation.Bean;
import com.diy.framework.context.annotation.Component;
import com.diy.framework.web.mvc.Controller;
import com.diy.framework.web.mvc.view.ModelAndView;

@Component
public class WebConfig {

@Bean("/home")
public Controller homeController() {
return (req, resp) -> new ModelAndView("redirect:/lectures");
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/diy/framework/beans/factory/BeanScanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.diy.framework.beans.factory;

import org.reflections.Reflections;

import java.lang.annotation.Annotation;
import java.util.Set;
import java.util.stream.Collectors;

public class BeanScanner {

private final Reflections reflections;

public BeanScanner(final String... basePackages) {
reflections = new Reflections(basePackages);
}

public Set<Class<?>> scanClassesTypeAnnotatedWith(final Class<? extends Annotation> annotation) {
return reflections.getTypesAnnotatedWith(annotation)
.stream()
.filter(type -> (!type.isAnnotation() && !type.isInterface()))
.collect(Collectors.toSet());
}
}
146 changes: 146 additions & 0 deletions src/main/java/com/diy/framework/context/ApplicationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.diy.framework.context;

import com.diy.framework.beans.factory.BeanScanner;
import com.diy.framework.context.annotation.Autowired;
import com.diy.framework.context.annotation.Bean;
import com.diy.framework.context.annotation.Component;
import com.diy.framework.context.annotation.Controller;
import com.diy.framework.context.annotation.RequestMapping;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Collection;
import java.util.Collections;

public class ApplicationContext {

private final String basePackage;
private final Set<Class<?>> beanClasses = new HashSet<>();
private final Map<String, Object> beans = new HashMap<>();

public ApplicationContext(final String basePackage) {
this.basePackage = basePackage;
}

public void initialize() {
final BeanScanner beanScanner = new BeanScanner(basePackage);
beanClasses.addAll(beanScanner.scanClassesTypeAnnotatedWith(Component.class));
beanClasses.addAll(beanScanner.scanClassesTypeAnnotatedWith(Controller.class));

beanClasses.forEach(clazz -> {
if (isBeanInitialized(clazz)) {
return;
}

final Object bean = createInstance(clazz);
saveBean(generateBeanName(clazz), bean);
});

registerBeanMethods();
}

private void registerBeanMethods() {
// 이미 등록된 @Component 빈들을 순회
new ArrayList<>(beans.values()).forEach(componentBean -> {
final Method[] methods = componentBean.getClass().getDeclaredMethods();
Arrays.stream(methods)
.filter(method -> method.isAnnotationPresent(Bean.class))
.forEach(method -> registerBeanFromMethod(componentBean, method));
});
}

private void registerBeanFromMethod(final Object componentBean, final Method method) {
try {
final Bean beanAnnotation = method.getAnnotation(Bean.class);

// 이름: @Bean("name")이면 name, 없으면 메서드명
final String beanName = beanAnnotation.value().isEmpty()
? method.getName()
: beanAnnotation.value();

method.setAccessible(true);
final Object bean = method.invoke(componentBean);
method.setAccessible(false);

saveBean(beanName, bean);
} catch (Exception e) {
throw new RuntimeException("@Bean 메서드 실행 실패", e);
}
}

private Object createInstance(final Class<?> clazz) {
final Constructor<?> constructor = findConstructor(clazz);

try {
constructor.setAccessible(true);
final Object[] parameters = getConstructorParameters(constructor);

return constructor.newInstance(parameters);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
constructor.setAccessible(false);
}
}

private Constructor<?> findConstructor(final Class<?> clazz) {
final Constructor<?>[] constructors = clazz.getDeclaredConstructors();

if (constructors.length == 1) {
return constructors[0];
}

return findAutowiredConstructor(constructors);
}

private Constructor<?> findAutowiredConstructor(final Constructor<?>[] constructors) {
final Constructor<?>[] autowiredConstructors = Arrays.stream(constructors)
.filter(constructor -> constructor.isAnnotationPresent(Autowired.class))
.toArray(Constructor[]::new);

if (autowiredConstructors.length != 1) {
throw new RuntimeException("Autowired constructor not found");
}

return autowiredConstructors[0];
}

private Object[] getConstructorParameters(final Constructor<?> constructor) {
final List<Class<?>> parameterTypes = Arrays.stream(constructor.getParameterTypes()).toList();

if (!beanClasses.containsAll(parameterTypes)) {
throw new RuntimeException("parameter is not bean");
}

return parameterTypes.stream().map(parameterType -> {
if (isBeanInitialized(parameterType)) {
return beans.values().stream()
.filter(bean -> bean.getClass().equals(parameterType))
.findFirst().get();
}

final Object bean = createInstance(parameterType);
saveBean(generateBeanName(parameterType), bean);

return bean;
}).toArray();
}

public Map<String, Object> getBeans() {
return Collections.unmodifiableMap(beans);
}

private boolean isBeanInitialized(final Class<?> clazz) {
return beans.values().stream().anyMatch(bean -> bean.getClass().equals(clazz));
}

private void saveBean(final String beanName, final Object bean) {
beans.put(beanName, bean);
}

private String generateBeanName(final Class<?> clazz) {
final String simpleName = clazz.getSimpleName();
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/diy/framework/context/annotation/Autowired.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.diy.framework.context.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
12 changes: 12 additions & 0 deletions src/main/java/com/diy/framework/context/annotation/Bean.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.diy.framework.context.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
String value() default "";
}
11 changes: 11 additions & 0 deletions src/main/java/com/diy/framework/context/annotation/Component.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.diy.framework.context.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
13 changes: 13 additions & 0 deletions src/main/java/com/diy/framework/context/annotation/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.diy.framework.context.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Controller {
}
Loading