Java Category/Java

[JAVA] 리플렉션(Reflection)과 어노테이션(Annotation)

ReBugs 2023. 7. 23.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


리플렉션

리플렉션이란 실행 도중에 타입(클래스, 인터페이스 등)을 검사하고 구성 멤버를 조사하는 것을 말한다.

이클립스 개발환경에서 outline과 비슷하다고 생각하면 된다.

 

 

자바는 클래스와 인터페이스의 메타 정보를 Class 객체로 관리한다.

메타 정보
패키지 정보, 타입 정보, 멤버(생성자, 필드, 메소드) 정보 등을 말한다.

이러한 메타 정보를 프로그램에서 읽고 수정하는 행위를 리플렉션이라고 한다.

 

프로그램에서 Class 객체를 얻으려면 아래의 3가지 방법 중 하나를 이용한다.

Class clazz = 클래스이름.class; //클래스로부터 얻는 방법
Class clazz = Class.forName("패키지...클래스이름"); //클래스로부터 얻는 방법
Class clazz = 객체참조변수.getClass(); // 객체로부터 얻는 방법

 셋 중 어떤 방법을 사용하더라도 동일한 Class 객체를 얻을 수 있다.

 

패키지와 타입 정보 얻기

패키지와 타입(클래스, 인터페이스) 이름 정보는 아래와 같은 메소드를 통해 얻을 수 있다.

메소드 용도
Package getPackage() 패키지 정보 읽기
String getSimpleName() 패키지를 제외한 타입 이름
String getName() 패키지를 포함한 전체 타입 이름

 

Car.java

package ch12.sec11.exam01;
public class Car {
}

 

GetClassExample.java

package ch12.sec11.exam01;
public class GetClassExample {
	public static void main(String[] args) throws Exception {
		//how1
		Class clazz = Car.class;
		
		//how2
		//Class clazz = Class.forName("ch12.sec11.exam01.Car");
		
		//how3
		//Car car = new Car();
		//Class clazz = car.getClass();
		
		System.out.println("패키지: " + clazz.getPackage().getName());
		System.out.println("클래스 간단 이름: " + clazz.getSimpleName());
		System.out.println("클래스 전체 이름: " + clazz.getName());
	}
}
/*
패키지: ch12.sec11.exam01
클래스 간단 이름: Car
클래스 전체 이름: ch12.sec11.exam01.Car
*/

 

멤버 정보 얻기

타입(클래스, 인터페이스)가 가지고 있는 멤버 정보는 다음 메소드를 통해 얻을 수 있다.

메소드 용도
Constructor[] getDeclaredConstructors() 생성자 정보 읽기
Field[] getDeclaredFields() 필드 정보 읽기
Method[] getDeclaredMethod() 메소드 정보 읽기

메소드 이름에서 알 수 있듯이 각각 생성자 배열, 필드 배열, 메소드 배열을 리턴한다.

각각 생성자, 필드, 메소드에 대한 선언부 정보를 제공한다.

 

아래는 Car 클래스에서 선언된 생성자, 필드, 메소드의 선언부 정보를 얻고 출력하는 예제이다.

Car.java

package ch12.sec11.exam02;
public class Car {
	//필드
	private String model;
	private String owner;
	
	//생성자
	public Car() {
	}
	public Car(String model) {
		this.model = model;
	}
	
	//메소드
	public String getModel() { return model; }
	public void setModel(String model) { this.model = model; }
	public String getOwner() { return owner; }
	public void setOwner(String owner) { this.owner = owner; }
}

 

ReflectionExample.java

package ch12.sec11.exam02;
import java.lang.reflect.*;
	
public class ReflectionExample {
	public static void main(String[] args) throws Exception {
		Class clazz = Car.class;
		
		System.out.println("[생성자 정보]");
		Constructor[] constructors = clazz.getDeclaredConstructors();
		for(Constructor constructor : constructors) {
			System.out.print(constructor.getName() + "(");
			Class[] parameters = constructor.getParameterTypes();
			printParameters(parameters);
			System.out.println(")");
		 	}
		System.out.println();
		
		System.out.println("[필드 정보]");
		Field[] fields = clazz.getDeclaredFields();
		for(Field field : fields) {
			System.out.println(field.getType().getName() + " " + field.getName());
		}
		System.out.println();
		
		System.out.println("[메소드 정보]");
		Method[] methods = clazz.getDeclaredMethods();
		for(Method method : methods) {
			System.out.print(method.getName() + "(");
			Class[] parameters = method.getParameterTypes();
			printParameters(parameters);
			System.out.println(")");
		}
	}
			
	private static void printParameters(Class[] parameters) {
		for(int i=0; i<parameters.length; i++) {
			System.out.print(parameters[i].getName());
			if(i<(parameters.length-1)) {
				System.out.print(",");
			}
		}
	}
}
/*
[생성자 정보]
ch12.sec11.exam02.Car()
ch12.sec11.exam02.Car(java.lang.String)

[필드 정보]
java.lang.String model
java.lang.String owner

[메소드 정보]
getOwner()
setOwner(java.lang.String)
getModel()
setModel(java.lang.String)
*/

 

리소스 경로 얻기

Class 객체는 클래스 파일(~.class)의 경로 정보를 가지고 있기 때문에 이 경로를 기준으로 상대 경로에 있는 다른 리소스 파일의 정보를 얻을 수 있다.

이때 사용하는 메소드는 아래와 같다.

메소드 용도
URL getResource(String name) 리소스 파일의 URL 리턴
InputStream getResourceAsStream(String name) 리소스 파일의 InputStream 리턴
  • getResource() : 경로 정보가 담긴 URL 객체를 리턴
  • getResourceAsStream() : 파일의 내용을 읽을 수 있도록 InputStream 객체를 리턴

 

프로그램에서 이미지 파일의 절대 경로가 필요할 경우, Car.class가 있는 곳에서 상대 경로로 아래와 같이 얻을 수 있다.

Car.java

package ch12.sec11.exam03;
public class Car {
}

 

GetResourceExample.java

package ch12.sec11.exam03;

public class GetResourceExample {
	public static void main(String[] args) {
		Class clazz = Car.class;

		String photo1Path = clazz.getResource("photo1.jpg").getPath();
		String photo2Path = clazz.getResource("images/photo2.jpg").getPath();

		System.out.println(photo1Path);
		System.out.println(photo2Path);
	}
}
/*
/D:/Eclipse/thisisjava/bin/ch12/sec11/exam03/photo1.jpg
/D:/Eclipse/thisisjava/bin/ch12/sec11/exam03/images/photo2.jpg
*/

 

 

리플렉션 허용

모듈에서 은닉된 패키지는 기본적으로 다른 모듈에 의해 리플렉션을 허용하지 않는다.

하지만 경우에 따라서는 은닉된 패키지도 리플렉션을 허용해야 할 때가 있다.

모듈은 모듈 기술자를 통해 모듈 전체 또는 지정된 패키지에 대해 리플렉션을 허용할 수 있고, 특정 외부 모듈에서만 리플렉션을 허용할 수도 있다.

 

모듈 전체를 리플렉션 허용

open module 모듈이름 {
	//내용 작성
}

 

지정된 패키지에 대해 리플렉션 허용

module 모듈이름 {
    opens 허용할 패키지 이름;
    opens 허용할 패키지 이름2;
}

 

지정된 패키지에 대해 특정 외부 모듈에서만 리플렉션 허용

module 모듈이름 {
    opens 패키지1 to 외부모듈이름, 외부모듈이름2;
    opens 패키지2 to 외부모듈이름3;
}

 


 

어노테이션(Annotation)

@로 작성되는 요소를 어노테이션이라고 한다. 

어노테이션은 클래스 또는 인터페이스를 컴파일하거나 실행할 때 어떻게 처리해야 할 것인지를 알려주는 설정 정보이다.

어노테이션은 아래의 세 가지 용도로 사용된다.

  1. 컴파일 시 사용하는 정보 전달
  2. 빌드를 하는 프로그램이 코드를 자동으로 생성할 때 사용하는 정보 전달
  3. 실행 시 특정 기능을 처리할 때 사용하는 정보 전달

 

어노테이션은 자바 프로그램을 개발할 때 필수요소가 되었다.

Spring Framework 또는 Spring Boot는 다양한 종류의 어노테이션을 사용해서 웹 어플리케이션을 설정하는 데 사용된다.

 

어노테이션 타입 정의와 적용

  • 어노테이션도 하나의 타입이므로 먼저 정의를 해야 한다.
  • 어노테이션 정의 방법은 인터페이스를 정의하는 것과 유사
public @interface AnnotationName{
}

 

이렇게 정의한 어노테이션은 아래와 같이 사용한다.

@AnnotationName

 

  • 어노테이션은 필드와 유사한 속성을 가질 수 있다. 주의해야 할 것은 유사한 것이지 필드랑 속성은 다르다는 것이다.
  • 속성의 기본값은 default 키워드로 지정할 수 있다.
public @interface AnnotationName {
	String prop1(); //기본 값 없음
	int prop2() default 1; //기본 값 1
}

 

이렇게 정의한 어노테이션은 코드에서 아래와 같이 사용할 수 있다. prop1은 기본값이 없기 때문에 반드시 값을 넣어야 하고, prop2는 기본값이 있기 때문에 생략이 가능하다.

@AnnotationName(prop1 = "a")
@AnnotationName(prop1 = "a", prop2 = 3)

 

  • 어노테이션은 기본 속성인 value를 아래와 같이 가질 수 있다
  • value 속성을 가진 어노테이션을 코드에서 사용할 때는 값만 기술할 수 있다. 기술한 값은 value 속성에 자동으로 대입된다.
public @interface AnnotationName {
	String value();
	int prop2() default 1;
}

 

@AnnotationName("값")

 

하지만 value 속성과 다른 속성의 값을 동시에 주고 싶다면 value 속성 이름을 반드시 언급해야 한다.

@AnnotationName(value = "값", prop2 = 3)

 

어노테이션 적용 대상

  • 어노테이션은 설정 정보이다.
  • 어떤 대상에 설정 정보를 적용할 것인지(클래스에 적용할지 메소드에 적용할지..) 명시해야 한다.
  • 적용할 수 있는 대상의 종류는 아래의 표처럼 ElementType 열거 상수로 정의되어 있다.
ElementType 열거 상수 적용 요소
TPYE 클래스, 인터페이스, 열거 타입
ANNOTATION_TYPE 어노테이션
FIELD 필드
CONSTRUCTOR 생성자
METHOD 메소드
LOCAL_VARIABLE 로컬 변수
PACKAGE 패키지

 

  • 적용대상을 지정할 때는 @Target 어노테이션을 사용한다.
  • @Target의 기본 속성인 value는 ElementType 배열을 값으로 가진다.(적용 대상을 복수 개로 지정하기 위함)
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target ({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName {
	String value();
	int prop2() default 1;
}

이 어노테이션은 클래스, 필드, 메소드에 적용할 수 있다.

 

어노테이션 유지 정책

  • 어노테이션을 정의할 때 한 가지 더 추가해야 할 내용은 어노테이션을 언제까지 유지할 것인지 이다.
  • 어노테이션 유지 정책은 RetentionPolicy 열거 상수로 아래와 같이 정의되어 있다.
RetentionPolicy 열거 상수 어노테이션 적용 시점 어노테이션 제거 시점
SOURCE 컴파일할 때 적용 컴파일된 후에 제거됨
CLASS 메모리로 로딩할 때 적용 메모리로 로딩된 후에 제거됨
RUNTIME 실행할 때 적용 계속 유지됨

 

  • 유지 정책을 지정할 때에는 @Retention 어노테이션을 사용한다.
  • @Retention의 기본 속성인 value는 RetentionPolicy 열거 상수 값을 가진다.
  • 아래의 코드는 어노테이션 설정 정보를 이용할 수 있도록 유지 정책을 RUNTIME으로 지정한 것이다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target ({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName {
	String value();
	int prop2() default 1;
}

 

 

어노테이션 설정 정보 이용

  • 어노테이션은 아무런 동작을 가지지 않는 설정 정보이다.
  • 이 설정 정보를 이용해서 어떻게 처리할 것인지는 어플리케이션의 몫이다.
  • 어플리케이션은 리플렉션을 이용해서 적용 대상으로부터 어노테이션의 정보를 아래의 메소드로 얻어낼 수 있다.
리턴 타입 메소드명(매개변수) 설명
boolean isAnnotationPresent(AnnotationName.class) 지정한 어노테이션이 적용되었는지 여부
Annotation getAnnotation(AnnotationName.class) 지정한 어노테이션이 적용되어 있으면 어노테이션을 리턴하고, 그렇지 않다면 null을 리턴
Annotation[] getDeclaredAnnotations() 적용된 모든 어노테이션을 리턴

 

PrintAnnotation.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
	String value() default "-";
	int number() default 15;
}

 

Service.java

public class Service {
	@PrintAnnotation
	public void method1() {
		System.out.println("실행 내용1");
	}
	
	@PrintAnnotation("*")
	public void method2() {
		System.out.println("실행 내용2");
	}
	
	@PrintAnnotation(value="#", number=20)
	public void method3() {
		System.out.println("실행 내용3");
	}
}

 

PrintAnnotationExample.java

import java.lang.reflect.Method;
public class PrintAnnotationExample {
	public static void main(String[] args) throws Exception {
		Method[] declaredMethods = Service.class.getDeclaredMethods();
		for(Method method : declaredMethods) {
			//PrintAnnotation 얻기
			PrintAnnotation printAnnotation = method.getAnnotation
					(PrintAnnotation.class);

			//설정 정보를 이용해서 선 출력
			printLine(printAnnotation);

			//메소드 호출
			method.invoke(new Service());
			
			//설정 정보를 이용해서 선 출력
			printLine(printAnnotation);
		}
	}
	
	public static void printLine(PrintAnnotation printAnnotation) {
		if(printAnnotation != null) {
			//number 속성값 얻기
			int number = printAnnotation.number();
			for(int i=0; i<number; i++) {
				//value 속성값 얻기
				String value = printAnnotation.value();
				System.out.print(value);
			}
			System.out.println();
		}
	}
}
/*
####################
실행 내용3
####################
***************
실행 내용2
***************
---------------
실행 내용1
---------------
*/

댓글