이번 포스트에서는 자바 리플렉션과 이걸 사용하는 스프링에 대해서 알아보자
1. 자바 리플렉션이란
리플렉션이란?
- 리플렉션은 자바에서 클래스나 멤버에 대한 정보를 런타임에 조사하고, 조작할 수 있는 기능이다. 예를 들어, 클래스의 이름, 메서드, 필드, 생성자 등에 대한 정보를 프로그램 실행 중에 알아내고, 이를 통해 객체를 생성하거나 메서드를 호출할 수 있다. 이 기능 덕분에, 개발자는 코드의 유연성과 확장성을 높일 수 있다.
리플렉션 예시
- 코드를 보면 Class.forName("java.lang.String")은 String 클래스에 대한 Class 객체를 가지고 온다. 그리고 getDeclaredMethods() 메서드를 사용해서 가져온 클래스에 정의된 모든 메서드의 정보를 얻어내는 것이다. 이렇게 리플렉션을 활용하면, 런타임(Runtime)에 동적으로 클래스의 정보를 얻어내고, 이를 바탕으로 다양한 작업을 할 수 있다.
Class<?> clazz = Class.forName("java.lang.String");
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
자바에서 리플렉션의 중요성
- 자바의 리플렉션은 프레임워크와 라이브러리에서 매우 중요하게 사용된다. 특히 스프링 프레임워크에서는 리플렉션을 사용해서 많은 핵심 기능들을 구현한다. 예를 들어, 의존성 주입(Dependency Injection)이 그중 하나다. 스프링은 리플렉션을 통해서 클래스의 메타데이터를 분석하고, @Autowired 어노테이션이 붙은 필드를 찾아서 자동으로 의존성을 주입해 준다.
- 이 과정에서 스프링 컨테이너는 리플렉션을 사용해서 해당 필드의 타입에 맞는 빈(bean)을 찾고, 그 빈을 해당 필드에 할당한다. 이를 통해 개발자는 객체의 생성과 의존성 관리를 수동으로 하지 않아도 된다. 이런 방식으로 스프링은 개발자가 더 쉽고 유연하게 어플리케이션을 구성하고 확장할 수 있게 도와준다.
스프링의 @Autowired는 리플렉션을 사용하여 생성자 주입을 한다.
생성자 주입을 사용할 때 @Autowired 어노테이션이 생성자 위에 적혀 있으면, 스프링은 리플렉션을 사용해서 해당 생성자를 통해 클래스의 인스턴스를 생성하고, 필요한 의존성을 주입한다. 이 과정을 좀 더 구체적으로 살펴보면 다음과 같다.
생성자 찾기
- 스프링은 클래스에 @Autowired 어노테이션이 붙은 생성자를 특별히 주목한다. 이 어노테이션이 붙은 생성자는 스프링에게 '이 생성자를 사용하여 객체를 만들어라'라고 알려주는 신호와 같다. 스프링은 내부적으로 리플렉션 기술을 활용하여 이런 생성자들을 찾아낸다. 리플렉션을 사용하면 스프링은 프로그램이 실행되는 동안 클래스의 구조를 들여다보고, @Autowired 어노테이션이 적용된 생성자를 식별할 수 있다.
의존성 해석
- 스프링은 @Autowired가 붙은 생성자를 살펴보고, 그 안에 있는 매개변수들을 검토한다. 이 매개변수들은 스프링이 객체를 만들 때 주입해야 할 '의존성'들을 나타낸다. 스프링은 매개변수의 타입을 보고, 같은 타입의 객체(빈)를 스프링 컨테이너 안에서 찾아낸다. 즉, '이 생성자는 어떤 객체들을 필요로 하는구나' 하고 파악하는 것이다.
객체 생성 및 주입
- 스프링은 찾은 객체들을 생성자의 매개변수로 사용하여 새로운 객체를 만든다. 이때 리플렉션 기능이 활용되어, 스프링은 프로그램 실행 중에 실제 객체를 생성하고, 생성자를 통해 이 의존성들을 새로 만든 객체에 주입한다. 간단히 말해서, 스프링은 '이 클래스는 이런 객체들이 필요해'라고 매개변수를 통해 알게 된 후, 그에 맞게 자동으로 객체를 조립하고 준비한다.
예를 들어, 아래와 같은 클래스가 있다고 해보자
- 위의 코드에서 MyComponent 클래스는 MyService에 의존하고 있으며, 이 의존성은 생성자를 통해 주입된다. 스프링은 @Autowired 어노테이션이 붙은 생성자를 찾아서 MyService 타입의 빈을 매개변수로 전달하여 MyComponent의 인스턴스를 생성한다. 이 모든 과정에서 리플렉션은 스프링이 생성자를 찾고, 매개변수 타입을 확인하며, 새로운 인스턴스를 생성하는 데 사용된다.
@Component
public class MyComponent {
private final MyService myService;
@Autowired
public MyComponent(MyService myService) {
this.myService = myService;
}
}
2. 리플렉션의 기본 개념
리플렉션은 자바에서 객체의 클래스 정보를 런타임에 조사하고 조작할 수 있는 API를 제공한다. 이를 통해 클래스의 구조, 메서드, 필드 등에 대한 상세한 정보를 얻을 수 있다.
리플랙션의 클래스 정보 접근 방법
- 자바에서 클래스 정보에 접근하기 위해서는 먼저 Class 타입의 객체가 필요하다. 이 객체는 자바의 모든 클래스와 인터페이스에 대응되는 메타데이터를 담고 있다. 클래스 정보에 접근하는 방법은 크게 세 가지가 있다.
- Object 클래스의 getClass() 메소드 사용
- .class 문법 사용 (예: String.class)
- Class.forName() 메서드 사용
위의 3가지 방법은 아래와 같이 사용할 수 있다.
// 1. [Object의 getClass() 메소드 사용]
String str = "Hello";
Class<?> clazz1 = str.getClass();
// 2. [.class 문법 사용]
Class<?> clazz2 = String.class;
// 3. [Class.forName() 사용]
Class<?> clazz3 = Class.forName("java.lang.String");
Class 객체의 활용
- Class 객체를 사용하면 클래스의 이름, 메서드, 필드, 생성자 등 다양한 정보를 얻을 수 있다. 예를 들어, 클래스의 이름을 얻거나, 해당 클래스에서 정의된 모든 메서드와 필드의 목록을 얻는 것이 가능하다.
- 아래의 예시코드에서는 String.class를 사용해 String 클래스에 대한 Class 객체를 얻는다. 그다음엔 이 객체를 사용해서 클래스의 이름, 선언된 메서드, 필드를 얻고 출력한다. 이런 식으로 리플렉션을 활용하면 실행 중인 프로그램이 자신의 구조를 파악하고, 필요에 따라 동적으로 클래스의 속성이나 메서드를 사용할 수 있게 되는 것이다.
Class<?> clazz = String.class;
// 클래스 이름 얻기
System.out.println("Class name: " + clazz.getName());
// 모든 메소드 얻기
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method name: " + method.getName());
}
// 모든 필드 얻기
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field name: " + field.getName());
}
3. 자바 리플렉션의 필드(Field) 조작
리플렉션을 사용하면 클래스의 필드(멤버 변수)에 프로그래밍적으로 접근하고 조작할 수 있다. 이를 통해 런타임에 객체의 상태를 동적으로 변경하는 것이 가능하다.
필드 접근 및 수정
- 필드에 접근하기 위해서는 먼저 Class 객체를 통해 해당 필드의 Field 객체를 얻어야 한다. 필드에 접근하려면 getField() 또는 getDeclaredField() 메서드를 사용한다. 이후 Field 객체의 메소드를 사용해서 필드의 값을 읽거나 수정할 수 있다.
- getField(): public 필드에 접근할 때 사용한다.
- getDeclaredField(): 클래스에 선언된 모든 필드에 접근할 때 사용한다. (접근 제어자에 상관없이)
class Example {
public int publicField;
private String privateField;
}
Class<?> clazz = Example.class;
Field publicField = clazz.getField("publicField");
Field privateField = clazz.getDeclaredField("privateField");
// private 필드에 접근하기 위해서는 접근 가능하도록 설정
privateField.setAccessible(true);
Field 클래스 사용 예시
- 필드의 값 읽기나 수정은 get()과 set() 메소드를 사용한다.
- 이 예시에서는 Example 클래스의 인스턴스를 만들고, 리플렉션을 사용해 필드에 접근하고, 값을 읽고 수정하는 방법을 보여준다.
Example example = new Example();
// public 필드 읽기
int publicFieldValue = (Integer) publicField.get(example);
System.out.println("Public Field Value: " + publicFieldValue);
// private 필드 읽기
String privateFieldValue = (String) privateField.get(example);
System.out.println("Private Field Value: " + privateFieldValue);
// 필드 값 수정
publicField.set(example, 10); // public 필드 수정
privateField.set(example, "Hello World"); // private 필드 수정
4. 자바 리플렉션의 메서드(Method) 조작
리플렉션을 사용하면 클래스의 메서드에 접근하여 실행할 수 있다. 이를 통해 런타임에 동적으로 메서드를 호출하는 것이 가능하다.
메서드 호출 방법
- 메서드에 접근하기 위해서는 먼저 Class 객체를 통해서 해당 메서드의 Method 객체를 얻어야 한다. 메서드에 접근하려면 getMethod() 또는 getDeclaredMethod() 메서드를 사용한다.
- getMethod(): public 메서드에 접근할 때 사용한다.
- getDeclaredMethod(): 클래스에 선언된 모든 메서드에 접근할 때 사용한다. (접근 제어자에 상관없이)
class Example {
public void publicMethod() {
System.out.println("Public method");
}
private void privateMethod() {
System.out.println("Private method");
}
}
Class<?> clazz = Example.class;
Method publicMethod = clazz.getMethod("publicMethod");
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
// private 메소드에 접근하기 위해서는 접근 가능하도록 설정
privateMethod.setAccessible(true);
Method 클래스 사용 예시
- 메서드를 실행하려면 invoke() 메서드를 사용한다. invoke() 메서드에는 메서드를 실행할 객체와 매개변수를 전달해야 한다.
Example example = new Example();
// public 메소드 실행
publicMethod.invoke(example);
// private 메소드 실행
privateMethod.invoke(example);
리플렉션과 invoke()
- 자바의 리플렉션 API에서 Method 클래스의 invoke() 메서드는 리플렉션을 사용하여 특정 객체의 메서드를 실행할 때 사용된다. 이 메서드를 통해서 런타임에 어떤 객체의 어떤 메서드를 호출할지 결정할 수 있다.
스프링 프레임워크에서의 리플렉션 사용
- 스프링 프레임워크에서는 내부적으로 리플렉션을 사용하여 많은 작업을 처리한다. 예를 들어, 의존성 주입, AOP(Aspect-Oriented Programming), 이벤트 처리 등 많은 기능들이 리플렉션을 기반으로 동작한다. 이 과정에서 invoke() 메서드가 자주 사용된다.
스프링에서 디버깅 중 발견한 invoke()의 동작
디버그 중 발견
- 개발하면서 디버그를 찍어볼 때, 스프링의 내부 작업 중 하나인 리플렉션 동작을 관찰할 수 있다. 이 때 invoke() 메서드가 호출되는 것을 보는 것은, 스프링이 어떤 메서드를 동적으로 호출하고 있다는 것을 나타낸다.
리플렉션의 사용 이유
- 리플렉션을 사용하는 이유는 유연성과 일반화된 코드 실행 능력 때문이다. 리플렉션을 사용하면, 스프링은 런타임에 객체의 타입을 정확히 알지 못해도 해당 객체의 메소드를 호출할 수 있다. 이는 프레임워크가 다양한 상황과 다양한 객체 타입에 대응할 수 있게 해준다.
따라서, 스프링 부트에서 개발하면서 invoke() 메소드가 호출되는 것을 본다면, 그것은 리플렉션을 통해서 어떤 메소드가 실행되고 있다는 신호이다. 이것은 스프링이 복잡한 작업을 단순화하고 자동화하기 위해 사용하는 일반적인 방법 중 하나다.
5. 생성자(Constructor) 조작
리플렉션을 사용하면 클래스의 생성자에 접근하여 객체를 동적으로 생성할 수 있다.
객체 생성 방법
- 생성자에 접근하기 위해서는 먼저 Class 객체를 통해 해당 생성자의 Constructor 객체를 얻어야 한다. getConstructor() 또는 getDeclaredConstructor() 메소드를 사용해 생성자에 접근할 수 있다.
- getConstructor(): public 생성자에 접근할 때 사용한다.
- getDeclaredConstructor(): 클래스에 선언된 모든 생성자에 접근할 때 사용한다. (접근 제어자에 상관없이)
class Example {
public Example() {
}
private Example(String arg) {
}
}
Class<?> clazz = Example.class;
Constructor<?> publicConstructor = clazz.getConstructor();
Constructor<?> privateConstructor = clazz.getDeclaredConstructor(String.class);
// private 생성자에 접근하기 위해서는 접근 가능하도록 설정
privateConstructor.setAccessible(true);
Constructor 클래스 사용 예시
- 객체를 생성하려면 Constructor 객체의 newInstance() 메소드를 사용해야 한다.
// public 생성자를 사용한 객체 생성
Example example1 = (Example) publicConstructor.newInstance();
// private 생성자를 사용한 객체 생성
Example example2 = (Example) privateConstructor.newInstance("argument");
6. 리플렉션의 실제 사용 사례
리플렉션은 자바 프로그래밍에서 다양한 방식으로 활용되고 있다. 특히, 많은 프레임워크들이 리플렉션을 사용해 유연성과 확장성을 제공하고 있다.
프레임워크에서의 활용
스프링 프레임워크(Spring Framework)
- 스프링에서는 리플렉션을 사용해서 의존성 주입(Dependency Injection)을 구현한다. 예를 들어, @Autowired 어노테이션을 사용하면 스프링 컨테이너가 리플렉션을 이용해 필요한 객체를 클래스에 자동으로 주입해 준다. (이 설명은 위에 적혀있다.)
하이버네이트(Hibernate)
- ORM(Object-Relational Mapping) 프레임워크인 하이버네이트는 리플렉션을 사용해 객체와 데이터베이스 테이블 간의 매핑을 동적으로 처리한다.
JUnit 테스트 프레임워크
- JUnit에서는 리플렉션을 활용해서 테스트 케이스를 동적으로 로드하고 실행한다. @Test 어노테이션이 달린 메소드를 찾아내 테스트를 수행하는 방식이다.
7. 리플렉션의 장단점
리플렉션은 자바 프로그래밍에서 매우 유용하지만, 사용 시 주의해야 할 몇 가지 측면이 있다.
보안 및 성능 이슈
보안 문제
- 리플렉션을 사용하면 private 메서드나 필드에 접근할 수 있다. 이는 보안 면에서 매우 큰 취약점이 될 수 있다. 예를 들어, 리플렉션을 통해 내부적으로 보호되어야 할 데이터에 접근하거나 변경할 수 있어서, 보안을 위협할 수 있다. (위험)
성능 저하
- 리플렉션은 런타임에 메서드나 필드를 찾고 접근하는 과정이 추가되기 때문에 일반적인 코드 실행에 비해 더 많은 시간과 자원을 소모한다. 그래서 성능이 중요한 애플리케이션에서는 리플렉션 사용을 신중하게 고려해야 한다.
유연성 제공
코드의 유연성 향상
- 리플렉션은 런타임에 클래스, 메서드, 필드 정보를 동적으로 조사하고 조작할 수 있게 해줘서 코드의 유연성을 크게 향상시킨다. 예를 들어, 외부 설정 파일에 따라 동적으로 객체를 생성하거나, 메서드를 호출하는 것과 같은 작업이 가능해진다.
프레임워크 및 라이브러리 개발에 유용
- 리플렉션은 프레임워크와 라이브러리 개발에서 매우 유용하다. 예를 들어, 스프링과 같은 프레임워크는 리플렉션을 통해 더 유연하고 확장 가능한 구조를 제공한다.
8. 결론 및 생각
리플렉션은 자바에서 매우 강력한 기능을 제공하지만, 그 사용법에 있어서는 신중함이 요구된다.
리플렉션의 적절한 사용 방법
적절한 사용
- 리플렉션은 필요한 경우에만 제한적으로 사용해야 한다. 보안과 성능 문제를 최소화하기 위해, 가능한 한 일반적인 코드 작성 방법을 우선 고려하고, 리플렉션은 필요할 때만 사용하는 것이 좋다.
보안 고려
- 리플렉션을 사용할 때는 보안 측면을 항상 고려해야 한다. 예를 들어, 외부 입력을 기반으로 리플렉션을 사용하는 경우, 잠재적인 보안 위험을 검토하고, 필요한 경우 접근 제어를 강화해야 한다.
성능 최적화
- 성능이 중요한 부분에서 리플렉션을 사용할 때는 성능 저하를 최소화하는 방법을 고려해야 한다. 예를 들어, 메서드나 필드의 정보를 캐싱하는 방법을 사용할 수 있다.
📌 마무리
스프링으로 개발을 하면서 코드에 문제가 생기면 디버깅을 꼭 하는데 이때마다 디버깅 순서를 하나씩 넘기다 보면 실행 부분의 핵심 로직에서 invoke() 메서드를 사용하는 클래스로 이동하는것이 보여서 이게 뭐지? 라는 생각을 자주 했었는데 이번에 리플랙션에 대해서 공부하다 이것이 뭔지 알게 되었다.
스프링이 @Autowired를 어떻게 사용하고 있는지도 알게 되었으며 이를 통해 스프링은 자바의 기능을 정말 잘 이해하고 사용하고 있는 대단한 프레임워크라는 것도 다시 한번 느낀것 같다.
이렇게 내용을 공부했으니 리플렉션을 직접 코드를 쳐보며 실습해보자👇🏻👇🏻
'JAVA' 카테고리의 다른 글
[Java] Optional로 Null 처리하기 (28) | 2023.12.28 |
---|---|
[Java] 자바 리플렉션(Reflection) 실습하기 (1) | 2023.11.18 |
[Java] 추상화란 무엇인가? (1) | 2023.11.02 |
Java I/O: BufferedReader, BufferedWriter, Buffer 사용법 (0) | 2023.11.01 |
[Java] 동시성과 병렬 처리 part2: 함정, 고급 패턴, 성능 최적화 (1) | 2023.11.01 |