[Java] 54. 리플렉션(Reflection)

2025. 7. 1. 17:02·Java/Reflection, Annotation

 

리플렉션

#Java/adv2


/리플렉션이 필요한 이유

  • 기존 WAS의 문제점
    • 기존 커맨드 패턴 하나의 클래스에 하나의 기능
    • 새로 커맨드 클래스를 만들때마다 URL 경로와 클래스를 매핑해야 함.
  • 해결 방법: 하나의 클래스 안에서 다양한 기능을 처리하는 것.
    • 메서드와 경로를 어떻게 매핑할까? 리플렉션을 이용해 URL 경로와 같은 이름의 메서드를 호출하자.

/클래스와 메타데이터

  • 리플렉션(Reflection): 클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능
    • "반사하다" 또는 "되돌아보다”. 프로그램이 자기 자신의 내부를 “반사하여” 들여다본다.
    • 프로그램이 실행 중에 자기 자신의 구조를 들여다보고, 그 구조를 변경하거나 조작할 수 있는 기능
    • 프로그램 실행 중에 클래스, 메서드, 필드 등에 대한 정보를 얻거나, 새로운 객체를 생성하고 메서드를 호출하며, 필드의 값을 읽고 쓸 수 있다.
  • 리플렉션을 통해 얻을 수 있는 정보는 다음과 같다.
    • 클래스의 메타데이터: 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스 등.
    • 필드 정보: 필드의 이름, 타입, 접근 제어자를 확인하고, 해당 필드의 값을 읽거나 수정할 수 있다.
    • 메서드 정보: 메서드 이름, 반환 타입, 매개변수 정보를 확인하고, 실행 중에 동적으로 메서드를 호출할 수 있다.
    • 생성자 정보: 생성자의 매개변수 타입과 개수를 확인하고, 동적으로 객체를 생성할 수 있다.
  • /클래스 메타데이터 조회: 클래스의 메타데이터를 조회할 수 있는 방법
    1. 클래스에서 찾기: Class<BasicData> basicDataClass1 = BasicData.class;
    2. 인스턴스에서 찾기: Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
    3. 문자로 찾기: Class<?> basicDataClass3 = Class.forName(reflection.data.BasicData);
  • /기본 정보 탐색: 클래스의 메타데이터에서 얻을 수 있는 정보
    • basicData.getName() = reflection.data.BasicData
    • basicData.getSimpleName() = BasicData
    • basicData.getPackage() = package reflection.data
    • basicData.getSuperclass() = class java.lang.Object
    • basicData.getInterfaces() = []
    • basicData.isInterface() = false
    • basicData.isEnum() = false
    • basicData.isAnnotation() = false
    • basicData.getModifiers() = 1: 수정자가 조합된 숫자
    • Modifier 유틸리티: 실제 수정자 정보 확인
      • Modifier.isPublic(modifiers) = true:
      • Modifier.toString() = public

/메서드 탐색과 동적 호출: 클래스 메타데이터로부터 메서드 정보를 확인할 수 있다.

  • /메서드 메타데이터
    • Class<BasicData> helloClass = BasicData.class;
    • Method[] methods = helloClass.getMethods(); // public 메서드만 찾아줌
      • getMethods(): 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드를 반환
    • Method[] declaredMethods = helloClass.getDeclaredMethods(); // 내가 선언한 모든 메서드(접근 제어자 무관)
      • getDeclaredMethods(): 해당 클래스에서 선언된 모든 메서드를 반환하며, 접근 제어자에 관계없이 반환. 상속된 메서드는 포함하지 않음
    • method 객체를 출력해보면 다음과 같이 출력된다.
      • declaredMethod = public void reflection.data.BasicData.call()
  • /동적 메서드 호출: Method 객체를 사용해서 메서드를 직접 호출할 수도 있다.
    • 정적 메서드 호출
      • helloInstance.call(); // 이 부분은 코드를 변경하지 않는 이상 정적이다.
    • 동적 메서드 호출 - 리플렉션 사용
      • Class<? extends BasicData> helloClass = helloInstance.getClass();
      • Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
        • 클래스 메타데이터가 제공하는 getMethod() 에 메서드 이름, 사용하는 매개변수의 타입을 전달하면 원하는 메서드를 찾을 수 있다.
      • Object returnValue = method1.invoke(helloInstance, "hi");
        • 변수를 통해 메서드 정보를 가져온 다음, 메서드에 실행할 인스턴스와 인자를 전달하면, 해당 인스턴스에 있는 메서드를 실행할 수 있다.
  • /동적 메서드 호출 - 예시
    • 사용자가 숫자 두 개와 “add”를 입력하면 calculator.add()을 호출하고, 숫자 두 개와 “sub”를 입력하면 calculator.sub()를 호출한다.

/필드 탐색과 값 변경: 리플렉션을 활용해서 필드를 탐색하고, 필드의 값을 변경할 수 있다.

  • /필드 탐색
    • Class<BasicData> helloClass = BasicData.class;
    • Field[] fields = helloClass.getFields();
      • fields(): 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환
    • Field[] declaredFields = helloClass.getDeclaredFields();
      • declaredFields(): 해당 클래스에서 선언된 모든 필드를 반환하며, 접근 제어자에 관계없이 반환. 상속된 필드는 포함하지 않음
  • /필드 값 변경
    • Class<? extends User> aClass = user.getClass();
    • Field nameField = aClass.getDeclaredField("name");
      • name은 private 필드이다.
    • nameField.setAccessible(true);
      • private 필드에 접근 허용.
      • 참고로 setAccessible(true) 기능은 Method에도 제공한다.
    • nameField.set(user, "userB");
      • field.set()에 객체와 변경하고자 하는 값을 전달하여 필드의 값을 수정한다.
  • 리플렉션 주의사항
    • private에 직접 접근하는 것은 객체지향 원칙에 위배되는 행위로 간주될 수 있다.
    • 리플렉션을 통해 private 접근을 남발하면 캡슐화, 유지보수성 등에 악영향을 주게 된다.
    • 예상치 못한 버그를 초래할 수도 있다. 리플렉션 기능에 대해서는 컴파일러가 오류를 잘 잡지 못하는 편이다.
    • 리플렉션은 주로 테스트나 라이브러리 개발 같은 특별한 상황에서 유용하게 사용되지만, 일반적인 애플리케이션 코드에서는 권장되지 않는다

/리플렉션 - 활용 예제

  • 데이터를 저장해야 하는데, 저장할 때는 반드시 null을 사용하면 안 된다고 가정해보자.
    • 수많은 객체의 필드를 일일히 if문으로 null인지 체크하기에는 너무 번거롭다.
  • /리플렉션을 활용한 필드 기본 값 도입
    public class FieldUtil {
        public static void nullFieldToDefault(Object target) throws IllegalAccessException {
            Class<?> aClass = target.getClass();
            Field[] declaredFields = aClass.getDeclaredFields();
    
            for (Field field : declaredFields) {
                field.setAccessible(true);
                if (field.get(target) != null) {
                    continue;
                }
                if (field.getType() == String.class) {
                    field.set(target, "");
                } else if (field.getType() == Integer.class) {
                    field.set(target, 0);
                }
            }
        }
    }
    
    • 리플렉션을 활용한 유틸리티 기능을 만들고 이 메서드를 호출하면, 객체에 존재하는 모든 필드를 다 탐색하여 값을 바꿔준다.
      • FieldUtil.nullFieldToDefault(user);
      • FieldUtil.nullFieldToDefault(team);
    • 이처럼 리플렉션을 활용하면 기존 코드로 해결하기 어려운 공통 문제를 손쉽게 처리할 수도 있다.
    • 비즈니스 로직을 작성할 때 리플렉션은 권장되지 않지만, 공통으로 적용할 수 있는 유틸리티나, 프레임워크 및 라이브러리를 개발할 때는 리플렉션을 효과적으로 사용할 수 있다.

/생성자 탐색과 객체 생성: 필드, 메서드와 마찬가지로 리플렉션을 통해 생성자를 탐색하고, 생성자를 통해 객체를 생성할 수 있다.

  • /생성자 탐색
    • Class<?> aClass = Class.forName("reflection.data.BasicData");
    • Constructor<?>[] constructors = aClass.getConstructors();
      • aclass.getXXXs() 뭔지 알겠죠?
    • Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
      • aclass.getDeclaredXXXs() 뭔지 알겠죠?
  • /생성자 활용: 객체 생성
    • Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
      • 여기서는 매개변수로 String을 사용하는 생성자를 조회한다.
    • constructor.setAccessible(true); // private 생성자도 접근 가능하게 만든다.
    • Object reflectionInstance = constructor.newInstance("hello");
      • 찾은 생성자를 사용해서 객체를 생성한다. 여기서는 "hello"라는 인자를 넘겨준다.
      • 생성한 인스턴스를 이용하여 동적으로 메서드 호출도 가능하다.
      • Method method1 = aClass.getDeclaredMethod("call");
      • method1.invoke(reflectionInstance);
  • 스프링을 공부하다보면 내가 작성한 클래스를 프레임워크가 대신 만들어주기도 하는데, 이러한 리플렉션 기능이 사용된 것이다.

/HTTP 서버6 - 리플렉션 서블릿: 커맨드 패턴을 적용했던 WAS를 리플렉션을 통해 개선해보자.

  • 예를 들어 /site1이 입력되면 site1()이라는 메서드를 이름으로 찾아서 호출하도록 해보자.
  • 관련된 기능은 하나의 클래스로 모아보자.
public class SiteControllerV6 {
	public void site1(HttpRequest request, HttpResponse response) {
		response.writeBody("<h1>site1</h1>");
	}
	public void site2(HttpRequest request, HttpResponse response) {
		response.writeBody("<h1>site2</h1>");
	}
}
  • /리플렉션 서블릿
public class ReflectionServlet implements HttpServlet {
	private final List<Object> controllers;
	public ReflectionServlet(List<Object> controllers) {
		this.controllers = controllers;
	}
	@Override
	public void service(HttpRequest request, HttpResponse response) throws
IOException {
		String path = request.getPath();
		for (Object controller : controllers) {
			Class<?> aClass = controller.getClass();
			Method[] methods = aClass.getDeclaredMethods();
			for (Method method : methods) {
				String methodName = method.getName();
				if (path.equals("/" + methodName)) {
					invoke(controller, method, request, response);
					return;
				}
			}
		}
		throw new PageNotFoundException("request=" + path);
	}

	private static void invoke(Object controller, Method method, HttpRequest
request, HttpResponse response) {
		try {
			method.invoke(controller, request, response);
		} catch (InvocationTargetException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}	
	}
}
  • List<Object> controllers : 생성자를 통해 여러 컨트롤러들을 보관할 수 있다.
  • 기존에는 경로와 매핑된 커맨드 객체를 찾아서 호출했지만, 이제는 ReflectServlet이라는 디폴트 서블릿이 대부분 요청을 받는다.
  • 이 서블릿은 요청이 오면 모든 컨트롤러를 순회한다.
  • 경로와 일치하는 메서드를 리플렉션을 통해 찾고, 해당 메서드를 호출해준다.
public class ServerMainV6 {
	private static final int PORT = 12345;
	public static void main(String[] args) throws IOException {
		List<Object> controllers = List.of(new SiteControllerV6(), newSearchControllerV6());

		HttpServlet reflectionServlet = new ReflectionServlet(controllers);
		ServletManager servletManager = new ServletManager();
		servletManager.setDefaultServlet(reflectionServlet);
		servletManager.add("/", new HomeServlet());
		servletManager.add("/favicon.ico", new DiscardServlet());

		HttpServer server = new HttpServer(PORT, servletManager);
		server.start();
	}
}
  • servletManager.setDefaultServlet(reflectionServlet)
    • 이 부분이 중요하다. 리플랙션 서블릿을 기본 서블릿으로 등록하는 것이다. 이렇게 되면 다른 서블릿에서 경로를 찾지 못할 때 우리가 만든 리플렉션 서블릿이 항상 호출된다!
    • 그리고 다른 서블릿은 등록하지 않는다. 따라서 항상 리플렉션 서블릿이 호출된다.
    • 그런데 아쉽게도 HomeServlet 은 등록해야 한다. 왜냐하면 / 라는 이름은 메서드 이름으로 매핑할 수 없기 때문이다.
      • HomeServlet 은 여기서 크게 중요한 부분은 아니므로 was.v5.servlet 에 있는 클래스를 import 해서 사용하자.
    • /favicon.ico도 마찬가지로 메서드 이름으로 매핑할 수 없다. 왜냐하면 favicon.ico 라는 이름으로 메서드를 만들 수 없기 때문이다.
  • 커맨드 패턴의 단점을 리플렉션을 통해 해결한 부분
    • 하나의 클래스에 하나의 기능만 만들 수 있다.
    • 새로 만든 클래스를 URL 경로와 항상 매핑해야 한다.
  • 남은 문제점
    • 요청 이름과 메서드 이름을 다르게 하고 싶다면?
    • / , /favicon.ico 와 같이 자바 메서드 이름으로 처리하기 어려운 URL은 어떻게 처리할까?

Ref) 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런

'Java > Reflection, Annotation' 카테고리의 다른 글

[Java] 56. HTTP 서버에 Reflection & Annotation 활용  (0) 2025.07.01
[Java] 55. 애노테이션  (0) 2025.07.01
'Java/Reflection, Annotation' 카테고리의 다른 글
  • [Java] 56. HTTP 서버에 Reflection & Annotation 활용
  • [Java] 55. 애노테이션
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (456)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (129)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (5)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 54. 리플렉션(Reflection)
상단으로

티스토리툴바