ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체 생성과 파괴 - 아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
    Study/Effective Java 2022. 12. 12. 15:12

    자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

    유연하지 못한 정적 유틸리티 클래스
    public class AutoLottoNumberGenerator {
    
        private static final List<Integer> LOTTO_TOTAL_NUMBERS = IntStream.rangeClosed(1, 45)
                .boxed()
                .collect(Collectors.toList());
    
        private AutoLottoNumberGenerator() {
        }
    
        public static List<Integer> generate(int size) {
            List<Integer> lottoTotalNumbers = new ArrayList<>(LOTTO_TOTAL_NUMBERS);
            Collections.shuffle(lottoTotalNumbers);
    
            return lottoTotalNumbers.stream()
                    .limit(size)
                    .collect(Collectors.toList());
        }
    }

    많은 클래스들은 하나 이상의 자원에 의존한다. 위 소스코드는 로또 번호를 자동으로 생성하기 위한 정적 유틸리티 클래스이다.

     

    public class Lotto {
        
        private static final int DEFAULT_LOTTO_NUMBER_SIZE = 6;
        
        private final Set<LottoNumber> lottoNumbers;
        
        public Lotto() {
            List<Integer> numbers = AutoLottoNumberGenerator.generate(DEFAULT_LOTTO_NUMBER_SIZE);
            this.lottoNumbers = numbers.stream()
                    .map(number -> new LottoNumber(number))
                    .collect(Collectors.toSet());
        }
        
        public Set<LottoNumber> getLottoNumbers() {
            return Collections.unmodifiableSet(lottoNumbers);
        }
    }

    Lotto 클래스는 6자리의 로또 번호 리스트를 가진 클래스이다. Lotto를 생성하는 시점에 앞서 작성한 AutoLottoNumberGenerator 유틸리티 클래스를 활용하여 랜덤으로 번호를 생성한 뒤 활용할 수 있다.

     

    위와 같은 방법은 자동 생성을 위한 AutoLottoNumberGenerator 클래스에만 의존하고 있다. 만약 요구사항이 추가되어 수동으로 번호를 입력하는 기능을 추가해야 한다면, Lotto 클래스를 직접 수정하여 반영해야 한다. 이것이 의미하는 바는 비즈니스 로직의 핵심 도메인을 수정해야만 반영이 가능하다는 것을 의미한다.

     

    이렇게, 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식은 적합하지 않다.

     

    Lotto 클래스는 다양한 Lotto 생성 전략을 가질 수 있어야 한다. 이것을 이뤄내기 위해서는 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 활용해야 한다.

     

    의존 객체 주입
    @FunctionalInterface
    public interface LottoNumberGenerator {
        List<Integer> generate(int size);
    }

    위 예제를 생성자를 통해 의존 객체를 주입하는 방식으로 변경해 보겠다. 위 소스코드는 번호 생성 전략을 구현하기 위한 LottoNumberGenerator 인터페이스이다. 추상 메서드를 오직 1개만 가진 인터페이스이기 때문에 함수형 인터페이스로 활용이 가능하다.

     

    public class AutoLottoNumberGenerator implements LottoNumberGenerator {
    
        private static final int START_INCLUSIVE = 1;
        private static final int END_INCLUSIVE = 45;
    
        private static final List<Integer> LOTTO_TOTAL_NUMBERS = IntStream.rangeClosed(START_INCLUSIVE, END_INCLUSIVE)
                .boxed()
                .collect(Collectors.toList());
    
        private AutoLottoNumberGenerator() {
        }
    
        @Override
        public List<Integer> generate(int size) {
            List<Integer> lottoTotalNumbers = new ArrayList<>(LOTTO_TOTAL_NUMBERS);
            Collections.shuffle(lottoTotalNumbers);
    
            return lottoTotalNumbers.stream()
                    .limit(size)
                    .collect(Collectors.toList());
        }
    }

    이제 LottoNumberGenerator 인터페이스를 구현하여 자동 생성 기능을 작성한다. 이전과 대부분의 구현은 유사하지만 더 이상 정적으로 generate() 메서드를 사용하지 않는다.

     

    public class Lotto {
    
        private static final int DEFAULT_LOTTO_NUMBER_SIZE = 6;
    
        private final Set<LottoNumber> lottoNumbers;
    
        public Lotto(LottoNumberGenerator lottoNumberGenerator) {
            List<Integer> numbers = lottoNumberGenerator.generate(DEFAULT_LOTTO_NUMBER_SIZE);
            this.lottoNumbers = numbers.stream()
                    .map(number -> new LottoNumber(number))
                    .collect(Collectors.toSet());
        }
    
        public Set<LottoNumber> getLottoNumbers() {
            return Collections.unmodifiableSet(lottoNumbers);
        }
    }

    마지막으로, Lotto 클래스는 생성시점에 해당 전략을 주입받도록 수정한다.

     

    의존 객체 주입의 장점
    class LottoTest {
    
        @DisplayName("자동으로 로또 번호를 발급")
        @Test
        public void 자동_로또_번호_발급_테스트() {
            Assertions.assertThat(new Lotto().getLottoNumbers())
                    .isNotIn(new LottoNumber(0), new LottoNumber(46));
        }
    }

    의존 객체 주입 패턴은 해당 객체에게 유연성을 부여하고, 테스트 용이성을 개선해 준다. 유틸리티 클래스에 의존성을 가진 기존 코드를 테스트하기 위해서는 랜덤으로 생성되는 로또 번호를 활용해야 한다. 이것은 확실한 테스트를 진행하는 방식이 아니다.

     

    class LottoTest {
    
        @DisplayName("수동으로 로또 번호를 발급")
        @Test
        public void 수동_로또_번호_발급_테스트() {
            // given
            List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
            LottoNumberGenerator lottoNumberGenerator = (size) -> numbers;
    
            // when
            Lotto lotto = new Lotto(lottoNumberGenerator);
    
            // then
            Assertions.assertThat(lotto.getLottoNumbers())
                    .contains(new LottoNumber(1),
                            new LottoNumber(2),
                            new LottoNumber(3),
                            new LottoNumber(4),
                            new LottoNumber(5),
                            new LottoNumber(6));
        }
    }

    의존 객체 주입 방식과 같이 객체의 생성 시점에 로또 번호 생성 전략을 주입하게 되면 외부에서 번호 생성을 관리할 수 있기 때문에, 테스트의 유연성이 높아진다.

     

    최종 정리

    클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접 만들게 해서도 안 된다. 대신 필요한 자원을 생성자에 넘겨주자. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.


    출처

     이펙티브 자바 Effective Java 3/E. 조슈아 블로크 저자(글) · 개앞맵시(이복연) 번역

    https://github.com/woowacourse-study/2022-effective-java/blob/main/02%EC%9E%A5/%EC%95%84%EC%9D%B4%ED%85%9C_05/%EC%9E%90%EC%9B%90%EC%9D%84%20%EC%A7%81%EC%A0%91%20%EB%AA%85%EC%8B%9C%ED%95%98%EC%A7%80%20%EB%A7%90%EA%B3%A0%20%EC%9D%98%EC%A1%B4%20%EA%B0%9D%EC%B2%B4%20%EC%A3%BC%EC%9E%85%EC%9D%84%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC.md

     

    728x90

    댓글

Designed by Tistory.