코드를 세세하게 알려고 하는 것 보다는 reflection이 왜 쓰였고 어떻게 사용되는지 등을 알아야한다! 코드가 중요한게 아냐~!
필기
DS : 주소분석해서 controller 찾아줌
이제 패키지를 검사해야됨
컴파일된 .class 를 검사
forName()를 패키지명+클래스명 알면 class 객체에 담고
Component annotation이 붙어있으면 newInstance()로 new 할 수 있음
객체를 하나만들어서 사용하는 싱글톤
hashSet인 이유
set은 중복 허용X
dispatcher가 controller가 아닌 hashset 의존 -> hashset 크기만큼 돌면서
BoardController 등으로 다운캐스팅해서 쓰는게 아니라(왜냐면 controller가 여러개면 UserController일때는 안되니까) invoke로 메서드를 때려야됨
spring은 org.example.first만 스캔한다-> 따라서 ex04처럼 안해줘도 됨
component scan 단점
전체 분석 -> 꼭 필요할 때만 서야함 -> 런타임때마다 분석하고 호출하기 때문에 시스템이 느려진다
참고
0. 프로젝트 생성

1. 끊임없는 유지보수가 필요해
A 회사가 Dispatcher를 만들어서 B회사에게 납품한다고 가정하자. B회사에서 Dispatcher를 납품받아서 UserController와 App 클래스를 통해 사용하게 된다. 그러나 A회사에서 Dispatcher를 만들 때 UserController에 대해서 전혀 알지 못한다. 따라서 추상화를 통한 동적 바인딩을 위해 Controller라는 interface를 만들어서 Dispatcher가 Controller에 의존하도록 한다. 그리고 B회사에게 interface를 알려주고 Controller를 구현해서 UserController를 만들어라고 말해줘야 한다.
이러한 경우 B회사가 UserController에 기능을 추가하고 싶어도 UserController에 바로 추가하지 못하고 A회사에 연락해서 기능을 추가해야 한다. 즉, B회사에서 기능을 추가하고 싶을 때마다, 또 다른 납품 받은 회사들마다 계속해서 유지보수가 필요해진다.
package ex01;
import java.util.Scanner;
// B회사에서 만드는 걸 A회사는 모르니까 추상화가 필요 + 동적 바인딩
interface Controller{
void login();
void join();
void logout();
}
// 1. A회사
class Dispatcher{
//dispatcher은 controller에 의존한다
Controller controller;
// B회사가 implement만 받으면 가능
public Dispatcher(Controller controller) {
this.controller = controller;
}
// 라우팅(어떤 네 트워크 안에서 통신 데이터를 보낼 때 최적의 경로를 선택하는 과정)하는 걸 만듦
public void routing(String path){
if(path.equals("/login")){
controller.login();
} else if(path.equals("/join")){
controller.join();
} else if(path.equals("/logout")){
controller.logout();
}
}
}
// A회사: 내가 잘 만들어뒀는데 구매할래ㅎㅎ?
// 구매한다고 하면 1) interface 알려줘야 됨 2)Controller 구현해서 만들어라고 말해줘야됨
// 2. B회사 (구매할게 - Controller 구현해서 만들어)
class UserController implements Controller{
@Override
public void login() {
System.out.println("Login call");
}
@Override
public void join() {
System.out.println("Join call");
}
@Override
public void logout() {
System.out.println("Logout call");
}
}
public class App {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
UserController uc = new UserController();
// uc.login처럼 함수를 호출하는게 아니라 scanner로 path를 받아서
Dispatcher ds = new Dispatcher(uc);
ds.routing(path);
}
}

2. 리플렉션 (ex02)
Class 클래스
java의 클래스와 인터페이스는 컴파일된 이후 class 파일로 생성되는데 이때 class 파일에는 클래스나 인터페이스명, 필드나 메서드 정보 등 메타데이터가 포함되어있다.
Class 클래스는 Java Reflection API이 제공하는 클래스 중 하나로 컴파일 된 .class 파일에서 클래스에 대한 메타데이터를 동적으로 조회하고 사용할 수 있는 기능을 제공한다. ⇒ 클래스명, 메서드, 필드 등을 동적으로 조회 및 조작 가능
1. App 클래스에서 리플렉션을 알아보자
- 리플렉션이란
- 모든 것을 동적으로 분석하는 것
- runtime(동적 ↔ compile time(정적))에 클래스에 대한 정보를 조회하고 메서드나 필드에 접근 가능하도록 기능을 제공한다.
- Class 객체를 이용해서 클래스의 정보를 알아내는 것
- 코드 이해하기
- Method[] methods = con.getClass().getMethods();
- con 객체의 클래스인 UserController.class의 모든 public 메서드를 가져와서 Method[] 배열로 저장한다.
- getClass()
- 객체가 속한 클래스의 정보를 알 수 있다.
- 해당 객체의 런타임 클래스인 Class<?> 객체를 반환한다.
- println()으로 출력하면 toString()이 호출되고 Class 클래스의 toString() 메서드는 ”class” + 클래스의 전체 이름을 반환한다.
- getMethods()
- Java Reflection API에서 제공하는 기능
- 해당 클래스와 부모 클래스 및 인터페이스에서 상속받은 public 메서드 목록을 배열로 반환
- if(method.getName().equals("login")) {method.invoke(con);}
- UserController 클래스의 모든 메서드를 돌아가면서 하나씩 가져와서 이름이 login과 같은지 비교하고 일치할 경우 해당 메서드를 실행한다.
- invoke()
- Java Reflection API에서 제공하는 기능
- getMethods 메서드 등을 통해 가져온 Method 객체를 통해서 호출된다.
- 특정 객체의 메서드를 런타임 시에 동적으로 실행 가능하다.
- heap에 떠있는 어떤 객체를 사용하는지 괄호 안에 인수로 명시한다.
⇒ 특정 객체의 메서드를 동적으로 메모리에서 찾아서 invoke가 호출하여 사용 가능하다.
package ex02;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class App {
public static void main(String[] args) {
// heap에 UserController 뜸 + hdd에 그 클래스의 코드 들어있음
UserController con = new UserController();
// hdd에 잇는 코드 자체를 부른 것(new해서 heap에서 온 거 아님 new 안해도 쓸 수 있음)
// heap에 모든 메서드를 런타임때 분석해서 담음 -> login logout join가 담김
// getMethods()의 return 타입이 java.lang.reflect.Method[]임 (함수 위에 마우스 올려보면 됨)
// 부모인 Object의 함수까지 리턴됨
// UserController라는 파일을 찾아서 그 안에 어떤 코드가 있는지 찾음
Method[] methods = con.getClass().getMethods();
for (Method method : methods) {
try {
if(method.getName().equals("login")){
method.invoke(con);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
// 동적으로 메모리에서 찾아서 함수 때릴수 잇음
// invoke하면 logout 호출
// 동적으로 찾아서 invoke가 호출
// 어떤 heap에 있는 건지 괄호 안에 e.g.) UserController con1 = new / UserController con2 = new 있으면 con1이나 con2
/*try {
methods[0].invoke(con);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}*/
}
}
2. Annotation & Dispatcher 클래스에서 처리하기
- Annotation이란?
- Annotation = reflection에 깃발 추가 (해당 깃발이 있는 곳만 찾아서 처리)
- ~
- 깃발은 다 만들어져 있으므로 깃발 사용법만 알면 되는 게 Spring
- Annotation 커스텀하기
- @Target : annotation 적용할 위치를 선택한다.
- @Target(ElementType.METHOD) : method위에만 붙일 수 있음
- @Retention : annotation 적용 시점을 선택한다.
- @Retention(RetentionPolicy.RUNTIME) : runtime
- @Retention(RetentionPolicy.SOURCE) : compile
- 코드 이해하기 - RequestMapping annotation
- 메서드 위에 적용되는 annotation이다.
- Scanner로 path를 받아서 분석할 때 적용된다.(runtime)
- value() 속성에 path 문자열이 전달된다. (그냥 value라는 속성으로 문자열 인수를 전달받는 것 e.g.) @RequestMapping(value=”/login”))
package ex02;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Dispatcher 만드는 회사가 만들면 됨
// Scanner로 path 받아서 분석할 때
@Retention(RetentionPolicy.RUNTIME) // annotation 언제 작동? ((RUNTIME)runtime or (SOURCE)compile시 둘 중 하나)
@Target(ElementType.METHOD) // method위에만 붙일 수 있음
public @interface RequestMapping {
String value();
}
- 코드 이해하기 - Dispatcher class
- UserController에 의존한다.
- path를 routing 메서드를 통해 인수로 받게되면 UserController의 메서드들을 하나씩 가져온 다.
- 메서드에 RequestMapping annotation이 적용되었는지 확인하고 적용되었을 경우 객체를 반환한다.
- getAnnotation()
- 인수로 받은 특정 annotation이 존재하면 해당 annotation 타입의 클래스 객체를 반환하고 없을 경우 null을 반환한다.
- annotation 클래스의 Class 객체를 인수로 받기 때문에 annotation명.class가 인수로 들어온다. (annotation 클래스를 직접 참조)
- RequestMapping이 적용되지 않은 메서드의 경우 건너뛰고 다음 메서드를 확인한다.
- 만약 RequestMapping이 적용된 메서드에서 annotation의 value속성 값이 path와 일치한다면 해당 메서드를 호출한다.
getClass() vs .class vs forName() (Class 객체를 가져오는 방법)
- getClass(): new를 통해 객체를 생성했을 때만 사용 가능하다. 객체에 대한 클래스 정보를 가져온다. 특정 객체의 클래스를 반환한다. (Object 클래스의 getClass() 호출) (e.g. con.getClass()) ⇒ 컴파일 되어야만 사용 가능
- .class : 클래스명을 입력하여 Class 클래스를 가져올 수 있다. ⇒ 컴파일 되어야만 사용 가능
- forName() : Class가 new를 통해 객체를 생성할 수 없기 때문에 사용하는 메서드이다. 패키지명이 포함된 클래스명을 문자열로 받아서 해당 클래스를 동적으로 가져온다. (e.g. Class.forName()) ⇒ 동적으로 runtime에 로딩
package ex02;
import java.lang.reflect.Method;
public class Dispatcher {
UserController con;
public Dispatcher(UserController con) {
this.con = con;
}
public void routing(String path) { // /login
Method[] methods = con.getClass().getMethods();
for (Method method : methods) {
RequestMapping rm = method.getAnnotation(RequestMapping.class);
if (rm == null) continue; // 다음 for문으로 바로 넘어감
if (rm.value().equals(path)) {
try {
method.invoke(con);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
- 결과
- UserController 클래스에서 @RequestMapping를 메서드에 적절하게 적용한다면 Dispatcher 클래스를 수정할 필요 없이 메서드를 추가할 수 있다.
package ex02;
public class App {
public static void main(String[] args) {
Dispatcher ds = new Dispatcher(new UserController());
ds.routing("/userinfo");
}
}
package ex02;
public class UserController {
@RequestMapping("/login")
public void login() {
System.out.println("login call");
}
@RequestMapping("/join")
public void join() {
System.out.println("join call");
}
@RequestMapping("/logout")
public void logout() {
System.out.println("logout call");
}
@RequestMapping("/userinfo")
public void userinfo() {
System.out.println("userinfo call");
}
}
3. 파라미터 검사 로직 추가 (ex03)
1. UserController 클래스의 메서드에 매개변수 추가됨
- path를 입력받을 때 username이라는 매개변수를 함께 받는다.
package ex03;
public class UserController {
// /login?username=ssar
@RequestMapping("/login")
public void login(String username) {
System.out.println("login call: " + username);
}
@RequestMapping("/join")
public void join() {
System.out.println("join call");
}
@RequestMapping("/logout")
public void logout() {
System.out.println("logout call");
}
@RequestMapping("/userinfo")
public void userinfo() {
System.out.println("userinfo call");
}
}
package ex03;
public class App {
public static void main(String[] args) {
Dispatcher ds = new Dispatcher(new UserController());
ds.routing("/login?username=ssar");
}
}
2. Dispatcher 클래스에서 path 검사 시에 매개변수도 검사하는 로직 추가
- /login?username=ssar인 path가 /login이나 /logout … 등으로 시작할 경우 참
- /login 이하의 ?username=ssar을 parsing 한다.
- getParameters() : Parameter[] 배열을 반환하고 각 Parameter 객체는 메서드의 매개변수에 대한 정보를 가진다.
- 만약 매개변수가 존재하고 string 타입이라면 path를 “?”를 기준으로 split한 1번지(username=ssar)를 다시 “=” 기준으로 split한 1번지(ssar)를 username의 값으로 가져온다.
- con 객체와 username 값와 함께 메서드를 호출한다.
- 파라미터가 없는 경우에는 파라미터 없이 con객체와 함께 메서드를 호출한다.
package ex03;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class Dispatcher {
UserController con;
public Dispatcher(UserController con) {
this.con = con;
}
public void routing(String path) { // /login
Method[] methods = con.getClass().getMethods();
for (Method method : methods) {
RequestMapping rm = method.getAnnotation(RequestMapping.class);
if (rm == null) continue; // 다음 for문으로 바로 넘어감
// path = /login?username=ssar
// rm.value() => RequestMapping("/login")
if (path.startsWith(rm.value())) {
try {
// 1. 파라미터 검사
Parameter[] parameters = method.getParameters();
if (parameters.length > 0 && parameters[0].getType().equals(String.class)) {
String username = path.split("\\?")[1].split("=")[1];
method.invoke(con, username);
} else {
method.invoke(con);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
RequestMapping annotation 코드는 동일하다
package ex03;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Dispatcher 만드는 회사가 만들면 됨
// Scanner로 path 받아서 분석할 때
@Retention(RetentionPolicy.RUNTIME) // annotation 언제 작동? ((RUNTIME)runtime or (SOURCE)compile시 둘 중 하나)
@Target(ElementType.METHOD) // method위에만 붙일 수 있음
public @interface RequestMapping {
String value();
}
4. Component Scan - 하나의 controller가 아닌 여러개의 controller 찾아서 깃발 꽂기
1. controller 하나 더 추가하기
package ex04;
@Component
public class BoardController {
@RequestMapping("/write")
public void write() {
System.out.println("write call");
}
@RequestMapping("/delete")
public void delete() {
System.out.println("delete call");
}
}
2. Component Annotation 생성
- Controller 클래스 위에 붙어서 Controller 클래스를 구분해내는 깃발 역할
component annotation 코드 추가

3. ex04 패키지의 class 파일 전부 찾기
- 어차피 .class가 아닌 파일에는 @Component가 있을 일이 없으므로 .class만 찾는다
package ex04;
import java.io.File;
import java.net.URL;
public class App {
public static void main(String[] args) {
// 1. @Component가 붙으면 new해서 컬렉션에 담기
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
URL packageUrl = classLoader.getResource("ex04");
File packageDir = new File(packageUrl.getFile());
for (File file : packageDir.listFiles()) {
if (file.getName().endsWith(".class")) {
String className = "ex04." + file.getName().replace(".class", "");
System.out.println(className);
}
}
}
}

4. Component Annotation가 있으면 해당 클래스의 객체 생성해서 HashSet에 담기
- 객체를 하나만 만들어서 사용하므로 singleton이다.
- HashSet으로 만든 이유는 중복을 허용하지 않기 위해서이다.
package ex04;
import java.io.File;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
public class App {
public static void main(String[] args) {
// 1. @Component가 붙으면 new해서 컬렉션에 담기
Set<Object> instances = new HashSet();
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
URL packageUrl = classLoader.getResource("ex04");
File packageDir = new File(packageUrl.getFile());
for (File file : packageDir.listFiles()) {
if (file.getName().endsWith(".class")) {
String className = "ex04." + file.getName().replace(".class", "");
//System.out.println(className);
try {
Class cls = Class.forName(className);
if (cls.isAnnotationPresent(Component.class)) {
Object instance = cls.getDeclaredConstructor().newInstance();
instances.add(instance);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} // for 종료
for (Object instance : instances) {
System.out.println(instance.getClass().getName());
}
}
}

Dispatcher class에는 @Component가 없기 때문에 출력 안되고 @Component가 붙인 controller class만 출력되는 것을 알 수 있다!



5. dispatcherServlet 클래스
- componentScan 메서드 : 패키지명을 인수로 받아서 해당 패키지의 클래스 중 Component annotation이 붙은 클래스만 new해서 HashSet인 instances에 담고 instances를 반환한다,
- 이전에는 Dispatcher 클래스가 특정 Controller 클래스에 의존했지만 이제는 특정 Controller 가 아닌 HashSet에 의존한다. 즉 Component Annotation을 가진 Controller 클래스를 new한 객체들이 담긴 instances를 HashSet의 크기만큼 돌면서 각 Controller 클래스를 확인할 수 있다.
- controller가 여러 개이면 특정 Controller 클래스만 지정해서 쓰면 다른 Controller 클래스는 사용할 수 없으므로 UserController나 BoardController 등으로 다운캐스팅해서 쓰는게 아니라 Controller 클래스를 돌면서 그 안의 메서드를 확인하고 invoke로 메서드를 호출하는 방식
- spring은 org.example.first만 스캔한다 => 따라서 “ex04”처럼 따로 지정 안해줘도 된다.
package ex04;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
public class DispatcherServlet {
// 특정 class에 의존할 필요 없음
// 책임 : @Component new해서 hashset에 저장
public Set<Object> componentScan(String packageName){ // ex04만 하면 너무 제한적이니까
// 1. @Component가 붙으면 new해서 컬렉션에 담기
Set<Object> instances = new HashSet();
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
URL packageUrl = classLoader.getResource(packageName);
File packageDir = new File(packageUrl.getFile());
for (File file : packageDir.listFiles()) {
if (file.getName().endsWith(".class")) {
String className = packageName+ "." + file.getName().replace(".class", "");
//System.out.println(className);
try {
Class cls = Class.forName(className);
if (cls.isAnnotationPresent(Component.class)) {
Object instance = cls.getDeclaredConstructor().newInstance();
instances.add(instance);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
} // for 종료
return instances;
}
public void routing(Set<Object> instances, String path) {
for (Object instance : instances) {
Method[] methods = instance.getClass().getMethods();
for (Method method : methods) {
RequestMapping rm = method.getAnnotation(RequestMapping.class);
if (rm == null) continue; // 다음 for문으로 바로 넘어감
if (rm.value().equals(path)) {
try {
method.invoke(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
}
package ex04;
import java.io.File;
import java.net.URL;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class App {
public static void main(String[] args) {
// RequestMapping, Component, DispatcherServlet (돈 주고 삼 = Spring Web이라는 라이브러리가 제공)
Scanner sc = new Scanner(System.in);
String path = sc.nextLine(); // /write
// component scan 먼저
DispatcherServlet dispatcherServlet = new DispatcherServlet();
Set<Object> instances = dispatcherServlet.componentScan("ex04");
dispatcherServlet.routing(instances, path);
}
}
UserController는 @Component 붙는 거 이외에는 ex03 패키지의 UserController와 동일하다
package ex04;
@Component
public class UserController {
@RequestMapping("/login")
public void login() {
System.out.println("login call");
}
@RequestMapping("/join")
public void join() {
System.out.println("join call");
}
@RequestMapping("/logout")
public void logout() {
System.out.println("logout call");
}
@RequestMapping("/userinfo")
public void userinfo() {
System.out.println("userinfo call");
}
}
RequestMapping annotation 코드도 동일하다
package ex04;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Dispatcher 만드는 회사가 만들면 됨
// Scanner로 path 받아서 분석할 때
@Retention(RetentionPolicy.RUNTIME) // annotation 언제 작동? ((RUNTIME)runtime or (SOURCE)compile시 둘 중 하나)
@Target(ElementType.METHOD) // method위에만 붙일 수 있음
public @interface RequestMapping {
String value();
}
6. 실행결과
⇒ BoardController든 UserController든 상관없이 invoke로 호출돼서 사용가능


7. component scan 단점
전체 분석 -> 꼭 필요할 때만 서야함 => 런타임때마다 분석하고 호출하기 때문에 시스템이 느려진다
Share article