책을 읽다가 리플렉션에 대한 내용이 언급되었다. 리플랙션? 그게 뭐지라는 생각을 가지고 찾아보니, 동적으로 클래스를 사용할 때 이용되는 것 같았다. 그렇다면 이게 왜 필요한 것일까?
자바의 리플렉션은 프로그램이 실행 중에 자신의 구조를 파악하고, 조작할 수 있는 기능을 말한다. 즉, 컴파일 시간이 아닌 실행 중에 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.
리플렉션은 힙 영역에 로드된 class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드롸 메서드를 접근 제어자와 상관없이 사용할 수 있도록 지원하는 API이다.
여기서 로드된 class란 JVM의 클래스 로더에서 클래스 파일에 대한 로딩 후, 해당 클래스 정보를 담은 인스턴스를 생성하여 메모리 힙 영역에 저장된 것을 의미한다.
리플렉션은 주로 프레임워크나 라이브러리에서 사용된다. Spring, Hibernate 등의 프레임워크에서는 사용자가 정의한 클래스나 메서드에 대한 정보를 얻거나, 해당 구성 요소를 동적으로 조작하기 위해 리플렉션을 사용한다.
예를 들어 Spring의 @Controller, @Service, @Repository만 클래스에 추가해도 해당 클래스가 Bean Factory에 저장되는데 사용자가 해당 클래스를 빈으로 등록하지 않아도 프레임워크에서 리플렉션을 이용하여 빈을 등록하는 것이다.
또한 리프렉션은 유닛 테스트에서 사용된다. private 메서드나 필드에 접근하거나, 동적으로 테스트 대상을 생성할 때 리플렉션을 사용할 수 있다.
리플렉션을 이용하면 총 4가지의 정보를 가져올 수 있다. Class, Constructor, Method, Field를 가져올 수 있다. 다음은 예시 코드이다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Builder
public Team(String name) {
this.name = name;
}
private String getTeamName() {
return this.name;
}
public void printTeamName() {
System.out.println(this.getTeamName());
}
}
이렇게 구성된 클래스의 정보를 조회하고 수정할 때 리플렉션을 이용하여 정보를 가져올 수 있다. (접근 제어자와 상관없이)
class TeamTest {
Team team;
@BeforeEach
void setUp() {
team = Team.builder()
.name("팀1")
.build();
}
@Test
void 리플렉션_테스트_클래스_이름_조회() {
// 리플렉션을 통해 클래스의 이름을 조회할 수 있다.
System.out.println(team.getClass().getName());
}
@Test
void 리플레션_테스트_클래스_이름으로_해당_클래스_조회() throws ClassNotFoundException {
// 리플렉션을 통해 클래스의 이름으로 해당 클래스를 조회할 수 있다.
Class<?> aClass = Class.forName("com.example.domain.team.entity.Team");
System.out.println(aClass.getName());
}
@Test
void 리플렉션_테스트_모든_생성자_조회() {
// 리플렉션을 통해 모든 생성자를 조회할 수 있다.
Arrays.stream(team.getClass()
.getDeclaredConstructors())
.forEach(System.out::println);
}
@Test
void 리플렉션_테스트_PUBLIC_생성자_조회() {
// 리플렉션을 통해 public 생성자를 조회할 수 있다.
Arrays.stream(team.getClass()
.getConstructors())
.forEach(System.out::println);
}
@Test
void 리플렉션_테스트_PRIVATE_메서드_조회() throws NoSuchMethodException {
// 리플렉션을 통해 private 메서드를 조회할 수 있다.
Method method = team.getClass().getDeclaredMethod("getTeamName");
System.out.println(method);
}
@Test
void 리플렉션_테스트_모든_메서드_조회() {
// 리플렉션을 통해 모든 메서드를 조회할 수 있다. (상속받은 메서드 포함, 접근 제한자 상관 없음)
Arrays.stream(team.getClass().getDeclaredMethods())
.forEach(System.out::println);
}
@Test
void 리플렉션_테스트_모든_필드_조회() {
// 리플렉션을 통해 모든 필드를 조회할 수 있다. (상속받은 필드 포함, 접근 제한자 상관 없음)
Arrays.stream(team.getClass().getDeclaredFields())
.forEach(System.out::println);
}
@Test
void 리플렉션_테스트_필드_변경() throws NoSuchFieldException {
// 리플렉션을 통해 private 필드를 변경할 수 있다.
Field field = team.getClass().getDeclaredField("name");
field.setAccessible(true); // private 필드에 접근하기 위해선 필요하다.
try {
field.set(team, "팀2");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(team.getName());
}
}
이렇게 리플렉션을 이용하면 접근제어자에 상관없이 동적으로 프로그램의 행동을 변경할 수 있다. 또한 프로그램 코드 없이도 외부의 클래스나 메서드를 로딩하고 조작할 수가 있다.
위의 테스트의 뿐만 아니라 리플렉션을 이용하면 다양한 조작과 수정이 가능해지는데 이렇게 리플렉션을 이용하면 부작용이 생길 수 있다.
첫번째, 리플렉션을 사용하면 코드가 복잡해지고 가독성이 떨어질 수 있다. 두번째, 리플렉션은 직접적인 코드보다 비용이 크므로, 성능에 영향을 미칠 수 있다. 세번쨰, 보안 매니저가 설정된 경우, 리플렉션을 통한 접근이 제한될 수 있다. 마지막으로 컴파일 타임에 체크할 수 없는 오류가 런타임에 발생할 수 있다.
따라서 리플레션은 반드시 필요한 경우에만 사용하고, 그 외의 경우에는 일반적인 프로그래밍 기법을 사용하는 것이 좋아 보인다. 또한 접근 제어자를 무시하고 클래스를 조작하는 것이기 때문에 객체지향의 원칙인 캡슐화를 저해하므로 주의해서 사용하자.