작년 이맘때 궁금하지만 어려워서 넘어갔던 기술들이 있었습니다. 대표적으로 엘라스틱 서치와 리플렉션이 있었는데 최근에 하나씩 공부하면서 지난번과 달리 비교적 쉽게 체득할 수 있었는데요. 그 이유는 학습 방법의 변화 덕분입니다.
작년에 리플렉션을 공부할 때는 무작정 개념과 사용방법만 익히려고 했는데 이번에는 Why
에 초점을 맞춰서 공부했더니 더 쉽게 이해하고 활용을 잘할 수 있게 되었습니다.
'이 기술은 왜 나왔을까? 왜 사용할까?' 를 이해하는 것은 중요합니다. 그래서 작년의 저와 같은 사람에게 도움이 되었으면 해서 리플렉션이 무엇인지 정리를 해보기로 했습니다.
리플렉션의 사용방법은 다루지 않습니다. 사용 방법은 아래 링크를 추천합니다
리플렉션이란 ?
리플렉션은 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
여기서 구체적인 클래스 타입을 알지 못해도
가 무슨 말일까요? 저는 이 부분에서 이해하기가 정말 어려웠습니다.
'내가 작성한 코드인데 왜 몰라? 당연히 아는 거 아냐?' 이런 생각이 들었지만, 나중에서야 접근방법이 잘못된 것을 알 수 있었습니다. 이 문장을 이해하려면 리플렉션을 언제, 왜 사용해야 하는지 알아야 합니다.
Why?
자바는 정적인 언어라 부족한 부분이 많은데 이 동적인 문제를 해결하기 위해서 리플렉션을 사용합니다.
정적 언어, 동적 언어 ?
- 정적 언어: 컴파일 시점에 타입을 결정 ex) Java, C, C++ 등..
- 동적 언어: 런타임 시점에 타입을 결정 ex) Javascript, Python, Ruby 등..
리플렉션은 애플리케이션 개발에서보다는 프레임워크, 라이브러리에서 많이 사용됩니다.
프레임워크, 라이브러리는 사용하는 사람이 어떤 클래스를 만들지 모릅니다. 이럴 때 동적으로 해결해주기 위해서 리플렉션을 사용합니다.
대표적인 사용 예로는 스프링의 DI(dpendency injection), Proxy, ModelMapper 등이 있습니다.
코드로 예를 볼까요?
@Controller
@RequestMapping("/articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
....
@PostMapping
public String write(UserSession userSession, ArticleDto.Request articleDto){
...
}
@GetMapping("/{id}")
public String show(@PathVariable int id, Model model) {
...
}
}
스프링을 사용할 때 @Controller
를 넣어주면 인스턴스를 생성 하지 않아도 스프링이 알아서 생성해서 빈으로 관리해줍니다.
- 스프링은
ArticleController
의 존재를 어떻게 알고 만들어주는 것일까요? ArticleService
라는 필드는 어떻게 주입해주는 걸까요?- 모든 메소드의 파라미터 개수, 타입이 다른데 어떻게 알고 해당하는 값을 바인딩 해줄까요?
ArticleController을 작성한 개발자는 클래스의 정보를 알겠지만, 스프링은 모릅니다.
이 문제를 해결하기 위해서 리플렉션을 사용합니다. (스프링이 ArticleController의 정보를 알아내기 위해서)
대략 흐름을 보자면
@Controller
를 갖고있는 클래스를 스캔- 해당하는 클래스의 인스턴스 생성 및 필드 DI
How?
간단한 사용법
그럼 어떻게 사용하는지 간단하게 보겠습니다.
class Article {
private int id;
private String title;
private LocalDateTime date;
}
Article 클래스의 필드 정보
를 리플렉션을 이용하여 출력하겠습니다.
Class<?> clazz = Article.class; // Article의 Class를 가져온다.
Field[] fields = clazz.getDeclaredFields(); // Article의 모든 필드를 가져온다.
for (final Field field : fields) { // field의 type, name 출력
System.out.printf("%s %s\n", field.getType(), field.getName());
}
출력 결과
int id
class java.lang.String title
class java.time.LocalDateTime date
이런 방식으로 사용하는 데요. 사실 이 방식은 Article.class
을 직접 명시해줬으므로 처음에 말했던 '내가 작성한 코드인데 왜 몰라? 당연히 아는 거 아냐?' 와 일치합니다. 그래서 한 번 프레임워크 기준에서 작성해보겠습니다.
간단한 DI 프레임워크
이번 예시로는 간단하게 스프링처럼 DI를 구현 하겠습니다.
아래 코드는 백기선님의 리플렉션 강의를 참고해서 작성했습니다.
먼저 AutoWried 어노테이션을 만들어주고
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {
}
ContainerService (인스턴스를 생성하고 필드를 DI 해주는 역할)
public class ContainerService {
public static <T> T getObject(Class<T> classType) {
// 기본생성자를 통해서 인스턴스를 만든다.
T instance = createInstance(classType);
// 클래스의 모든 필드를 불러온다.
Stream.of(classType.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(AutoWired.class)) // 어노테이션에 AutoWired를 갖는 필드만 필터
.forEach(field -> {
try {
// 필드의 인스턴스 생성
Object fieldInstance = createInstance(field.getType());
// 필드의 접근제어자가 private인 경우 수정가능하게 설정
field.setAccessible(true);
// 인스턴스에 생성된 필드 주입
field.set(instance, fieldInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
return instance;
}
private static <T> T createInstance(final Class<T> classType) {
try {
// 해당 클래스 타입의 기본생성자로 인스턴스 생성
return classType.getConstructor().newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
이로써 간단한 DI 프레임워크가 만들어졌습니다.
이제는 이 프레임워크를 사용하는 애플리케이션 개발자 관점에서 코드를 작성하겠습니다.
ArticleController, ArticleService
public class ArticleController {
@AutoWired
private ArticleService articleService;
public void foo(){
articleService.foo();
}
}
public class ArticleService {
public void foo() {
System.out.println("call foo");
}
}
프레임워크를 이용해서 ArticleController 인스턴스 생성
public static void main(String[] args){
ContainerService containerService = new ContainerService();
ArticleController articleController = containerService.getObject(ArticleController.class);
articleController.foo();
}
실행결과
call foo
여기까지 DI 프레임워크를 구현해 봤는데요. 여기서 ArticleController.class
를 직접 명시해줬지만 ArticleService
에 대해서는 직접 명시해주지 않았는데 알아서 생성해서 주입해줬습니다.
조금 더 한다면 @Controller
어노테이션을 만들어서 해당하는 클래스를 가져와서containerService.getObject()
에 ArticleController.class
를 직접 명시하지 않고도 할 수 있습니다. 현재는 어떠한 흐름이다만 보여주기 위해서 이 부분은 생략했습니다.
추가로 리플렉션을 사용하면서 주의해야할 점이 몇가지 있습니다.
- 지나친 사용은 성능 이슈를 야기할 수 있다. 반드시 필요한 경우에만 사용할 것
- 이미 인스턴스를 만들었음에도 불구하고 굳이 필드와 리플렉션을 이용해서 접근하거나 사용할 경우
- 컴파일 타임에 확인되지 않고 런타임 시에만 발생하는 문제를 만들 가능성이 있다.
- 접근 지시자를 무시할 수 있다.
리플렉션은..
컴파일한 클래스 정보를 활용해 동적으로 프로그래밍이 가능하도록 지원하는 API
Refereneces
'Java' 카테고리의 다른 글
JUnit 5 Parameterized Tests 사용하기 (0) | 2019.12.28 |
---|---|
[객체지향] OCP (개방 폐쇄의 원칙 : Open Close Principle) (0) | 2019.12.28 |
[객체지향] if-else의 문제점 (0) | 2019.12.28 |
[Java] toString()에 대해서 (0) | 2019.12.28 |
if문 없이 문자열 계산하기 (feat. 전략패턴, Enum) (0) | 2019.05.12 |