Programing/Java

Effective Java - Reflection

안중환 2016. 3. 8. 14:56
  • 1_Class 정보
     
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    // Reflection(Introspection)
    // 개념 : Class 이름만으로도 클래스의 정보(필드, 메소드)를 찾거나
    //       객체를 생성할 수 있는 기능.
    
    import java.lang.reflect.Method;
    import java.lang.reflect.Modifier;
    
    public class Example1 {
      public static void main(String[] args) throws Exception {
        // 1. Class 얻기
        // a. 객체를 통해서 얻는 방법.
        // Class personClass = new Person().getClass();
    
        // b. Class type으로 얻어내는 방법.
        // Class personClass = Person.class;
    
        // c. 문자열로 얻어내는 방법.
        Class personClass = Class.forName("kr.co.ioacademy.Person"); // 패키지 정보까지 정확히 넣어야된다.
    
        // 2. Class 이름
        System.out.println(personClass.getName());       // kr.co.ioacademy.Person
        System.out.println(personClass.getSimpleName()); // Person
        System.out.println(personClass.getCanonicalName());
    
        int mods = personClass.getModifiers();
    
        // 3. Class 속성
        System.out.println("public : " + Modifier.isPublic(mods));
        System.out.println("final : " + Modifier.isFinal(mods));
        System.out.println("abstract : " + Modifier.isAbstract(mods));
    
        // 4. Method 속성
        Method[] methods = personClass.getMethods();
        for (Method m : methods) {
          System.out.println(m.getName());
          System.out.println(m.getParameterCount());
          Class[] params = m.getParameterTypes();
          for (Class p : params) {
            System.out.println("\t" + p.getName());
          }
        }
      }
    }
    
    final class Person {
      private String name;
      private int age;
    
      public Person() {
        this.name = "";
        this.age = 0;
      }
    
      public Person(String name, int age) {
        this.name = name;
        this.age = age;
      }
    
      @Override
      public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
      }
    }
    

  • 2_객체 read / write
    : public field vs. private field
     
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.lang.reflect.Field;
    
    // 객체의 필드를 읽거나 쓰는 방법.
    public class Example2 {
      public static void main(String[] args) throws Exception {
        Point point = new Point();
    
        Class pointClass = point.getClass();
    
        // public field read / write
        Field xField = pointClass.getField("x");
        Field yField = pointClass.getField("y");
    
        System.out.println(xField.get(point) + ", " + yField.get(point));
    
        xField.setInt(point, 10);
        // xField.set(point, 10);
        yField.set(point, 20);
        System.out.println(xField.get(point) + ", " + yField.get(point));
    
        Field[] fields = pointClass.getDeclaredFields();
        for (Field f : fields)
          System.out.println(f.getName());
    
    
        // private field read / write
        Field zField = pointClass.getDeclaredField("z");
        zField.setAccessible(true);
        zField.set(point, 100);
    
        System.out.println(zField.get(point));
      }
    }
    
    class Point {
      public int x;
      public int y;
    
      private int z;
      public int z() {
        return z;
      }
    }
    


  • 3_객체 동적 생성
     
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.lang.reflect.Constructor;
    
    // Reflection 을 통해서 객체를 생성하는 방법. - 동적 생성
    // Spring - DI(Dependency Injection)
    // iOS
    public class Example3 {
      public static void main(String[] args) throws Exception {
        Class personClass = Person.class;
    
        // 1. 기본 생성자를 통한 객체 생성
        Person person = (Person) personClass.newInstance();
        System.out.println(person);
    
        // 2. 사용자 정의 생성자를 통한 객체 생성
        Class[] paramTypes = {
          String.class, int.class
        };
    
        // Constructor constructor = personClass.getConstructor(paramTypes);
        Constructor constructor = personClass.getDeclaredConstructor(paramTypes);
        System.out.println(constructor);
    
        Object[] cargs = {
            "Tom", 42
        };
    
        constructor.setAccessible(true);
        Person person2 = (Person) constructor.newInstance(cargs);
        System.out.println(person2);
      }
    }
    
    class Person {
      private String name;
      private int age;
    
      public Person() {
        this.name = "";
        this.age = 0;
      }
    
      private Person(String name, int age) {
        this.name = name;
        this.age = age;
      }
    
      @Override
      public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
      }
    }
    

  • 3_Refilection 활용
    - 1_Key-Value Coding

     
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.lang.reflect.Field;
    
    // Reflection 활용 1.
    // Key-Value Coding : setter / getter
    // Objective-C 기본으로 제공하고 있는 기능.
    public class Example4 {
      public static void main(String[] args) {
        Person person = new Person();
    
        String input = "name";
        String value = "Tom";
    
        person.setValue(input, value);
        person.setValue("phone", "010-1111-2222");
        person.setValue("age", 200);
    
        System.out.println(person.getValue("name"));
        System.out.println(person.getValue("phone"));
        System.out.println(person.getValue("age"));
    
    
        // 일반적인 getter / setter 사용
        /*  
        if (input.equals("name"))
          person.setName(value);
        else if (input.equals("phone"))
          person.setPhone(value);
        */
      }
    }
    
    class Person {
      private String name;
      private String phone;
      private int age;
    
      public Object getValue(String key) {
        Class clazz = this.getClass();
        Field field;
    
        try {
          field = clazz.getDeclaredField(key);
          return field.get(this);
        } catch (NoSuchFieldException | IllegalAccessException e) {
          e.printStackTrace();
        }
    
        return null;
      }
    
      // Reflection을 사용하지 않고 key-value coding
      // but, field가 많아진다면??
      /*  
      public void setValue(String key, Object value) {
      switch (key) {
        case "name":
          this.name = (String) value;
          break;
        case "phone":
          this.phone = (String) value;
          break;
        case "age":
          this.age = (int) value;
          break;
       }
     }*/
      
      public void setValue(String key, Object value) {
        Class clazz = this.getClass(); // Person.class
        try {
          Field filed = clazz.getDeclaredField(key);
          filed.set(this, value);
        } catch (NoSuchFieldException | IllegalAccessException e) {
          e.printStackTrace();
        }
      }
    
      @Override
      public String toString() {
        return "Person{" +
            "name='" + name + '\'' +
            ", phone='" + phone + '\'' +
            '}';
      }
    }
    

    - 2_팩토리
     
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import org.hamcrest.Factory;
    
    // Reflection 활용 2.
    // 팩토리
    // 개념 : 객체를 생성하는 객체
    // 장점 : 객체 생성에 관한 코드를 한곳에 모아서 중앙집중적으로 관리하는 것이 가능하다.
    // 단점 : 도형의 종류가 늘어남에 따라 팩토리의 코드는 수정되어야만 한다.
    //        OCP(Open Closed Principle) 를 만족할 수 없다.
    
    abstract class Shape {
    	abstract void print();
    }
    class Rect extends Shape {
    	@Override
    	void print() {
    		System.out.println("Rect");
    	}
    }
    class Circle extends Shape {
    	@Override
    	void print() {
    		System.out.println("Circle");
    	}
    }
    class Triangle extends Shape {
    	@Override
    	void print() {
    		System.out.println("Triangle");
    	}
    }
    
    class ShapeFactory {
      public Shape createShape(String name) {
        Class clazz = null;
        try {
          clazz = Class.forName(name);
          return (Shape) clazz.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
          e.printStackTrace();
        }
    
        return null;
      }
    
    //  public Shape createShape(String name) {
    //    if (name.equals("Rect"))
    //      return new Rect();
    //    else if (name.equals("Circle"))
    //      return new Circle();
    //    else if (name.equals("Triangle"))
    //      return new Triangle();
    //
    //    return null;
    //  }
    }
    
    public class Example5 {
    	public static void main(String args[]) {
    		ShapeFactory factory = new ShapeFactory();
    		Object r = factory.createShape("kr.co.ioacademy.Rect");
    		Object c = factory.createShape("kr.co.ioacademy.Circle");
    		Object t = factory.createShape("kr.co.ioacademy.Triangle");
    		
    		if(r instanceof Rect)
    			((Rect)r).print();
    		if(c instanceof Circle)
    			((Circle)c).print();
    		if(t instanceof Triangle)
    			((Triangle)t).print();
    	}
    }
    

  • 4_Reflection 성능 고찰

    package kr.co.ioacademy; //iocademy 윤찬식 강사님 // Reflection 성능. // 1. Refelction 은 염려할만큼의 성능 저하는 없다. // 2. 잘 설계된 Reflection은 객체 지향의 철학을 어긋나지 않으면서, // 더 좋은 코드를 만들어 낼 수 있다. public class Example6 { public static void doRegular() throws Exception { long start = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { Point p = new Point(); p.print(); } System.out.println(System.currentTimeMillis() - start); } public static void doReflection() throws Exception { long start = System.currentTimeMillis(); Class clazz = Class.forName("kr.co.ioacademy.Point"); for (int i = 0; i < 1000000; i++) { Point p = (Point) clazz.newInstance(); p.print(); } System.out.println(System.currentTimeMillis() - start); } public static void main(String[] args) throws Exception { doRegular(); doReflection(); } } class Point { private int x; private int y; public void print() { } }



  • Reflection에 대한 오해와 진실
    (출처: https://kmongcom.wordpress.com/2014/03/15/%EC%9E%90%EB%B0%94-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%A4%ED%95%B4%EC%99%80-%EC%A7%84%EC%8B%A4/)

    사용처

    현장에서 자바 개발자들이 Reflection을 직접 사용하는 것은 극히 드문 일이다. 그것은 Reflection이 적용될 수 있고 또한 적용되어야 할 곳은 라이브러리 클래스, 공통 컴포넌트 클래스, 그리고 프레임워크와 같이 Reflection을 통해 얻는 이득(재사용성, 확장성, 생산성, 유연성 등)이 극대화될 수 있는 곳이어야 하기 때문이다.

    1. Java Serialization

     
    객체를 직렬화(Serialization) 해야 할 경우 Serializable 인터페이스를 구현한다. 그리고, 직렬화된 객체를 읽기 위해서는 java.io.ObjectInputStream 클래스의 readObject() 메서드를 이용한다. 필요에 따라 Serializable 인터페이스를 구현한 클래스가 readObject() 메서드를 구현할 수도 있다. 이때, java.io.ObjectInputStream 클래스의 readObject() 메서드는 내부적으로 Reflection을 이용하여 직렬화된 객체의 readObject() 메서드를 호출한다.

    a4

    그림 4. Reflection을 사용한 Serialization

    2. Apache Commons BeanUtils library

    Struts 프레임워크를 적용한 프로젝트에서 개발한 경험이 있다면, Apache Commons 프로젝트의 BeanUtils 라이브러리 사용을 고민해 본 경험 또한 있을 것이다. Struts 프레임워크는 HttpRequest 객체의 파라미터를 이용하여 ActionForm 클래스의 객체를 생성한다. 이 ActionForm 클래스의 객체를 생성하는 곳에서도 Reflection이 적용되었다.

    Struts를 이용할 경우, 가장 성가신 부분은 ActionForm 클래스의 객체를 대응하는 VO 클래스의 객체로 변환하는 작업이다. 이 작업은 서비스 레이어를 Struts에 종속되지 않게 하기 위해 또는, 레이어 분리를 위해 반드시 수행되어야 한다. 만일 지금까지 ActionForm 객체를 서비스 레이어로 바로 넘겼다면 다시 한번 생각해 보라. VO 클래스 사용에 따른 레이어의 분리와, VO를 사용하지 않음으로써 얻는 개발 생산성 증가, 둘 중 어느 한가지를 택한 것인지.

     이때, 사용할 수 있는 것이 Commons BeanUtils 라이브러리이다. Beanutils.copyProperties(Object dest, Object orig)를 이용하여 간단히 ActionForm 객체를 VO 객체로 변환할 수 있다. 규칙은 ActionForm 클래스의 인스턴스 변수명과 VO 클래스의 인스턴수 변수명이 같아야 한다는 것이다. 이 규칙을 따른다면, Reflection의 마술이 여러분을 위한 모든 작업을 수행해 줄 것이다.

    이슈

    1. Reflection을 사용한 코드는 느리다

    개발자들 사이에 공공연히 진실로 받아들여지는 이 말은 사실이 아니다. 적절히 사용한 Reflection은 오히려 성능을 향상시킬 수 있으며, 또한 많은 이득을 제공한다. 뿐만 아니라, 성능만을 고려한 구현이 객체 지향의 설계 원칙들을 역행한다면, 오히려 이는 더욱 나쁜 결과를 낳게 된다.

    2. Reflection을 이용하여 개발한 프로그램은 에러가 발생하기 쉽고 디버깅이 어렵다

    Reflection은 컴파일 시 타입 체킹을 할 수 없다. 따라서, 런타임시 잘못된 파라미터로 인해 런타임 에러가 발생하기 쉽다. 이는 사실이다. 하지만, 적절히 사용된 런타임 에러 메시지를 이용해 충분히 디버깅이 쉬운 환경으로 만들 수 있다.

    3. Reflection을 사용한 코드는 복잡하다

    Reflection을 사용한 코드는 일반적인 객체 생성, 메서드 호출 코드에 비교하면 복잡한 것이 사실이다. 하지만 클래스의 타입을 비교하여 객체를 생성하는 코드의 경우, 대량의 if/else문을 사용하는 것보다 Reflection을 이용하여 재사용 가능한 컴포넌트로 만든다면, 오히려 코드를 단순화한다.

    4. 성능(Performance) vs. 유연성(Flexibility)

    앞서 말한 바와 같이 “Reflection을 사용한 코드는 느리다”는 사실은 사실이 아니다. 이 말은 Reflection을 사용할 경우 성능이 떨어지지 않는다라는 얘기가 아니다. 오히려, 성능이 떨어진다는 결과가 다수 존재한다. 아래는 Dennis Sosnoski(5. Java Programming dynamics, Part 2: Introducing reflection)가 측정한 Reflection에 관한 성능 결과이다. 그림에서 알 수 있듯이, Reflection을 사용할 경우, 직접(Direct) 또는 참조(Reference)의 경우에 비해 2 ~ 4배 정도 느리다.


    a5

    그림 5. 필드 변수 Access 시간

    a6

    그림 6. 메서드 호출 시간

    이 결과를 통해 알 수 있는 사실은 “Reflection에 따른 성능 저하”가 아니라 “성능 측정 결과, Reflection을 사용한 
    지금 이 경우에는 성능이 저하되는 것을 검증했다”라는 것이다. 최적화 또는 성능 개선(Optimization)시 유의해야 할 점은 반드시 최적화 이전과 이후의 성능을 측정하여, 성능개선이 가시적으로 보일 때에만 적용해야 한다는 것이다. 만일 최적화가 필요하다고 느낀다면, 아래 규칙을 따르라.

    a7

    그림 7. 최적화 규칙

    Reflection과 관련된 성능에 관한 논쟁은 오해에서 비롯된 것이다. 이것은 JDK 초기 버전(1.3.0 이전 버전)의 경우 Reflection의 성능이 현저히 떨어졌다. 하지만 이후의 JDK 버전에서는, Reflection의 중요성을 인식한 Sun의 지속적인 노력으로 성능이 개선되고 있으며, 앞으로도 개선의 여지가 남아 있다. 뿐만 아니라 잘 적용한 Reflection은 많은 이득을 제공한다.

    Reflection을 통해 얻을 수 있는 가장 큰 이득은 시스템 유연성(Flexibility)이다. “그림 3″에서 보는 바와 같이 Mouse 컨트롤러는 미래의 어떤 마우스와도 동작할 수 있다. 이처럼 성능보다 유연성이 더 중요한 상황이 많다

    물론 Reflection 적용에 따른 가시적인 성능 저하를 확신한다면, 다른 대안을 생각해 볼 수 있다. 대안에는, Reflection 대신 interface를 통한 메서드 호출, 코드 자동 생성(Code Generation), 또는 최악의 경우 하드 코딩이 있다.

    5. Compile vs. Run-time Type Checking

     자바의 경우 컴파일 단계에 강력한 Type Checking을 지원한다. 아래와 같이 두 개의 클래스가 틀릴 경우, 컴파일이 에러가 발생한다.

    a8

    그림 8. 컴파일 에러 예

    Reflection은 실제 클래스 없이 클래스의 이름 또는 메서드의 이름만을 이용한다. 따라서, 아래와 같이, 개발 단계 또는 컴파일 단계에는 Type Checking을 하지 않는다.

    a9

    그림 9. Checked Exception 예

    대신, 실행시 발생할 수 있는 Exception을 처리하기 위한 try/catch문을 추가해야 한다.

    자바가 제공하는 Exception의 종류에는 Checked Exception, Run-time Exception 그리고 error가 있다. 이중 Checked Exception의 경우, 위와 같이 컴파일 단계에 catch하거나 throw해야만 컴파일 오류가 발생하지 않는다. 이는 Checked Exception은 예외 상황에서 프로그램적으로 복구할 수 있는 방법이 존재하는 경우를 위한 Exception이기 때문이다.

    하지만 Reflection 사용에 따른 Exception으로부터 복구할 수 있는 상황이란 거의 없다. 예를 들어, 위의 Class Not Found Exception이 발생한다면, 이는 클라이언트가 잘못된 클래스 명을 넘겨주었거나 또는 해당 클래스가 없을 2가지 경우다. 이는 모두 프로그램 에러 상황으로 오히려 Run-time Exception에 가깝다.

     결국, Reflection을 사용함으로써, 개발단계에서 조기에 발견될 수도 있었을 프로그램 에러들이 런타임시에 발생하게 되는 것이다. 이처럼 에러 상황이 늦게 발견하면 디버깅이 어려워지게 된다. 이와 같은 경우를 위해서, 런타임시 디버깅을 위해 상세한 에러 메시징 기능을 포함하는 것이 좋다.

    Drug heals the pain, Overdose kills the gain

    Reflection의 사용에 관한 사실들은 사실 사실이 아니다. 성능, 디버깅, 그리고 복잡성과 관련된 내용들은 잘못 사용된 예에서 파생한 오해들이다.

    Reflection은 염려할 만큼의 성능저하를 가져오지 않는다. 대량의 if/else문이나 switch문 대신, 잘 설계된 Reflection은 객체지향 철학을 어기지 않으면서도 더 좋은 성능을 발휘할 수도 있다.

    또한 디버깅이 어려운 것은 컴파일 단계에 처리할 수 있는 오류들이 실행 단계에 발생하기 때문이 아니다. 더 근본적인 이유는 Reflection을 사용할 경우에 발생할 런타임 에러 메시지를 최대한 상세하게 그리고 친절하게 표시하도록 Exception 전략을 설계하지 못했기 때문이다. 잘 설계된 Exception 처리 전략은 Reflection 뿐만 아니라 시스템의 전체적인 디버깅을 쉽게 만든다.

    Reflection의 복잡성은 사실 개발자 개개인의 초점에 맞추었을 때의 얘기다. 하지만, Reflection의 사용은 개발자의 관점이 아닌 아키텍트 중심으로 설계되어야 한다. Reflection이 가장 유용한 곳이 바로 시스템의 아키텍처를 이루는 컴포넌트들이기 때문이다. 오히려 잘 설계된 Reflection이 제공하는 서비스는 개발을 더욱 단순화 한다.

    하지만 지나친 사용은 화를 부를 수 있다. Reflection을 적절히 사용했고 많은 이득이 따른다 하더라도, 더 간단한 해결책이 존재한다면, Reflection을 사용하지 말 것을 권한다. 단순한 해결책은 언제 어느 경우에나 최상의 선택이다.

    Reflection의 사용은 양날의 검과 같다. 잘 사용한다면, 이름을 불러주었을 때, 여러분의 꽃이 되어 줄 것이다.