Java Reflection에 대하여

김용현·2024년 11월 7일
0

Java

목록 보기
2/3

이번엔 자바의 Reflection에 대해 알아보겠습니다!

최근 사이드 프로젝트를 진행하다가 마주치게 된 기술인데, 이를 적절히 잘 활용하면 보다 창의적이고 효율적인 코딩이 가능할 것 같다는 생각에 이번 기회에 한 번 정리해보도록 할게요!!

Java Reflection이란?

런타임에 동적으로 Java의 클래스 정보들에 대해 접근하고 이를 조작할 수 있는 자바 API

쉽게 말하면 특정 클래스의 이름, 멤버 변수, 생성자, 멤버 메소드 등 클래스를 이루고 있는 것들에 접근할 수 있도록 해주는 기능이라고 생각하면 됩니다!

JPA를 사용한다면 꼭 @Entity 클래스에는 빈 생성자가 필요하다는 메시지! 한번 쯤은 보신적 있으실 것 같아요~

이처럼 기본 생성자를 요구하는 이유가 바로 이 Reflection을 사용하기 위함입니다!

그렇다면 JPA에서 기본 생성자를 요구하는 이유는 무엇일까요?

리플렉션 기술의 동작 원리와 사용 방법에 대해 알아보며 이유를 알아봅시다!

어떻게 런타임에 클래스 정보를 가져올까?

개발자가 작성한 코드(.java)는 자바 컴파일러에 의해 .class 파일(바이트 코드)로 변환되고 클래스 로더에 전달되어 JVM의 메모리 영역(Runtime Data Areas)에 올라가게 됩니다. 이 때 클래스, 필드 등의 정보는 Method Area에 저장이 되는데 이 Method Area에 저장된 정보들을 가져다가 활용하게 됩니다!

자바 컴파일 과정 참고

참고

java 8 이후로는 JVM의 내부 구조에 변화가 있었습니다. 8버전 이전까지는 힙 영역에 속하는 PermGen에 method area에 속하는 정보가 저장되었다고 해요!

그러나 PermGen 영역은 힙 영역이기 때문에 메모리 부족 현상이 비교적 자주 발생했고, 8버전부터는 Native memory를 사용하는 MetaSpace 영역으로 바꾸어 메모리 부족 문제를 해결했다고 합니다.

따라서, 위 이미지에 등장하는 Method Area가 자바 7버전까지는 실제 메모리에선 permGen 영역, 자바 8버전 이후부터는 MetaSpace 영역에 존재하는 것이죠!

MetaSpace에 관하여

그럼 이제 Java Reflection을 어떻게 사용하는지 구체적으로 알아봅시다.

먼저 java reflection으로 가져올 수 있는 대표적인 정보는 다음과 같아요.

  • 필드(목록) 가져오기
  • 메소드 (목록) 가져오기
  • 상위 클래스 가져오기
  • 인터페이스 (목록) 가져오기
  • 애노테이션 가져오기
  • 생성자 가져오기
  • 생성자를 통해 인스턴스 객체 생성하기
  • (이 외에도 더 많이 있습니다!)

가져오는 것 외에도 접근 제어를 변경하거나, 안에 값을 직접 설정해줄 수도 있습니다.

코드로 알아보기

간단한 예제를 통해 코드로 어떻게 구현하는지 알아볼게요

클래스 가져오기

리플렉션의 시작은 Class 라는 객체를 통해 이루어집니다. 앞서 말한 Method Area에 저장된 클래스 관련 정보들이 이 Class 라는 객체에 저장되고, 이를 통해 접근 및 조작을 하는 것입니다!

KClass 객체 생성하기

import kotlin.reflect.KClass

class Member(
    val name: String,
    private val age: Int,
    val email: String,
)
fun main() {
    //java인 경우
    val javaClazz: Class<Member> = Member::class.java
    val javaclazz2 = Class.forName("Member")

    //kotlin인 경우
    val kotlinClazz: KClass<Member> = Member::class
}

다음과 같이 Class, KClass 객체를 획득할 수 있습니다.

이후에는 이렇게 획득한 객체로 해당 클래스 정보들에 접근할 수 있습니다!

import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.declaredMembers

open class Parent(
    val DNA: String
)

private class Child(
    val name: String,
    val age: Int,
    val email: String,
): Parent("father") {

    fun run() {
        println("running")
    }
}

fun main() {
    //java인 경우
    println("===========java============")
    val javaClazz: Class<Child> = Child::class.java

    println(javaClazz.getMethod("run"))
    println(javaClazz.constructors.forEach { println(it) })
    println(javaClazz.getDeclaredField("name"))
    println(javaClazz.interfaces.forEach { println(it) })
    println(javaClazz.annotations.forEach { println(it) })

    println("\n=====kotlin========")
    //kotlin인 경우
    val kotlinClazz: KClass<Child> = Child::class
    println(kotlinClazz.members)
    println(kotlinClazz.declaredMembers)
    println(kotlinClazz.declaredMemberProperties)
    println(kotlinClazz.constructors)
    println(kotlinClazz.visibility)
}

다음과 같이 클래스의 멤버, 생성자, 가시성 정보 등 다양한 메타 데이터들을 가져올 수 있습니다.

참고로 자바에서 reflection과 관련된 클래스는 Class, Method, Field, Constructor, Modifier 등의 클래스가 존재하며,

코틀린은 기본적으로 자바의 클래스와 더불어 코틀린에 특화된 KClass, KFunction, KProperty, KParameter, KType이 존재합니다.

이 외에도 많은 리플렉션 API들이 제공됩니다. 구체적인 기능들은 필요할 때마다 찾아서 적용할 수 있을 거에요!
리플렉션 API 기능 추가 자료

Reflection 활용 사례

JPA의 기본 생성자 생성 이유

앞으로 다시 돌아가서

이처럼 JPA에서 기본 생성자의 생성을 강제하는 이유는 뭘까요?

바로 리플렉션을 이용해 런타임에 동적으로 객체를 생성할 때 기본 생성자로 객체를 생성하고 필드에 값을 주입하는 것이 가장 편리한 방법이기 때문입니다!

예를 들어 JPA Entity의 생성자가 여러 개일 경우 런타임에 어떤 생성자를 사용하여 객체를 생성할 지 애매할 거에요

리플렉션 API: 객체 만들어 달라는데 뭐로 만들지?? 😱

이 때문에 기본 생성자로 일단 객체를 생성하고 필요한 필드를 수정해버리면 복잡하게 생각할 필요가 없어지기 때문에 기본 생성자의 구현을 강제하는 것입니다.

@Autowired의 DI 과정

출처

서치하다가 좋은 예제가 있어 가져왔습니다. 다음은 DI의 내부 동작 과정 구현한 것이에요!

스프링 @Autowired 실제 내부 동작 과정
빨간 박스 안의 로직을 보면,

  1. T 객체 생성
  2. T 클래스의 멤버 함수들을 가져와 Autowired 어노테이션 체크 (with reflection)
  3. Autowired 어노테이션이 붙은 타입의 객체 생성 (with reflection)
  4. 해당 필드의 접근 제어를 public으로 바꾸고 값 주입

이처럼 DI 또한 리플렉션이 아주 중요한 역할을 한다는 것을 알 수 있네요!

여기서 잠깐, 중간에 field.setAccessible(true) 에 주목해볼게요.
이 메소드는 field의 접근 제어를 변경하는 함수로 자바 리플렉션을 이용하면 특정 필드의 접근 제어가 private이더라도 이를 public하게 바꿔 접근할 수 있어요!

추가로 실제 스프링 부트의 InjectionMetadata.class의 inject 메소드를 보면, 중간에 ReflectionUtils.makeAccessible()를 통해 메소드의 접근 제어를 변경하는 것도 알 수 있습니다.

이렇게 리플렉션을 사용하면 멤버 필드의 접근 제어는 물론, 안에 값을 바꿔버릴 수도 있기 때문에 사용 시 각별한 주의가 필요합니다.

참고
kotlin의 경우 val로 선언된 필드는 kotlin 리플렉션으로는 변경할 수 없습니다! 반드시 변경이 필요하다면 java 리플렉션을 사용해야 해요!

이 외에도 Intellij의 자동완성, 어노테이션 기능, Dto의 json 직렬화/역직렬화 등 스프링과 JPA 프레임워크의 많은 부분에서 이 리플렉션이 사용되고 있습니다.
어노테이션 리플렉션 원리

리플렉션의 장단점

앞에서 살펴봤듯이 리플렉션 API 기능은 런타임에 동적으로 다양한 일들을 가능하도록 해줍니다.

그러나

이러한 리플렉션을 대부분의 사람들은 제한된 환경에서 사용해야 한다고 말하고 있어요!
그도 그럴 것이 리플렉션을 사용하면 private이 의미가 없어집니다 접근자를 바꿔서 마음껏 내부 값을 바꿀 수 있기 때문에 이를 마구잡이로 사용하면 코드가 더욱 예측하기 어려워 지겠죠?

사용할 일이 있다면 꼭! 많은 고민을 통해 적절한 상황에 제한된 방법으로 사용하는 것이 바람직할 것 같네요

이와 더불어 리플렉션의 장단점이 추가적으로 무엇이 있는지 간단하게 정리해서 알아봅시다

장점

  • 창의적이고 효율적인 구현 가능
  • 동적으로 유연한 설계 가능

단점

  • 성능이 떨어질 수 있다. (JVM 컴파일러가 최적화를 할 수 없다.)
  • 남용 시 코드가 예측하기 어려워진다.
  • 가독성이 떨어질 수 있다.
  • 컴파일 타임에 타입 체크를 할 수 없다. (런타임에 타입 에러 발생)

여기까지 스프링과 JPA를 사용하면서 넘겨 들었던 리플렉션에 대해 정리해봤습니다! 종종 개발하면서 리플렉션을 활용해보는 기회가 있었으면 좋겠네요!

마무리

스프링 프레임워크처럼 누구나에게 동일한 기능을 제공하기 위해선 리플렉션을 적극 활용해야 하기 때문에 주로 라이브러리나 프레임워크 구현에 많이 사용된다고 합니다. 실제 우리가 사용할 일은 아주 많지는 않겠지만, 꼭 맞는 상황에 적절하게 사용한다면 보다 효율적인 코드를 만들어 낼 수도 있겠네요!

JPA 강의 혹은 블로그 글에서 단순히 리플렉션 기능을 활용한다~ 정도로만 넘어갔었는데, 이번 기회에 정리해볼 수 있어 좋았습니다! 새로운 도구를 하나 얻은 것 같네요!

공부하면서 JVM 구조에 대해서도 살짝 알게 된 것 같은데, 완전 신기하고 재밌는 것 같아요!! 다음에 한 번 주제로 잡고 정리해보도록 하겠습니다 😎 😎

글 읽어주셔서 감사합니다!

📚 참고자료

https://sabarada.tistory.com/190
https://tjdtls690.github.io/studycontents/java/2023-01-27-reflection01/

https://velog.io/@alsgus92/Java-Reflection%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%96%B8%EC%A0%9C%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C

https://velog.io/@blackbean99/SpringBoot-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98%EC%9C%BC%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-Autowired

profile
평생 여행 다니는게 꿈 💭 👊 😁 🏋️‍♀️ 🦦 🔥

0개의 댓글