녕의 학습 기록

Java Reflection을 사용해 private 필드 값 설정하기 본문

Dev

Java Reflection을 사용해 private 필드 값 설정하기

kjyyjk 2025. 1. 10. 01:32

문제 상황

서비스 레이어의 단위 테스트를 위해 TestRepository 코드를 작성하던 중 save() 내부에서 파라미터로 받은 엔티티의 id를 설정해주어야 했다. 하지만 id 필드의 접근 제어자는 private이고, setter를 열어두지 않았기 때문에 TestRepository에서 id값을 설정할 수가 없었다.

public class GroupTestRepository implements GroupRepository {
    private Map<Long, Group> storage = new HashMap<>();
    private long sequence = 0L;

    @Override
    public Group save(Group group) {
        if (group.getId() != null) {
            storage.replace(group.getId(), group);
        } else {
            ++sequence;
            group.setId(sequence); // 컴파일 에러 발생
            storage.put(sequence, group);
        }
        return group;
    }

 

시도한 것

1. 접근 제어자 public으로 수정 / setter 열기

id필드의 접근 제어자를 public으로 바꾸거나, setter를 두면 TestRepository에서 id값을 손쉽게 설정할 수 있다. 하지만 접근 제어자를 private으로 둔 것은 외부로부터 접근을 방지하고 데이터를 더 안전하게 보관하기 위함이었다. id뿐만 아니라 엔티티의 다른 필드들 또한 같은 이유로 private으로 선언해두었다. 필요한 경우에는 getter/setter와 같은 public 메서드를 통해서만 외부에서 접근 가능 하도록 설계했다. 그렇기 때문에 public으로 바꾸거나 setter를 만드는 행위는 오로지 테스트를 위해서만이라고 볼 수 있다. 이는 곧 객체지향 원칙인 캡슐화를 위반하는 것이며 바람직하지 못한 방법이라고 생각한다. 또한 테스트를 위해 메인 코드를 수정하는 것은 유지 보수성을 저하해 의도하지 않은 장애로 이어지기 쉽다.

 

2. Java Reflection 사용

Java Reflection을 사용하면 private 필드에도 접근하고 값도 설정할 수 있다. 실제로 Jpa의 구현체인 하이버네이트도 save() 호출 시 내부적으로 Reflection을 사용해 id값을 설정한다.

 

Class<? extends Group> class = group.getClass();

 

getClass()는 해당 객체의 런타임 시점 클래스 정보를 가지고 온다.

 

Field id = class.getDeclaredField("id");

 

Class.getDeclaredField()는 파라미터로 주어진 이름과 일치하는 필드를 찾고, 해당 필드를 반영하는 Field 객체를 반환한다. 만약 파라미터의 필드명과 일치하는 필드가 없으면 NoSuchFieldException 예외를 발생한다. 주의할 점으로 Class.getField()라는 유사한 메서드도 존재한다. 하지만 getField()는 public 필드에만 접근할 수 있다. 따라서 private 필드에 접근하기 위해서는 꼭 getDeclaredField()를 사용해야 한다.

 

id.setAccessible(true);

 

id 필드의 접근제어자는 private이므로, Field.setAccessible(true)를 호출해 필드에 대한 접근 가능 여부를 조작한다. 실제 필드의 접근 제어자가 바뀌는 것은 아니고, 필드가 반영된 Field에 대한 동작 방식을 변경하는 것이다.

 

id.set(group, sequence);

 

첫번째 인자에 있는 객체(group)의 id 필드를 두번째 인자의 값(sequence)으로 설정한다.

 

전체 코드는 다음과 같다.

public Group save(Group group) {
    if (group.getId() != null) {
        storage.replace(group.getId(), group);
    } else {
        try {
            ++sequence;
            Class<? extends Group> class = group.getClass();
            Field id = class.getDeclaredField("id");
            id.setAccessible(true);
            id.set(group, sequence);
            storage.put(sequence, group);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    return group;
}

 

이렇게 Reflection 기술을 사용해서 private 필드인 id 값을 설정할 수 있었다.

 

하지만 이러한 과정을 편리하게 사용할 수 있게끔 Spring에서 유틸 클래스를 제공하고 있어, 나는 그 방법을 택했다.

 

해결 방법

Spring에서 제공하는 ReflectionTestUtils는 단위 테스트 또는 통합 테스트에서 사용할 수 있는 Reflection 기반 유틸 메서드들을 모아놓은 클래스이다. ReflectionTestUtils 클래스의 설명에는 Jpa와 하이버네이트에서 private 필드에 접근하는 것처럼, public이 아닌 필드에 접근할 수 있는 것이 유용할 때가 있다고 나와 있다. 딱 내가 직면한 상황과 일치했고, 코드 또한 간결했기 때문에 이 방법을 사용해 문제를 해결했다.  사용 방법은 다음과 같다.

 

ReflectionTestUtils.setField(group, "id", sequence);

 

이 한 줄의 코드로 앞서 살펴보았던 4줄의 코드와 같은 결과를 확인할 수 있다.

 

전체 코드는 다음과 같다.

public Group save(Group group) {
    if (group.getId() != null) {
        storage.replace(group.getId(), group);
    } else {
        ++sequence;
        ReflectionTestUtils.setField(group, "id", sequence); // 끝
        storage.put(sequence, group);
    }
    return group;
}

 

알게된 것

Reflection 기술을 사용하면 클래스의 필드 뿐만 아니라 메서드, 생성자에도 접근할 수 있다. Spring의 의존성 주입이나 Jpa의 구현체인 하이버네이트에서도 내부적으로 Relfection을 사용한다. 다만 privtate 리소스에 접근하는 것은 결국 객체지향 프로그래밍 원칙인 캡슐화를 위반하는 행위이기 때문에 정말 필요한 곳에서만 사용하는 것이 좋다.

 

스프링에서 제공하는 ReflectionTestUtils 유틸리티 클래스를 사용하면 단위 테스트나 통합 테스트 시에 Reflection 기술을 좀 더 손쉽게 사용할 수 있다.

 

이번에는 Reflection을 사용해 private 필드의 값을 설정 해보았는데, 다음에는 @Autowired와 같이 의존성 주입이나 @PostConstruct와 같이 라이프사이클 콜백 메서드를 임의로 호출하는 작업을 해볼 수도 있을 것이다.

 

참고

https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html

https://cloudsoswift.github.io/post/develop/java/reflection/

https://tech.kakaopay.com/post/mock-test-code/