이번 강의에서는 간단한 클라이언트-서버 네트워크 프로그램을 만들어보자.
⬛ 리플렉션 개념
🟢 리플렉션
Java 프로그램이 실행 중에 자기 자신을 들여다보고, 구조를 분석하거나 조작할 수 있는 기능을 의미한다.
이를 통해 클래스, 메서드, 필드 등의 메타데이터를 동적으로 탐색하거나 실행할 수 있다.
🟢 리플렉션 제공 기능
- 클래스 정보 조회:
- 클래스 이름, 부모 클래스, 구현된 인터페이스, 패키지 정보를 얻을 수 있음.
- 메서드, 필드, 생성자 등의 정보를 조회할 수 있음.
- 메서드 호출:
- 특정 메서드의 이름을 통해 런타임에 메서드를 동적으로 실행할 수 있음.
- 필드 값 변경:
- 객체의 필드 값을 직접 변경하거나 읽을 수 있음.
- 객체 생성:
- 특정 클래스의 생성자를 이용해 객체를 동적으로 생성할 수 있음.
⬛ 클래스 메타데이터 조회
public class BasicV1 {
public static void main(String[] args) throws ClassNotFoundException {
// 1. 클래스에서 찾기
Class<BasicData> basicDataClass1 = BasicData.class;
System.out.println("basicDataClass1 = " + basicDataClass1);
// 2. 인스턴스에서 찾기
BasicData basicInstance = new BasicData();
Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
System.out.println("basicDataClass2 = " + basicDataClass2);
// 3. 문자로 찾기
String className = "reflection.data.BasicData"; // 패키지명 포함
Class<?> basicDataClass3 = Class.forName(className);
System.out.println("basicDataClass3 = " + basicDataClass3);
}
}
⬛ 클래스 기본 정보 탐색
public class BasicV2 {
public static void main(String[] args) throws ClassNotFoundException {
// 클래스 정보를 가져옵니다.
Class<BasicData> basicData = BasicData.class;
// 클래스 이름 출력
System.out.println("basicData.getName() = " + basicData.getName());
// 간단한 클래스 이름 출력 (패키지 제외)
System.out.println("basicData.getSimpleName() = " + basicData.getSimpleName());
// 클래스가 속한 패키지 정보 출력
System.out.println("basicData.getPackage() = " + basicData.getPackage());
// 부모 클래스 정보 출력
System.out.println("basicData.getSuperclass() = " + basicData.getSuperclass());
// 클래스가 구현한 인터페이스 목록 출력
System.out.println("basicData.getInterfaces() = " + Arrays.toString(basicData.getInterfaces()));
// 이 클래스가 인터페이스인지 여부 출력
System.out.println("basicData.isInterface() = " + basicData.isInterface());
// 이 클래스가 열거형(enum)인지 여부 출력
System.out.println("basicData.isEnum() = " + basicData.isEnum());
// 이 클래스가 애노테이션인지 여부 출력
System.out.println("basicData.isAnnotation() = " + basicData.isAnnotation());
// 클래스의 접근 제어자 및 기타 수정자 정보를 숫자로 반환 (1은 public 접근 제어자)
int modifiers = basicData.getModifiers();
System.out.println("basicData.getModifiers() = " + modifiers);
// 클래스가 public 인지 확인
System.out.println("isPublic = " + Modifier.isPublic(modifiers));
// 수정자를 문자열로 변환하여 출력
System.out.println("Modifier.toString() = " + Modifier.toString(modifiers));
}
}
⬛ 메서드 탐색과 동적 호출
🔷 메서드 탐색
public class MethodV1 {
public static void main(String[] args) {
Class<BasicData> helloClass = BasicData.class;
System.out.println("====== methods() =====");
Method[] methods = helloClass.getMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
System.out.println("====== declaredMethods() =====");
Method[] declaredMethods = helloClass.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println("declaredMethod = " + method);
}
}
}
- getMethods():
- public 메서드만 반환하며, 상속된 메서드도 포함.
- getDeclaredMethods():
- 클래스에서 선언된 모든 메서드를 반환하며, 접근 제어자에 관계없이 포함.
🔷 동적 메서드 호출
public class MethodV2 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
BasicData helloInstance = new BasicData();
Class<? extends BasicData> helloClass = helloInstance.getClass();
String methodName = "hello";
Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
Object returnValue = method1.invoke(helloInstance, "hi");
System.out.println("returnValue = " + returnValue);
}
}
getDeclaredMethod() 로 특정 메서드를 찾고, invoke()로 실행 가능.
- 예: "hello"라는 메서드를 실행하고 hi라는 인자를 전달.
⬛ 필드 탐색과 값 변경
public class FieldV2 {
public static void main(String[] args) throws Exception {
User user = new User("id1", "userA", 20);
System.out.println("기존 이름 = " + user.getName());
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name");
// private 필드 접근 허용
nameField.setAccessible(true);
nameField.set(user, "userB");
System.out.println("변경된 이름 = " + user.getName());
}
}
- 필드 탐색: getDeclaredField("name")으로 특정 필드를 찾는다.
- 값 변경: setAccessible(true)로 private 필드에 접근 후 값을 변경한다.
⬛ 리플렉션 활용 예제
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) {
if (field.getType() == String.class) {
field.set(target, "");
} else if (field.getType() == Integer.class) {
field.set(target, 0);
}
}
}
}
}
기능: 객체의 필드 값을 탐색하고, null인 경우 기본값으로 초기화합니다.
- String → ""
- Integer → 0
⬛ 생성자 탐색과 객체 생성
public class ConstructV2 {
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("reflection.data.BasicData");
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance("hello");
Method method1 = aClass.getDeclaredMethod("call");
method1.invoke(instance);
}
}
- 생성자 탐색: getDeclaredConstructor()로 특정 생성자를 찾는다.
- 객체 생성: newInstance()로 해당 생성자를 사용해 객체를 만든다.
⬛ 기존 코드 개선 - 맵핑의 번거로움
문제점: 기존 방식에서는 URL 요청 경로마다 특정 서블릿 클래스를 매핑해야 했다.
servletManager.add("/site1", new Site1Servlet());
servletManager.add("/site2", new Site2Servlet());
servletManager.add("/search", new SearchServlet());
해결:
1. 리플렉션으로 동적 메서드 호출
URL 경로와 메서드 이름이 동일하다면, 리플렉션을 통해 해당 메서드를 호출한다. 필요시 컨트롤러도 하는일별로 나눈다.
- URL /site1 → 메서드 site1() 호출.
- URL /search → 메서드 search() 호출.
public class ReflectController {
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 void search(HttpRequest request, HttpResponse response) {
String query = request.getParameter("q");
response.writeBody("<h1>Search</h1>");
response.writeBody("<ul>");
response.writeBody("<li>query: " + query + "</li>");
response.writeBody("</ul>");
}
}
2. 리플렉션 서블릿 구현
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(); // URL 요청 경로
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);
}
}
}
요청 URL 경로 (path)와 메서드 이름 (method.getName())을 비교하여 일치하는 메서드를 찾는다.
찾은 메서드를 invoke()를 통해 동적으로 호출한다.
해결
- URL과 메서드 이름이 자동으로 매핑되므로, 추가적인 설정 없이도 새로운 메서드만 작성하면 된다.
- 예: 새로운 URL /newFeature가 추가되면, ReflectController 클래스에 newFeature() 메서드를 작성하면 동작한다.
⬛ 기존 코드 개선 - 클래스당 하나의 기능
문제점: 기존 방식에서는 URL 요청 경로마다 특정 서블릿 클래스를 매핑해야 했다.
public class Site1Servlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
response.writeBody("<h1>site1</h1>");
}
}
public class Site2Servlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
response.writeBody("<h1>site2</h1>");
}
}
해결: 컨트롤러 통합
public class ReflectController {
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 void search(HttpRequest request, HttpResponse response) {
String query = request.getParameter("q");
response.writeBody("<h1>Search</h1>");
response.writeBody("<ul>");
response.writeBody("<li>query: " + query + "</li>");
response.writeBody("</ul>");
}
}
⬛ 정리
맵핑 문제
- URL 경로와 메서드 이름을 동적으로 연결하여, 매번 매핑 작업을 할 필요가 없다.
- 리플렉션을 사용해 URL 요청에 맞는 메서드를 런타임에 찾아 실행한다.
한 클래스당 한 기능 문제
- 하나의 클래스에 여러 관련 기능을 포함할 수 있도록 설계한다.
- 필요에 따라 기능을 논리적으로 분리된 컨트롤러로 나누되, 같은 유형의 기능은 한 클래스로 관리하여 코드 중복을 줄이고 유지보수를 간편하게 한다.
서블릿매니저 역할
- URL 요청이 들어오면, 매니저는 URL을 확인한다.
- ReflectController에서 URL과 이름이 같은 메서드를 찾아 실행한다.
- 서블릿 매니저가 URL과 메서드를 자동으로 매핑하기 때문에, 매핑 관리의 번거로움을 줄인다.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 2편 섹션15 HTTP 서버 활용 (0) | 2024.12.02 |
---|---|
[Java] 김영한의 실전 자바 - 고급 2편 섹션14 애노테이션 (4) | 2024.11.28 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션12 HTTP 서버 만들기, 서블릿 (3) | 2024.11.21 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션6 네트워크,자원 정리 (0) | 2024.11.04 |
[Java] 김영한의 실전 자바 - 고급 2편 섹션5 네트워크 기본이론 / HTTP (2) | 2024.10.31 |