Programming-[Backend]/Java

자바 기초 강의 정리 - 7. 리플렉션, 어노테이션, 클래스 로더

컴퓨터 탐험가 찰리 2024. 7. 14. 21:42
728x90
반응형

 

 

인프런 얄코의 제대로 파는 자바 강의를 듣고 정리한 내용이다.

 

중요하거나 실무를 하면서 놓치고 있었던 부분들 위주로만 요약 정리한다.

자세한 내용은 강의를 직접 수강하는 것이 좋다.

https://www.inflearn.com/course/%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%8C%8C%EB%8A%94-%EC%9E%90%EB%B0%94/dashboard

 

 


 

 

1. 리플렉션

 

런타임에 클래스, 인터페이스, 메소드, 필드 등을 분석하고 조작할 수 있는 기능이다. 적절히 사용하지 않으면 성능, 보안 등에 문제가 발생할 수 있다.

 

 

1.1 Class 자료형

reflection을 이용하면 class 자료형을 통해 특정 클래스의 정보들을 불러올 수 있다.

public class reflection {

    public static void main(String[] args) throws ClassNotFoundException, 
    NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Button> btnClass1 = Button.class;
        //  예외 던짐 : ClassNotFoundException
        Class<?> btnClass2 = Class.forName("org.example.reflection.Button");

        boolean areSame = btnClass1 == btnClass2;
        
        Constructor<?>[] constructors = btnClass1.getConstructors();
        // NoSuchMethodExecption 발생 가능
        Constructor<?> constructorArgs = btnClass1.getConstructor(String.class, int.class);
        
        // InvocationTargetException, InstantiationException, IllegalAccessException
        Button btn1A = (Button) constructors[0].newInstance("Enter", 3);
    }
}

 

 

  • Class.forName 메서드로 특정 위치의 클래스를 불러온다. 그리고 ? 와일드카드를 사용하여 어떤 클래스가 올 지 모름을 표시할 수 있다.
  • getConstructors, getConstructor 메서드로 클래스의 모든 생성자를 가져오거나, 특정 인자를 받는 클래스를 가져올 수 있다.
  • Constructor에 newInstance() 메서드를 사용하여  인스턴스를 만들 수 있다. 다만 해당하는 클래스로 캐스팅을 해줘야한다. InvocationTargetException, InstantiationException, IllegalAccessException을 발생시킬 수 있다.

 

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //위 코드에 이어서 중략...
		
        // 필드 불러오기
        Field[] fields = btnClass1.getDeclaredFields();

        for (var f: fields) {
            System.out.printf("이름: %s,\n 타입: %s,\n 클래스: %s \n", f.getName(), f.getType(), f.getDeclaringClass());
        }
        
        //메서드 불러오기
        Method[] methods = btnClass1.getDeclaredMethods();

        for (var m : methods) {
            System.out.printf("메서드명: %s, \n 타입: %s \n 반환 타입: %s \n\n",
                    m.getName(),
                    Arrays.stream(m.getParameterTypes())
                            .map(Class::getSimpleName)
                            .collect(Collectors.joining(", ")),
                    m.getReturnType()
            );
        }
        
        //필드 기반으로 인스턴스의 값 얻어오기, 필드값 설정
        Object btn2 = btnClass2.getConstructor(String.class, int.class).newInstance("Tab", 4);
        Field btn2Disabled = btnClass2.getField("disabled");
        boolean btn2DisabledValue = (boolean) btn2Disabled.get(btn2);

        btn2Disabled.set(btn2, true);
        
        //메서드 호출
        Method onClick = btnClass2.getMethod("onClick", boolean.class, int.class, int.class);
        onClick.invoke(btn2, false, 123, 456);
    }

 

  • getDeclaredFields() : 클래스에 있는 필드의 정보들을 불러올 수 있다.
  • getDeclaredMethods() : 메서드 정보들을 불러온다.
  • Field 클래스를 기반으로해서 특정 인스턴스의 해당 필드값을 얻어올 수도 있다. 그리고 그 필드의 값을 설정할 수도 있다.
  • private field 경우 필드에 접근이 불가한데, .setAccessible() 메서드를 적용해서 강제로 접근도 가능하다.
  • invoke() 메서드를 Method 클래스에 적용하여 인스턴스의 메서드가 실행되도록 할 수도 있다.
  • getSuperClass(), getInterfaces() 메서드로 해당 클래스의 부모, 인터페이스들의 정보도 얻어올 수 있다.

 

2.  어노테이션

 

2.1 어노테이션

직접 어노테이션을 만들어볼 수 있다.

  

@interface를 붙여서 어노테이션을 만들어준다. intellij의 기능으로 새로운 파일을 만들 때 Annotation 타입으로 만들어도 된다.

public @interface CustomAnnt {
}

 

 

어노테이션에 아무런 설정을 하지 않을 경우, 아래 처럼 클래스나 필드 모두에 어노테이션을 달아줄 수 있다.

@CustomAnnt
public class MyClass {

    @CustomAnnt
    int field;
}

 

 

 

2.2 메타 어노테이션

어노테이션에 제약을 걸어주는 어노테이션을 메타 어노테이션이라 한다.

 

@Retention

어노테이션이 유지되는 범위를 정해준다.

@Retention(RetentionPolicy.SOURCE)
public @interface CustomAnnt {

}

 

  • SOURCE: 소스 파일에만 적용한다. 컴파일러가 컴파일 전에 확인하라고 달아준다. 실제 컴파일 후 코드에는 영향을 미치지 않는다. ex) @Override
  • CLASS: 기본 값이며 컴파일 타임에 읽힐 목적으로 사용한다. ex) Lombok의 생성 코드 @Getter 등
  • RUNTIME: 실행 시에 사용하도록 적용하는 목적이다.

 

파일을 Compile한 뒤 build 폴더에 들어가서 각 @Retention 정책이 적용된 클래스를 확인해보면, SOURCE 정책이 적용된 필드는 어노테이션이 적용되지 않고 CLASS, RUNTIME 정책의 어노테이션이 적용된 필드들만 어노테이션이 달려있는 것을 볼 수 있다.

 

public class main {
    public static void main(String[] args) {
        System.out.println(new MyClass(1, 2, 3));
    }
}

 

 

@Target

어노테이션을 어디에 달아줄 수 있는지를 설정해준다.

@Target(ElementType.ANNOTATION_TYPE)

 

종류로는 ANNOTATION_TYPE(다른 어노테이션 위에), CONSTRUCTOR, METHOD, FIELD, PACKAGE, PARAMETER, TYPE(클래스, 인터페이스, enum 등의 타입)가 있다. 여러 개를 한 번에 적용할 수도 있다.

 

@Inherited

어노테이션 위에 이 어노테이션을 달아놓으면 해당 어노테이션을 적용한 클래스의 자식 클래스도 부모에게 달린 어노테이션들을 적용받는다.

 

@Repeatable

여러 개의 정보를 반복적으로 주고 싶을 떄 사용한다.

@Repeatable(Repeats.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatT {
    int a();
    int b();
}

 

@Retention(RetentionPolicy.RUNTIME)
public @interface Repeats {
    RepeatT[] value();
}

 

RepeatT 어노테이션에 @Repeatable을 달고 Repeats 클래스를 참조하도록 했다. 그리고 Repeats에서는 RepeatT 클래스를 배열로 받아오도록해서 value()로 지정했다. 또한 두 클래스의 @Retention 정책을 똑같이 맞춰주었다.

 

이후 아래처럼 @RepeatT 어노테이션을 반복적으로 적용할 수 있다.

@CustomAnnt
public class MyClass {
   
    @RepeatT(a = 1, b = 2)
    @RepeatT(a = 3, b = 4)
    boolean repeat;
}

 

 

2.3 어노테이션 작성

 

앞서 배운 내용들을 바탕으로 annotation을 직접 만들어보자.

@Retention(RetentionPolicy.RUNTIME)
public @interface Count {

    int value() default 1;
}

 

value라는 int 값을 만들고 default 값을 1로 주었다. 괄호()를 주어 메서드 형식으로 선언한다는 것도 기억하자.

일반적으로 값이 1개만 있을 때는 value라는 이름으로 작성한다. Count라는 이름 자체가 어떤 의미를 가지는지 말해주기 때문이다.

 

public class main {

    @Count
    private int cats;

    @Count(value = 2)
    private int dogs;

    @Count(3)
    private int horses;


    public static void main(String[] args) {
    }
}

 

   

요소가 여러 개 있을 때는 배열로 선언하고, 인자로 줄 때는 {} 안에 작성해주면 된다. 요소가 없으면 {}로 적어주어야한다.

@Retention(RetentionPolicy.RUNTIME)
public @interface LocsAvail {
    String[] visit();
    String[] delivery();
    String[] quick();
}

 

@LocsAvail(
            quick = {"서울", "대전", "강원"}, // {} 안에 작성
            visit = "판교", // 하나만 있을 시 {} 생략 가능
            delivery = {} // 요소가 없을 시 {} 필요
    )
    private Object store;

 

 

강의에서는 클래스의 필드값 검증 예제를 소개한다. 위에서 배운 어노테이션을 만들고, 앞서 배운 리플렉션을 통해서 어떤 인스턴스의 각 필드값들을 검증한다. 예를 들어 이름의 경우 이름은 제외하고 성만 표시하고자 한다면, 다음과 같이 @Blind 어노테이션을 만들고 리플렉션을 통해 런타임에 적용할 수 있다.

 

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Blind { }

 

public static Object verifyObj (Object obj) throws Exception {
        Class<?> objClass = obj.getClass();

        for (Field f : objClass.getDeclaredFields()) {
            f.setAccessible(true);

            Object val = f.get(obj);

            //  🧪 필드의 어노테이션 검증 및 처리
            for (Annotation a : f.getAnnotations()) {

                //  첫 글자 외 *으로
                if (a instanceof Blind) {
                    String s = (String) val;
                    f.set(obj, s.substring(0, 1) + "*".repeat(s.length() - 1));
                }
    
    // 중략...

 

Object의 getClass() 메서드를 사용해서 클래스 정보를 가져오고, 여기서 getDeclaredFields() -> getAnnotations()를 통해 Annotation을 가져온 후 Annotation이 Blind의 instance인지를 검사하여 필드값을 set 처리해주는 것을 볼 수 있다.

 

 

 

3. 클래스로더

 

3.1 클래스 로더 개념

클래스 로더는 자바 프로그램 실행 시 클래스 파일들을 로드해서 메모리에 올리는 작업을 한다. 3가지 계층으로 이루어진다.

 

1. 클래스 로드

  1. 부트스트랩 클래스 로더: 자바의 기본 및 핵심 라이브러리의 클래스들을 로드한다. ex) Object, String, List...
  2. 확장/플랫폼 클래스 로더: 확장 라이브러리 클래스들을 로드한다. JDBC, JNDI, JMX, Swing...
  3. 시스템/애플리케이션 클래스 로더: 사용자가 프로그래밍한 클래스, 프로젝트에 추가한 라이브러리의 클래스 들을 로드한다.

 

2. 링크

검증: 파일 형식 및 자원 할당 검사

준비: 메모리 할당, 변수 초기화

연결: 클래스/인터페이스의 참조를 다른 클래스/인터페이스들에 연결

 

 

3. 초기화

클래스, 변수들 초기화, 정적(static) 블록 실행

 

 

3.2 클래스 로더 기능 활용하기

 

클래스 로더의 기능을 활용해서 클래스로더가 갖고 있는 정보를 바탕으로 현재 메모리에 올라간 클래스의 정보들을 가져올 수 있다. 강의의 코드를 살펴보자.

 

Thread()에서 getContextClassLoader()로 ClassLoader 클래스를 불러올 수 있다. 그리고 클래스 로더의 getResource() 메서드로 경로에 해당하는 파일의 URL을 가져올 수 있다.

//  ⭐️ 패키지 이름을 받아 해당 패키지에 있는 클래스들을 가져오는 메소드
    public static List<Class<?>> getClasses(String packageName) {
        List<Class<?>> classes = new ArrayList<>();
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        //  패키지 이름을 경로 형식으로 변환
        String path = packageName.replace('.', '/');

        //  ⭐️ ClassLoader의 기능으로 경로에 해당하는 URL을 가져옴
        java.net.URL resource = classLoader.getResource(path);
        if (resource == null) {
            System.out.println("리소스가 없습니다.");
            return null;
        }

        String filePath = resource.getFile(); // 🔴 디렉토리 확인해보기

        //  URL 문자열 디코딩 - 💡 https://youtu.be/1jo6q4dihoU
        filePath = java.net.URLDecoder
                .decode(filePath, StandardCharsets.UTF_8);

        java.io.File file = new java.io.File(filePath);
        if (file.isDirectory()) {
            for (String fileName : file.list()) {
                if (fileName.endsWith(".class")) {

                    //  💡 끝의 .class을 잘라내어 클래스명을 가져옴
                    String className = packageName
                            + '.' + fileName
                            .substring(0, fileName.length() - 6);

                    //  💡 클래스명으로 Class 객체 가져옴
                    Class<?> cls = null;
                    try {
                        //  ⭐ 내부적으로 ClassLoader 사용
                        cls = Class.forName(className);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                    classes.add(cls);
                }
            }
        } else {
            System.out.println("디렉토리가 아닙니다.");
            return null;
        }
        return classes;
    }

 

 

위 코드에서 가져온 classes 정보는 List<Object>이고, 여기서 리플렉션에서 배웠던 것처럼 생성자, 메서드, 필드 등을 가져오거나 변경처리할 수 있다.

 

스프링 등 각종 프레임워크도 이런식으로 classLoader에 접근하여 처리하는 방식으로 Dependency Injection 등의 기능을 구현하는데 사용한다.

728x90
반응형