3 분 소요

💡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;
    }

위 엔티티는 다음과 같은 사항들을 만족한다.

  1. Setter 가 없다.
  2. 필드를 변경하는 메서드가 명시적이다. (책임 소재 분명)
  3. 기본 생성자를 사용할 수 없다.

하지만 여기서 한 가지 의문점이 생긴다. 그럼 대체 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 속성 값으로 insertableupdatable 옵션을 제공하기 때문에 id 필드에 Setter 를 사용하여 테스트에서만 사용하는 방법도 무관해보인다.

아무리 봐도 이게 더 간단해 보이는 것은 함정

개인적인 견해지만 엔티티에 단지 테스트 로직만을 위해 setter 를 작성해야 하는것은 편리하기는 하겠지만 아름다워 보이지는 않는다 🤔

[부록2]

실무에서 JPA 를 사용하며 느낀 결과 Setter 가 없는 엔티티를 선언 할 때에는 위와 같이 번거로운 방법을 사용하지 않고, 팩토리 메서드 패턴을 통해 테스트용 객체를 생성하는 것이 좋다 🌝

태그: ,

카테고리:

업데이트: