목차
*테스트 코드 작성시 JUnit5, Assertj 사용
Baeldung 테스트 학습
class name 활용하여 인스턴스 생성하기
Job 인터페이스
public interface Job {
String getJobType();
}
MaintenanceJob 클래스
public class MaintenanceJob implements Job{
@Override
public String getJobType() {
return "Maintenance Job";
}
}
*PaintJob, RepairJob 클래스도 반환값만 다르고 동일하게 구현
PlatinumJobCard 클래스
setJobType()에 String과 Class 타입으로 받아 인스턴스 생성 후 필드 주입하고, startJob()을 호출하는 동작을 수행한다
public class PlatinumJobCard<T extends Job>{
private T jobType;
public void setJobType(String clazzName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<T> clazz = (Class<T>) Class.forName(clazzName);
this.jobType = clazz.getDeclaredConstructor().newInstance();
}
public void setJobType(Class<T> jobTypeClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
this.jobType = jobTypeClass.getDeclaredConstructor().newInstance();
}
public String startJob() {
return "Start Platinum " + this.jobType.getJobType();
}
}
테스트 코드
- setJobType() 메소드 사용시 패키지 경로까지 정확하게 기재해야 한다
- 만약 존재하지 않는 경로를 사용한 경우 ClassNotFoundException 발생한다
public class ConstructorByClassNameTest {
@DisplayName("")
@Test
void setJobTypeByString() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
PlatinumJobCard<RepairJob> platinumJobCard1 = new PlatinumJobCard<>();
platinumJobCard1.setJobType("nextstep.ref.ex1.RepairJob");
assertThat(platinumJobCard1.startJob()).isEqualTo("Start Platinum Repair Job");
}
@DisplayName("")
@Test
void classNotFoundExceptionTest() {
PlatinumJobCard<RepairJob> platinumJobCard = new PlatinumJobCard<>();
assertThatThrownBy(() -> platinumJobCard.setJobType("nextstep.ref.ex1.Example"))
.isInstanceOf(ClassNotFoundException.class);
}
@DisplayName("")
@Test
void setJobTypeClass() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
PlatinumJobCard<RepairJob> platinumJobCard1 = new PlatinumJobCard<>();
platinumJobCard1.setJobType(RepairJob.class);
assertThat(platinumJobCard1.startJob()).isEqualTo("Start Platinum Repair Job");
}
}
*참고. 인스턴스 생성
# before java 9
clazz.newInstance()
# java 9
clazz.getDeclaredConstructor().newInstance()
clazz.newInstance() 는 자바 9버전 부터 @Deprecated
Private Constructor로 인스턴스 생성
private constructor의 경우
- 클래스의 객체가 인스턴스화하는 방식을 제한할 때 사용된다
- 싱글톤, 빌더, 팩토리 등과 같은 디자인 패턴에서 많이 활용된다
public class PrivateConstructorClass {
private PrivateConstructorClass() {
System.out.println("Used the private constructor!");
}
}
절차
① 인스턴스화 하려는 class object 가져온다
② class object에서 getDeclaredContrucotr() 정보 가져온다 → private 생성자 한 개에 대한 정보 가져옴
③ 해당 Constructor 객체에서 setAccessible() 메소드 호출하여 true 설정한다 →가시성과 접근성 확보하기 위함
④ 해당 Constructor의 newInstance() 메소드 호출하여 인스턴스 생성한다
public class PrivateConstructorTest {
@DisplayName("private 생성자로 인스턴스 생성할 수 있다")
@Test
void instanceByPrivateConstructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<PrivateConstructorClass> declaredConstructor = PrivateConstructorClass.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
PrivateConstructorClass privateConstructorClass = declaredConstructor.newInstance();
assertThat(privateConstructorClass).isNotNull();
}
}
Invoke Private Method
Reflection API 나 ReflectionTestUtils 을 사용하는 방법이 있다 (*이때 ReflectionTestUtils 테스트에서만 사용 권장)
LongArrayUtil 클래스
public class LongArrayUtil {
public static int indexOf(long[] array, long target) {
return indexOf(array, target, 0, array.length);
}
private static int indexOf(long[] array, long target, int start, int end) {
for(int i = start; i < end; i++) {
if(array[i] == target) {
return i;
}
}
return -1;
}
}
LongArrayUtil 클래스에 있는 private static method를 실행할 때
아래와 같이 메소드명과 메소드 파라미터 타입 정보를 기재 후 accessible을 true로 설정한다
→가시성과 접근성 확보하기 위함, 이때 true 설정 하지 않을 경우 IllegalAccessException 발생
public class InvokePrivateMethod {
@DisplayName("")
@Test
void invokePrivateMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
long[] arr = new long[] {1L, 2L, 3L, 4L, 5L};
Method indexOf = LongArrayUtil.class.getDeclaredMethod("indexOf",
long[].class, long.class, int.class, int.class);
indexOf.setAccessible(true);
assertThat(indexOf.invoke(LongArrayUtil.class, arr, 5L, 0, arr.length))
.isEqualTo(4);
}
@DisplayName("")
@Test
void noSuchMethodException() {
assertThatThrownBy(() -> LongArrayUtil.class.getDeclaredMethod("indexOf", long[].class))
.isInstanceOf(NoSuchMethodException.class);
}
}
*파라미터 인자를 틀리게 할 경우 NoSuchMethodException이 발생한다
*public method인 경우 setAccessible() 생략
ReflectionTestUtils 클래스의 method를 활용하면 한 줄 간소화 시킬 수 있다
@DisplayName("")
@Test
void reflectionTestUtils() {
long[] arr = new long[] {1L, 2L, 3L, 4L, 5L};
int value = ReflectionTestUtils.invokeMethod(LongArrayUtil.class, "indexOf", arr, 1L, 0, arr.length);
assertThat(value).isZero(); // ok
}
Method Parameter 확인
생성자와 setter method에 있는 파라미터 정보를 리플렉션 API 활용하여 확인해본다
public class Person {
private String fullName;
public Person(String fullName) {
this.fullName = fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
}
테스크 코드
public class MethodParameterTest {
@Test
void whenGetConstructParams_thenOk() throws NoSuchMethodException {
Class<? extends Person> clazz = Person.class;
List<Parameter> parameters = Arrays.asList(clazz.getConstructor(String.class).getParameters());
Optional<Parameter> parameter = parameters.stream().filter(Parameter::isNamePresent).findFirst();
assertThat(parameter.get().getName()).isEqualTo("fullName");
}
@Test
void whenGetMethodParams_thenOk() throws NoSuchMethodException {
Class<Person> clazz = Person.class;
List<Parameter> setFullNameParams = Arrays.asList(clazz.getMethod("setFullName", String.class).getParameters());
Optional<Parameter> parameter = setFullNameParams.stream().filter(Parameter::isNamePresent).findFirst();
assertThat(parameter.get().getName()).isEqualTo("fullName");
}
}
*isNamePresent() : 파라미터명이 있는 경우 true, 그외 false
Private Field Value 할당 (set)
타입 | Field#{method} |
byte | setByte(..) |
short | setShort(..) |
int | setInt(..) |
long | setLong(..) |
float | setFloat(..) |
double | setDouble(..) |
char | setChar(..) |
boolean | setBoolean(..) |
String (Object) | set(..) |
*primitive type의 wrapper class 사용할 경우 unboxing 지원함
*primitive type의 narrowing을 지원한다 (타입 범위 좁히기)
Person 클래스 생성
public class Person {
private byte age;
private short uidNumber;
private int pinCode;
private long contactNumber;
private float height;
private double weight;
private char gender;
private boolean active;
private String name;
public Person() {}
}
테스트 코드
public class PrivateFieldTest {
@DisplayName("")
@Test
void setPrivateField() throws NoSuchFieldException, IllegalAccessException {
// given, when
Person person = new Person();
Class<Person> clazz = Person.class;
byte ageValue = 26;
short uidNumberValue = 5555;
int pinCodeValue = 411057;
long contactNumberValue = 123456789L;
float heightValue = 6.12342f;
double weightValue = 75.254;
char genderValue = 'M';
boolean activeValue = true;
String nameValue = "HongGilDong";
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
age.setByte(person, ageValue);
Field uidNumber = clazz.getDeclaredField("uidNumber");
uidNumber.setAccessible(true);
uidNumber.setShort(person, uidNumberValue);
Field pinCode = clazz.getDeclaredField("pinCode");
pinCode.setAccessible(true);
pinCode.setInt(person, pinCodeValue);
Field contactNumber = clazz.getDeclaredField("contactNumber");
contactNumber.setAccessible(true);
contactNumber.setLong(person, contactNumberValue);
Field height = clazz.getDeclaredField("height");
height.setAccessible(true);
height.setFloat(person, heightValue);
Field weight = clazz.getDeclaredField("weight");
weight.setAccessible(true);
weight.setDouble(person, weightValue);
Field gender = clazz.getDeclaredField("gender");
gender.setAccessible(true);
gender.setChar(person, genderValue);
Field active = clazz.getDeclaredField("active");
active.setAccessible(true);
active.setBoolean(person, activeValue);
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
name.set(person, nameValue);
// then
assertThat(person).extracting("age", "uidNumber", "pinCode", "contactNumber", "height", "weight", "gender", "active", "name")
.containsExactly(ageValue, uidNumberValue, pinCodeValue, contactNumberValue, heightValue, weightValue, genderValue, activeValue, nameValue);
}
}
*마찬가지로 JVM Runtime에서 아래 예외를 던질 수 있다
- set* method의 할당 인자값 타입이 맞지 않는 경우 IllegalArgumnetException 발생
- private field 에 접근할 때 accessible = true 하지 않은 경우 IllegalAccessException 발생
Private Field Value 읽기 (get)
타입 | Field#{method} |
byte | getByte(Object o) |
short | getShort(Object o) |
int | getInt(Object o) |
long | getLong(Object o) |
float | getFloat(Object o) |
double | getDouble(Object o) |
char | getChar(Object o) |
boolean | getBoolean(Object o) |
String (Object) | get(Object o) |
아래와 같은 Person 클래스가 주어질 때
public class Person {
private byte age = 26;
private short uidNumber = 5555;
private int pinCode = 411057;
private long contactNumber = 123456789L;
private float height = 6.12342f;
private double weight = 75.254;
private char gender = 'M';
private boolean active = true;
private String name = "HongGilDong";
public Person() {}
}
Setter와 동일한 과정을 거쳐 private field의 값을 가져올 수 있다
public class PrivateFieldTest {
@Test
void getPrivateField() throws NoSuchFieldException, IllegalAccessException {
Person person = new Person();
Class<? extends Person> clazz = person.getClass();
byte ageResult = 26;
short uidNumberResult = 5555;
int pinCodeResult = 411057;
long contactNumberResult = 123456789L;
float heightResult = 6.12342f;
double weightResult = 75.254;
char genderResult = 'M';
boolean activeResult = true;
String nameResult = "HongGilDong";
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
assertThat(age.getByte(person)).isEqualTo(ageResult);
Field uidNumber = clazz.getDeclaredField("uidNumber");
uidNumber.setAccessible(true);
assertThat(uidNumber.getShort(person)).isEqualTo(uidNumberResult);
Field pinCode = clazz.getDeclaredField("pinCode");
pinCode.setAccessible(true);
assertThat(pinCode.getInt(person)).isEqualTo(pinCodeResult);
Field contactNumber = clazz.getDeclaredField("contactNumber");
contactNumber.setAccessible(true);
assertThat(contactNumber.getLong(person)).isEqualTo(contactNumberResult);
Field height = clazz.getDeclaredField("height");
height.setAccessible(true);
assertThat(height.getFloat(person)).isEqualTo(heightResult);
Field weight = clazz.getDeclaredField("weight");
weight.setAccessible(true);
assertThat(weight.getDouble(person)).isEqualTo(weightResult);
Field gender = clazz.getDeclaredField("gender");
gender.setAccessible(true);
assertThat(gender.getChar(person)).isEqualTo(genderResult);
Field active = clazz.getDeclaredField("active");
active.setAccessible(true);
assertThat(active.getBoolean(person)).isEqualTo(activeResult);
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
assertThat(name.get(person)).isEqualTo(nameResult);
}
}
*마찬가지로 JVM Runtime에서 아래 예외를 던질 수 있다
- get* method의 타입이 맞지 않는 경우 IllegalArgumnetException 발생
- private field 에 접근할 때 accessible = true 하지 않은 경우 IllegalAccessException 발생
- 그리고 getDeclaredField("없는 필드명") , getDeclaredField(null) 인 경우 NoSuchFieldException 이나 NPE 발생
Get Field's Annotations
Retention.RUNTIME 애노테이션 정보를 리플렉션 API 통해 읽을 수 있다
@Retention(RetentionPolicy.RUNTIME)
public @interface FirstAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface SecondAnnotation {
}
@Retention(RetentionPolicy.SOURCE)
public @interface ThirdAnnotation {
}
멤버 필드에 애노테이션을 모두 표기한다
public class ClassWithAnnotations {
@FirstAnnotation
@SecondAnnotation
@ThirdAnnotation
private String classMember;
}
테스트 코드
public class RefAnnotationsTest {
@Test
void whenCallingGetDeclaredAnnotations_thenOnlyRuntimeAnnotationsAreAvailable() throws NoSuchFieldException {
Class<ClassWithAnnotations> clazz = ClassWithAnnotations.class;
Field classMemberField = clazz.getDeclaredField("classMember");
Annotation[] annotations = classMemberField.getAnnotations();
assertThat(annotations).hasSize(2);
}
@Test
void whenCallingIsAnnotationPresent_thenOnlyRuntimeAnnotationsAreAvailable() throws NoSuchFieldException {
Class<ClassWithAnnotations> clazz = ClassWithAnnotations.class;
Field classMemberField = clazz.getDeclaredField("classMember");
assertThat(classMemberField.isAnnotationPresent(FirstAnnotation.class)).isTrue();
assertThat(classMemberField.isAnnotationPresent(SecondAnnotation.class)).isTrue();
assertThat(classMemberField.isAnnotationPresent(ThirdAnnotation.class)).isFalse(); // Retention.SOURCE
}
}
참고
https://www.baeldung.com/java-objects-make-using-class-name
https://www.baeldung.com/java-private-constructor-access
https://www.baeldung.com/java-call-private-method
https://www.baeldung.com/java-invoke-static-method-reflection
https://www.baeldung.com/java-parameter-reflection
https://www.baeldung.com/java-set-private-field-value
https://www.baeldung.com/java-reflection-read-private-field-value
https://www.baeldung.com/java-get-field-annotations
https://www.baeldung.com/java-reflection-class-fields
'공부 > Java' 카테고리의 다른 글
[넥스트스탭] 자동차 경주 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
---|---|
[넥스트스탭] 숫자 야구 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[Java] Stream Quiz 개인 풀이 (출처. 망나니 개발자 기술 블로그) (0) | 2023.08.15 |
[Java] Stream 최종 연산(Terminal Operation) (0) | 2023.08.05 |
[Java] Stream 중간 연산 (Stream Intermediate Operation) (0) | 2023.08.03 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!