Java 재활 훈련 9일차 - Object class, record, System properties, StringBuilder, StringTokenizer, Wrapper class, reflection, Annotation

0

java

목록 보기
9/18

Library

Object class

클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암묵적으로 java.lang.Object 클래스를 상속하게 된다. 따라서, 모든 클래스는 Object의 자손 클래스이다.

Object
   ^
   |
   ------------------------------
   |        |         |        |
System    String    Number    ...

모든 객체들은 Object가 가진 메서드를 사용할 수 있다.

메서드용도
boolean equals(Object ob)객체의 번지를 비교하고 결과를 리턴
int hashCode()객체의 해시코드를 리턴
String toString()객체 문자 정보를 리턴

equals() 메서드의 매개변수 타입이 Object이므로 자동 타입 변환에 의해 모든 객체가 매개값으로 대입될 수 있다. equals 메서드는 비교 연산자인 ==과 동일한 결과를 반환한다.

일반적으로 Objectequals 메서드는 오버라이드해서 동등 비교용으로 사용된다. 동등 비교란 비록 객체가 달라져 주소값이 다르더라도, 내부의 데이터가 같은 지를 비교하는 것을 말한다. String의 경우 equals는 두 문자열 객체 간의 주소 비교가 아닌 문자열 데이터의 비교로 이루어진다.

hashCode()는 객체 해시코드로 객체를 식별하는 정수를 말한다. ObjecthashCode 메서드는 객체의 메모리 번지를 이용해서 해시코드를 생성하기 때문에 객체마다 다른 정수값을 리턴한다.

hashCodeequals와 마찬가지로 동등성 비교를 위해서 사용되는데, hashCode도 각 클래스마다의 오버라이드를 통해서 어떤 정수값을 리턴할 지 구현하는 것이 일반적이다. 동등성 비교에 있어서 일반적인 패턴은 hashCode 메서드를 호출해서 두 객체 간의 정수 값이 같다면 '같은 객체'로 보고 equals 판단을 하게 된다. equals의 경우는 내부 데이터를 기준으로 하기 때문에 두 객체를 동등한 객체인지 아닌 지 확인하기 위해 사용한다.

hashCode() 리턴값 --동일--> equals() 리턴값 --동일--> 동등 객체
    |                      |
   다름                    다름
    |                      |
    |                      v
    -----------------> 다른 객체

추후에 나올 HashSet은 '동등 객체'를 중복으로 저장하지 않는다. HashSethashCodeequals 메서드를 이용해서 등등 객체인지 아닌지 판단한다.

toString() 메서드는 객체의 문자 정보를 반환한다. 객체의 문자 정보란 객체를 문자열로 표현한 값을 말한다. 기본적으로 ObjecttoString메서드는 클래스명@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 메서드를 오버라이드한 코드도 자동으로 추가된다.

  • Main.java
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은 아니라는 것이다.

System properties 읽기

java는 JVM위에서 동작하기 때문에 OS별 함수 등을 직접 동작시킬 수 없다. 대신에 System을 통해서 일부 동작들에 대해서 도움을 받을 수 있다. 대표적으로 system properties를 읽을 수 있는데, system properties란 자바 프로그램이 시작될 때 자동으로 설정되는 시스템의 속성을 말한다. 즉, 운영체제 정보, 사용자 정보, 자바 버전, 기본 사양 등과 같은 값이다.

속성 이름설명
java.specification.version자바 스펙 버전17
java.homeJDK 디렉터리 경로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의 전체 값을 ㄹ익는 코드이다.

  • Main.java
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

String, StringBuilder, StringTokenizer

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가 제공하는 조작 메서드는 다음과 같다.

리턴 타입메서드(매개 변수)설명
StringBuilderappend문자열을 끝에 추가
StringBuilderinsert문자열을 지정된 위치에 추가
StringBuilderdelete문자열의 일부를 삭제
StringBuilderreplace문자열의 일부를 대체
StringtoString완성된 문자열을 리턴

StringBuilder는 문자열 조작 메서드들을 실행한 후에 문자열을 반환하기 위해서 toString()을 사용해야한다. 메서드 체이닝을 지원하기 때문에 다음과 같이 사용할 수 있다.

  • Main.java
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
    }
}
  1. StringBuilder(): StringBuilder 객체를 생성한다.
  2. append("DEF"): DEF 문자열을 추가한다.
  3. insert(0, "ABC"): 0번째 인덱스에 ABC를 추가한다.
  4. delete(3,4): 3번째 인덱스에서 4번째 인덱스까지를 삭제한다. 단, 4번재 인덱스는 포함 X
  5. toString: 문자열 객체로 반환

만약, 문자열이 구분자(delimeter)로 연결된 경우, 구분자를 기준으로 문자열을 분리하려면 Stringsplit 메서드를 이용하거나, StringTokenizer를 사용하면 된다. split은 정규 표현식으로 구분하고 StringTokenizer는 문자로 구분한다는 차이점이 있다.

split은 다음과 같이 다양한 구분자가 필요한 경우 정규 표현식으로 파싱이 가능하다.

  • Main.java
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를 사용할 수도 있다.

  • Main.java
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

Wrapper 클래스

java의 기본 타입인 byte, char, short, int, long, double, boolean의 값을 갖는 객체를 생성할 수 있다. 이러한 객체는 원래의 값에 추가적인 기능을 제공하기 위해 사용하여 원래의 값을 포장한다는 점에서 wrapper 클래스로 불린다.

다음이 대표적인 wrapper 클래스이다.

기본 타입포장 클래스
byteByte
charCharacter
shortShort
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean

wrapper 객체는 포장하고 있는 기본 타입의 값을 변경할 수 없고, 단지 객체로 생성하는 데 목적이 있다. 이런 객체가 필요한 이유는 collection객체 때문인데, collection 객체는 기본 타입의 값은 저장할 수 없고, 객체만 저장할 수 있다.

기본 타입의 값을 wrapper 객체로 만드는 과정을 boxing이라고 하고, 반대로 wrapper 객체에서 기본 타입의 값을 얻어 내는 과정을 unboxing이라고 한다. 참고로 박싱과 언박싱에 별다른 메서드는 필요 없다.

Integer obj = 100l // 박싱
int value = obj; // 언박싱

언박싱은 다음과 같이 연산과정에서도 발생한다. obj는 50이 되기 전에 언박싱된다.

int value = obj + 50;
  • Main.java
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 메서드로 내부 값을 비교할 수 있다.

  • Main.java
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 객체끼리의 공유되어 사용되어 == 비교연산을 해도 동일하다는 결과가 나올 수 있다.

타입값의 범위
booleantrue, false
char\u0000~\u007f
byte, short, int-128~127

단, 이는 언제나 wrapper 객체 간의 메모리 비교이기 때문에 equals로 비교하는 것이 좋다.

relfection

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 패키지를 만들어주도록 하자.

  • ch12.sec11.exam01 패캐지의 Car.java
package ch12.sec11.exam01;

public class Car {
}

이제 해당 패키지를 불러와서 패키지 메타 정보를 리플렉션을 통해 읽어보도록 하자.

  • Main.java
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, 메서드를 추가해주도록 하자.

  • Car.java
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;}
}
  • Main.java
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은 클래스 또는 인터페이스를 컴파일하거나 실행할 때 어떻게ㅐ 처리해야할 것인지를 알려주는 설정 정보이다. annotation은 다음 3가지 용도로 주로 사용된다.

  1. 컴파일 시 사용하는 정보를 전달
  2. 빌드 툴이 코드를 자동 생성할 때 사용하는 정보를 전달
  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의 기본 속성인 valueElementType 배열을 값으로 가진다. 이것은 적용 대상을 복수 개로 지정하기 위함이다.

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의 정보를 다음 메서드로 얻어낼 수 있다.

리턴 타입메서드명(매개변수)설명
booleanisAnnotationPresent(AnnotationName.class)지정한 annotation이 적용되었는 지 확인
AnnotationgetAnnotation(AnnotationName.class)ㅣ정한 annotation이 있다면, 어노테이션을 리턴하고, 그렇지 않다면 null을 반환
Annotation[]getDeclaredAnnotations()적용된 모든 annotation 반환
  • PrintAnnotation.java
@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");
    }
}

Service의 메서드들에 모두 @PrintAnnotation을 적용했지만, 딱히 변화되는 것은 없다. 다른 코드에서 해당 리플렉션을 통해서 Service에 적용된 annotation인 @PrintAnnotation 설정 정보를 얻어낸 후, 로그를 찍어주도록 하자.

  • Main.java
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
####################

0개의 댓글

Powered by GraphCDN, the GraphQL CDN