-
Notifications
You must be signed in to change notification settings - Fork 0
고동훤, 김현종 리플렉션
간단한 리플렉션의 각종 메서드를 테스트 해볼 수 있는 요구사항입니다. 리플렉션을 사용해 클래스의 메타데이터로부터 메서드 정보를 불러올 수 있으며, getType()이나 getDeclaredType()등의 차이에 대해 학습하였습니다.
public class Junit3Runner {
@Test
public void runner() throws Exception {
Class<Junit3Test> clazz = Junit3Test.class;
Method[] methods = clazz.getMethods();
for (Method method : methods) {
String name = method.getName();
if (name.startsWith("test") && method.getParameterCount() == 0) {
method.invoke(clazz.getDeclaredConstructor().newInstance());
}
}
}
}
public class Junit3Test {
public void test1() {
System.out.println("Running Test1");
}
public void test2() {
System.out.println("Running Test2");
}
public void test3(String test) {
System.out.println("Running Test2");
}
public void three() {
System.out.println("Running Test3");
}
}
test로 시작하는 이름의 메서드를 찾아서 새로운 인스턴스에서 실행시켰습니다. 동적 바인딩으로 인한 exception 발생을 방지하기 위해 method를 invoke할 때 파라미터를 체크합니다. clazz.newInstance()는 deprecated됐기 때문에 clazz.getDeclaredConstructor().newInstance()를 사용하였습니다.
public class Junit4Runner {
@Test
public void run() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyTest.class) && method.getParameterCount() == 0) {
method.invoke(clazz.getDeclaredConstructor().newInstance());
}
}
}
}
public class Junit4Test {
@MyTest
public void one() throws Exception {
System.out.println("Running Test1");
}
@MyTest
public void two() throws Exception {
System.out.println("Running Test2");
}
public void testThree() throws Exception {
System.out.println("Running Test3");
}
}
어노테이션 정보 또한 불러올 수 있습니다. @MyTest이 붙어있는 메서드를 불러와서 실행하였습니다.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ElapsedTime {
}
public class AnnotationRunner {
private static final Logger logger = LoggerFactory.getLogger(AnnotationRunner.class);
@Test
public void run() throws Exception {
Class<AnnotationTest> clazz = AnnotationTest.class;
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(ElapsedTime.class) && method.getParameterCount() == 0) {
long startTime = System.currentTimeMillis();
method.invoke(clazz.getDeclaredConstructor().newInstance());
long endTIme = System.currentTimeMillis();
logger.info("Elapsed time: {} ms", endTIme - startTime);
}
}
}
}
public class AnnotationTest {
@ElapsedTime
public void timeTest1() throws InterruptedException {
Thread.sleep(1000);
System.out.println("박재성입니다");
}
@ElapsedTime
public void timeTest2() throws InterruptedException {
Thread.sleep(1000);
System.out.println("호눅스입니다");
}
public void timeTest3() throws InterruptedException {
Thread.sleep(1000);
System.out.println("17세죠");
}
}
어노테이션이 붙은 메서드를 대상으로 기능을 추가할 수 있습니다.
public class ReflectionTest {
private static final Logger logger = LoggerFactory.getLogger(ReflectionTest.class);
@Test
public void constructor() throws Exception {
Class<Question> clazz = Question.class;
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] parameterTypes = constructor.getParameterTypes();
logger.debug("paramer length : {}", parameterTypes.length);
for (Class paramType : parameterTypes) {
logger.debug("param type : {}", paramType);
}
}
}
@Test
public void privateFieldAccess() throws NoSuchFieldException, IllegalAccessException {
Class<Student> clazz = Student.class;
logger.debug(clazz.getName());
Student student = new Student();
setField(clazz, "name", "재성", student);
setField(clazz, "age", 17, student);
Assertions.assertThat(student.getName()).isEqualTo("재성");
Assertions.assertThat(student.getAge()).isEqualTo(17);
}
private <T> void setField(Class<T> clazz, String fieldName, Object value, T instance) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(instance, value);
}
@Test
public void argsConstructor() throws Exception {
Class<User> clazz = User.class;
Constructor<User> constructor = clazz.getDeclaredConstructor(String.class, Integer.class);
User newUser = constructor.newInstance("재성", 17);
Assertions.assertThat(newUser.getName()).isEqualTo("재성");
Assertions.assertThat(newUser.getAge()).isEqualTo(17);
}
}
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public boolean matchName(String name) {
return this.name.equals(name);
}
public static boolean ageIsInRange1(User user) {
boolean isInRange = false;
if (user != null && user.getAge() != null
&& (user.getAge() >= 30
&& user.getAge() <= 45)) {
isInRange = true;
}
return isInRange;
}
public static boolean ageIsInRange2(User user) {
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((age == null) ? 0 : age.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (age == null) {
if (other.age != null)
return false;
} else if (!age.equals(other.age))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
메타데이터 조회뿐만 아니라 동적으로 접근제어자를 수정할 수 있다!
Java Reflection API는 클래스, 인터페이스, 메서드, 필드 등 클래스 구조의 런타임 조작을 가능하게 합니다. 강력한 기능이지만 사용 시 주의해야 할 몇 가지 사항이 있습니다:
-
성능 저하:
- Reflection은 일반적인 메서드 호출보다 성능이 떨어집니다. 메서드, 필드 접근, 객체 생성 모두 성능에 영향을 미칠 수 있습니다.
- 자주 호출되는 코드에서 Reflection을 사용하면 성능 문제가 발생할 수 있습니다.
-
컴파일 타임 타입 체크 부족:
- Reflection을 사용하면 컴파일 시 타입 체크가 되지 않습니다. 런타임 오류가 발생할 가능성이 높아집니다.
- 메서드명이나 필드명을 문자열로 사용하기 때문에 오타가 있을 경우 컴파일러가 잡아주지 못합니다.
-
보안 문제:
- Reflection을 통해 private 필드나 메서드에 접근할 수 있어 보안 문제가 발생할 수 있습니다.
- Java 보안 매니저(Security Manager)가 이를 제한할 수 있지만, 사용자가 이를 우회할 방법을 찾을 수도 있습니다.
-
캡슐화 위반:
- Reflection을 사용하면 클래스의 캡슐화를 깨뜨릴 수 있습니다. 이는 클래스의 내부 구현에 의존하게 되어 유지보수가 어려워질 수 있습니다.
- 외부에서 클래스 내부의 private 멤버에 접근하는 것은 객체 지향 설계 원칙에 어긋납니다.
-
호환성 문제:
- 클래스의 내부 구현이 변경되면 Reflection 코드가 동작하지 않을 수 있습니다. 이는 API의 버전이 바뀔 때 문제를 일으킬 수 있습니다.
- 클래스의 필드나 메서드가 삭제되거나 이름이 변경되면 Reflection을 사용하는 코드가 런타임 오류를 발생시킬 수 있습니다.
-
복잡성 증가:
- Reflection은 코드의 가독성을 떨어뜨리고, 디버깅과 유지보수를 어렵게 만듭니다.
- 복잡한 Reflection 코드는 이해하기 어렵고, 디버깅도 힘들어집니다.
- 필요한 경우에만 사용: Reflection은 강력한 도구지만 필요한 경우에만 사용하고, 가능한 경우 표준 메서드 호출을 사용하십시오.
- 캐싱 사용: 반복적인 Reflection 호출이 필요한 경우, 리플렉션 결과를 캐싱하여 성능 문제를 완화할 수 있습니다.
-
안전한 접근:
AccessibleObject.setAccessible(true)
호출 시 주의하십시오. 이는 보안 위험을 증가시킬 수 있습니다. -
예외 처리: Reflection 사용 시 발생할 수 있는 다양한 예외 (예:
NoSuchMethodException
,IllegalAccessException
등)를 적절히 처리하십시오. - 문서화: Reflection을 사용하는 코드에는 충분한 주석을 달아, 왜 Reflection이 필요한지, 어떤 리소스를 접근하는지 명확히 하십시오.
Reflection은 그 필요성에 따라 강력한 도구가 될 수 있지만, 그 사용은 신중히 고려되어야 합니다. 필요에 따라 사용하되, 적절한 대안이 있다면 먼저 검토해보는 것이 좋습니다.
다양한 요구사항을 충족해야하는 프레임워크를 만들 때 유용하게 사용할 수 있겠다. 웹 서버 만들 때에는 속도가 더 중요한 것 같지만, 그 위에서 실행시킬 어플리케이션을 개발함에 있어서는 유용할 것 같다.
리플렉션에 대한 기초 지식을 쌓을 수 있었습니다. 당장 was 프로젝트에 적용할 수 있을 것 같습니다. 페어프로그래밍을 하면서 페어와 구현에 대해 이야기하거나 공부 내용을 한번 정리해보는 식으로 이야기하니 이해더 더 잘 됐던거 같습니다.