일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 자바의 정석 기초편 ch5
- 스프링 트랜잭션
- 자바의 정석 기초편 ch9
- 자바로 계산기 만들기
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch11
- 데이터 접근 기술
- 자바 중급2편 - 컬렉션 프레임워크
- 스프링 mvc2 - 검증
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch4
- 자바 고급2편 - io
- 2024 정보처리기사 시나공 필기
- 자바로 키오스크 만들기
- 자바의 정석 기초편 ch1
- 자바 고급2편 - 네트워크 프로그램
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 타임리프
- 자바 중급1편 - 날짜와 시간
- 자바 기초
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch14
- 람다
- 자바의 정석 기초편 ch2
- 스프링 고급 - 스프링 aop
- 스프링 mvc2 - 로그인 처리
- @Aspect
- 스프링 입문(무료)
- Today
- Total
개발공부기록
리플렉션, 리플렉션이 필요한 이유, 클래스와 메타 데이터, 메서드 탐색과 동적 호출, 필드 탐색과 값 변경, 리플렉션 - 활용 예제, 생성자 탐색과 객체 생성, HTTP 서버 6 - 리플렉션 서블릿 본문
리플렉션, 리플렉션이 필요한 이유, 클래스와 메타 데이터, 메서드 탐색과 동적 호출, 필드 탐색과 값 변경, 리플렉션 - 활용 예제, 생성자 탐색과 객체 생성, HTTP 서버 6 - 리플렉션 서블릿
소소한나구리 2025. 3. 5. 14:37출처 : 인프런 - 김영한의 실전 자바 - 고급2편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
리플렉션이 필요한 이유
커맨드 패턴으로 만든 서블릿의 단점
앞에서 커맨드 패턴으로 만든 서블릿은 매우 유용하지만 몇 가지의 단점이 있음
- 하나의 클래스에 하나의 기능만 만들 수 있음
- 새로 만든 클래스를 URL 경로와 항상 매핑해야 함
문제1: 하나의 클래스에 하나의 기능만 만들 수 있음
package was.v5.servlet;
public class HomeServlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) {
// 기능 구현
}
}
public class SearchServlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) {
// 기능 구현
}
}
public class Site1Servlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) {
// 기능 구현
}
}
public class Site2Servlet implements HttpServlet {
@Override
public void service(HttpRequest request, HttpResponse response) {
// 기능 구현
}
}
기능 하나를 만들 때마다 각각 별도의 클래스를 만들고 구현해야 했음
이것은 복잡한 기능에서는 효과적이지만 간단한 기능을 만들 때는 클래스가 너무 많이 만들어지기 때문에 부담스러움
만약 하나의 클래스 안에서 다양한 기능을 처리하도록 하여 비슷한 기능끼리 모아두면 작은 기능 하나를 추가할 때마다 클래스를 계속 만들지 않아도 됨
- SiteController 클래스
- site1() 메서드
- site2() 메서드
- 필요시 site 관련 메서드들 추가...
- SearchController 클래스
- search() 메서드
- 필요시 search 관련 메서드들 추가..
문제 2: 새로 만든 클래스를 URL 경로와 항상 매핑해야 함
servletManager.add("/", new HomeServlet());
servletManager.add("/site1", new Site1Servlet());
servletManager.add("/site2", new Site2Servlet());
servletManager.add("/search", new SearchServlet());
servletManager.add("/favicon.ico", new DiscardServlet());
// 기능 추가시 서블릿 매니저에 URL을 매핑
새로운 기능을 하나 추가할 때마다 위와 같은 매핑 작업을 함께 추가해야 했음
위에서 예시를 든 SiteController 클래스의 메서드를 보면 URL의 경로와 메서드의 이름이 같은 것을 확인할 수 있는데 만약 URL 경로의 이름과 같은 이름의 메서드를 찾아서 호출할 수 있다면 번거로운 매핑 작업을 제거할 수 있을 것임
자바 프로그램 실행 중에 이름으로 메서드를 찾고 찾은 메서드를 호출하려면 자바의 리플렉션 기능을 먼저 알아야 함
클래스와 메타 데이터
리플렉션(Reflection)
클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능을 리플렉션이라 함
리플렉션을 통해 프로그램 실행 중에 클래스, 메서드, 필드 등에 대한 정보를 얻거나 새로운 객체를 생성하고 메서드를 호출하며 필드의 값을 읽고 쓸 수 있음
리플렉션을 통해 얻을 수 있는 정보들
- 클래스의 메타데이터: 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스 등
- 필드 정보: 필드의 이름, 타입, 접근 제어자를 확인하고 해당 필드의 값을 읽거나 수정할 수 있음
- 메서드 정보: 메서드 이름, 반환 타입, 매개변수 정보를 확인하고 실행 중에 동적으로 메서드를 호출할 수 있음
- 생성자 정보: 생성자의 매개변수 타입과 개수를 확인하고 동적으로 객체를 생성할 수 있음
** 참고 - 리플렉션 용어
- 리플렉션이라는 용어는 영어 단어 reflect에서 유래된 것으로 반사하다, 되돌아보다라는 의미를 가지고 있음
- 리플렉션은 프로그램이 실행 중에 자기 자신의 구조를 들여다보고 그 구조를 변경하거나 조작할 수 있는 기능을 의미하는데 쉽게 말해서 리플렉션을 통해 클래스, 메서드, 필드 등의 메타데이터를 런타임에 동적으로 조사하고 사용할 수 있다는 뜻임
- 마치 거울에 비친 자신을 보는 것과 같이 프로그램이 자기 자신의 내부를 반사(reflect)하여 들여다본다는 의미임
클래스 메타데이터 조회
BasicData
- 예제를 위한 기본 클래스
package reflection.data;
public class BasicData {
public String publicField;
private int privateField;
public BasicData() {
System.out.println("BasicData.BasicData");
}
private BasicData(String data) {
System.out.println("BasicData.BasicData: " + data);
}
public void call() {
System.out.println("BasicData.call");
}
public String hello(String str) {
System.out.println("BasicData.hello");
return str + " hello";
}
private void privateMethod() {
System.out.println("BasicData.privateMethod");
}
void defaultMethod() {
System.out.println("BasicData.defaultMethod");
}
protected void protectedMethod() {
System.out.println("BasicData.protectedMethod");
}
}
BasicV1
package reflection;
import reflection.data.BasicData;
public class BasicV1 {
public static void main(String[] args) throws ClassNotFoundException {
// 클래스 메타데이터(데이터에 정의되어있는 데이터) 조회 방법 3가지
// 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);
}
}
/* 실행 결과
basicDataClass1 = class reflection.data.BasicData
BasicData.BasicData
basicDataClass2 = class reflection.data.BasicData
basicDataClass3 = class reflection.data.BasicData
*/
클래스의 메타데이터는 Class라는 클래스로 표현되며 Class라는 클래스를 획득하는 3가지 방법이 있음
클래스에서 찾기
- 클래스 명에 .class를 사용하여 획득
Class<BasicData> basicDataClass1 = BasicData.class;
인스턴스에서 찾기
- 인스턴스에서 .getClass() 메서드를 호출하여 획득
- 반환 타입을 보면 실제 인스턴스가 BasicData 타입일 수도 있지만 그 자식 타입일 수도 있기 때문에 Class<? extends BasicData>로 표현됨
BasicData basicInstance = new BasicData();
Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
문자로 찾기
- 이 부분이 가장 흥미로운데 단순히 문자로 클래스의 메타데이터를 조회할 수 있음
- 예를 들면 콘솔에서 사용자 입력으로 원하는 클래스를 동적으로 찾을 수 있음
String className = "reflection.data.BasicData"; // 패키지명 주의
Class<?> basicDataClass3 = Class.forName(className);
기본 정보 탐색
BasicV2
package reflection;
public class BasicV2 {
public static void main(String[] args) {
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());
System.out.println("basicData.isEnum() = " + basicData.isEnum());
System.out.println("basicData.isAnnotation() = " + basicData.isAnnotation());
int modifiers = basicData.getModifiers();
System.out.println("basicData.getModifiers() = " + modifiers);
System.out.println("isPublic = " + Modifier.isPublic(modifiers));
System.out.println("Modifier.toString(modifiers) = " + Modifier.toString(modifiers));
}
}
/* 실행 결과
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
isPublic = true
Modifier.toString(modifiers) = public
*/
- 클래스 이름, 패키지, 부모 클래스, 구현한 인터페이스, 수정자 정보 등 다양한 정보를 획득할 수 있음
- 수정자(Modifier)는 접근 제어자와 비 접근 제어자(기타 수정자)로 나눌 수 있는데, getModifiers()를 통해 수정자가 조합된 숫자를 얻고 Modifer를 사용하여 실제 수정자 정보를 확인할 수 있음
- 접근 제어자: public, protected, default(package-private), private
- 비 접근 제어자: static, final, abstract, synchronized, volatile 등
메서드 탐색과 동적 호출
메서드 메타 데이터
MethodV1
package reflection;
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("method = " + method);
}
}
}
/* 실행 결과
====== methods() =====
method = public void reflection.data.BasicData.call()
method = public java.lang.String reflection.data.BasicData.hello(java.lang.String)
method = public boolean java.lang.Object.equals(java.lang.Object)
method = public java.lang.String java.lang.Object.toString()
method = public native int java.lang.Object.hashCode()
method = public final native java.lang.Class java.lang.Object.getClass()
method = public final native void java.lang.Object.notify()
method = public final native void java.lang.Object.notifyAll()
method = public final void java.lang.Object.wait(long) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait() throws java.lang.InterruptedException
====== declaredMethods() ======
method = public void reflection.data.BasicData.call()
method = private void reflection.data.BasicData.privateMethod()
method = void reflection.data.BasicData.defaultMethod()
method = public java.lang.String reflection.data.BasicData.hello(java.lang.String)
method = protected void reflection.data.BasicData.protectedMethod()
*/
- Class.getMethods() 또는 Class.getDeclaredMethods()를 호출하면 Method라는 메서드의 메타 데이터를 얻을 수 있으며 Method 클래스는 메서드의 모든 정보를 가지고 있음
getMethods() vs getDeclaredMethods()
- getMethods(): 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드를 반환
- getDeclaredMethods(): 해당 클래스에서 선언된 모든 메서드를 접근 제어자에 관계없이반환하며 상속된 메서드는 포함하지 않음
동적 메서드 호출
MethodV2
package reflection;
public class MethodV2 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 정적 메서드 호출 - 일반적인 메서드 호출
BasicData helloInstance = new BasicData();
helloInstance.call(); // 이 부분은 코드를 변경하지 않는 이상 정적임
// 동적 메서드 호출 - 리플렉션 사용
Class<? extends BasicData> helloClass = helloInstance.getClass();
String methodName = "hello";
// 메서드 이름을 변수로 변경
Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
Object returnValue = method1.invoke(helloInstance, "h1");
System.out.println("returnValue = " + returnValue);
}
}
/* 실행 결과
BasicData.BasicData
BasicData.call
BasicData.hello
returnValue = h1 hello
*/
리플렉션을 사용하면 매우 다양한 체크 예외가 발생함
일반적인 메서드 호출 - 정적
- 인스턴스의 참조를 통해 메서드를 호출하는 방식이 일반적인 메서드 호출 방식으로 코드를 변경하지 않는 이상 call() 대신 다른 메서드로 변경하는 것이 불가능함
- 호출하는 메서드가 이미 코드로 작성되어 있어서 정적으로 변경할 수 없는 상태임
동적 메서드 호출 - 리플렉션 사용
Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
Object returnValue = method1.invoke(helloInstance, "h1");
- 리플렉션을 사용하면 동적으로 메서드를 호출할 수 있음
- 클래스 메타데이터가 제공하는 getMethod()에 메서드 이름, 사용하는 매개변수의 타입을 전달하면 원하는 메서드를 찾을 수 있으며 예제에서는 hello라는 이름에 String 매개변수가 있는 메서드를 찾음
- method.invoke() 메서드에 실행할 인스턴스와 인자를 전달하면 해당 인스턴스에 있는 메서드를 실행할 수 있으며 예제에서는 helloInstance 인스턴스에 있는 String 매개변수가 있는 hello 메서드를 호출함
- 여기서 메서드를 찾을 때 methodName이 String 변수로 되어있는 것을 확인할 수 있는데 methodName은 변수이기 대문에 사용자 콘솔 입력을 통해서 얼마든지 호출할 methodName을 변경할 수 있음
- 즉 여기서 호출할 메서드 대상은 정적으로 코드에 정해진 것이 아니라 언제든지 동적으로 변경할 수 있기 때문에 동적 메서드 호출이라 함
동적 메서드 호출 예시
Calculator
- 덧셈, 뺄셈 기능만 있는 계산기 클래스
package reflection.data;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
Method3
- 리플렉션을 활용하여 calculator 인스턴스의 메서드를 콘솔 입력을 통해 동적으로 호출해서 사용하는 것을 확인할 수 있음
package reflection;
public class MethodV3 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Scanner scanner = new Scanner(System.in);
System.out.print("호출 메서드: ");
String methodName = scanner.nextLine();
System.out.print("숫자: ");
int num1 = scanner.nextInt();
System.out.print("숫자2: ");
int num2 = scanner.nextInt();
Calculator calculator = new Calculator();
// 호출할 메서드를 변수 이름으로 동적으로 선택
Class<Calculator> aClass = Calculator.class;
Method method = aClass.getMethod(methodName, int.class, int.class);
Object returnValue = method.invoke(calculator, num1, num2);
System.out.println("returnValue = " + returnValue);
}
}
/*
== 실행결과1 ==
호출 메서드: add
숫자: 1
숫자2: 2
returnValue = 3
== 실행결과2 ==
호출 메서드: sub
숫자: 4
숫자2: 2
returnValue = 2
*/
필드 탐색과 값 변경
필드 탐색
FieldV2
package reflection;
public class FieldV1 {
public static void main(String[] args) {
Class<BasicData> helloClass = BasicData.class;
System.out.println("===== fields() =====");
Field[] fields = helloClass.getFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
System.out.println("===== declaredFields() =====");
Field[] declaredFields = helloClass.getDeclaredFields();
for (Field field : declaredFields) {
System.out.println("field = " + field);
}
}
}
/* 실행 결과
===== fields() =====
field = public java.lang.String reflection.data.BasicData.publicField
===== declaredFields() =====
field = public java.lang.String reflection.data.BasicData.publicField
field = private int reflection.data.BasicData.privateField
*/
getFields() vs getDeclaredFields()
- 앞서 설명한 getMethods(), getDeclaredMethods()와의 차이와 동일함
- getFields(): 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환
- getDeclaredFields(): 해당 클래스에서 선언된 모든 필드를 접근 제어자에 관계없이반환하며 상속된 필드는 포함하지 않음
필드 값 변경
User
- 모든 필드가 private인 User클래스
package reflection.data;
public class User {
private String id;
private String name;
private Integer age;
// 기본생성자, 모든 필드를 초기화하는 생성자, 각 필드의 게터, 세터, toString() 오버라이드 생략
}
FieldV2
package reflection;
public class FieldV2 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
User user = new User("id2", "userA", 20);
System.out.println("기존 이름 = " + user.getName());
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name");
// private 필드에 접근 허용, private 메서드도 이와 같은 방식으로 호출 가능함
// nameField.set(user, "userB"); // 그냥 바꾸면 접근할 수 없어서 IllegalAccessException 발생
nameField.setAccessible(true); // 접근 가능하도록 변경
nameField.set(user, "userB"); // 변경 가능
System.out.println("변경된 이름 = " + user.getName());
}
}
/* 실행 결과
기존 이름 = userA
변경된 이름 = userB
*/
- 사용자의 이름이 userA인데 리플렉션을 사용하여 name 필드에 직접 접근한 다음 userB로 이름을 변경
- getDeclaredField("name")으로 name필드를 조회했지만 name 필드는 private 접근 제어자를 사용하기 때문에 직접 접근해서 값을 변경하는 것이 불가능함
- 리플렉션은 private 필드에 접근할 수 있는 특별한 기능인 setAccessible(true)를 제공하며, 이는 private method에도 동일하게 적용할 수 있음
- 위처럼 private 필드에 접근이 가능하도록 변경하고 namefield.set(user, "userB")를 통해 사용자의 이름을 변경하면 private 접근 제어자임에도 필드의 값을 변경할 수 있게 됨
** 리플렉션과 주의사항
리플렉션을 활용하면 private 접근 제어자에도 직접 접근해서 값을 변경할 수 있지만 이는 객체 지향 프로그래밍의 원칙을 위반하는 행위로 간주될 수 있음
private 접근 제어자는 클래스 내부에서만 데이터를 보호하고 외부에서의 직접적인 접근을 방지하기 위해 사용되는데 리플렉션을 통해 이러한 접근 제한을 무시하는 것은 캡슐화 및 유지보수성에 악영향을 미칠 수 있음
예를 들어 클래스의 내부 구조나 구현 세부 사항이 변경될 경우 리플렉션을 사용한 코드는 쉽게 깨질 수 있으며 이는 예상치 못한 버그를 초래할 수 있음
따라서 리플렉션을 사용할 때는 반드시 신중하게 접근해야 하며 일반적으로는 접근 메서드를 사용하는 것이 맞음
리플렉션은 주로 테스트나 라이브러리 개발 같은 특별한 상황에서 유용하게 사용되지만 일반적인 애플리케이션의 비즈니스 로직을 구성하는 코드에서는 권장되지 않음
이를 무분별하게 사용하면 코드의 가독성도 떨어지고 안정성이 매우 크게 저하되므로 사용해야 할 때와 사용하지 말아야 할 때를 명확하게 구분해서 사용해야 함
리플렉션 - 활용 예제
리플렉션 활용
상황 가정
프로젝트에서 데이터를 저장해야 하는데 저장할 때 반드시 null을 사용하면 안 된다고 가정하면 null 값을 다른 기본 값으로 모두 변경해야 함
- String이 null 이면 "" 빈문자로 변경
- Integer가 null 이면 0으로 변경
Team
package reflection.data;
public class Team {
private String id;
private String name;
// 기본생성자, 모든 필드를 초기화하는 생성자, 각 필드의 게터, 세터, toString() 오버라이드 생략
}
FieldV3 - 리플렉션 사용 전
package reflection;
public class FieldV3 {
public static void main(String[] args) {
User user = new User("id1", null, null);
Team team = new Team("team1", null);
System.out.println("==== before ====");
System.out.println("user = " + user);
System.out.println("team = " + team);
if (user.getId() == null) {
user.setId("");
}
if (user.getName() == null) {
user.setName("");
}
if (user.getAge() == null) {
user.setAge(0);
}
if (team.getId() == null) {
team.setId("");
}
if (team.getName() == null) {
team.setName("");
}
System.out.println("==== after ====");
System.out.println("user = " + user);
System.out.println("team = " + team);
}
}
/* 실행 결과
==== before ====
user = User{id='id1', name='null', age=null}
team = Team{id='team1', name='null'}
==== after ====
user = User{id='id1', name='', age=0}
team = Team{id='team1', name=''}
*/
- User, Team 객체에 입력된 정보 중에 null 데이터를 모두 기본 값으로 변경해야 한다고 가정했기 때문에 이를 해결하기 위해서는 각각의 객체에 들어있는 데이터를 직접 다 찾아서 값을 입력해야 함
- 여기에서는 if 문으로 모든 객체의 필드의 값이 null인지 체크하는 코드를 작성하였음
- 만약 User, Team 뿐만 아니라 수십, 수백 개의 객체에 해당 기능을 적용해야 한다면 매우 많은 번거로운 코드를 작성해야 하는데 리플렉션을 활용하면 이 문제를 깔끔하게 해결할 수 있음
FieldUtil - 리플렉션을 활용한 필드 기본 값 도입
package reflection;
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);
}
}
}
}
- 어떤 객체든 받아서 기본 값을 적용하는 유틸리티 클래스를 생성함
- 이 유틸리티는 리플렉션을 활용하여 필드의 값을 조사한 다음에 null이면 기본 값을 적용하며 private 접근제어자에도 적용할 수 있도록 setAccessible(true)를 활용하였음
- String이 null이면 빈문자열, Integer가 null이면 0으로 변경하게 됨
FieldV4
package reflection;
public class FieldV4 {
public static void main(String[] args) throws IllegalAccessException {
User user = new User("id1", null, null);
Team team = new Team("team1", null);
System.out.println("==== before ====");
System.out.println("user = " + user);
System.out.println("team = " + team);
FieldUtil.nullFieldToDefault(user);
FieldUtil.nullFieldToDefault(team);
System.out.println("==== after ====");
System.out.println("user = " + user);
System.out.println("team = " + team);
}
}
/* 실행 결과
==== before ====
user = User{id='id1', name='null', age=null}
team = Team{id='team1', name='null'}
==== after ====
user = User{id='id1', name='', age=0}
team = Team{id='team1', name=''}
*/
코드와 실행 결과를 보면 리플렉션을 사용한 덕분에 User, Team 뿐만 아니라 수많은 객체가 존재한다고 하여도 매우 편리하게 기본 값을 적용할 수 있게 되었음
이 처럼 리플렉션을 활용하면 기존 코드로 해결하기 어려운 공통 문제를 손쉽게 처리할 수도 있음
이런 식으로 전체적으로 적용되는 이런 유틸 등을 만들 때에는 리플렉션을 사용해 볼 수 있으나 일반적인 애플리케이션의 비즈니스 로직을 만드는 코드에서는 리플렉션을 사용하는 것은 부적합함
생성자 탐색과 객체 생성
생성자 탐색
ConstructV1
package reflection;
public class ConstructV1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("reflection.data.BasicData");
System.out.println("===== constructors() =====");
Constructor<?>[] constructors = aClass.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println("constructor = " + constructor);
}
System.out.println("===== declaredConstructors() =====");
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
for (Constructor<?> constructor : declaredConstructors) {
System.out.println("constructor = " + constructor);
}
}
}
/* 실행 결과
===== constructors() =====
constructor = public reflection.data.BasicData()
===== declaredConstructors() =====
constructor = public reflection.data.BasicData()
constructor = private reflection.data.BasicData(java.lang.String)
*/
- 필드, 메서드와 동일하게 생성자도 탐색할 수 있음
- 생성자는 상속이라는 개념이 없기 때문에 getConstructors()는 자신의 public 생성자가 출력되고 getDeclaredConstructors()는 접근제어자에 관계없이 모든 생성자가 출력됨
생성자 탐색
ConstructV2
package reflection;
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");
System.out.println("instance = " + instance);
Method method1 = aClass.getDeclaredMethod("call");
method1.invoke(instance);
}
}
/* 실행 결과
BasicData.BasicData: hello
instance = reflection.data.BasicData@6f496d9f
BasicData.call
*/
- Class<?> aClass = Class.forName("reflection.data.BasicData")을 사용해서 클래스 정보를 동적으로 조회한 후 getDeclaredConstructor(String.class)를 통해 String 매개변수가 있는 생성자를 조회함
- 조회된 생성자는 private 생성자이므로 setAccessible(true)를 사용해서 private 생성자를 접근 가능하게 만든 후 constructor.newInstance("hello")로 찾은 생성자를 사용하여 객체를 생성함
- 객체가 생성되었으므로 해당 인스턴스에 call이라는 이름의 메서드를 동적으로 찾아서 호출이 가능한 것을 확인할 수 있음
이번 예제를 잘 보면 클래스를 동적으로 찾아서 인스턴스를 생성하고 메서드도 동적으로 호출했음
Basicdata의 타입이나 call() 메서드를 직접 호출하는 부분을 직접 코딩하지 않았지만 클래스를 찾고 생성하고 생성한 클래스의 메서드를 호출하는 방법 모두 동적으로 처리하였음
** 참고
- 스프링 프레임워크나 다른 프레임워크 기술들을 사용해 보면 내가 만든 클래스를 프레임워크가 대신 생성해 줄 때가 있는데 이것이 가능했던 이유가 리플렉션을 활용하여 동적으로 객체를 생성했기 때문임
HTTP 서버 6 - 리플렉션 서블릿
리플렉션을 활용한 서블릿 만들기
처음에 설명했던 커맨드패턴으로 만들었던 서블릿의 단점을 보완하는 리플렉션을 활용한 서블릿 기능을 작성
개발자는 XxxController라는 클래스를 생성한 후 기능을 개발한 후 이 컨트롤러의 메서드를 리플렉션으로 읽고 호출하면 됨
** 참고 - 컨트롤러 용어
- 컨트롤러는 애플리케이션의 제어 흐름을 제어(control)함
- 요청을 받아 적절한 비즈니스 로직을 호출하고 그 결과를 뷰에 전달하는 등의 작업을 수행함
여기에서는 아래처럼 관련된 기능을 하나의 클래스로 모아서 클래스를 나눠서 개발을 진행
- SiteController
- site1() 메서드
- site2() 메서드
- SearchController
- search() 메서드
리플렉션을 처리하는 서블릿 구현
서블릿은 자바 진영에서 이미 표준으로 사용하는 기술이므로 서블릿은 그대로 사용하면서 새로운 기능 구현을 진행
(물론 우리가 만든 서블릿은 자바 표준이 아니지만 여기서는 자바 표준 서블릿을 사용한다고 가정)(
앞서 만든 HTTP 서버에서 was.httpserver 패키지는 다른 곳에서 제공하는 변경할 수 없는 라이브러리라고 가정하여 was.httpserver의 코드를 전혀 변경하지 않고 그대로 재사용하면서 기능을 추가
SiteControllerV6
package was.v6;
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>");
}
}
- /site1, /site2를 처리함
- site1(), site2()는 단순하고 서로 비슷한 기능을 제공한다고 가정
SearchControllerV6
package was.v6;
public class SearchControllerV6 {
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>");
}
}
- /search를 처리함
- search()는 복잡하고 단독 기능을 제공한다고 가정
참고로 Xxxcontroller에서 호출 대상이 되는 메서드는 반드시 HttpRequest, HttpResponse를 인자로 받아야 함
기존 코드에서 이런 컨트롤러들을 호출하도록 만들려면 서블릿에 이런 기능을 구현하면 됨
ReflectionServlet
package was.httpserver.servlet.reflection;
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();
// SiteController, SearchController
for (Object controller : controllers) {
Class<?> aClass = controller.getClass(); // SiteController
Method[] methods = aClass.getDeclaredMethods(); // site1, site2
for (Method method : methods) {
String methodName = method.getName();
if (path.equals("/" + methodName)) { // /site1.equals(/site1)
invoke(controller, method, request, response);
return;
}
}
}
throw new PageNotFoundException("request=" + path);
}
private void invoke(Object controller, Method method, HttpRequest request, HttpResponse response) {
try {
method.invoke(controller, request, response);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
- List<Object> controllers: 생성자를 통해 여러 컨트롤러들을 보관할 수 있음
- 이 서블릿은 요청이 오면 모든 컨트롤러를 순회하고 선언된 메서드 정보를 통해 URL의 요청 경로와 메서드이름이 맞는지 확인함
- 만약 메서드 이름이 맞다면 invoke()를 통해 해당 메서드를 동적으로 호출하며 이때 HttpRequest, HttpResponse 정보도 함께 넘겨주므로 대상 메서드는 반드시 HttpRequest, HttpResponse를 인자로 받아야 함
ServerMainV6
package was.v6;
public class ServerMainV6 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
List<Object> controllers = List.of(new SiteControllerV6(), new SearchControllerV6());
HttpServlet reflectionServlet = new ReflectionServlet(controllers);
ServletManager servletManager = new ServletManager();
servletManager.setDefaultServlet(reflectionServlet);
// 아래 두개의 경로는 메서드이름으로 만들수 없어서 v5에서 만들어둔 것을 사용
servletManager.add("/", new HomeServlet());
servletManager.add("/favicon.ico", new DiscardServlet());
HttpServer server = new HttpServer(PORT, servletManager);
server.start();
}
}
- 결과는 기존의 V5와 동일함
new ReflectionServlet(controllers)
- 리플렉션 서블릿을 생성하고 사용할 컨트롤러들을 인자로 전달함
servletManager.setDefaultServlet(reflectionServlet)
- 이 부분이 중요한데 리플렉션 서블릿을 기본 서블릿으로 등록하여 다른 서블릿에서 경로를 찾지 못할 때 직접 만든 ReflectionServlet이 항상 호출됨
- 다른 서블릿은 등록하지 않기 때문에 항상 리플렉션 서블릿이 호출됨
- 다만 /라는 이름과 favicon.ico라는 이름은 메서드 이름으로 매핑할 수 없기 때문에 HomeServlet과 DiscardServlet만 예외적으로 등록하였음
작동 순서
- 웹 브라우저가 /site1을 요청
- 서블릿 매니저는 등록된 서블릿 중에 /site1을 찾음
- 등록된 서블릿 중에 /site1을 찾을 수 없으므로 기본 서블릿(default Servlet)으로 등록한 ReflectionServlet을 호출함
- 등록된 서블릿은 /=HomeServlet, /favicon.ico=DiscardServlet임
- ReflectionServlet은 컨트롤러를 순회하면서 site1()이라는 이름의 메서드를 찾아서 호출함
- 등록한 SiteControllerV6, SearchControllerV6을 순회하며 이때 HttpRequest, HttpResponse도 함께 전달함
- site1() 메서드가 실행됨
정리
기존 HTTP 서버의 코드를 전혀 변경하지 않고 서블릿만 잘 구현해서 완전히 새로운 기능을 도입하였으며 덕분에 커맨드 패턴으로 만든 서블릿의 단점을 해결하였음
- 하나의 클래스에 하나의 기능만 만들 수 있음 -> 각 컨트롤러에 기능별로 모아서 관리할 수 있음
- 새로 만든 클래스를 URL 경로와 항상 매핑해야 함 -> 기본 서블릿으로 리플렉션을 활용한 서블릿을 등록하여 등록된 모든 컨트롤러를 순회해서 사용할 수 있으므로 기능이 추가돼도 매핑하지 않아도 됨
남은 문제점
- 리플렉션 서블릿은 요청 URL과 메서드 이름이 같다면 해당 메서드를 동적으로 호출할 수 있는데 요청 URL과 메서드 이름을 다르게 하고 싶다면 지금 만든 기능으로는 구현이 불가능함
- 그리고 앞서 /, /favicon.ico와 같이 자바 메서드 이름으로 처리하기 어려운 URL도 해결이 불가능함
- 자바는 카멜케이스로 메서드이름을 사용하지만 URL은 주로 -(dash)를 구분자로 사용하기 때문에 /add-member와 같은 URL을 매핑할 수 없음
'자바 로드맵 강의 > 고급 2 - 입출력, 네트워크, 리플렉션' 카테고리의 다른 글
HTTP 서버 활용, HTTP 서버 - 애노테이션 서블릿(시작, 동적 바인딩, 성능 최적화), HTTP 서버 활용 - 회원 관리 서비스 (0) | 2025.03.07 |
---|---|
애노테이션, 애노테이션이 필요한 이유, 애노테이션 정의, 메타 애노테이션, 애노테이션과 상속, 애노테이션 활용 - 검증기, 자바 기본 애노테이션 (0) | 2025.03.06 |
HTTP 서버 만들기, HTTP 서버(시작, 동시 요청, 기능 추가, 요청, 응답, 커맨드 패턴), URL 인코딩, 웹 애플리케이션 서버의 역사 (0) | 2025.03.04 |
네트워크 - 채팅 프로그램, 채팅 프로그램(설계, 클라이언트, 서버) (0) | 2025.03.01 |
네트워크 - 프로그램, 문제 - 직접 채팅 프로그램 만들기1 (0) | 2025.02.28 |