예전에 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 메서드나 필드에 접근할 수 있다. 이는 객체의 내부를 노출시키고 보안을 침해할 수 있다.
때문에 이를 예방하는 몇가지 방안을 떠올려야한다.
자바는 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 메서드 호출");
}
}
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 메서드 호출");
}
}
클래스 로딩을 제한하여 리플렉션을 통한 클래스 로딩을 방지할 수 있다. 예를 들어, 특정 클래스를 로딩하는 것을 금지하거나, 클래스 로더를 커스터마이징하여 리플렉션을 통한 클래스 로딩을 제어할 수 있다.
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 메서드 호출");
}
}
보안적으로 민감한 기능에 대해서는 보안 패턴을 적용하여 리플렉션을 통한 악의적인 접근을 방지할 수 있다. 예를 들어, 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);
}
}