[스프링부트] 5. Reflection

문정준's avatar
Mar 13, 2025
[스프링부트] 5. Reflection

1. 협업의 어려움

  • A, B 두 회사에서 프로그램을 개발 중
    • A 회사에서는 프로토콜에 따른 응답을 보낼 수 있도록 메서드를 연결하는 프로그램을 제작
    • B 회사에서는 연결을 통해 사람들에게 응답을 보내는 서버 프로그램을 제작
  • A 회사가 자사 프로그램을 B 회사에게 판매
    • B 회사는 직접 메서드들을 서버와 연결할 수고가 없어짐
  • B 회사는 A 회사가 설계해둔 구조(인터페이스)를 받아서, 메서드들을 구현
    • A 회사는 어떤 회사가 기능을 어떻게 사용할지 모르므로 인터페이스로 메서드 구현
notion image
 
 
  • B 회사에서 프로그램에 기능을 추가하고 싶으면 A 회사와 contact 필요
    • A 회사는 B가 추가할 기능을 연결하고, 인터페이스에 이를 연결하는 번거로움 발생
    • B 회사에서는 기능을 추가할 때마다 A 회사와 contact 해야 하는 번거로움 발생
    • A 회사의 경우, 이 프로그램을 다른 회사에 판매하였을 때에도 똑같은 문제 발생 가능
    • notion image
 

2. Reflection & Annotation

  • 동적으로 메서드에 접근할 수 있는 기술
  • 메서드 이름, 매개 변수, 리턴 값 등 다양한 요소를 가지고 활용 가능
  • 요구 (Request)에 따른 동적인 반응(Response)이 가능하므로 유연성 있는 프로그램 제작 가능
  • 어노테이션 (Annotation)과 함께 사용하여 시간 절약, 코드 간소화 등에 큰 효과를 볼 수 있음
 
✏️

어노테이션(Annotation)이란?

  • JVM이 인식할 수 있는 힌트
  • 어노테이션 내에 코드가 어떻게 작동해야 하는 지를 서술해두고, 메인 코드에서는 이를 생략하고 어노테이션을 붙여서 코드를 대신 설명
  • 작동 타겟, 작동 시간 등 관리가 가능
 

Example

  • 큰 도로변의 가로수를 정리하는 데, 기둥에 상처가 난 나무만 정리하려고 한다.
  • 기존 방법의 경우, 모든 나무들을 일일이 찾아다니면서 상처가 났는지 확인해야 한다.
    • 나무에 상처가 났다면, 어느 나무가 상처가 났는지 기억해두고 다음 나무를 찾아야 한다.
    • 나무를 추가로 심었는데 그 나무에 상처가 났다면, 다시 새로 심은 나무에 찾아가 상처가 났는지 확인하고, 그 위치를 외워둬야 한다.
    • 상처가 난 나무를 다 확인했다면 벌목 담당자에게 나무의 위치를 알려준다.
    • 벌목 담당자는 그 위치를 기억해서 나무를 정리한다.
      • 나무를 자를 때에도 Full Scan이 필요 : 시간이 매우 오래 걸림
notion image
 
  • 어노테이션 (Annotation)을 활용하면, 이를 더욱 손쉽게 처리할 수 있다.
    • 상처가 난 나무의 위치를 외우지 않고, 이 나무를 정리하면 된다는 표시깃발을 걸어둔다.
    • 나무를 새로 심을 때에 나무에 상처가 있는지 확인하고, 상처가 나 있다면 깃발을 걸어둔다.
    • 깃발을 다 걸고 난 후, 벌목 담당자에게 깃발이 달린 나무만 정리하라고 알려준다.
    • 벌목 담당자는 깃발이 걸린 나무만 정리한다.
      • 깃발만 따라가면 되므로 모든 위치를 찾아 갈 필요가 없음
notion image
 

3. 코드 작성

  • Dispatcher : Reflection, Annotation을 활용하여 메서드를 동적으로 연결
    • UserController의 메서드를 전부 읽어서 methods 배열에 저장
    • UserController에서 Annotation이 붙은 메서드만 체크
      • 경로까지 일치하면 메서드 invoke ( 서버 연결 )
    • 프로그램을 배포한 후의 유지/보수에서 자유롭다는 이점
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 : Annotation을 붙여서, 서버에 연결한 메서드들만 정의하면 자동(동적) 연결
    • 새로 메서드를 추가해도, Annotation을 부착하여 path와 동작만 정의하면 자동 연결 가능
    • 다른 클래스와 직접 연결하지 않아도 되는 간편함
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"); } }
 
  • App : 메인 서버 프로그램
    • 가동 시 클라이언트 측으로부터 요청을 받아 응답
    • 리플렉션을 적용해둔 메서드를 사용
package ex02; public class App { public static void main(String[] args) { Dispatcher ds = new Dispatcher(new UserController()); // path : Scanner로 받으면 됨 ds.routing("/userinfo"); } }
 
  • RequestMapping : 요청에 대한 응답을 태그(@)처럼 미리 지정
    • 작동 대상, 작동 시간 결정 가능 : Dynamic
package ex02; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 메서드 위에만 붙일 수 있음 : ElementType.METHOD // 클래스 위에 붙이고 싶으면 : ElementType.TYPE // 작동 시간 결정(Timing) (RUNTIME, SOURCE(COMPILE)) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequestMapping { String value(); }
 

결과

notion image
 
✏️

Reflection과 Annotation은 세트!

  • 단독으로만 사용하면 그 효과가 적음
    • Reflection 자체는 Full Scan이 필요하므로 리소스 사용량이 많아지면 속도 저하
      • 다량의 클래스 파일, 장문의 코드 등
    • Annotation은 태그(깃발)의 역할로, 탐색의 시간은 줄어드나 코드 간소화는 어려움
 
✏️

Reflection이 무조건 좋은 건가요?

  • Reflection에는 분석 시간이 소요
    • 코드 내에 해당 Annotation이 존재하는 지 확인 (Parsing & Mapping)
    • 클래스 개수가 많거나, 클래스 내의 코드가 길어지면 그만큼 파싱 시간 증가
  • 정말 필요한 기능에만 사용하는 것이 좋음
 

4. 활용

  • App
    • Component Scan 활용 : 패키지 내의 @Component가 달린 클래스를 Search
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); } } } }
  • DispatcherServlet
package ex04; import java.lang.reflect.Method; public class DispatcherServlet { UserController con; public DispatcherServlet(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
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"); } }
  • BoardController
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"); } }
  • Component
package ex04; 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 Component { }
 

결과

notion image
 
✏️

Component Scan

  • 클래스 파일들을 스캔하여 조건에 맞는 클래스만 찾아내는 기술
    • 여러 클래스를 동시에 호출하고 싶을 때 사용
    • Reflection과 Annotation을 이용하여 클래스 목록을 저장, 목록 크기만큼 invoke
    • 기존의 하나의 클래스만 new하여 사용해야 하는 단점 해소
    • 패키지를 분석해서 클래스를 찾아야 하므로 분석에 사용하는 시간 증가 (속도 감소)
 
notion image
 
 
  • App 수정
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()); } } }
 

결과

notion image
 
 

5. 코드 완성

  • App에서는 path를 받아 요청 및 응답만 수행
  • Component Scan과 routing은 DispatcherServlet에서 수행
 
  • App
package ex04; import java.util.Scanner; import java.util.Set; public class App { public static void main(String[] args) { // RequestMapping, Component, DispatcherServlet (돈 주고 삼 = SpringWeb) Scanner sc = new Scanner(System.in); String path = sc.nextLine(); DispatcherServlet ds = new DispatcherServlet(); Set<Object> instances = ds.componentScan("ex04"); ds.routing(instances, path); } }
  • DispatcherServlet
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 { public Set<Object> componentScan(String packageName) { // 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 = "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); } } } return instances; } public void routing(Set<Object> instances, String path) { // /login 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); } } } } } }
  • 나머지는 동일
 

결과

  • BoardController에 존재하는 write 메서드를 동적 호출 가능
notion image
Share article

sxias