-
학습 순서
1. 리플렉션이란 무엇일까?
2. 리플렉션에 주요 클래스와 인터페이스 살펴보기
3. 리플렉션을 이해하기 위한 선행 어노테이션 학습 하기
4. 리플렉션을 이해하기 위한 시나리오 코드 3단계1. 리플렉션이란 무엇일까?
리플렉션(Reflection)은 자바의 핵심 기능 중 하나로, 프로그램 실행 중에 클래스의 정보를 분석하고 조작할 수 있는 부분을 의미합니다.
즉, 리플렉션을 사용하면 컴파일 시점에 클래스의 구조를 미리 알 필요 없이, 실행 시점에 동적으로 클래스의 메서드, 필드, 생성자 등에 접근하고 호출할 수 있습니다 이는 일부 프레임워크나 라이브러리에서 매우 유용하게 사용되고 있습니다.2. 주요 클래스와 인터페이스 살펴보기
자바에서 리플렉션을 사용하려면 java.lang.reflect 패키지에 있는 클래스와 인터페이스를 사용할 수 있습니다.
keyword : Class, Field, Method, Constructor2.1 클래스 정보 가져오기
Class 객체를 통해 클래스 정보에 접근할 수 있습니다. 클래스 정보를 가져오려면 Class.forName() 메서드를 사용하거나, 객체의 getClass() 메서드를 호출 또는 리터럴로 직접 xxx.class 를 붙여서 참조합니다
// Class.forName() 사용 Class<?> clazz = Class.forName("com.example.MyClass"); // 객체.getClass() 사용 MyClass obj = new MyClass(); Class<?> clazz = obj.getClass(); // .class 사용 Class<?> clazz = MyClass.class;
2.2 필드 정보 가져오기
Class 객체의 getField() 또는 getDeclaredField() 메서드를 사용하여 필드 정보에 접근할 수 있습니다. getField()는 public 필드에만 접근할 수 있고, getDeclaredField()는 모든 접근 제한자의 필드에 접근할 수 있습니다.
// public 필드에만 접근 가능 Field field = clazz.getField("fieldName"); // 모든 접근제한자에 접근 가능 Field field = clazz.getDeclaredField("fieldName");
2.3 메서드 정보 가져오기
Class 객체의 getMethod() 또는 getDeclaredMethod() 메서드를 사용하여 메서드 정보에 접근할 수 있습니다. getMethod()는 public 메서드에만 접근할 수 있고, getDeclaredMethod()는 모든 접근 제한자의 메서드에 접근할 수 있습니다.
// public 메서드에만 접근 가능 Method method = clazz.getMethod("methodName", parameterTypes); // 모든 접근제한자 메서드에 접근 가능 Method method = clazz.getDeclaredMethod("methodName", parameterTypes);
2.4 생성자 정보 가져오기
Class 객체의 getConstructor() 또는 getDeclaredConstructor() 메서드를 사용하여 생성자 정보에 접근할 수 있습니다. getConstructor()는 public 생성자에만 접근할 수 있고, getDeclaredConstructor()는 모든 접근 제한자의 생성자에 접근할 수 있습니다.
// 클래스 정보 가져오기 Class<?> clazz = MyClass.class; // public 생성자 정보 가져오기 Constructor<?> publicConstructor = clazz.getConstructor(String.class); // 모든 생성자 정보 가져오기 Constructor<?>[] constructors = clazz.getDeclaredConstructors(); for (Constructor<?> constructor : constructors) { System.out.println(constructor); }
package ch01; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; public class MainTest { public static void main(String[] args) throws Exception { Class<?> clazz = Class.forName("ch01.UserController"); System.out.println(clazz); System.out.println("clazz : " + clazz.toString()); // 필드 접근 Field publicField = clazz.getField("name"); Field privateField = clazz.getDeclaredField("age"); System.out.println("name Field : " + publicField); System.out.println("age Field : " + privateField); // 메서드 접근 방법 Method publicMethod = clazz.getMethod("login"); Method privateMethod = clazz.getDeclaredMethod("join"); System.out.println("method login : " + publicMethod); System.out.println("method join : " + privateMethod); // 생성자 접근 방법 Constructor<?>[] constructors = clazz.getConstructors(); for (Constructor<?> cls: constructors) { System.out.println("생성자 : " + cls); } } }
3. 리플렉션을 이해하기 위한 선행 어노테이션 학습 하기
자바의 어노테이션(Annotation)은 프로그램의 코드에 메타데이터를 추가하는 기능을 제공하는 것으로, 컴파일러나 런타임 시에 코드를 처리하는 도구들에게 추가적인 정보를 제공합니다.
어노테이션은 @어노테이션이름(속성1=값1, 속성2=값2, ...) 형태로 사용하며, 주로 클래스, 메서드, 변수, 매개변수 등에 부착하여 사용됩니다.
자바에서 제공하는 어노테이션은 크게 세 가지 유형으로 나눌 수 있습니다.
1.빌트인 어노테이션(Built-in Annotation)
@Override: 상위 클래스나 인터페이스의 메서드를 오버라이드함을 나타냅니다.
@Deprecated: 해당 요소(클래스, 메서드, 필드 등)가 더 이상 사용되지 않음을 나타냅니다.
@SuppressWarnings: 컴파일러의 경고를 무시하도록 지정합니다.
2.메타 어노테이션(Meta Annotation)
@Retention: 어노테이션의 유지 정책을 지정합니다. (소스, 클래스, 런타임)
@Target: 어노테이션을 부착할 수 있는 대상(클래스, 메서드, 필드 등)을 지정합니다.
3.커스텀 어노테이션(Custom Annotation)
개발자가 직접 정의한 어노테이션으로, 애플리케이션에 맞는 사용자 정의 어노테이션을 만들 수 있습니다.
어노테이션은 주로 코드의 가독성을 높이고, 자동화된 코드 생성, 테스트, 디버깅 등에 활용됩니다.3.1 커스텀 어노테이션
커스텀 어노테이션 정의 하는 방법
public @interface MyCustomAnnotation { String value() default ""; // value() - 요소 , default - 기본값 정의 가능 }
커스텀 어노테이션 사용하는 방법
@MyCustomAnnotation(value = "값지정가능") public class MyClass { // ... }
커스텀 어노테이션 만들어 보기
@Retention(value = RetentionPolicy.RUNTIME) // 이 어노테이션은 런타임에도 수명 주기 유지 @Target(ElementType.TYPE) // 이 어노테이션을 클래스 레벨에서만 사용할 수 있도록 정의 public @interface CustomAnnotation { // 커스텀 어노테이션을 정의 String value(); // 어노테이션 요소를 정의 }
@Retention(속성=값) - 어노테이션의 수명,유지되는 범위를 지정 하는데 사용
RetentionPolicy.SOURCE - 소스 코드에서만 유지, 컴파일 과정에서는 제거 RetentionPolicy.CLASS - 컴파일러에 의해 클래스 파일까지 유지되지만, 런타임 시 무시 RetentionPolicy.RUNTIME - 이 런타임 시에도 유지되어야 하며, 따라서 런타임 시에 리플렉션을 통해 어노테이션 정보를 읽을 수 있다는 것을 나타냅니다
@Target(속성=값) - 사용될 수 있는 Java 요소를 지정
ElementType.TYPE - 클래스, 인터페이스 (비롯한 애노테이션 타입), 또는 열거형에서 사용 선언 가능 ElementType.FIELD - 필드 선언 (즉, 인스턴스 변수)에 사용 가능
ElementType.METHOD: 메서드 에서 사용 가능
4. 리플렉션을 이해하기 위한 시나리오 코드 3단계
package ex02; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) // 생명 주기 (런타임) @Target(ElementType.METHOD) // 메서드 앞에 사용하는 어노테이션으로 정의 public @interface RequestMapping { String uri(); // 어노테이션 요소(메서드) }
package ex02; 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 = "/save") public void save() { System.out.println("save 메서드 호출"); } }
package ex02; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Scanner; public class App { // ex02 UserController 입니다. public static void findUri(UserController uc, String uri) throws Exception { boolean isFind = false; // 리플렉션을 활용해서 UserController 선언된 메서드 정보를 가져오자 Method[] methods = uc.getClass().getMethods(); for (Method mt : methods) { // 해당 메서드에 선언된 RequestMapping 어노테이션을 가져오기 Annotation anno = mt.getDeclaredAnnotation(RequestMapping.class); RequestMapping rm = (RequestMapping) anno; if (rm != null) { if (rm.uri().equals(uri)) { isFind = true; mt.invoke(uc); // break; } } } 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(); findUri(new UserController(), uri); } }
2단계 코드에서 App.java 파일에서 findUri 함수안에서 리플렉션을 기법을 사용하고 있기 때문에 커스텀 어노테이션을 활용한 UserController에서는 /login, /join 메서드 이 외에 추가적인 메서드를 만들더라도 잘 동작할 수 있는 구조로 설계하였습니다. 하지만 UserController 클래스 이외에 다른 클래스를 사용할 수 없는 한계점을 가지고 있습니다.
4.3 단계별 학습 3단계 진행
3단계에서 리플렉션과 커스텀어노테이션을 활용한 컴포넌트 스캔이라는 개념에 대해 알아 봅시다. 스프링 부트에서 컴포넌트 스캔(Component Scan)은 스프링 프레임워크에서 제공하는 기능 중 하나로, 자동으로 빈(Bean)으로 등록할 클래스를 스캔하여 스프링 애플리케이션 컨텍스트에 등록하는 기능입니다. 이를 통해 개발자는 수동으로 빈을 등록하지 않고도 자동으로 컴포넌트를 찾아 스프링의 의존성 주입(Dependency Injection)과 관리 기능을 사용할 수 있습니다.
package ex03; import ex02.UserController; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.net.URL; import java.util.HashSet; import java.util.Scanner; import java.util.Set; public class App { /** * 이 함수는 특정 패키지 내외(ex02 or ex03) 모든 클래스를 스캔하고 * @Controller 어노테이션이 붙어 있는 클래스들을 확인하여 Set<Class<?>> * 형태의 자료구조에 담고 반환합니다. * */ public static Set<Class<?>> componentScan(String pkg) throws Exception{ // 현재 스레드의 컨텍스트 클래스 로더를 가져 옵니다. // 여기서는 특정 스레드가 어떤 클래스를 로드해야 하는지 결정하는데 사용(클래스 로더) ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Set<Class<?>> classes = new HashSet<>(); // pkg -> ex02, ex03 // 클래스 로더를 사용하여, 주어진 패키지명에 해당하는 URL을 가져 옵니다. URL packageUrl = classLoader.getResource(pkg); File packageDirectory = new File(packageUrl.toURI()); // 해당 패키지(디렉토리)에 있는 모든 파일을 순회 합니다. for (File file : packageDirectory.listFiles()){ // App.class, UserController.class, .. if (file.getName().endsWith(".class")){ // .class 문자열 값 제거 String className = pkg + "." + file.getName().replace(".class", ""); System.out.println("className" + className); Class<?> cls = Class.forName(className); // 자료 구조에 추출한 클래스 이름 넣기 classes.add(cls); } } // System.out.println(packageDirectory); // System.out.println(packageUrl); return classes; } /** *이 함수는 사용자가 입력한 URI 형식에 값(uri)을 넘겨받아 componentScan 함수에서 * 실행한 Set<Class<?>> 타입에 데이터들 중에 사용자가 입력한 값과 * @RequestMapping 어노테이션의 요소 값(uri)을 비교하여 일치한다면 * 해당 메서드를 호출 시키는 역할을 합니다. * */ public static void findUri(Set<Class<?>> classes, String uri) throws Exception{ Boolean isFind = false; for(Class<?> cls : classes){ // @Controller 어노테이션이 존재하는지 확인 if (cls.isAnnotationPresent(Controller.class)){ // 생성자를 통해서 메모리에 올리고 참조 변수를 담아 둔다. Object instance = cls.getDeclaredConstructor().newInstance(); // 해당 클래스에 모든 메서드 정보를 담아 둔다. Method[] methods = cls.getDeclaredMethods(); for (Method mt : methods){ Annotation anno = mt.getDeclaredAnnotation(RequestMapping.class); if(anno != null){ RequestMapping rm = (RequestMapping) anno; if (rm.uri().equals(uri)){ isFind = true; // uri 값이 일치 한다면 해당 메서드를 호출 합니다. mt.invoke(instance); break; } } } } } //end of for if(isFind == false){ System.out.println("404 Not Found"); } } // main 함수 public static void main(String[] args) throws Exception { // 컴퍼넌트 스캔 수행 Set<Class<?>> classes = componentScan("ex03"); // ex02, ex03 //사용자 입력을 받는 객체 필요 (브라우저 주소값 입력) Scanner sc = new Scanner(System.in); String uri = sc.nextLine(); // 실제 URI 맵핑 확인 findUri(classes, uri); } }
package ex03; @Controller public class BoardController { @RequestMapping(uri = "/put") public void put(){ System.out.println("put 메서드가 실행 됨"); } }
package ex03; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) // 클래스 요소에 선언 가능한 어노테이션으로 정의 public @interface Controller { }
package ex03; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) // 메서드에 선언 가능한 어노테이션 public @interface RequestMapping { String uri(); // 어노테이션 요소 (메서드) }
package ex03; @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="/save") public void save(){System.out.println("save() 메서드 호출");} }