공유된 기술노트 · 자바 · 3. 고급 개념
자바 리플렉션: 실행 중 클래스 정보 활용과 SQL 생성기 구현
1. 리플렉션이 무엇인가?
자바에서 리플렉션(Reflection)은 "소스코드를 열어서 클래스 정보를 확인하지 않고도, 실행 중에 객체를 통해서 클래스 정보를 확인할 수 있게 해준다"는 개념입니다.
일반적으로 우리는 코드를 작성할 때 클래스의 구조(필드, 메서드 등)를 확인할 수 있습니다. 하지만 리플렉션은 소스코드를 보는 것이 아니라 프로그램이 실행 중(Run-time)에 메모리에 올라온 객체를 통해서 클래스의 구조를 동적으로 알 수 있게 해주는 기능입니다.
2. 리플렉션 API는 무엇인가?
리플렉션을 구현하기 위해 자바는 java.lang 패키지에 여러 도구를 제공합니다. 가장 핵심적인 도구들은 다음과 같습니다.
Class객체: 자바에서 모든 클래스와 인터페이스는 실행 시점에Class타입의 객체로 표현됩니다. 이 객체를 통해 해당 클래스의 메타데이터(이름, 필드 목록, 메서드 목록 등)를 조회할 수 있습니다.getDeclaredFields(): 해당 클래스에 선언된 모든 필드(변수)를Field객체의 배열로 반환합니다.getMethod()/getDeclaredMethods(): 해당 클래스의 메서드를Method객체로 반환합니다.
이러한 API들은 마치 "클래스의 블랙박스"를 열어 그 안에 무엇이 들어있는지 들여다보는 렌즈와 같습니다.
3. 왜 리플렉션 API가 필요한지
"클래스를 컴파일할 때 이미 알 수 있는데, 왜 실행 중에 다시 확인해야 하는가?"라는 의문이 생길 수 있습니다. 리플렉션이 필요한 대표적인 상황은 다음과 같습니다.
- 프레임워크 개발: Spring, Hibernate 같은 프레임워크는 사용자가 정의한 클래스를 미리 알 수 없습니다. 프레임워크는 사용자가 만든 클래스를 실행 중에 분석하여 SQL 문장을 생성하거나 의존성 주입(DI)할 객체를 찾거나 데이터 매핑을 수행할 수 있습니다.
- 동적 처리: 입력값이 어떤 클래스의 어떤 속성에 대입해야 하는지를 동적으로 판단하고 대입할 수가 있습니다.
- 도구 클래스: 직렬화(Serialization), JSON 변환기처럼 데이터 구조를 자동으로 분석해야 하는 도구들을 만들 수가 있습니다.
즉, 동적으로 클래스 정보를 얻어서 자동화하고 싶을 때 리플렉션이 사용됩니다.
4. 어떤 기능들을 가지고 있는지
리플렉션 API를 통해 수행할 수 있는 주요 기능들을 정리하면 다음과 같습니다.
- 클래스 정보 조회: 클래스 이름, 패키지, 상속 관계, 구현한 인터페이스 확인.
- 필드(Field) 조회: 필드의 이름, 타입, 접근 권한(Private, Public 등) 확인.
- 메서드(Method) 조회: 메서드 이름, 파라미터 타입, 반환 타입 확인.
- 객체 생성:
newInstance()메서드를 통해 클래스의 인스턴스를 동적으로 생성. - 필드 값 읽기/쓰기:
Field객체의get()과set()메서드를 통해 객체의 필드 값을 직접 읽거나 변경. - 메서드 호출:
Method객체의invoke()메서드를 통해 특정 메서드를 동적으로 실행.
5. SQL 생성기 예제 설명
이제 리플렉션의 실용성을 보여주는 예제를 살펴보겠습니다. 우리는 User 클래스를 정의하고, 이 클래스의 필드를 분석할 수 있다면 자동으로 SELECT과 INSERT SQL 문장을 생성하는 프로그램을 만들 수 있습니다.
목표 클래스:
public class User {
private int id;
private String username;
private String email;
// Getter, Setter, Constructor 생략
}실제로 위의 클래스 정보를 동적으로 얻어서 SQL문을 만드는 코드를 작성해보겠습니다. 그 절차는 다음과 같습니다.
동작 원리:
- 우선
User클래스 정보를 담고 있는Class객체를 얻습니다.
- 우선
getDeclaredFields()를 호출하여id,username,email필드를 가져옵니다.- 각 필드의 이름을 순회하며:
- SELECT 문:
SELECT id, username, email FROM user형태로 조합합니다. - INSERT 문:
INSERT INTO user (id, username, email) VALUES (?, ?, ?)형태로 조합합니다.
- SELECT 문:
이 과정을 통해, 만약 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 (?, ?, ?)코드 해설
Class<?> clazz = User.class;:User.class는 컴파일 시점에User클래스의Class객체를 참조합니다. 이 객체는User클래스의 모든 메타데이터를 담고 있습니다.Field[] fields = clazz.getDeclaredFields();: 이 메서드는User클래스 내부에 선언된 모든 필드를Field객체의 배열로 반환합니다.Field객체는 필드의 이름(getName()), 타입(getType()), 접근 권한(getModifiers()) 등을 조회할 수 있는 핸들러 역할을 합니다.Arrays.stream(fields).map(Field::getName)...:Field객체 배열을 스트림으로 변환하고, 각Field객체에서getName()메서드를 호출하여 필드 이름 문자열("id","username","email")만 추출합니다.String.join(", ", columns): 추출된 컬럼 이름 배열을 쉼표로 구분된 문자열로 결합하여 SQL 문장에 맞게 포맷팅합니다.
이처럼 리플렉션 API를 사용하면 클래스의 구조를 코드로 하드코딩하지 않고, 실행 중에 클래스를 분석하여 동적으로 로직을 구성할 수 있습니다. 이는 유지보수성과 확장성을 크게 높여줍니다.
이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.