[Java] Reflection 을 이용하여 private 필드 값 변경하기
💡Refferecne: 백기선 - The JAVA
최근에 자바 관련 수업을 듣다가 리플렉션에 대한 내용을 조금 공부했다. 굉장히 강력한 기능을 가지고 있지만, 어디에 사용해야 좋을지 좀처럼 감이 오지 않았는데 마침 나름대로 고민하던 내용에 사용해보니 괜찮은 것 같아 기록을 남긴다.
1. Setter를 사용하지 말아야 하는 이유
도메인 주도 설계에서는 책임 소재가 불분명한 Setter 사용을 권장하지 않는다. 더 나아가서 아예 Setter 및 기본 생성자 (NoArgsConstructor) 를 작성하지 않는 것을 권장한다. 엔티티의 값이 Setter 를 통한 변경에 너무 쉽게 노출되면 추적이 어려울 뿐더러 이슈가 발생 했을 때 책임을 가리기 위한 시간이 너무 많이 소요되기 때문이다. (유지보수 비용 증가)
JPA 관련 강의를 들으며 이러한 의도에 충분히 공감하고 다음 사이드 프로젝트 진행 시 꼭 Setter 를 최대한 배제하며 설계 해 보아야겠다고 생각했다.
2. 영영 값을 채울 수 없는 필드
다음과 같은 엔티티를 작성했다고 가정해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Getter
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
public void changeUserInfo(String name, String email) {
this.name = name;
this.email = email;
}
protected User() {
}
@Builder
public User(String name, String email) {
this.name = name;
this.email = email;
}
위 엔티티는 다음과 같은 사항들을 만족한다.
- Setter 가 없다.
- 필드를 변경하는 메서드가 명시적이다. (책임 소재 분명)
- 기본 생성자를 사용할 수 없다.
하지만 여기서 한 가지 의문점이 생긴다. 그럼 대체 id
필드는 어떻게 채우는가?
3. id 필드를 채워야 하는 경우
위 엔티티는 Auto Generated 전략을 사용하고 있기 때문에 임의로 id
필드에 값을 채울 이유는 없어 보인다. 따라서 Setter 및 생성자 모두 id
필드에 값을 채울 수 없도록 설계하였는데 임의로 id 값이 채워진 객체가 필요한 경우가 있었다. 바로 단위 테스트를 작성할 때 이다.
가령 특정 정보에 접근 권한을 체크 하는 validator 가 있다고 가정해보자.
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@RequiredArgsConstructor
class FooValidator {
private final UserRepository userRepository;
public void isAccessible(Long userId) {
Optional<User> result = userRepository.findById(userId);
//some check process...
}
FooValidator.java
다음과 같은 validator 를 단위 테스트 하기 위해서는 임의의 User
객체를 반환하도록 Stubbing 할 필요가 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ExtendWith(MockitoExtension.class)
class FooValidatorTest {
@Mock
UserRepository userRepository;
@InjectMocks
GroupValidatorImpl validator;
@Test
void isAccessible() {
//given
User givenUser = new User("name", "email");
doReturn(Optional.of(givenUser)).when(userRepository).findById(1L);
//when
//some test process...
}
위의 테스트 실행 시 서비스 로직의 유효성 유무와 관계없이 테스트는 성공할 수 없다. Repository 가 원하는 id
값을 가진 객체를 반환하도록 stubbing 하지 못하기 때문이다.
4. Reflection 을 통해 private 필드 값을 채워보기
리플렉션을 통해 private 필드의 값을 수정하는 방법은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
//Class 객체 가져오기
Class<Foo> fooClass = Foo.class;
//Field 객체 가져오기
//getField 메서드도 있으나 접근제한자의 영향을 받는다. (private 접근 불가)
Field varField = fooClass.getDeclaredField("var");
//private 필드에 접근하기 위해서는 해당 설정을 수정해야 한다.
varField.setAccessible(true);
//Field 값 채우기
varField.set(인스턴스변수명, 채울 값);
이걸 테스트에 적용하면 아래와 같이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@ExtendWith(MockitoExtension.class)
class FooValidatorTest {
@Mock
UserRepository userRepository;
@InjectMocks
GroupValidatorImpl validator;
@Test
void isAccessible() {
//given
User givenUser = new User("name", "email");
//Reflection
Class<User> userClass = User.class
Field userIdField = userClass.getDeclaredField("id");
userIdField.setAccessible(true);
userIdField.set(givenUser, 1L);
doReturn(Optional.of(givenUser)).when(userRepository).findById(1L);
//when
//some test process...
}
이렇게 Reflection 을 통해 마치 리포지터리를 통하여 객체를 조회한 것 같은 테스트를 작성해 보았다.
[부록] 과연 이렇게 까지 Setter 를 지양해야 하는가?
사실 실무에서 DDD 패턴을 적용해가며 업무를 해본 경험이 없으므로 이 질문은 아직 나도 정답을 모르겠다. (사실 아직 JPA 조차 사용해본 적이 없다.) 굳이 리플렉션을 사용하지 않아도 이미 @Column
속성 값으로 insertable
과 updatable
옵션을 제공하기 때문에 id
필드에 Setter 를 사용하여 테스트에서만 사용하는 방법도 무관해보인다.
개인적인 견해지만 엔티티에 단지 테스트 로직만을 위해 setter 를 작성해야 하는것은 편리하기는 하겠지만 아름다워 보이지는 않는다 🤔
[부록2]
실무에서 JPA 를 사용하며 느낀 결과 Setter 가 없는 엔티티를 선언 할 때에는 위와 같이 번거로운 방법을 사용하지 않고, 팩토리 메서드 패턴을 통해 테스트용 객체를 생성하는 것이 좋다 🌝