Skip to content

Dev

Reflection

Java, Reflection1 min read

Java의 리플렉션에 대한 간단한 설명과 사용법, 그리고 어노테이션과 리플렉션을 사용한 예제에 대해 정리하였다.

1. Reflection

리플렉션은 클래스 내부 정보인 클래스명, 메서드명, 필드명 혹은 값 등을 들여다 볼 수 있을 뿐만 아니라 인스턴스 생성 및 값 할당, 메서드 실행을 할 수 있는 API이다. (JVM 내부 영역을 살핀다)

Player라는 클래스를 생성하여 reflection을 통해 해당 클래스를 들여다 보자.

1public class Player {
2
3 private Team team;
4 private String playerName;
5 @Range(min = 1, max = 10)
6 private int number;
7 private String position;
8 private boolean hasReceiveBall;
9
10 public Player(){}
11
12 public Player(String playerName, int number, boolean hasReceiveBall){
13 this.playerName = playerName;
14 this.number = number;
15 this.hasReceiveBall = hasReceiveBall;
16 }
17
18 public void shootingBall(){
19 System.out.println("ShootingBall");
20 }
21
22 //Getter, toString
23
24}
25public class ReflectionTest {
26
27 @Test
28 public void reflection_classInformation() throws Exception{
29
30 Class<?> clazz = Class.forName("sports.Player");
31 Constructor<?> constructor = clazz.getConstructor(String.class, int.class, boolean.class);
32 Player son = (Player) constructor.newInstance("손흥민", 11, true);
33
34
35 assertEquals("sports.Player", clazz.getName());
36 assertNotNull(son);
37 assertEquals(11, son.getNumber());
38
39 //reflections 라이브러리
40 Reflections reflections = new Reflections("sports");
41 Set<Class<?>> classes = reflections.getSubTypesOf(Object.class);
42
43 }
44}

먼저, Class.forName api를 통해 클래스 정보를 얻은 후, 이후에 해당 클래스가 가지고 있는 context에 접근할 수 있다. 위의 예제는 리플렉션을 통해 생성자에 접근하여 인스턴스를 생성하였다.

또한, 기본적으로 제공하는 java api에서는 리플렉션을 통해서 단 하나의 클래스만 접근할 수 있는 걸로 알고 있다. 여기서 특정 패키지 이하의 모든 클래스들에 대한 정보를 가져오고 싶을 경우 reflections라는 라이브러리를 사용해서 여러 클래스 정보들을 가져올 수 있다.

이렇게 클래스 context 정보를 가져오게 되면, method와 field뿐만 아니라 annotation이 선언된 부분까지 접근 및 실행 할 수 있게 된다.

1//Field 조작
2public class SportsTest {
3
4 @Test
5 public void reflection_field() throws Exception{
6
7 Class<?> clazz = Class.forName("sports.Player");
8 Player player = (Player) clazz.newInstance(); //인스턴스 생성
9
10 Field field = player.getClass().getDeclaredField("playerName");
11
12 field.setAccessible(true); //private 필드는 true로 설정해줘야 한다.
13 field.set(player, "손흥민");
14
15 assertEquals("playerName", field.getName());
16 assertEquals(String.class, field.getType());
17 assertEquals("손흥민", player.getPlayerName());
18
19 Field[] fields = player.getClass().getDeclaredFields(); //클래스 내부 모든 필드
20 String fieldName = "";
21 for (Field f : fields) {
22 if (f.isAnnotationPresent(Range.class)) {
23 fieldName = f.getName();
24 }
25 }
26
27 assertEquals("number", fieldName);
28
29 }
30}

코드 순서는 Class context -> Field context에 접근하여 리플렉션을 사용하는 것을 알 수 있다. Field중에 @Range 어노테이션이 붙은 필드를 찾아내어 해당 필드가 'number'가 맞는지 확인하는 코드이다.

다음은 메서드 context에 접근하여 메서드를 실행해 보겠다. 메서드 실행 조건은 Player 클래스의 hasReceiveBall 필드가 true일 경우 shootingBall 메서드를 실행하는 테스트이다.

1public class SportsTest {
2
3 @Test
4 public void reflection_method() throws Exception {
5 List<Player> players = createPlayerList();
6 for (Player player : players) {
7 if(player.isHasReceiveBall()) {
8 player.getClass().getMethod("shootingBall").invoke(player);
9 }
10 }
11 }
12}

invoke 메서드를 사용하면 해당 인스턴의 메서드를 실행할 수 있다.


2. Annotation을 이용하여 Reflection

어노테이션은 일종의 라벨지처럼 클래스, 필드, 생성자, 메서드에 다양하게 붙어서 주석처럼 마크하는 역할과 특정 기능들을 수행하게 하는 API이다.

커스텀 어노테이션을 만들어, Player 클래스의 number 필드에 제약조건을 걸어보겠다.

1@Retention(RetentionPolicy.RUNTIME)
2@Target(ElementType.FIELD)
3public @interface Range {
4
5 int min() default 0;
6 int max() default 10;
7}
8public class Player{
9
10 @Range(min = 1, max = 20)
11 private int number;
12
13 // 생략....
14}

Field에만 적용가능한 Range라는 커스텀 어노테이션을 만들었다. 최소 가능 숫자와 최대 가능 숫자를 기입하여, 인스턴스 number 필드의 숫자를 검증하는 역할을 한다.

1public class SportsTest {
2
3 @Test(expected = IllegalArgumentException.class)
4 public void annotation_reflection() {
5 int playerNumber = 99;
6 Player player1 = new Player("류현진", playerNumber, true);
7
8 Field[] fields = player1.getClass().getDeclaredFields();
9 Range a;
10 for (Field f : fields) {
11 if ((a = f.getAnnotation(Range.class)) instanceof Range) {
12 validateNumberRange(a, playerNumber);
13 }
14 }
15 }
16 public void validateNumberRange(Range a, int targetNumber){
17 if (a.min() > targetNumber || targetNumber > a.max()){
18 throw new IllegalArgumentException();
19 }
20 }
21}

Player 클래스의 제약조건으로 number 숫자는 1부터 20까지만 가능하다. 하지만, 생성된 player1 인스턴스의 number 숫자는 99였다. 리플렉션을 통해 가져온 Annotation Range타입의 클래스로 부터 선언된 min, max 값을 구하여 number 필드를 비교하여 필드 검증을 수행한 걸 볼 수 있다.


3. 언제 사용하는게 좋을까 ?

소스분석 도구나 스프링 프레임워크, JPA 등 다양한 곳에서 리플렉션을 사용하고 있다. 다양한 환경에 따라 목적이 다르겠지만, 일반적으로 런타임 환경에서 동적인 클래스 생성 및 조작이 필요할 경우 사용하면 좋을 것 같다.

리플렉션을 잘못 사용 할 경우 성능저하를 유발할 수도 있으니 꼭 필요한 상황인지 확인도 필요하다. 메서드 조작 예제에서 인스턴스 생성 후 일반적인 메서드 실행방법인 player.shootingBall()를 사용할 수 있는 상황임에도 invoke 메서드를 사용했었다. 이렇듯 리플렉션을 사용하다보면 이런 함정에 빠지기가 쉬운 것 같다.


[Reference]