[자바, Java] 우아한 테크 코스 5기 프리코스 2주차 - 테스트만을 위한 생성자 구현에 대한 견해

테스트만을 위한 생성자 구현은 괜찮은가??



1. 이 주제를 가져온 이유

난 테스트만을 위한 생성자 구현을 굉장히 지향하는 편이다. 그러나 내가 기존에 알던 것과는 달리, 이 주제에 대한 부정적인 평가도 보였다. 그러한 반대 입장을 보고 다른 사람들은 어떻게 생각할 지 궁금했다.

이번 우테코 5기 프리코스부터는 우테코 프리코스에 참가하는 모든 사람들이 토론하고 질의응답을 할 수 있는 커뮤니티(GitHub Discussioin) 가 제공된다. 그 안에서도 여러가지 탭 목록이 있지만, 그 중 ‘아고라’라는 토론을 할 수 있는 게시판에 이 주제에 대한 토론을 여는 글을 작성했었다.

지금까지의 내 견해와 부정적인 의견, 그리고 아고라에서 토론을 나눈 내용까지 합해서 이 주제에 대해 정리를 해보고자 한다.


2. 내가 이 주제에 대해 긍정적인 견해를 가지게 된 이유

‘엘레강트 오브젝트’라는 서적의 생성자 파트를 보면,

하나의 클래스는 2~3개의 메서드와 5~10개의 생성자를 포함하는 것이 적당하다. 응집도가 높고 견고한 클래스에는 적은 수의 메서드와 상대적으로 더 많은 수의 생성자가 존재한다. 생성자가 많아지면 유연성이 향상된다.

이처럼 생성자가 많아질수록, 클래스의 사용성이 좋아지는 것을 말하고 있다. 난 메서드는 최대한 줄이고 생성자는 늘려도 된다는 이 의견에 전적으로 동의한다.

생성자는 setter 메서드처럼 객체의 값을 함부로 바꾸는 것이 아닌, 해당 객체의 초기화만을 책임지기 때문에, 생성자의 개수를 늘린다고 해도 Thread-safe 하다고 생각한다. 또한 생성자가 많아질수록 클래스 사용의 유연성이 증가하여 사용자 입장에서도 굉장히 편리하게 사용할 수 있다는 것도 좋은점으로 본다.

실제로 난 테스트 코드를 작성할 때 이러한 유연성의 효과를 굉장히 많이 받는다는 것을 체감하고 있다. 그 부분에 대해 먼저 정리해보자.


3. 테스트만을 위한 생성자 구현의 장점

이 주제의 문장만 봐도 알 수 있듯이, ‘비즈니스 로직과는 상관 없이 오직 수월한 테스트만을 위한 생성자를 구현하는 것’은 어떤 장점이 있을 까??

먼저 이 주제에 대한 간단한 예시를 구현해보겠다. (실제로 내가 프리코스 내에서 구현한 로직은 문제 유출의 위험이 있을 수 있기에 가져오지 않았다.)


1. 기본 생성자만 있는 경우

일단 기본 생성자만 있을때의 비즈니스 로직과 테스트 구현을 한 모습이다.

public class Speaker {
    private int volume;
    
    public void increaseVolume() {
        volume++;
    }
    
    public static Speaker findBiggestSpeaker(final List<Speaker> speakers) {
        int maxVolume = findMaxVolume(speakers);
        
        return speakers.stream()
                .filter(speaker -> speaker.volume == maxVolume)
                .findAny()
                .orElse(null);
                
    }
    
    private static int findMaxVolume(final List<Speaker> speakers) {
        return speakers.stream()
                .mapToInt(Speaker::volume)
                .max()
                .orElse(0);
    }
    
    private int volume() {
        return volume;
    }
}
class SpeakerTest {
    @Test
    void isBiggerVolume() {
        Speaker speaker1 = new Speaker();
        speaker1.increaseVolume();
        speaker1.increaseVolume();
        
        Speaker speaker2 = new Speaker();
        speaker2.increaseVolume();
    
        Speaker speaker3 = new Speaker();
        speaker3.increaseVolume();
        speaker3.increaseVolume();
        speaker3.increaseVolume();
        
        assertThat(Speaker.findBiggestSpeaker(List.of(speaker1, speaker2, speaker3))).isEqualTo(speaker3);
    }
}


SpeakerTest 에선 여러 스피커들 중에 볼륨이 가장 큰 스피커를 찾아내는 테스트를 하고 있다. 그런데 여기서 한 가지 위화감이 드는 부분이 있다. 테스트를 위해 각 스피커의 볼륨을 설정을 해줘야 한다. 그런데 예시에선 각 스피커의 볼륨을 increaseVolume() 메서드를 통해 일일이 볼륨을 올려주고 테스트를 진행하고 있다.

과연 이러한 테스트가 최선인 것일까?? 만약 테스트 해볼 스피커의 개수가 4개라면?? 아니면 5개?? 그 이상이라면?? 점점 더 테스트하기 곤란해지고, 테스트를 하기 귀찮아 질 것이다.


2. 테스트만을 위한 생성자까지 구현한 경우

이제 생성자를 하나 더 추가해서 좀 더 효율적인 테스트를 진행해 보겠다.

public class Speaker {
    private int volume;
    
    public Speaker() {
        this(0);
    }
    
    public Speaker(final int volume) { // 볼륨 값을 받아서 초기화하는 생성자 추가
        this.volume = volume;
    }
    
    public void increaseVolume() {
        volume++;
    }
    
    public static Speaker findBiggestSpeaker(final List<Speaker> speakers) {
        int maxVolume = findMaxVolume(speakers);
        
        return speakers.stream()
                .filter(speaker -> speaker.volume == maxVolume)
                .findAny()
                .orElse(null);
                
    }
    
    private static int findMaxVolume(final List<Speaker> speakers) {
        return speakers.stream()
                .mapToInt(Speaker::volume)
                .max()
                .orElse(0);
    }
    
    private int volume() {
        return volume;
    }
}
class SpeakerTest {
    
    @Test
    void isBiggerVolume() {
        Speaker speaker1 = new Speaker(2);
        Speaker speaker2 = new Speaker(1);
        Speaker speaker3 = new Speaker(3);
        
        assertThat(Speaker.findBiggestSpeaker(List.of(speaker1, speaker2, speaker3))).isEqualTo(speaker3);
    }
}


볼륨 값을 받아서 초기화하는 생성자를 하나 더 추가함으로써, 생성자를 통해 바로 볼륨을 설정해주고 테스트를 진행할 수 있게 되었다. 일일이 increaseVolume() 메서드를 통해 볼륨을 설정해서 테스트 할 필요가 없어졌다는 것이다.

만약 비교해볼 스피커가 3개가 아니라 그 이상으로 많아진다면 그 테스트 효율성의 차이는 훨씬 더 커질 것이다.


4. 부정적인 의견

사실 이 주제를 다루는 글을 지금 작성하는 이유는 부정적인 의견을 보고 이러한 입장에 대해서도 한 번 고민해보기 위해서이다. 대표적인 부정적인 의견들은 어떤 것들이 있을까??


1. 비즈니스 로직의 이해를 방해할 수 있다

볼륨 값을 받아서 초기화하는 생성자는 테스트만을 위해 구현된 생성자다. 그래서 당연히 비즈니스 로직과는 전혀 상관없는 생성자가 되어버린다. 만약 제 3자가 비즈니스 로직을 보았을 때, ‘볼륨 값을 마음대로 초기화 할 수 있는 생성자는 어디서 쓰이고 왜 필요한 것이지??’ 라는 의문점이 들게 된다.

이러한 의문점이 드는 것 자체가 비즈니스 로직의 이해를 방해하는 것이라 볼 수 있다.


2. 비즈니스 로직에서 사용하면 안될 때, 제 3자가 사용할 수 있는 위험이 존재한다

예를 들어, ‘Speaker의 volume은 항상 초기 값이 0이다’ 라는 요구사항이 있다고 가정해보자. 그런데 볼륨 초기 값이 0이 아닌 Speaker 를 제 3자가 만들 수 있는 상황이라면, 이는 요구사항의 의도와는 다른 상황 즉, 버그를 만들 수 있는 것이다.

그래서 이 의견을 내신 분은 대안 책 중 하나로, 비즈니스 로직이 아니라 테스트 파일 내에서 ‘볼륨을 한번에 조절할 수 있는 메서드’를 만드는 방법을 제시한 적이 있다.

물론 이 제안은 테스트 코드의 가독성을 떨어뜨릴 수 있다는 부작용이 존재할 수 있다고 생각하지만, 굉장히 일리가 있는 제안이라고 생각한다. 왜냐하면 이는 비즈니스 로직과 테스트 로직 중 어느 쪽에 더 중요성의 무게를 두느냐에 따라 차이가 있을 수 있다고 생각하기 때문이다.

물론 난 테스트 코드의 품질도 프로덕션 코드 만큼이나 높은 품질을 유지해야 한다고 생각한다. 그러나 이러한 대안 책이 생각을 확장하는 계기가 되었다.


5. 마무리 하며…

이번 우테코 5기 프리코스를 진행하면서 한 주 한 주 지날 때마다, 객체지향 생활 체조 원칙과 우테코에서 제공한 요구사항들을 지키며 미션을 구현 하는 것, 커뮤니티에서 여러가지 주제를 놓고 토론 및 리뷰를 하는 것 등등을 통해 많은 것을 배우고 실력을 쌓아가는 굉장히 유용한 시간이라는 것을 체감하게 된다.

앞으로의 결과가 어떻게 나올 진 모르겠지만, 프리코스를 진행하는 와중에도 이미 프리코스에 대한 깊은 의미와 철학을 느끼게 된다.





© 2021. All rights reserved.

----------Powered by Hydejack----------

Jun's Development Blog