리플렉션
#Java/adv2
- 기존 WAS의 문제점
- 기존 커맨드 패턴 하나의 클래스에 하나의 기능
- 새로 커맨드 클래스를 만들때마다 URL 경로와 클래스를 매핑해야 함.
- 해결 방법: 하나의 클래스 안에서 다양한 기능을 처리하는 것.
- 메서드와 경로를 어떻게 매핑할까? 리플렉션을 이용해 URL 경로와 같은 이름의 메서드를 호출하자.
- 리플렉션(Reflection): 클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능
- "반사하다" 또는 "되돌아보다”. 프로그램이 자기 자신의 내부를 “반사하여” 들여다본다.
- 프로그램이 실행 중에 자기 자신의 구조를 들여다보고, 그 구조를 변경하거나 조작할 수 있는 기능
- 프로그램 실행 중에 클래스, 메서드, 필드 등에 대한 정보를 얻거나, 새로운 객체를 생성하고 메서드를 호출하며, 필드의 값을 읽고 쓸 수 있다.
- 리플렉션을 통해 얻을 수 있는 정보는 다음과 같다.
- 클래스의 메타데이터: 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스 등.
- 필드 정보: 필드의 이름, 타입, 접근 제어자를 확인하고, 해당 필드의 값을 읽거나 수정할 수 있다.
- 메서드 정보: 메서드 이름, 반환 타입, 매개변수 정보를 확인하고, 실행 중에 동적으로 메서드를 호출할 수 있다.
- 생성자 정보: 생성자의 매개변수 타입과 개수를 확인하고, 동적으로 객체를 생성할 수 있다.
- /클래스 메타데이터 조회: 클래스의 메타데이터를 조회할 수 있는 방법
- 클래스에서 찾기:
Class<BasicData> basicDataClass1 = BasicData.class;
- 인스턴스에서 찾기:
Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
- 문자로 찾기:
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()
를 호출한다.
- 사용자가 숫자 두 개와 “add”를 입력하면
/필드 탐색과 값 변경: 리플렉션을 활용해서 필드를 탐색하고, 필드의 값을 변경할 수 있다.
- /필드 탐색
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
인지 체크하기에는 너무 번거롭다.
- 수많은 객체의 필드를 일일히 if문으로
- /리플렉션을 활용한 필드 기본 값 도입
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은 어떻게 처리할까?
'Java > Reflection, Annotation' 카테고리의 다른 글
[Java] 56. HTTP 서버에 Reflection & Annotation 활용 (0) | 2025.07.01 |
---|---|
[Java] 55. 애노테이션 (0) | 2025.07.01 |