공유된 기술노트 · 자바 · 3. 고급 개념

newlec · 2026. 9. 23.

자바 리플렉션: 실행 중 클래스 정보 활용과 SQL 생성기 구현

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

자바에서 리플렉션(Reflection)은 "소스코드를 열어서 클래스 정보를 확인하지 않고도, 실행 중에 객체를 통해서 클래스 정보를 확인할 수 있게 해준다"는 개념입니다.

일반적으로 우리는 코드를 작성할 때 클래스의 구조(필드, 메서드 등)를 확인할 수 있습니다. 하지만 리플렉션은 소스코드를 보는 것이 아니라 프로그램이 실행 중(Run-time)에 메모리에 올라온 객체를 통해서 클래스의 구조를 동적으로 알 수 있게 해주는 기능입니다.

2. 리플렉션 API는 무엇인가?

리플렉션을 구현하기 위해 자바는 java.lang 패키지에 여러 도구를 제공합니다. 가장 핵심적인 도구들은 다음과 같습니다.

  • Class 객체: 자바에서 모든 클래스와 인터페이스는 실행 시점에 Class 타입의 객체로 표현됩니다. 이 객체를 통해 해당 클래스의 메타데이터(이름, 필드 목록, 메서드 목록 등)를 조회할 수 있습니다.
  • getDeclaredFields(): 해당 클래스에 선언된 모든 필드(변수)를 Field 객체의 배열로 반환합니다.
  • getMethod() / getDeclaredMethods(): 해당 클래스의 메서드를 Method 객체로 반환합니다.

이러한 API들은 마치 "클래스의 블랙박스"를 열어 그 안에 무엇이 들어있는지 들여다보는 렌즈와 같습니다.

3. 왜 리플렉션 API가 필요한지

"클래스를 컴파일할 때 이미 알 수 있는데, 왜 실행 중에 다시 확인해야 하는가?"라는 의문이 생길 수 있습니다. 리플렉션이 필요한 대표적인 상황은 다음과 같습니다.

  1. 프레임워크 개발: Spring, Hibernate 같은 프레임워크는 사용자가 정의한 클래스를 미리 알 수 없습니다. 프레임워크는 사용자가 만든 클래스를 실행 중에 분석하여 SQL 문장을 생성하거나 의존성 주입(DI)할 객체를 찾거나 데이터 매핑을 수행할 수 있습니다.
  2. 동적 처리: 입력값이 어떤 클래스의 어떤 속성에 대입해야 하는지를 동적으로 판단하고 대입할 수가 있습니다.
  3. 도구 클래스: 직렬화(Serialization), JSON 변환기처럼 데이터 구조를 자동으로 분석해야 하는 도구들을 만들 수가 있습니다.

즉, 동적으로 클래스 정보를 얻어서 자동화하고 싶을 때 리플렉션이 사용됩니다.

4. 어떤 기능들을 가지고 있는지

리플렉션 API를 통해 수행할 수 있는 주요 기능들을 정리하면 다음과 같습니다.

  • 클래스 정보 조회: 클래스 이름, 패키지, 상속 관계, 구현한 인터페이스 확인.
  • 필드(Field) 조회: 필드의 이름, 타입, 접근 권한(Private, Public 등) 확인.
  • 메서드(Method) 조회: 메서드 이름, 파라미터 타입, 반환 타입 확인.
  • 객체 생성: newInstance() 메서드를 통해 클래스의 인스턴스를 동적으로 생성.
  • 필드 값 읽기/쓰기: Field 객체의 get()set() 메서드를 통해 객체의 필드 값을 직접 읽거나 변경.
  • 메서드 호출: Method 객체의 invoke() 메서드를 통해 특정 메서드를 동적으로 실행.

5. SQL 생성기 예제 설명

이제 리플렉션의 실용성을 보여주는 예제를 살펴보겠습니다. 우리는 User 클래스를 정의하고, 이 클래스의 필드를 분석할 수 있다면 자동으로 SELECTINSERT SQL 문장을 생성하는 프로그램을 만들 수 있습니다.

목표 클래스:

public class User {
    private int id;
    private String username;
    private String email;
    
    // Getter, Setter, Constructor 생략
}

실제로 위의 클래스 정보를 동적으로 얻어서 SQL문을 만드는 코드를 작성해보겠습니다. 그 절차는 다음과 같습니다.

동작 원리:

    1. 우선User 클래스 정보를 담고 있는 Class 객체를 얻습니다.
  1. getDeclaredFields()를 호출하여 id, username, email 필드를 가져옵니다.
  2. 각 필드의 이름을 순회하며:
    • SELECT 문: SELECT id, username, email FROM user 형태로 조합합니다.
    • INSERT 문: INSERT INTO user (id, username, email) VALUES (?, ?, ?) 형태로 조합합니다.

이 과정을 통해, 만약 User 클래스에 phone 필드가 추가된다면, SQL 생성기 코드를 수정하지 않아도 자동으로 phone 컬럼이 포함된 SQL이 생성됩니다.

6. API를 이용해서 구현해보기

아래는 Java 21 기준의 실제 구현 코드입니다. 각 단계별로 코드가 무엇을 하는지 자세히 설명합니다.

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.stream.Collectors;

public class ReflectionSqlGenerator {

    public static void main(String[] args) {
        // 1. 대상 클래스의 Class 객체를 얻습니다.
        // Class<?>는 제네릭으로 어떤 클래스든 받을 수 있음을 의미합니다.
        Class<?> clazz = User.class;
        
        System.out.println("=== 클래스 이름: " + clazz.getName() + " ===");

        // 2. 클래스에 선언된 모든 필드를 가져옵니다.
        // getDeclaredFields()는 private 필드도 포함하여 반환합니다.
        Field[] fields = clazz.getDeclaredFields();

        // 3. 필드 이름을 추출하여 SQL 생성에 사용합니다.
        // Stream API를 사용하여 필드 배열을 필드 이름 문자열 배열로 변환합니다.
        String[] columnNames = Arrays.stream(fields)
                .map(Field::getName) // Field 객체에서 이름을 추출
                .toArray(String[]::new);

        // 4. SELECT 문 생성
        String selectSql = generateSelectSql(columnNames, "user");
        System.out.println("SELECT: " + selectSql);

        // 5. INSERT 문 생성
        String insertSql = generateInsertSql(columnNames, "user");
        System.out.println("INSERT: " + insertSql);
    }

    /**
     * 컬럼 이름 배열을 받아 SELECT 문장을 생성합니다.
     */
    private static String generateSelectSql(String[] columns, String tableName) {
        // 컬럼 이름을 쉼표로 연결합니다. 예: "id, username, email"
        String columnList = String.join(", ", columns);
        
        // 최종 SQL 문장 조합
        return "SELECT " + columnList + " FROM " + tableName;
    }

    /**
     * 컬럼 이름 배열을 받아 INSERT 문장을 생성합니다.
     */
    private static String generateInsertSql(String[] columns, String tableName) {
        // 컬럼 이름을 쉼표로 연결합니다.
        String columnList = String.join(", ", columns);
        
        // VALUES 부분의 플레이스홀더(?) 생성
        // 컬럼 개수만큼 "?"를 쉼표로 연결합니다. 예: "? , ? , ?"
        String placeholders = Arrays.stream(columns)
                .map(c -> "?")
                .collect(Collectors.joining(", "));
        
        // 최종 SQL 문장 조합
        return "INSERT INTO " + tableName + " (" + columnList + ") VALUES (" + placeholders + ")";
    }
}

실행 결과 예시:

=== 클래스 이름: User ===
SELECT: SELECT id, username, email FROM user
INSERT: INSERT INTO user (id, username, email) VALUES (?, ?, ?)

코드 해설

  1. Class<?> clazz = User.class;: User.class는 컴파일 시점에 User 클래스의 Class 객체를 참조합니다. 이 객체는 User 클래스의 모든 메타데이터를 담고 있습니다.

  2. Field[] fields = clazz.getDeclaredFields();: 이 메서드는 User 클래스 내부에 선언된 모든 필드를 Field 객체의 배열로 반환합니다. Field 객체는 필드의 이름(getName()), 타입(getType()), 접근 권한(getModifiers()) 등을 조회할 수 있는 핸들러 역할을 합니다.

  3. Arrays.stream(fields).map(Field::getName)...: Field 객체 배열을 스트림으로 변환하고, 각 Field 객체에서 getName() 메서드를 호출하여 필드 이름 문자열("id", "username", "email")만 추출합니다.

  4. String.join(", ", columns): 추출된 컬럼 이름 배열을 쉼표로 구분된 문자열로 결합하여 SQL 문장에 맞게 포맷팅합니다.

이처럼 리플렉션 API를 사용하면 클래스의 구조를 코드로 하드코딩하지 않고, 실행 중에 클래스를 분석하여 동적으로 로직을 구성할 수 있습니다. 이는 유지보수성과 확장성을 크게 높여줍니다.

이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.