클래스를 선언할 때 extends
키워드로 다른 클래스를 상속하지 않으면 암묵적으로 java.lang.Object
클래스를 상속하게 된다. 따라서, 모든 클래스는 Object
의 자손 클래스이다.
Object
^
|
------------------------------
| | | |
System String Number ...
모든 객체들은 Object
가 가진 메서드를 사용할 수 있다.
메서드 | 용도 |
---|---|
boolean equals(Object ob) | 객체의 번지를 비교하고 결과를 리턴 |
int hashCode() | 객체의 해시코드를 리턴 |
String toString() | 객체 문자 정보를 리턴 |
equals()
메서드의 매개변수 타입이 Object
이므로 자동 타입 변환에 의해 모든 객체가 매개값으로 대입될 수 있다. equals
메서드는 비교 연산자인 ==
과 동일한 결과를 반환한다.
일반적으로 Object
의 equals
메서드는 오버라이드해서 동등 비교용으로 사용된다. 동등 비교란 비록 객체가 달라져 주소값이 다르더라도, 내부의 데이터가 같은 지를 비교하는 것을 말한다. String
의 경우 equals
는 두 문자열 객체 간의 주소 비교가 아닌 문자열 데이터의 비교로 이루어진다.
hashCode()
는 객체 해시코드로 객체를 식별하는 정수를 말한다. Object
의 hashCode
메서드는 객체의 메모리 번지를 이용해서 해시코드를 생성하기 때문에 객체마다 다른 정수값을 리턴한다.
hashCode
도 equals
와 마찬가지로 동등성 비교를 위해서 사용되는데, hashCode
도 각 클래스마다의 오버라이드를 통해서 어떤 정수값을 리턴할 지 구현하는 것이 일반적이다. 동등성 비교에 있어서 일반적인 패턴은 hashCode
메서드를 호출해서 두 객체 간의 정수 값이 같다면 '같은 객체'로 보고 equals
판단을 하게 된다. equals
의 경우는 내부 데이터를 기준으로 하기 때문에 두 객체를 동등한 객체인지 아닌 지 확인하기 위해 사용한다.
hashCode() 리턴값 --동일--> equals() 리턴값 --동일--> 동등 객체
| |
다름 다름
| |
| v
-----------------> 다른 객체
추후에 나올 HashSet
은 '동등 객체'를 중복으로 저장하지 않는다. HashSet
은 hashCode
와 equals
메서드를 이용해서 등등 객체인지 아닌지 판단한다.
toString()
메서드는 객체의 문자 정보를 반환한다. 객체의 문자 정보란 객체를 문자열로 표현한 값을 말한다. 기본적으로 Object
의 toString
메서드는 클래스명@16진수해시코드
로 구성된 문자열르 반환한다.
Object obj = new Object();
System.out.println(obj.toString()); // java.lang.Object@de6ced
toString
메서드를 오버라이딩하여 객체 설명에 필요한 정보들을 쓰는 것이 일반적인 방법이다.
데이터 전달을 위한 DTO(data transfer object)를 작성할 때 반복적으로 사용되는 코드를 줄이기 위해 Java14
부터 'record'가 도입되었다. 가령 사람의 정보를 전달하기 위해 Person
DTO과 다음과 같다고 하자.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return this.name;
}
public int age() {
return this.age;
}
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object obj) {
...
}
@Override
public String toString() {
...
}
}
다음은 Person
클래스로 내부 field인 name
, age
를 가지고 있다. name
, age
는 입력 초반에만 설정되고 바뀌지 않으므로 final
로 설정되어있다. 각 field에 대한 getter가 있고 DTO인 Person
에 대한 hashCode
, equals
, toString
오버라이드 메서드가 있다.
이러한 DTO 코드가 많아짐에 따라, java에서는 java14부터 record를 도입하여 효율적으로 처리하도록 한 것이다. class
대신에 record
를 사용하고 생성자의 입력을 받고 getter
public record Person(String name, int age) {
}
해당 코드는 바로 위의 코드와 동일한 코드이다. 선언된 record에 매개변수로 입력된 것들은 하나의 final 맴버 변수가 되어, 생성자, getter가 자동으로 추가된다. 또한, hashCode
, equals
, toString
메서드를 오버라이드한 코드도 자동으로 추가된다.
public class Main {
public static void main(String[] args) {
Person person = new Person("gyu", 10);
System.out.println("name: " + person.name() + " " + "age: " + Integer.toString(person.age()));
System.out.println("toString: " + person.toString());
Person person1 = new Person("gyu", 10);
if(person.equals(person1)) {
System.out.println("person is equal with person1");
}
}
}
결과는 다음과 같다.
name: gyu age: 10
toString: Person[name=gyu, age=10]
person is equal with person1
나중에 '롬복'이라는 모듈을 추가해서 자동으로 setter, getter, toString, equals 등을 만들어줄 수 있는데 record
와의 가장 큰 차이점은 record
는 맴버 변수가 final
로 설정되는 반면에 lombok
은 아니라는 것이다.
java는 JVM위에서 동작하기 때문에 OS별 함수 등을 직접 동작시킬 수 없다. 대신에 System
을 통해서 일부 동작들에 대해서 도움을 받을 수 있다. 대표적으로 system properties를 읽을 수 있는데, system properties란 자바 프로그램이 시작될 때 자동으로 설정되는 시스템의 속성을 말한다. 즉, 운영체제 정보, 사용자 정보, 자바 버전, 기본 사양 등과 같은 값이다.
속성 이름 | 설명 | 값 |
---|---|---|
java.specification.version | 자바 스펙 버전 | 17 |
java.home | JDK 디렉터리 경로 | C:\Program Files\Java\jdk-17.0.3 |
os.name | 운영체제 | Windows 10 |
user.name | 사용자 이름 | xxx |
user.home | 사용자 홈 디렉터리 경로 | C:\Users\xxx |
user.dir | 현재 디렉터리 경로 | C:\ThisisJavaSecondEdition\workspace\thisisjava |
다음의 예시는 system properties의 전체 값을 ㄹ익는 코드이다.
import java.util.Properties;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Properties props = System.getProperties();
Set keys = props.keySet();
for (Object okey : keys) {
String skey = (String) okey;
String v = System.getProperty(skey);
System.out.println("key: " + skey + " | v: " + v);
}
}
}
쭉 모든 system properties 값들이 나올 것이다.
key: java.specification.version | v: 23
key: sun.jnu.encoding | v: UTF-8
...
key: socksNonProxyHosts | v: local|*.local|169.254/16|*.169.254/16
key: java.class.version | v: 67.0
java에서 문자열 관련 주요 클래스는 다음과 같다.
클래스 | 설명 |
---|---|
String | 문자열을 저장하고 조작할 때 사용 |
StringBuilder | 효율적인 문자열 조작 기능이 필요할 때 사용 |
StringTokenizer | 구분자로 연결된 문자열을 분리할 때 사용 |
String 클래스는 문자열을 저장하고 조작할 때 사용한다. 프로그램을 개발하다보면 byte[]
을 문자열로 변환하는 경우가 종종있다. 이는 네트워크 통신으로 얻은 byte 배열을 문자열로 변환할 때 자주 사용한다. 이때 String
생성자 중에서 다음 두 가지를 사용해 String
객체로 변환해줄 수 있다.
String data = "java";
byte[] arr1 = data.getBytes();
// 방법1, byte배열을 'UTF-8'로 인코딩
String str = new String(bytes);
// 방법2 byte배열을 두번째 인자로 인코딩
String str1 = new String(arr1, "EUC-KR");
이것은 꽤 유의미한데, 한글의 경우 UTF-8에서는 3바이트가 되고, EUC-KR에서는 2바이트가 된다. 따라서, 인코딩할 떄 사용한 인코딩 타입으로 문자셋으로 디코딩 해야 올바른 한글을 복원할 수 있다.
String은 내부 문자열을 수정할 수 없다. 즉, 불변성을 가진 immutable 객체라는 것이다. 다음의 코드를 보도록 하자.
String data = "ABC";
data += "DEF";
위의 코드는 사실 두개의 String 객체가 만들어진다.
--------- String(ABC)
/
X
/
data
\
\
---------> String(ABCDEF)
String(ABC)
에 DEF
를 추가한 것이 아니라, ABCDEF
string객체를 새로만들고, data
가 레퍼런싱하는 객체를 바꿔준 것이 전부이다. 따라서, 가비지 컬렉터가 동작하면 String(ABC)
는 사라지게 되고, String(ABCDEF)
만 남게된다. 이는 효율적으로 좋지 않다.
잦은 문자열 변경 작업이 있다면 String
보다는 StringBuilder
를 사용하는 것이 좋다. StringBuilder
는 내부 buffer에 문자열을 저장해두록 그 안에서 추가, 수정, 삭제 작업을 하도록 설계되어 있다. 따라서 String
처럼 새로운 객체를 만들지 않고도 문자열을 조작할 수 있다. StringBuilder
가 제공하는 조작 메서드는 다음과 같다.
리턴 타입 | 메서드(매개 변수) | 설명 |
---|---|---|
StringBuilder | append | 문자열을 끝에 추가 |
StringBuilder | insert | 문자열을 지정된 위치에 추가 |
StringBuilder | delete | 문자열의 일부를 삭제 |
StringBuilder | replace | 문자열의 일부를 대체 |
String | toString | 완성된 문자열을 리턴 |
StringBuilder
는 문자열 조작 메서드들을 실행한 후에 문자열을 반환하기 위해서 toString()
을 사용해야한다. 메서드 체이닝을 지원하기 때문에 다음과 같이 사용할 수 있다.
import java.util.Properties;
import java.util.Set;
public class Main {
public static void main(String[] args) {
String data = new StringBuilder().append("DEF").insert(0, "ABC").delete(3,4).toString();
System.out.println(data); // ABCEF
}
}
StringBuilder()
: StringBuilder
객체를 생성한다.append("DEF")
: DEF
문자열을 추가한다.insert(0, "ABC")
: 0번째 인덱스에 ABC
를 추가한다.delete(3,4)
: 3번째 인덱스에서 4번째 인덱스까지를 삭제한다. 단, 4번재 인덱스는 포함 XtoString
: 문자열 객체로 반환만약, 문자열이 구분자(delimeter)로 연결된 경우, 구분자를 기준으로 문자열을 분리하려면 String
의 split
메서드를 이용하거나, StringTokenizer
를 사용하면 된다. split
은 정규 표현식으로 구분하고 StringTokenizer
는 문자로 구분한다는 차이점이 있다.
split
은 다음과 같이 다양한 구분자가 필요한 경우 정규 표현식으로 파싱이 가능하다.
public class Main {
public static void main(String[] args) {
String data = "ABC&DEF,GHI,JK-LMN";
String[] names = data.split("&|,|-");
for(String name: names) {
System.out.println(name);
}
}
}
결과가 다음과 같이 나온다.
ABC
DEF
GHI
JK
LMN
구분자를 통해 문자열들이 구분된 것을 볼 수 있다.
그러나, 다음과 같이 여러 종류가 아닌 한 종류의 구분자만이 있다면 StringTokenizer
를 사용할 수도 있다.
public class Main {
public static void main(String[] args) {
String data = "ABC/DEF/GHI/JK/LMN";
StringTokenizer st = new StringTokenizer(data, "/");
while (st.hasMoreTokens()) {
String token = st.nextToken();
System.out.println(token);
}
}
}
StringTokenizer
객체의 메서드인 nextToken
는 구분자로 분리한 문자열을 가져오는데, 만약 더 이상 없다면 exception을 반환한다. 따라서, nextToken
을 호출하기 이전에 hasMoreTokens
를 호출하여 확인하는 것이 좋다.
결과는 다음과 같다.
ABC
DEF
GHI
JK
LMN
java의 기본 타입인 byte
, char
, short
, int
, long
, double
, boolean
의 값을 갖는 객체를 생성할 수 있다. 이러한 객체는 원래의 값에 추가적인 기능을 제공하기 위해 사용하여 원래의 값을 포장한다는 점에서 wrapper 클래스로 불린다.
다음이 대표적인 wrapper 클래스이다.
기본 타입 | 포장 클래스 |
---|---|
byte | Byte |
char | Character |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
wrapper 객체는 포장하고 있는 기본 타입의 값을 변경할 수 없고, 단지 객체로 생성하는 데 목적이 있다. 이런 객체가 필요한 이유는 collection객체 때문인데, collection 객체는 기본 타입의 값은 저장할 수 없고, 객체만 저장할 수 있다.
기본 타입의 값을 wrapper 객체로 만드는 과정을 boxing
이라고 하고, 반대로 wrapper 객체에서 기본 타입의 값을 얻어 내는 과정을 unboxing
이라고 한다. 참고로 박싱과 언박싱에 별다른 메서드는 필요 없다.
Integer obj = 100l // 박싱
int value = obj; // 언박싱
언박싱은 다음과 같이 연산과정에서도 발생한다. obj
는 50이 되기 전에 언박싱된다.
int value = obj + 50;
public class Main {
public static void main(String[] args) {
// Boxing
Integer obj = 100;
System.out.println("Value: "+ obj ); // Value: 100
// Unboxing
int value = obj;
System.out.println("Value: " + value); // Value: 100
// 연산 시 Unboxing
int res = obj + 100;
System.out.println("result: " + res); // result: 200
}
}
wrapper 객체와 기본 타입 간에는 기본적으로 boxing과 unboxing이 그렇게 까다롭지 않아서 자유롭게 사용할 수 있다.
단, wrapper 객체도 엄연히 객체이기 때문에 ==
와 !=
같은 비교 연산자는 wrapper 내부의 기본 타입 값끼리의 비교가 아닌 객체의 메모리 주소 값 비교이다.
Integer o1 = 2000;
Integer o2 = 2000;
System.out.println(o1 == o2); // false
대신 wrapper 클래스에서는 equals
메서드로 내부 값을 비교할 수 있다.
public class Main {
public static void main(String[] args) {
// Boxing
Integer o1 = 2000;
Integer o2 = 2000;
System.out.println(o1 == o2); // false
System.out.println(o1.equals(o2)); // true
}
}
단, 효율성을 위해 다음의 범위에 대해서는 wrapper 객체끼리의 공유되어 사용되어 ==
비교연산을 해도 동일하다는 결과가 나올 수 있다.
타입 | 값의 범위 |
---|---|
boolean | true, false |
char | \u0000~\u007f |
byte, short, int | -128~127 |
단, 이는 언제나 wrapper 객체 간의 메모리 비교이기 때문에 equals
로 비교하는 것이 좋다.
java는 클래스와 인터페이스의 메터 정보를 Class
객체로 관리한다. 여기서 메타 정보란 패키지 정보, 타입 정보, 맴버(생성자, field, method) 등을 말한다. 이러한 메터 정보를 프로그램에서 읽고 수정하는 행위를 리플렉션(reflection)이라고 한다.
프로그램에서 Class
객체를 얻으려면 다음 3가지 방법 중 하나를 이용하면 된다.
// 클래스로부터 얻는 방법
Class clazz = 클래스이름.class;
Class clazz = Class.forName("패키지..클래스이름");
// 객체로부터 얻는 방법
Class clazz = 객체참조변수.getClass();
가령 String
클래스의 Class
객체는 다음과 같이 얻을 수 있다.
Class clazz1 = String.class;
Class clazz2 = Class.forName("java.lang.String");
String str = "감자바";
Class clazz3 = str.getClass();
이러한 Class
객체를 통해서, 해당 클래스의 메타정보들을 읽을 수 있는데 패키지와 티입(클래스, 인터페이스) 이름 정보는 다음의 메서드를 통해 얻을 수 있다.
메서드 | 용도 |
---|---|
Package getPackage() | 패키지 정보 읽기 |
String getSimpleName() | 패키지 정보를 제외한 타입 이름 |
String getName() | 패키지 정보를 포함한 전체 타입 이름 |
ch12.sec11.exam01
패키지를 만들어주도록 하자.
package ch12.sec11.exam01;
public class Car {
}
이제 해당 패키지를 불러와서 패키지 메타 정보를 리플렉션을 통해 읽어보도록 하자.
import ch12.sec11.exam01.Car;
public class Main {
public static void main(String[] args) {
Class clazz = Car.class;
System.out.println("패키지" + clazz.getPackage().getName()); // 패키지ch12.sec11.exam01
System.out.println("클래스 풀네임" + clazz.getName()); // 클래스 풀네임ch12.sec11.exam01.Car
System.out.println("클래스 이름 " + clazz.getSimpleName()); // 클래스 이름 Car
}
}
맴버 정보를 얻는 방법은 다음과 같다.
메서드 | 용도 |
---|---|
Constructor[] getDeclaredConstructors() | 생성자 정보 읽기 |
Field[] getDeclaredFields() | 필드 정보 읽기 |
Method[] getDeclaredMethods() | 메서드 정보 읽기 |
확인을 위해 Car
클래스에 생성자와 field, 메서드를 추가해주도록 하자.
package ch12.sec11.exam01;
public class Car {
//field
private String model;
private String owner;
// constructor
public Car() {
}
public Car(String model) {
this.model = model;
}
// method
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;}
}
import ch12.sec11.exam01.Car;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
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.println(constructor.getName() + "(");
Class[] parameters = constructor.getParameterTypes();
printParameters(parameters);
System.out.println(")");
}
System.out.println();
System.out.println("field 정보");
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.println(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.println(parameters[i].getName());
if (i < parameters.length - 1) {
System.out.println(".");
}
}
}
}
결과는 다음과 같다.
생성자 정보
ch12.sec11.exam01.Car(
)
ch12.sec11.exam01.Car(
java.lang.String
)
field 정보
java.lang.String model
java.lang.String owner
메서드 정보
getOwner(
)
setOwner(
java.lang.String
)
setModel(
java.lang.String
)
getModel(
)
코드에서 잘보면 @
으로 작성된 요소가 있는데, 이를 어노테이션(annotation)이라고 한다. annotation은 클래스 또는 인터페이스를 컴파일하거나 실행할 때 어떻게ㅐ 처리해야할 것인지를 알려주는 설정 정보이다. annotation은 다음 3가지 용도로 주로 사용된다.
가령 컴파일 시에 사용하는 정보 전달의 대표적인 예는 @Override
어노테이션이다. @Override
는 컴파일러가 메서드 오버라이드를 검사하도록 설정한다. 만약 확실히 오버라이드되지 않았다면 컴파일러는 에러를 발생시킨다.
annotation도 하나의 타입이므로 annotation을 사용하기 위해서는 먼저 정의부터 해야한다. annotation을 정의하는 방법은 interface
를 정의하는 방법과 유사하다. 다음과 같이 @interface
뒤에 사용할 annotation이 온다.
public @interface AnnotationName {
}
다음의 annotation은 @AnnotationName
와 같이 사용할 수 있다.
annotation은 property를 가질 수 있는데, 다음과 같이 property를 정의할 수 있다.
public @interface AnnotationName {
String prop1();
int prop2() default 1;
}
prop1
, prop2
는 하나의 property로 해당 annotation을 사용할 때 입력처럼 받을 수 있다.
prop1
은 기본값이 없기 때문에 반드시 값을 기술해야한다. 단, default 값이 있다면 생략이 가능하다.
@AnnotationName(prop1="값");
@AnnotationName(prop1="값", prop2=3);
annotation은 기본 property인 value
를 다음과 같이 가질 수 있다.
public @interface AnnotationName {
String value();
int prop2() default 1;
}
value
속성을 가진 annotation을 코드에서 사용할 때에는 다음과 같이 값만 기술할 수 있다. 이 값은 value
속성에 자동으로 대입된다.
@AnnotationName("값");
하지만 value
속성과 다른 속성의 값을 동시에 주고 싶다면, value
속성 이름을 반드시 언급해야한다.
@AnnotationName(value="값", prop2=3);
java에서 annotation은 설정 정보라고 했다. 그렇다면 어떤 대상에 설정 정보를 적용할 것인지, 즉 클래스에 적용할 것인지, 메서드에 적용할 것인지 명시해야한다. 적용할 수 있는 대상의 종류는 ElementType
열거 상수로 정의되어 있다.
ElementType 열거 상수 | 적용 요소 |
---|---|
TYPE | 클래스, 인터페이스, 열거 타입 |
ANNOTATION_TYPE | 어노테이션 |
FIELD | 필드 |
CONSTRUCTOR | 생성자 |
METHOD | 메서드 |
LOCAL_VARIABLE | 로컬 변수 |
PACKAGE | 패키지 |
적용 대상을 지정할 때는 @Target
annotation을 사용한다. @Target
의 기본 속성인 value
는 ElementType
배열을 값으로 가진다. 이것은 적용 대상을 복수 개로 지정하기 위함이다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName {
}
AnnotationName
annotation은 class
, field
, method
에만 적용이 가능하고, 생성자는 적용이 불가능하다.
@AnnotationName
public class ClassName {
@AnnotationName
private String field;
// 불가 @AnnotationName
public ClassName() { }
@AnnotationName
public void methodName() {}
}
annotation을 정의할 때 한 가지 더 추가해야 할 내용은 @AnnotationName
을 언제까지 유지할 것인지를 결정하는 것이다. annotation 유지 정책은 RetentionPolicy
열거 상수로 다음과 같이 정의되어 있다.
RetentionPolicy 열거 상수 | annotation 적용 시점 | annotation 제거 시점 |
---|---|---|
SOURCE | 컴파일 할 때 사용 | 컴파일된 후에 제거됨 |
CLASS | 메모리로 로딩할 때 사용 | 메모리로 로딩된 후에 제거됨 |
RUNTIME | 실행할 때 적용 | 계속 유지됨 |
유지 정책을 지정할 때에는 @Retention
annotation을 사용한다. @Retention
의 기본 속성인 value는 RetentionPolicy
열거 상수 값을 가진다. 다음은 실행 시에도 annotation 설정 정보를 유지할 수 있도록 유지 정책을 RUNTIME
으로 지정한 예이다.
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName {
}
annotation 내부에는 property이외에는 구현이나 메서드를 가질 수 없는데, Annotation
의 설정 정보를 어떻게 처리할 지는 reflection을 이용해서 더 다양한 구현이 가능하다. 리플렉션은 적용 대상으로부터 annotaion의 정보를 다음 메서드로 얻어낼 수 있다.
리턴 타입 | 메서드명(매개변수) | 설명 |
---|---|---|
boolean | isAnnotationPresent(AnnotationName.class) | 지정한 annotation이 적용되었는 지 확인 |
Annotation | getAnnotation(AnnotationName.class) | ㅣ정한 annotation이 있다면, 어노테이션을 리턴하고, 그렇지 않다면 null 을 반환 |
Annotation[] | getDeclaredAnnotations() | 적용된 모든 annotation 반환 |
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
String value() default "-";
int number() default 15;
}
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");
}
}
Service
의 메서드들에 모두 @PrintAnnotation
을 적용했지만, 딱히 변화되는 것은 없다. 다른 코드에서 해당 리플렉션을 통해서 Service
에 적용된 annotation인 @PrintAnnotation
설정 정보를 얻어낸 후, 로그를 찍어주도록 하자.
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Method[] declaredMethods = Service.class.getDeclaredMethods();
for(Method method: declaredMethods) {
PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);
method.invoke(new Service());
printLine(printAnnotation);
}
}
public static void printLine(PrintAnnotation printAnnotation) {
if (printAnnotation != null ) {
int number = printAnnotation.number(); // annotation property
System.out.println("number:" + number);
for(int i = 0; i < number; i++) {
String value = printAnnotation.value(); // annotation property
System.out.print(value);
}
System.out.println();
}
}
}
위의 코드에서 아무것도 의미가 없던 지시자인 @PrintAnnotation
를 리플렉션으로 찾아 메서드를 호출시켜주고, @PrintAnnotation
의 property에 따라 출력 값을 다르게 뽑아주고 있다.
그 결과는 다음과 같다.
실행 내용1
number:15
---------------
실행 내용2
number:15
***************
실행 내용3
number:20
####################