CodeGym /행동 /JAVA 25 SELF /리플렉션 입문: Class 클래스, 왜 필요한가

리플렉션 입문: Class 클래스, 왜 필요한가

JAVA 25 SELF
레벨 62 , 레슨 0
사용 가능

1. 리플렉션이란 무엇인가

Java의 리플렉션은 프로그램이 실행 중에 자신의 구조를 조사하고 심지어 변경할 수 있게 해 주는 메커니즘입니다. 마치 스스로의 내부를 들여다보며 “내게 어떤 필드와 메서드가 있지? 생성자는 무엇이지? 나는 대체 어떤 클래스지?”라고 알아보고 — 심지어 자신의 private 메서드도 호출할 수 있는 것과 같습니다.

공식 정의:
리플렉션은 실행 중에 클래스, 인터페이스, 필드, 메서드, 생성자에 대한 정보를 얻고 그것들을 다룰 수 있게 해 주는 API입니다.

리플렉션이 왜 필요한가?

  • 프레임워크: Spring, Hibernate, JUnit은 리플렉션으로 “마법”을 구현합니다: 객체 자동 생성, 의존성 주입, 이름이나 애너테이션으로 메서드 호출.
  • 직렬화: 객체를 JSON/XML로 변환하고 다시 되돌리려면 필드를 알아내고 접근할 수 있어야 합니다.
  • 테스트: 애너테이션(예: @Test)이 붙은 메서드를 자동으로 찾고 호출.
  • 클래스 동적 로딩: 실행 중 이름으로 클래스를 로딩(플러그인, 드라이버).
  • IDE와 코드 분석: 자동 완성, 검사, 리팩터링.

생활 속 예시

public class Person {
    private String name;
    private int age;

    public void sayHello() {
        System.out.println("안녕하세요, 제 이름은 " + name);
    }
}

리플렉션을 사용하면 실행 중에 Person 클래스에 nameage 필드, 그리고 sayHello 메서드가 있다는 것을 알 수 있습니다. 더 나아가, private 필드 값을 바꾸거나 이름으로 메서드를 호출할 수도 있습니다.

약간의 유머
리플렉션은 “매트릭스” 같다고 할 수 있습니다: 여러분의 모든 코드는 실행 중에 조사하고 바꿀 수 있는 데이터라는 사실을 자각하는 것이죠. 다만 보안이라는 빨간 알약은 잊지 마세요!

2. Class 클래스: 리플렉션의 핵심

Java에서는 모든 객체와 모든 타입이 실행 중에 특별한 Class 타입의 객체와 연결됩니다. 이 객체가 바로 타입에 대한 “메타정보”입니다. java.lang.Class<T> 클래스는 리플렉션 세계로 들어가는 중심 진입점입니다.

Class 객체를 얻는 방법?

몇 가지 방법이 있습니다:

  • .class
    클래스가 컴파일 타임에 이미 알려져 있다면 가장 직접적이고 안전한 방법입니다:
    Class<Person> personClass = Person.class;
  • 객체를 통해: getClass()
    이미 인스턴스가 있다면:
    Person p = new Person();
    Class<?> clazz = p.getClass();
    여기서 clazz는 객체 p의 실제 클래스를 설명하는 Class 객체입니다.
  • 문자열로: Class.forName()
    클래스 이름을 실행 중에만 알 수 있다면:
    Class<?> clazz = Class.forName("com.example.Person");
    플러그인, 드라이버 등 동적 로딩에 적합합니다.

예: 클래스 이름 얻기

Person p = new Person();
Class<?> clazz = p.getClass();
System.out.println(clazz.getName()); // 출력: Person 또는 com.example.Person

표: Class 객체를 얻는 주요 방법

방법 사용 시점
MyClass.class
컴파일 시점에 클래스가 알려져 있을 때
String.class
object.getClass()
객체가 있을 때
someList.getClass()
Class.forName(name)
런타임에 클래스 이름만 알려져 있을 때
Class.forName("java.util.Date")

도식: 객체와 그 Class

+-------------------+
|   Person p        |
+-------------------+
         |
         v
+-------------------+
|  p.getClass()     |
+-------------------+
         |
         v
+-------------------+
|   Class<Person>   |
+-------------------+

Class로 타입 확인

가끔 객체가 특정 클래스에 속하는지 확인해야 합니다:

if (p.getClass() == Person.class) {
    System.out.println("이건 확실히 Person!");
}

상속을 고려하려면 — 일반적인 instanceof 연산자:

if (p instanceof Person) {
    // 평소처럼 동작한다
}

3. 언제 리플렉션이 필요하고 언제는 아닌가

큰 힘에는 큰 책임이 따릅니다. 리플렉션은 강력한 도구지만, 정말 필요할 때만 사용해야 합니다.

리플렉션이 꼭 필요한 곳

  • 프레임워크와 라이브러리: Spring, Hibernate, JUnit, Jackson — 자동화, “마법”, 그리고 더 적은 보일러플레이트 코드.
  • 직렬화: 객체를 JSON/XML로 바꾸려면 필드를 알아야 합니다.
  • 플러그인과 확장: 클래스를 동적으로 로딩하며, 구조를 미리 알 수 없습니다.

리플렉션을 남용하면 안 되는 이유

  • 타입 안정성 저하: 오류가 런타임에 드러납니다.
  • 유지보수 어려움: 코드가 덜 명확해집니다.
  • 성능: 직접 호출보다 느립니다.
  • 보안: 캡슐화를 우회해 private 데이터를 노출시킬 수 있습니다.

예: 리플렉션이 필요 없는 경우

p.sayHello();

예: 리플렉션이 반드시 필요한 경우
미니 테스트 프레임워크를 작성한다고 합시다. 사용자는 메서드를 @Test 애너테이션으로 표시하고, 프로그램은 이를 자동으로 찾아 호출해야 합니다 — 리플렉션 없이 불가능합니다.

4. 데모: Class와 친해지기

예: 클래스 이름과 슈퍼클래스 출력

public class ReflectionDemo {
    public static void main(String[] args) {
        String s = "Hello, reflection!";
        Class<?> clazz = s.getClass();

        System.out.println("클래스 이름: " + clazz.getName());
        System.out.println("단순 이름: " + clazz.getSimpleName());
        System.out.println("패키지: " + clazz.getPackageName());
        System.out.println("슈퍼클래스: " + clazz.getSuperclass().getName());
    }
}

결과:

클래스 이름: java.lang.String
단순 이름: String
패키지: java.lang
슈퍼클래스: java.lang.Object

예: 기본 타입의 Class 얻기

Class<Integer> intClass = int.class;
System.out.println(intClass.getName()); // int

Class<?> doubleClass = double.class;
System.out.println(doubleClass.getName()); // double

예: 배열의 Class 얻기

int[] arr = new int[10];
Class<?> arrClass = arr.getClass();
System.out.println(arrClass.getName()); // [I (배열용 특수 형식)

예: 타입 소속 여부 확인

if (arrClass.isArray()) {
    System.out.println("이건 배열입니다!");
}

5. 실습: 미니 프로그램 “이건 어떤 클래스?”

클래스의 전체 이름(예: "java.util.ArrayList")을 입력받아 해당 클래스의 기본 정보를 출력하는 프로그램을 작성합니다.

import java.util.Scanner;

public class ClassInfoPrinter {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.print("클래스의 전체 이름을 입력하세요: ");
        String className = scanner.nextLine();

        Class<?> clazz = Class.forName(className);

        System.out.println("클래스 이름: " + clazz.getName());
        System.out.println("패키지: " + clazz.getPackageName());
        System.out.println("슈퍼클래스: " + clazz.getSuperclass().getName());

        Class<?>[] interfaces = clazz.getInterfaces();
        System.out.print("구현 인터페이스: ");
        for (Class<?> i : interfaces) {
            System.out.print(i.getName() + " ");
        }
        System.out.println();
    }
}

실행 예:

클래스의 전체 이름을 입력하세요: java.util.ArrayList
클래스 이름: java.util.ArrayList
패키지: java.util
슈퍼클래스: java.util.AbstractList
구현 인터페이스: java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable 

Class와 리플렉션에 관한 흥미로운 사실

  • JVM에 로드된 각 타입에는 하나의 대표자 — Class 객체 — 가 있습니다. 같은 타입의 모든 인스턴스는 동일한 Class<T>를 공유합니다.
  • 타입의 종류를 판별할 수 있습니다: isInterface(), isEnum(), isArray(), isPrimitive() 등.
  • Class.forName에 잘못된 이름을 주면 ClassNotFoundException 예외가 발생합니다.
  • 리플렉션을 사용하면 컴파일 타임에 클래스를 몰라도 객체를 생성할 수 있습니다 — 이는 다음 강의에서 다룹니다.

6. 리플렉션을 처음 접할 때 흔한 실수

오류 №1: 리플렉션은 빠르다고 기대함.
사실 리플렉션은 일반적인 메서드 호출이나 필드 접근보다 느립니다. 사소한 것마다 사용하지 마세요.

오류 №2: 존재하지 않는 클래스의 Class를 얻으려 함.
이름을 틀리면 ClassNotFoundException이 발생합니다. 이 예외를 처리하세요.

오류 №3: Class 객체와 인스턴스를 혼동함.
Class 객체는 타입의 구조를 설명할 뿐, 그 타입의 인스턴스가 아닙니다.

오류 №4: 불필요한 리플렉션 사용.
일반 코드로 충분하다면 — 그렇게 하는 것이 더 낫습니다. 리플렉션은 동적 동작, 프레임워크, 플러그인과 같은 작업에 필요합니다.

오류 №5: private 필드와 메서드를 부주의하게 다룸.
리플렉션은 캡슐화를 우회할 수 있게 하므로, 특히 큰 프로젝트에서 오류와 보안 문제로 이어질 수 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION