Skip to content

고동훤, 김현종 리플렉션

bellringstar edited this page Jul 8, 2024 · 10 revisions

학습 내용

클래스 정보 출력하기

간단한 리플렉션의 각종 메서드를 테스트 해볼 수 있는 요구사항입니다. 리플렉션을 사용해 클래스의 메타데이터로부터 메서드 정보를 불러올 수 있으며, getType()이나 getDeclaredType()등의 차이에 대해 학습하였습니다.

test로 시작하는 메서드 실행

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()를 사용하였습니다.

@Test 애노테이션 메소드 실행

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는 클래스, 인터페이스, 메서드, 필드 등 클래스 구조의 런타임 조작을 가능하게 합니다. 강력한 기능이지만 사용 시 주의해야 할 몇 가지 사항이 있습니다:

  1. 성능 저하:
    • Reflection은 일반적인 메서드 호출보다 성능이 떨어집니다. 메서드, 필드 접근, 객체 생성 모두 성능에 영향을 미칠 수 있습니다.
    • 자주 호출되는 코드에서 Reflection을 사용하면 성능 문제가 발생할 수 있습니다.
  2. 컴파일 타임 타입 체크 부족:
    • Reflection을 사용하면 컴파일 시 타입 체크가 되지 않습니다. 런타임 오류가 발생할 가능성이 높아집니다.
    • 메서드명이나 필드명을 문자열로 사용하기 때문에 오타가 있을 경우 컴파일러가 잡아주지 못합니다.
  3. 보안 문제:
    • Reflection을 통해 private 필드나 메서드에 접근할 수 있어 보안 문제가 발생할 수 있습니다.
    • Java 보안 매니저(Security Manager)가 이를 제한할 수 있지만, 사용자가 이를 우회할 방법을 찾을 수도 있습니다.
  4. 캡슐화 위반:
    • Reflection을 사용하면 클래스의 캡슐화를 깨뜨릴 수 있습니다. 이는 클래스의 내부 구현에 의존하게 되어 유지보수가 어려워질 수 있습니다.
    • 외부에서 클래스 내부의 private 멤버에 접근하는 것은 객체 지향 설계 원칙에 어긋납니다.
  5. 호환성 문제:
    • 클래스의 내부 구현이 변경되면 Reflection 코드가 동작하지 않을 수 있습니다. 이는 API의 버전이 바뀔 때 문제를 일으킬 수 있습니다.
    • 클래스의 필드나 메서드가 삭제되거나 이름이 변경되면 Reflection을 사용하는 코드가 런타임 오류를 발생시킬 수 있습니다.
  6. 복잡성 증가:
    • Reflection은 코드의 가독성을 떨어뜨리고, 디버깅과 유지보수를 어렵게 만듭니다.
    • 복잡한 Reflection 코드는 이해하기 어렵고, 디버깅도 힘들어집니다.

Reflection 사용 시 권장 사항

  • 필요한 경우에만 사용: Reflection은 강력한 도구지만 필요한 경우에만 사용하고, 가능한 경우 표준 메서드 호출을 사용하십시오.
  • 캐싱 사용: 반복적인 Reflection 호출이 필요한 경우, 리플렉션 결과를 캐싱하여 성능 문제를 완화할 수 있습니다.
  • 안전한 접근: AccessibleObject.setAccessible(true) 호출 시 주의하십시오. 이는 보안 위험을 증가시킬 수 있습니다.
  • 예외 처리: Reflection 사용 시 발생할 수 있는 다양한 예외 (예: NoSuchMethodException, IllegalAccessException 등)를 적절히 처리하십시오.
  • 문서화: Reflection을 사용하는 코드에는 충분한 주석을 달아, 왜 Reflection이 필요한지, 어떤 리소스를 접근하는지 명확히 하십시오.

Reflection은 그 필요성에 따라 강력한 도구가 될 수 있지만, 그 사용은 신중히 고려되어야 합니다. 필요에 따라 사용하되, 적절한 대안이 있다면 먼저 검토해보는 것이 좋습니다.

학습 소감

고동훤

다양한 요구사항을 충족해야하는 프레임워크를 만들 때 유용하게 사용할 수 있겠다. 웹 서버 만들 때에는 속도가 더 중요한 것 같지만, 그 위에서 실행시킬 어플리케이션을 개발함에 있어서는 유용할 것 같다.

김현종

리플렉션에 대한 기초 지식을 쌓을 수 있었습니다. 당장 was 프로젝트에 적용할 수 있을 것 같습니다. 페어프로그래밍을 하면서 페어와 구현에 대해 이야기하거나 공부 내용을 한번 정리해보는 식으로 이야기하니 이해더 더 잘 됐던거 같습니다.

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally