앱 개발에서의 Reflection — 정의, 리플렉션 메커니즘 및 적용 방법

저자: IT Sectr 게시일: 2026-05-17 읽는 시간: 8 분

Reflection(리플렉션)은 컴파일 시점에 타입을 알지 못해도 클래스, 메서드, 필드, 어노테이션을 얻어 코드가 자신의 구조를 검사할 수 있게 하는 런타임 메커니즘입니다. 이 도구는 많은 모바일 프레임워크의 기반입니다 — JSON 직렬화(Gson, Moshi), 의존성 주입(Dagger, Koin), 테스트 러너(JUnit, XCTest) 등입니다. Oracle Java Reflection Tutorial, 2024에 따르면, reflection은 모든 주요 라이브러리가 사용하는 Java 플랫폼의 필수 요소입니다.

핵심 요점

  • Reflection은 프로그램 실행 중 클래스, 메서드, 필드의 메타데이터에 접근하는 것입니다.
  • Java Reflection API는 동적 분석을 위해 Class, Method, Field, Constructor 클래스를 제공합니다.
  • Kotlin 리플렉션은 코루틴 및 직렬화와 통합된 KClass와 KFunction을 사용합니다.
  • Objective-C Runtime은 class_copyMethodList와 objc_getClass를 통한 reflection의 한 형태입니다.
  • reflection의 성능은 JIT 최적화가 없어 직접 호출보다 10~100배 낮습니다.

Reflection이란 무엇인가?

Reflection은 실행 중에 프로그램이 자신의 구조와 동작을 관찰하고 수정하는 능력입니다. 객체 지향 언어에서 이는 읽고 호출할 수 있는 데이터로 프로그램 요소를 나타내는 Class, Method, Field, Constructor 객체를 얻는 것을 의미합니다.

“reflection”이라는 용어는 1982년 인공지능 커뮤니티(Brian Cantwell Smith)에서 도입되었으며 Smalltalk 언어로 구현되었습니다. 모바일 개발에서 reflection은 Java ME와 Objective-C(1986, NextStep)에서 처음 등장했습니다. 오늘날 주요 모바일 플랫폼마다 자체 reflection API가 있습니다: Android용 Java/Kotlin, iOS용 Objective-C Runtime, Swift용 Swift Mirror API입니다.

reflection 메커니즘은 컴파일러가 바이트코드나 바이너리에 저장하는 메타데이터에 기반합니다. Android는 DEX 파일에 클래스에 대한 전체 정보를 저장하고, iOS는 Mach-O 세그먼트의 __objc_classlist 섹션에 저장합니다. 런타임은 이 메타데이터를 메모리에 로드하고 이를 탐색할 API를 제공합니다.

Java와 Kotlin에서 Reflection이 작동하는 방식

Java Reflection API는 java.lang.Class 클래스를 중심으로 구축됩니다. Java의 모든 객체는 .getClass() 또는 Class.forName()을 통해 Class로 변환할 수 있습니다. Class에서 모든 메서드, 필드, 생성자, 어노테이션, 슈퍼클래스가 추출됩니다. Kotlin은 Java의 reflection을 상속하고 kotlin.reflect 패키지에서 자체 KClass, KFunction, KProperty를 추가합니다.

kotlin
import kotlin.reflect.full.declaredMemberFunctions

data class User(
    val name: String,
    val email: String
)

fun inspectClass() {
    val kClass = User::class
    val properties = kClass.declaredMemberProperties
    val functions = kClass.declaredMemberFunctions

    properties.forEach { prop ->
        println("속성: ${prop.name}, 타입: ${prop.returnType}")
    }
}

이 예제에서 KClass는 data class User의 메타데이터를 제공합니다. declaredMemberProperties는 타입과 getter가 포함된 속성 목록을 반환합니다. Kotlin 리플렉션은 코루틴과 밀접하게 통합됩니다: KFunction은 suspend 수정자를 지원하므로 reflection을 통해 비동기 메서드를 호출할 수 있습니다.

Java Reflection: Class, Method, Field

Java reflection은 Class<?>, Method.setAccessible(), Field.get()과 함께 작동합니다. setAccessible(true)는 private 요소에 대한 Java 언어 접근 제어 검사를 비활성화합니다. 이는 강력하지만 위험한 메커니즘입니다: API 28부터 Android에서 숨겨진 시스템 메서드에 setAccessible을 호출하면 InaccessibleObjectException이 발생할 수 있습니다.

java
// Java reflection: private 메서드 호출
Class clazz = Class.forName("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
method.invoke(instance, "reflection test");

이 코드는 Class.forName()을 보여줍니다 — 문자열 이름으로 클래스를 동적으로 로드하는 것입니다. 이는 플러그인 아키텍처의 기초입니다: 클래스는 컴파일 시점에 알 수 없어도 런타임에 reflection을 통해 로드되고 실행될 수 있습니다. getDeclaredMethod(“privateMethod”, ...)는 이름과 매개변수 타입으로 메서드를 찾고, invoke가 이를 실행합니다.

Objective-C Runtime: 리플렉션의 대체 모델

Objective-C 런타임은 class_copyMethodList, class_copyPropertyList, objc_getAssociatedObject 함수를 제공합니다. Java와 달리 Objective-C는 기본적으로 private 메서드를 숨기지 않습니다 — 런타임은 클래스의 모든 메서드를 봅니다. 이것이 method swizzling이 setAccessible 없이 작동하는 이유를 설명합니다: 런타임에는 메타데이터 수준의 캡슐화가 없습니다.

모바일 개발에서 Reflection의 활용

Reflection은 모바일 개발의 핵심 라이브러리에서 사용됩니다. JSON 직렬화(Gson, Moshi, Kotlinx.serialization)는 reflection을 통해 객체의 속성을 얻고 JSON 키와 대조합니다. 의존성 주입(Dagger, Koin, Swinject)은 자동 의존성 주입을 위해 생성자와 필드를 분석합니다. ORM 라이브러리(Room, Realm)는 클래스를 데이터베이스 테이블에 매핑하기 위해 reflection을 사용합니다.

  • 직렬화 — Gson은 Field.get()을 통해 객체의 선언된 필드를 읽고 @SerializedName 어노테이션에 따라 JSON을 만듭니다.
  • 의존성 주입 — Dagger는 어노테이션 프로세싱으로 코드를 생성하고, Koin은 런타임 해석에 Kotlin 리플렉션을 사용합니다.
  • 테스트 — JUnit은 reflection으로 @Test가 있는 메서드를 찾아 호출합니다; Mockito는 동적 프록시로 mock을 만듭니다.
  • 데이터베이스 — Room은 컴파일 시점에 Class.getDeclaredFields()로 Entity 필드를 확인합니다(KAPT/KSP 경유).
  • 분석 및 모니터링 — Firebase Crashlytics는 reflection에 기반한 Throwable.getStackTrace()로 스택 트레이스를 얻습니다.

이러한 각 활용은 정확히 런타임에서 작동합니다 — 코드는 어떤 클래스를 만날지 미리 알지 못합니다. Reflection은 성능과 보안을 희생하면서 이러한 불확실성을 극복하는 보편적인 메커니즘을 제공합니다.

Reflection의 성능: 동적 접근의 비용

Reflection은 직접 메서드 호출보다 10~100배 느립니다. 그 이유는 JIT 최적화(devirtualization, inlining)의 부재, 호출마다의 타입 검사, 매개변수를 Object[]/varargs에 포장하는 것입니다. Android 14의 ART는 대상 메서드가 실행 시점까지 알려지지 않으므로 reflection 호출을 인라인 최적화할 수 없습니다.

작업직접 호출Reflection 경유저하율
매개변수 없는 메서드 호출~3 ns~120 ns40x
int 필드 읽기~1 ns~85 ns85x
매개변수 2개인 메서드 호출~4 ns~250 ns62x
생성자를 통한 인스턴스 생성~5 ns~180 ns36x
문자열로 클래스 해석~800 ns

데이터는 Google Pixel 8(Android 14, ART)에서 얻었습니다. reflection의 성능은 Android 버전마다 개선됩니다: Android 9에서 Method.invoke()를 통한 호출은 직접 호출보다 150배 느렸지만, Android 14에서는 40배입니다. ART는 최적화를 위해 내장된 method handle 메커니즘을 사용합니다.

성능에 민감한 부분에서 개발자는 reflection을 코드 생성으로 대체합니다: Dagger는 런타임 검색 대신 어노테이션 프로세싱을 사용하고, Kotlinx.serialization은 KSP로 직렬화기를 생성하며, Moshi는 컴파일 타임 codegen을 위해 @JsonClass(generateAdapter = true)를 적용합니다.

Reflection의 대안: 어노테이션과 코드 생성

어노테이션 프로세싱(KAPT, KSP)과 코드 생성은 모바일 개발에서 reflection의 주요 대안입니다. 이들은 메타데이터 분석을 런타임에서 컴파일 시점으로 옮깁니다: 코드는 앱 시작 전에 생성되므로 reflection 오버헤드를 제거하고 성능을 향상시킵니다.

kotlin
// KSP: reflection 대신 코드 생성
@Serializable
data class Config(
    val apiUrl: String,
    val timeout: Int
)

// KSP는 reflection 없이 ConfigSerializer를 생성한다
fun loadConfig(json: String): Config {
    return Config.serializer().decodeFromString(json)
}

이 예제에서 @Serializable은 Kotlinx.serialization의 어노테이션입니다. KSP(Kotlin Symbol Processing)는 컴파일 시점에 소스 코드를 분석하고 모든 @Serializable 클래스를 찾아 직렬화기를 생성합니다. 앱 실행 중에는 reflection이 사용되지 않습니다 — 직렬화기는 이미 기계어로 컴파일되어 있습니다.

접근 방식 비교

코드 생성은 더 나은 성능, 타입 안전성, 더 작은 바이너리 크기를 제공합니다(데드 코드 제거가 사용하지 않는 reflection 의존성을 제거합니다). reflection은 컴파일 시점에 타입을 알 수 없는 작업(동적 플러그인 로드, 런타임 프록시, 테스트 계측)에 여전히 필요합니다. Kotlin에 따르면 KSP를 사용한 Kotlinx.serialization은 reflection 기반의 Gson보다 3~5배 빠릅니다.

Android와 iOS에서 Reflection의 제한 사항

모바일 플랫폼의 Reflection에는 보안 및 성능 제한이 있습니다. Android는 API 28(Pie)부터 non-SDK 인터페이스에 대한 setAccessible을 제한합니다 — 숨겨진 시스템 메서드를 열려고 하면 예외나 경고가 발생합니다. Swift를 사용하는 iOS는 전통적인 의미의 reflection을 지원하지 않습니다: Swift Mirror API는 수정이나 메서드 호출 없이 속성(name, value) 읽기만 제공합니다.

Google Play는 플랫폼 제한을 우회하기 위해 reflection을 사용하는 앱을 거부합니다: 시스템 서비스 대체, SELinux 정책 수정, 보호된 권한 읽기. Apple도 reflection으로 비공개 API를 호출하는 앱을 차단합니다 — App Review 검사는 알려진 비공개 셀렉터가 있는 objc_msgSend의 문자열 시그니처를 바이너리에서 스캔합니다.

ProGuard/R8은 또 다른 제한입니다. 난독화와 코드 축소는 클래스와 메서드를 짧은 이름(a, b, c)으로 바꿉니다. 코드가 Class.forName(“com.example.MyClass”)을 사용하면 난독화 후 깨집니다. 해결책은 proguard-rules.pro의 keep 규칙입니다:

groovy
// reflection용 ProGuard keep 규칙
-keep class com.example.** { *; }
-keep class * implements java.io.Serializable { *; }
-keepclassmembers class * {
    @com.google.gson.annotations.SerializedName ;
}
-keepattributes Signature, InnerClasses, EnclosingMethod

-keep 규칙은 R8에게 reflection으로 사용되는 클래스를 이름 변경하지 말라고 지시합니다. 이 규칙이 없으면 난독화된 앱은 ClassNotFoundException으로 충돌합니다 — 런타임이 변경된 문자열 이름으로 클래스를 찾을 수 없습니다.

자주 묻는 질문

Reflection은 애플리케이션 성능에 해롭나요?

네, reflection은 직접 호출보다 10~100배 느립니다. 주요 이유: JIT 최적화(inlining, devirtualization) 부재, 매개변수 포장, 호출마다 타입 검사입니다. 프로덕션 코드에서는 KSP 또는 어노테이션 프로세싱을 통한 코드 생성으로 reflection을 대체하는 것이 권장됩니다.

Java reflection과 Kotlin reflection의 차이점은 무엇인가요?

Java reflection은 Class, Method, Field로 작동하며 private 멤버에는 setAccessible이 필요합니다. Kotlin reflection은 KClass, KFunction, KProperty를 사용하고 sealed class, data class, 코루틴(suspend 함수), null-safety를 지원합니다. Kotlin reflection은 Java reflection에 기반하지만 type-safe API를 추가합니다.

Reflection 사용 시 난독화 문제를 어떻게 피하나요?

reflection으로 사용되는 클래스, 메서드, 필드에 ProGuard/R8 keep 규칙을 추가하세요. 각 Class.forName(), getDeclaredMethod(), getDeclaredField()에 대해 해당하는 -keep 지시문이 있어야 합니다. GreenDAO와 Room 같은 도구는 keep 규칙을 자동으로 생성합니다.

Swift에 Reflection이 있나요?

Swift에는 완전한 의미의 reflection이 없습니다. Mirror API(Swift 2+)는 구조체나 클래스의 속성(이름, 값, 타입)을 읽을 수 있습니다. 메서드 호출, 필드 수정, 타입별 인스턴스 생성은 불가능합니다. 이를 위해 NSObject에서 @objc dynamic으로 상속할 때 Objective-C Runtime이 사용됩니다.

어떤 라이브러리가 Android에서 Reflection을 사용하나요?

Gson(JSON 직렬화), Retrofit(동적 프록시로 인터페이스 구현 생성), Mockito(mock 생성), Koin(의존성 주입), Room(KAPT로 컴파일 시점에 Entity 확인), Firebase Crashlytics(스택 트레이스 분석)입니다. 대부분의 라이브러리가 KSP/KAPT를 이용한 코드 생성으로 전환하고 있습니다.

요약

  • Reflection은 클래스, 메서드, 필드의 메타데이터에 접근하기 위한 런타임 메커니즘입니다.
  • Java reflection은 Class, Method, Field를 사용합니다; Kotlin은 코루틴 통합이 있는 KClass, KFunction, KProperty를 사용합니다.
  • Objective-C Runtime은 접근 제한 없이 class_copyMethodList와 objc_getClass를 제공합니다.
  • Reflection은 느립니다 — JIT 최적화가 없어 직접 호출보다 10~100배입니다.
  • 대안 — 코드 생성(KSP, KAPT)과 어노테이션 프로세싱 — 이 reflection 오버헤드를 제거합니다.
  • ProGuard/R8은 Class.forName()과 getDeclaredMethod()로 사용되는 클래스에 keep 규칙이 필요합니다.
  • reflection은 컴파일 시점에 타입을 알 수 없는 동적 플러그인 로드, DI, 테스트 프레임워크에 필수적입니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기