자바 리플렉션을 이용하여 특정 필드에 값 세팅하기(Java Reflection)

DY·2022년 4월 14일
0

자바 리플렉션에 대한 정의

실행 중인 자바 프로그램에서 동적으로 클래스 정보를 얻어와 처리할 수 있는 자바 기본 API
특정 클래스에 정의된 필드와 메서드, 생성자 정보도 조회할 수 있으며 메서드에 선언된 파라미터 정보를 조회하는 것도 가능
이렇게 조회한 필드, 메서드, 생성자 등의 정보를 이용하여 단순 값 조회/세팅부터 메서드 실행 등원하는 작업 처리 가능


이 글에서는 자바에서 제공하는 리플렉션 API를 이용해 클래스에 선언된 필드를 확인하고, 값을 수정/조회해 보려고 한다.
예시로 아래와 같이 일반적인 형태의 값을 저장하기 위한 도메인 클래스를 사용한다.
필드에 직접 접근하지 못하도록 필드에는 private 접근 제어자가 지정되어 있고, 대신 값을 세팅하고 가져가기 위한 getter/setter가 생성되어 있다.
lombok과 같은 라이브러리를 사용하면 getter/setter를 쉽게 만들 수 있지만 샘플을 명확히 하기 위해 일일이 생성해 두었다.

package dy.sample.reflection.domain;

public class NumberingItemInfo {
	private int item1;
	private int item2;
	private int item3;
	
	public int getItem1() {
		return item1;
	}
	
	public void setItem1(int item1) {
		this.item1 = item1;
	}
	
	public int getItem2() {
		return item2;
	}
	
	public void setItem2(int item2) {
		this.item2 = item2;
	}
	
	public int getItem3() {
		return item3;
	}
	
	public void setItem3(int item3) {
		this.item3 = item3;
	}
	
}

클래스에 정의된 필드 정보를 가져오는 방법

일단 대상 클래스 정보를 가져와야 하는데, 3가지 방법이 있다.

  • 대상 클래스를 지정해서 직접 클래스 정보를 가져오는 방법
Class targetClass = NumberingItemInfo.class;
  • 패키지명과 클래스명을 이용하여 가져오는 방법(try-catch 예외처리 필요)
Class targetClass = Class.forName("dy.sample.reflection.domain.NumberingItemInfo");
  • 생성된 인스턴스에서 가져오는 방법
NumberingItemInfo numberingItemInfo = new NumberingItemInfo();
numberingItemInfo.getClass();

이렇게 가져온 클래스 객체에서 사용 가능한 메서드들이 여럿 있는데, 그중 아래 메서드들을 이용하여 클래스에 선언된 필드 정보를 조회할 수 있다.

  • getDeclaredFields()
    클래스에 정의된 모든 필드 정보를 가져와 Field 타입의 배열로 반환한다. 이때, 필드의 접근 제어자 상관없이 모두 조회한다.
  • getFields()
    클래스에 정의된 모든 필드 정보를 가져와 Field 타입의 배열로 반환한다. 이때, 접근 제어자에 따라 외부에서 접근 가능한 경우에만 조회된다.
  • getDeclaredField(String name)
    클래스에 정의된 필드 중 지정한 이름과 동일한 필드 정보를 Field 타입의 객체로 반환한다. 만약, 지정한 이름의 필드를 찾지 못한다면 NoSuchFieldException이 발생하며 접근 제어자 상관없이 모두 조회 가능하다.
  • getField(String name)
    클래스에 정의된 필드 중 지정한 이름과 동일한 필드 정보를 Field 타입의 객체로 반환한다. 만약, 지정한 이름의 필드를 찾지 못한다면 NoSuchFieldException이 발생하며 접근 제어자에 따라 외부 접근이 가능한 필드만 찾을 수 있다.


    간단히 Field 객체에 대략 어떤 내용이 있는지 찍어보자.
NumberingItemInfo numberingItemInfo = new NumberingItemInfo();
Arrays.stream(numberingItemInfo.getClass().getDeclaredFields())
		.forEach(field -> System.out.println("field = " + field.toString()));

결과

field = private java.lang.String dy.sample.reflection.domain.NumberingItemInfo.item1
field = private java.lang.String dy.sample.reflection.domain.NumberingItemInfo.item2
field = private java.lang.String dy.sample.reflection.domain.NumberingItemInfo.item3

Field 클래스에 재정의 되어있는 toString 메서드에 의해 {접근 제어자} {타입} {패키지경로포함클래스명}.{필드명} 포맷으로 출력되었다. 추가로 Field 객체에는 필드에 적용된 어노테이션 정보, 제어자(static, final 등) 등의 정보도 포함하고 있다.
만약 getFields 메서드를 사용했다면 아무것도 출력되지 않았을 것이다. 모든 필드의 접근 제어자가 private이기 때문이다.

코드 샘플

드디어 리플렉션을 이용해 값을 세팅해 보려고 한다. item2 필드에 주저리주저리 작성한 문구를 넣을 거다. 우리는 이미 private 필드라는 것을 알고 있기 때문에 getDeclaredField 메서드를 사용할 것이다.

NumberingItemInfo numberingItemInfo = new NumberingItemInfo();
Field targetField = numberingItemInfo.getClass().getField("item2");
targetField.set(numberingItemInfo, "리플렉션으로 수정한 값-item2");

이렇게 작성한 후 실행해 보면... 대략 이런 오류가 뜰 것이다.

java.lang.IllegalAccessException: Class dy.sample.reflection.service.NumberingService can not access a member of class dy.sample.reflection.domain.NumberingItemInfo with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Field.set(Field.java:761)
	at dy.sample.reflection.service.NumberingService.main(NumberingService.java:15)

해당 필드는 private로 접근 제어자가 지정되어 있기 때문에 엑세스 할 수 없다는 내용이다.
getDeclaredField 메서드를 이용하여 private 필드의 정보를 가져올 수는 있었지만, 이렇게 쉽게 함부로 수정을 하는 것은 방지되어 있는 것이다.


하지만 아예 불가능한 건 아니다. 아래와 같이 코드를 수정한 후 다시 실행해 보자.

NumberingItemInfo numberingItemInfo = new NumberingItemInfo();
Field targetField = numberingItemInfo.getClass().getField("item2");
targetField.setAccessible(true);
targetField.set(numberingItemInfo, "리플렉션으로 수정한 값-item2");

위와 같이 setAccessible 메서드를 이용하여 해당 필드 접근 가능 여부를 true로 지정해 주고 다시 실행한다면, 특별한 오류 없이 종료될 것이다.


이번에는 값도 한 번 찍어보자.

NumberingItemInfo numberingItemInfo = new NumberingItemInfo();
Field targetField = numberingItemInfo.getClass().getDeclaredField("item2");
targetField.setAccessible(true);        // 필드 값에 접근 가능하도록 허용하기
targetField.set(numberingItemInfo, "리플렉션으로 수정한 값-item2");       // 값 세팅하기
		
System.out.println("Value of item2 : " + targetField.get(numberingItemInfo));   // 값 조회하기

결과

Value of item2 : 리플렉션으로 수정한 값-item2

Process finished with exit code 0

참고로 값을 조회할 때에도 private 접근 제한자가 지정되어 있는 경우, setAccessible을 통해 접근 허용 가능하도록 변경해 주어야 한다.
또한, 두 번째 줄의 Field 객체를 얻어오는 코드는 numberingItemInfo라는 인스턴스의 필드 정보를 가져온 것이 아니다. numberingItemInfo의 원본 클래스에 정의되어 있는, item2라는 이름의 필드 메타 정보를 가져온 것이다. 따라서 targetField 객체에는 numberingItemInfo에서 저장된 값이 들어있지는 않다.

사례

운영 중인 서비스에서 넘버링 된 필드를 처리해야 하는 경우가 있는데, 이 수가 15개나 되는 경우가 있었다.
만약 해당 필드 값들을 처리하는 로직에서 리플렉션을 활용하지 않는다면,

  • 필드 수만큼 코드를 작성해 줘야 하며 귀찮기도 하지만 코드 가독성에 좋지 않은 영향을 끼침
  • 필드 값을 저장하기 전에 처리 로직 등이 있고 만약 이 로직 일부가 변경되어야 하는 경우 필드 수만큼의 코드를 모두 수정해 줘야 함
  • 해당 객체(혹은 같은 규격의 테이블)를 사용한 다른 기능이 있을 경우, 그 기능이 구현된 코드에서도 똑같이 필드수 만큼 코드를 짜야 할 수도 있다.

위와 같은 문제가 있을 것이다.
그렇다고 무조건 좋을 것 같지는 않은 것이, 기껏 private로 범위를 지정해 둔 필드에 직접 접근해서 값을 임의로 수정한다는 점이 뭔가 큰 규칙을 깨는 듯한 느낌이 들기는 했다.
결국 개인의 판단에 따라 이러한 기능을 사용하고 말고를 결정할 것이겠지만 나의 경우와 같이 효율성 면에서 크게 차이를 보이는 케이스가 있다면 적절히 사용하는 것도 좋을 것 같다.

그 외

이 글에서는 Field에 대해서만 확인해 봤지만 이것 말고도 클래스에 정의된 메서드, 생성자, 어노테이션, 제어자, 인터페이스, 서브 클래스 등 많은 정보를 확인할 수 있고 또 이를 이용하여 필요에 따라 수행할 수 있는 작업들이 많다(특정 클래스에 정의된 메서드 정보를 조회해서 실행시킨다든지...). 단순히 필드명을 동적으로 지정해서 값을 세팅할 수 있는 방법은 없을까라는 궁금증에서 시작되어 리플렉션을 알게 되었고, 관련 정보들을 최대한 간략하게 분석해 봤는데 역시나 공부해야 할 것도 많고 개발하면서 고려해야 할 것도 많음을 느낀다.

참고한 사이트
https://happyitpark.tistory.com/6
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html
https://docs.oracle.com/javase/tutorial/reflect/

0개의 댓글

관련 채용 정보