[Java] 리플렉션이란?

이신영·2024년 6월 4일
1

Java

목록 보기
7/12
post-thumbnail

예전에 DI빈 등록방법에 대해 다뤄본 적이 있다. 분명 쓰는법은 알겠는데 몇가지 의문이 든다.

첫 번째 의문 : 의존성 주입(DI)

@AutoWired를 통해 편하게 외부의 객체를 빈으로 등록하여 의존성을 주입한 적이 있을것이다.

@Service
public class MyService {
    private final MyRepository myRepository;
    
    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }
}

생성자주입의 예시인데 @AutoWired를 사용하여 만약 MyService 빈을 생성할 때 MyRepository 빈을 찾아서 MyService의 생성자에 주입하게된다. 이렇게함으로 MyService는 MyRepository에 의존할 수 있다.

물론 이걸 처리해주는건 스프링 컨테이너다. 그럼 거기서 누가? 처리를 하지?

두 번째 의문 : 빈 등록

빈을 등록할 때는 xml파일이나 config를 정의하여 등록해도 되지만 이건 되게 번거롭다. 그래서 우리는 어노테이션을 주로 사용해서 등록하게된다.

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // 다른 설정 빈들이 있을 수 있음
}

이 과정에서 컴포넌트스캔을 통해 스프링 빈으로 등록하게 되는것이다.

오케이 그럼 컴포넌트스캔이 빈을 쉽게 등록해주는건 알겠어.. 거기서 누가!! 이걸 처리해주냐? 가 궁금하다 이말이야..!!

바로 그 누가?는 리플렉션 이였다.


리플렉션?

진짜 간단하게 말하면 클래스나 객체의 정보를 얻어내는 기능이다.

그래서 이게 왜 필요함?

이론적 설명

이 부분에 대해서는 진짜 이해가 빡! 올 수 있을만하게 정리해주신 블로그가 있었다.

말하자면 리플렉션은 아직 구체적으로 뭔지 모르는 클래스에 대한 클래스와 메서드정보를 동적으로 얻어올 수 있는 강력한 기능이다.

코드 예시

이제 중요한것을 알았으니 코드로 설명해보자면,

public class Person {
    private final String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void haveBirthday() {
        this.age++;
    }

    public int getAge() {
        return age;
    }
}

자바의 다형성 덕분에 아래와 같이 객체를 생성할 수 있다.

public static void main(String[] args) {
    Object obj = new Person("Alice", 25);
}

하지만 이 경우 obj라는 이름의 객체가 Person 클래스의 haveBirthday 메서드를 사용할 수 있을까?

정답은 불가능이다. 왜냐하면 자바는 컴파일러를 사용하며, 컴파일 타임에 타입이 결정되기 때문이다. obj라는 이름의 객체는 컴파일 타임에 Object로 타입이 결정되었기 때문에 Object 클래스의 인스턴스 변수와 메서드만 사용할 수 있다.

따라서 아래와 같은 코드는 필연적으로 컴파일 에러가 발생한다.

public static void main(String[] args) {
    Object obj = new Person("Alice", 25);
    obj.haveBirthday(); // 컴파일 에러 발생: java: cannot find symbol
}

생성된 obj라는 객체는 Object 클래스라는 타입만 알 뿐, Person 클래스라는 구체적인 타입은 모른다. 결국 컴파일러가 있는 자바는 구체적인 클래스를 모르면 해당 클래스의 정보에 접근할 수 없다.

이 불가능한 일을 가능하게 해주는 것이 Reflection API이다.

위에서 봤던 예제와 똑같은 상황에서 Reflection API를 활용해 Person 클래스의 haveBirthday 메서드를 호출해보자면,

import java.lang.reflect.Method;

public static void main(String[] args) throws Exception {
    Object obj = new Person("Alice", 25);
    Class personClass = Person.class;
    Method haveBirthday = personClass.getMethod("haveBirthday");

    // haveBirthday 메서드 실행, invoke(메서드를 실행시킬 객체, 해당 메서드에 넘길 인자)
    haveBirthday.invoke(obj, null);

    Method getAge = personClass.getMethod("getAge");
    int age = (int) getAge.invoke(obj, null);
    System.out.println(age);
    // 출력 결과: 26
}

haveBirthday 메서드가 실행되고 25로 초기화했던 Person 클래스 인스턴스 변수 age가 26으로 출력되는 것을 확인할 수 있다.

Reflection API로 구체적인 클래스 Person 타입을 알지 못해도 haveBirthday 메서드에 접근한 것이다.

Class personClass2 = Class.forName("Person");

위의 예제처럼 클래스의 이름만으로도 해당 클래스의 정보를 가져올 수 있다. 다시 말해서 Reflection API는 클래스의 이름만 가지고도 생성자, 필드, 메서드 등등 해당 클래스에 대한 거의 모든 정보를 가져올 수 있다.

어떻게 가능함?

자바에서는 JVM이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장된다. Reflection API는 이 정보를 활용한다. 그래서 클래스 이름만 알고 있다면 언제든 static 영역을 뒤져서 정보를 가져올 수 있는 것이다.


보안 문제

강한 능력을 가진 만큼 강한 책임을 가지고 있는법.. 제대로 사용하지않는 경우에 보안문제를 일으킬 수 있다.

특히 리플렉션을 사용하면 일반적으로 접근할 수 없는 private 메서드나 필드에 접근할 수 있다. 이는 객체의 내부를 노출시키고 보안을 침해할 수 있다.

때문에 이를 예방하는 몇가지 방안을 떠올려야한다.

1. Security Manager

자바는 Security Manager를 통해 애플리케이션의 보안 정책을 관리한다. 특히, 리플렉션을 사용하여 private 멤버에 접근하는 것을 제한할 수 있다.

public class Main {
    public static void main(String[] args) {
        // Security Manager
        System.setSecurityManager(new SecurityManager());

        // 리플렉션을 사용하여 private 메서드 호출
        try {
            MyClass myClass = new MyClass();
            Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod");
            privateMethod.setAccessible(true);
            privateMethod.invoke(myClass);
        } catch (Exception e) {
            System.out.println("보안 정책으로 인해 private 메서드에 접근할 수 없습니다.");
            e.printStackTrace();
        }
    }
}

class MyClass {
    private void privateMethod() {
        System.out.println("Private 메서드 호출");
    }
}

2. 리플렉션 API 사용

AccessibleObject.setAccessible(false)를 사용하여 리플렉션을 통해 접근할 수 없게 할 수 있다.

import java.lang.reflect.Method;
import java.lang.reflect.AccessibleObject;

public class Main {
    public static void main(String[] args) {
        try {
            MyClass myClass = new MyClass();
            Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod");
            
            // 보안 API를 사용하여 접근 제한 설정
            AccessibleObject.setAccessible(new AccessibleObject[]{privateMethod}, false);
            
            // private 메서드 호출 시도
            privateMethod.invoke(myClass);
        } catch (Exception e) {
            System.out.println("리플렉션을 통한 private 메서드 접근이 차단되었습니다.");
            e.printStackTrace();
        }
    }
}

class MyClass {
    private void privateMethod() {
        System.out.println("Private 메서드 호출");
    }
}

3. 클래스 로딩에서 제한하기

클래스 로딩을 제한하여 리플렉션을 통한 클래스 로딩을 방지할 수 있다. 예를 들어, 특정 클래스를 로딩하는 것을 금지하거나, 클래스 로더를 커스터마이징하여 리플렉션을 통한 클래스 로딩을 제어할 수 있다.

import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) {
        try {
            // 특정 클래스 로딩을 금지
            ClassLoader customClassLoader = new RestrictedClassLoader();
            Class<?> myClass = customClassLoader.loadClass("MyClass");
            Method privateMethod = myClass.getDeclaredMethod("privateMethod");

            // private 메서드 호출 시도
            privateMethod.invoke(myClass.newInstance());
        } catch (Exception e) {
            System.out.println("리플렉션을 통한 클래스 로딩이 차단되었습니다.");
            e.printStackTrace();
        }
    }
}

class RestrictedClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 특정 클래스 로딩 금지
        if (name.equals("MyClass")) {
            throw new ClassNotFoundException("해당 클래스를 로딩할 수 없습니다.");
        }
        return super.loadClass(name);
    }
}

class MyClass {
    private void privateMethod() {
        System.out.println("Private 메서드 호출");
    }
}

4. 보안 패턴 적용

보안적으로 민감한 기능에 대해서는 보안 패턴을 적용하여 리플렉션을 통한 악의적인 접근을 방지할 수 있다. 예를 들어, Proxy를 사용하여 중간 계층을 만들어 접근을 제어할 수 있다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        try {
            MyClass realObject = new MyClass();
            // Proxy를 사용하여 보안 패턴 적용
            MyInterface proxyObject = (MyInterface) Proxy.newProxyInstance(
                    MyClass.class.getClassLoader(),
                    new Class<?>[]{MyInterface.class},
                    new SecurityProxyHandler(realObject));

            // 보안 패턴을 적용한 객체를 통해 메서드 호출
            proxyObject.publicMethod();
            proxyObject.privateMethod();
        } catch (Exception e) {
            System.out.println("보안 패턴이 적용되어 private 메서드 호출이 차단되었습니다.");
            e.printStackTrace();
        }
    }
}

interface MyInterface {
    void publicMethod();
    void privateMethod();
}

class MyClass implements MyInterface {
    public void publicMethod() {
        System.out.println("Public 메서드 호출");
    }

    private void privateMethod() {
        System.out.println("Private 메서드 호출");
    }
}

class SecurityProxyHandler implements InvocationHandler {
    private final Object realObject;

    public SecurityProxyHandler(Object realObject) {
        this.realObject = realObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 보안 패턴 적용: private 메서드 호출 차단
        if (method.getName().equals("privateMethod")) {
            throw new IllegalAccessException("private 메서드에 접근할 수 없습니다.");
        }
        return method.invoke(realObject, args);
    }
}
profile
후회하지 않는 사람이 되자 🔥

0개의 댓글