[JAVA] 리플렉션 / 어노테이션 연습

merci·2023년 3월 10일
0

JAVA

목록 보기
9/10

어노테이션을 사용하면 코드의 불필요한 중복을 줄일수 있게 된다. 어노테이션을 만들어보자


기본적인 uri 매핑

일반적으로 uri를 매핑하면 다음의 구조를 가진다

public class UserController {	
	public void login() {
		System.out.println("login() 호출됨");
	}	
	public void join() {
		System.out.println("join() 호출됨");
	}
}
	// 디스패처 서블릿
	public static void main(String[] args) {
    	Scanner sc = new Scanner(System.in);
		String path = sc.nextLine();
        
		UserController uc = new UserController();		
		if(path.equalsIgnoreCase("/login")) {
			uc.login();
		}		
		if(path.equalsIgnoreCase("/join")) {
			uc.join();
		}
   }

사용자가 입력한 주소를 path로 가정했다.
if 조건으로 연결된 주소에 맞는 메소드를 실행시켜서 매핑을 시킨다.
이러한 구조의 단점은 새로운 기능이 필요할 경우 새로운 메소드와 if조건을 추가해야 하는 번거로움이다.

조금 더 편하게 하기 위해서 어노테이션을 만들어 이용해보자

@RequestMapping 어노테이션 추가

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
	String uri();
}

리플렉션이 런타임시에 여러가지를 확인하고 연결해주는데 여기서는 어노테이션을 이용

public class UserController {
	@RequestMapping( uri = "/login")
	public void login() {
		System.out.println("login() 호출됨");
	}	
	@RequestMapping( uri = "/join")
	public void join() {
		System.out.println("join() 호출됨");
	}
}
	// 디스패처 서블릿
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String path = sc.nextLine();
        
        UserController uc = new UserController();        
		Method[] methods = uc.getClass().getDeclaredMethods();
		System.out.println(methods.length);

실행하면 @RequestMapping을 연결시킨 메소드 배열의 길이를 출력해준다.

길이가 출력 된다는것은 컨트롤러의 메소드에 @RequestMapping을 붙이기만 하면 디스패처 서블릿에서 해당 메소드를 찾을수 있다는 것이다.
그러면 이제부터 일일이 if조건을 만들 필요가 없어진다.

이제 main() 메소드에서 입력받은 uri 만 실행시키고 싶다면 다음의 코드를 이용한다.

	Method[] methods = uc.getClass().getDeclaredMethods();
        Arrays.asList(methods).forEach((e)->{
		Annotation anno = e.getDeclaredAnnotation(RequestMapping.class);
        // System.out.println(anno) => @ex01.RequestMapping(uri="/join") 출력됨
		RequestMapping rm = (RequestMapping) anno;
        // System.out.println(rm.uri()); => /join 출력됨
		if(rm.uri().equalsIgnoreCase(path)) {
			try {
				e.invoke(uc);
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}
	});

사용자가 입력한 주소가 존재한다면 해당 주소와 이름이 같은 uri와 연결된 메소드를 실행시켜 준다. (invoke()로 호출)
이렇게 하면 기능을 보다 쉽게 추가할 수 있게 되었다.


@Controller 어노테이션 추가

이제 하나의 컨트롤러가 아닌 여러개의 컨트롤러에서 주소를 매핑시켜 보자
지금까지는 @RequestMapping 만 찾아서 매핑했지만 추가적으로 주소가 있는 컨트롤러까지 매핑해야 한다.
컨트롤러를 추가해서 매핑하고 싶다면 새로운 어노테이션을 만들면 된다.

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

이렇게 구현하면 스프링에서 사용하는 어노테이션을 만들수가 있다.

@Controller
public class UserController {
	@RequestMapping( uri = "/login")
	public void login() {
		System.out.println("login() 호출됨");
	}
	@RequestMapping( uri = "/join")
	public void join() {
		System.out.println("join() 호출됨");
	}
	@RequestMapping( uri = "/joinForm")
	public void joinForm() {
		System.out.println("joinForm() 호출됨");
	}
	@RequestMapping( uri = "/userInfo")
	public void userInfo() {
		System.out.println("userInfo() 호출됨");
	}
}

이번에는 UserController uc = new UserController();를 만들지 않고 어노테이션이 동작하는지 확인해보자

    public static void scan(Class<?> clazz, String path) throws Exception {
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
            	Annotation anno = method.getDeclaredAnnotation(RequestMapping.class);
    			RequestMapping rm = (RequestMapping) anno;
    			if(rm.uri().equalsIgnoreCase(path)) {
        				method.invoke(clazz.newInstance());
        		}
            }
        }
    }

위의 메소드를 사용해서 입력한 uri와 메소드를 매핑시키면

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String path = sc.nextLine();
		try {
			ControllerScanner.scan(UserController.class, path);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

실행코드가 어노테이션을 찾아서 입력한 uri를 컨트롤러의 메소드로 매핑시켜준다.


리플렉션 적용

이번에는 패키지에서 @Controller 가 붙은 클래스에서 @RequestMapping을 찾아서 매핑시키는 방법이다.
위에서 만들어 놓은 scan()를 오버로딩해서 재사용한다.

    public static void scan(String packageName, String inputPath) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        String path = packageName.replace(".", "/");
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            File file = new File(resource.toURI());
            for (File classFile : file.listFiles()) {
                String fileName = classFile.getName();
                if (fileName.endsWith(".class")) {
                    String className = fileName.substring(0, fileName.lastIndexOf("."));
                    Class<?> clazz = Class.forName(packageName + "." + className);
                    if (clazz.isAnnotationPresent(Controller.class)) {
                        scan(clazz, inputPath);
                    }
                }
            }
        }
    }
 
     public static void scan(Class<?> clazz, String path) throws Exception {
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
            	Annotation anno = method.getDeclaredAnnotation(RequestMapping.class);
    			RequestMapping rm = (RequestMapping) anno;
    			if(rm.uri().equalsIgnoreCase(path)) {
        				method.invoke(clazz.newInstance());
        		}
            }
        }
    }
    

여기서 class loadergetContextClassLoader()를 사용했다.

ClassLoader 클래스

  • 자바에서 모든 클래스는 java.lang.ClassLoader 인스턴스에 의해서 로딩된다.
  • 한 번 클래스가 JVM으로 로딩되면 같은 클래스는 다시 로딩되지 않는다.
  • ClassLoader 인스턴스는 클래스를 로딩하는 것 뿐만 아니라 다른 리소스도 읽을 수 있는 기능을 제공한다.
  • 현재 스레드의 getContextClassLoader 메소드를 통해 ClassLoader 인스턴스를 획득할 수 있다.
  • ClassLoader 인스턴스를 사용하면 특정 클래스의 인스턴스를 동적으로 생성할 수 있다.
    ( 레퍼런스 : https://junhyunny.blogspot.com/2019/03/thread-getcontextclassloader.html )

컨트롤러 하나를 추가하고 @Controller ,@RequestMapping 어노테이션을 추가했다.

@Controller
public class BoardController {
	@RequestMapping( uri = "/save")
	public void save() {
		System.out.println("save 호출됨");
	}
	
	@RequestMapping( uri = "/super")
	public void sse() {
		System.out.println("super 호출됨");
	}
}

실행 코드에서는 만든 메소드를 호출하고 uri 만 입력한다.

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String path = sc.nextLine();
		try {
			ControllerScanner.scan("ex01", path);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

패키지 안에 존재하는 컨트롤러를 찾아서 uri 를 매핑시켜주는 모습이다.

약간 다른 코드로는 아래와 같은 코드도 있다.

public class App {

    public static Set<Class> componentScan(String pkg) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Set<Class> classes = new HashSet<>();

        URL packageUrl = classLoader.getResource(pkg);
        File packageDirectory = new File(packageUrl.toURI());
        for (File file : packageDirectory.listFiles()) {
            if (file.getName().endsWith(".class")) {
                String className = pkg + "." + file.getName().replace(".class", "");
                //System.out.println(className);
                Class cls = Class.forName(className);
                classes.add(cls);
            }
        }
        return classes;
    }

    public static void findUri(Set<Class> classes, String uri) throws Exception {
        boolean isFind = false;
        for (Class cls : classes) {
            if (cls.isAnnotationPresent(Controller.class)) {
                Object instance = cls.newInstance();
                Method[] methods = cls.getDeclaredMethods();

                for (Method mt : methods) {
                    Annotation anno = mt.getDeclaredAnnotation(RequestMapping.class);
                    RequestMapping rm = (RequestMapping) anno;
                    if (rm.uri().equals(uri)) {
                        isFind = true;
                        mt.invoke(instance);
                    }
                }
            }
        }
        if(isFind == false){
            System.out.println("404 Not Found");
        }
    }

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        String uri = sc.nextLine();

        Set<Class> classes = componentScan("ex02");
        findUri(classes, uri);

    }

}
profile
작은것부터

0개의 댓글