(20230225) 엘레강트 테크 코스 5단계 1단계 – 사다리 오르기 1단계와 2단계 복습

안녕하세요~ 헤나입니다!

마지막으로 (20230217) 우아한 기술 과정 5 LEVEL 1 – 사다리 만들기 박람회 회고전

이 시간 “래더 다이어그램 작성 1단계 및 2단계” 회고전을 하려고 합니다.

: 리뷰어

: 저자

테스트 주도 개발

래더 미션에서 내가 설정한 목표는 “그것은 TDD와 함께 끝까지 간다.

가끔은 다음과 같이 생각해야 합니다.

  • 테스트 작성 순서
  • 테스트 작성 전 설계
  • 객체 A를 테스트하기 위해 객체 B 생성 및 테스트
  • 코드 리팩토링 시간
  • 어느 정도

다음 순서로 TDD를 했습니다.

“테스트 객체 생성 예외 -> 테스트 객체 생성 성공 -> 테스트 함수 예외 -> 테스트 함수 성공 -> 리팩토링”

테스트 시퀀스에 대한 하나의 정답은 없습니다. 그래도 한 번 짚고 넘어가야 해서 편할 것 같아서 여쭤봤습니다.

:
tdd 테스트 시퀀스는 다음과 같습니다.
객체 생성 예외 테스트 -> 객체 생성 성공 테스트 -> 함수 예외 테스트 -> 함수 성공 테스트 이 테스트는 필요한 순서부터 가장 작은 함수까지 작성하는 테스트로 테스트 순서에 대한 정답이 있는지 모르겠습니다.

:
Henna는 Object Creation Exception Test -> Object Creation Success Test -> Functional Exception Test -> Functional Success Test라고 했는데 저도 정답이라고 생각합니다. 그럼 객체 생성 성공 테스트 -> 객체 생성 예외 테스트 -> 함수 예외 테스트 -> 함수 성공 테스트는 오답일까요? 아니, 이것도 정답이다. ㅎㅎ 다양한 시행착오를 겪으면서 먼저 실패한 테스트를 만들고 TDD를 통해 성공적인 테스트를 만들고 자신에게 가장 적합하다고 느끼는 프로세스를 찾으면 가장 정답인 것 같아요!

TDD를 할 때 먼저 실패한 테스트를 작성하여 프레임워크를 설정합니다.

제이온 리뷰어님 말씀처럼 제게는 맞는 과정을 거치는 것 같지만, 어떤 책임이 있는 객체를 생성할 때

기능적 성공 테스트를 먼저 작성하고 개체에 필요한 것이 무엇인지 파악하는 것도 좋은 흐름입니다.

생성자 매개변수 유형

생성자 매개변수 유형에는 항상 객체의 필드가 있어야 합니까?

개체의 필드 값이 전달되지 않아도 상관 없습니다!

생성자에서 무언가를 생성하거나 변환하는 것을 좋아하지 않습니다.

생성자에서 작업보다는 생각으로 완성된 데이터를 받아보려고 합니다.

그래서 다음과 같이 참가자 생성자를 작성했습니다.

참가자(참가자) 목록<名称>분야로.

나는 그것을 당연하게 여긴다 목록<名称> 매개변수 및 클래스 필드로 유형 수신 이름주입하려고

public class Participants {

	private final List<Name> names;

    public Participants(final List<Name> names) {
        validateParticipants(names);
        this.names = names;
    }
    
    // ...
}

사다리꼴 컨트롤러 이름으로 (목록<字符串>) 받았다 목록<名称>그것을 변환

참가자의 생성자 매개변수로 전달됩니다.

public class LadderController {
	// ...

    public void run() {
        final Participants participants = repeatAndPrintCause(this::getParticipants);
        
        // ...
    }

    private Participants getParticipants() {
        final List<Name> names = inputView.readNames()
                .stream()
                .map(Name::new)
                .collect(Collectors.toList());
        return new Participants(names);
    }

제어 장치데이터를 가져올 수 있는 방법이 있지만 참가자 나는 안에 머무르는 것보다 더 좋은 방법을 생각했다.

J-On은 다른 접근 방식을 제안합니다.

:
참가자의 생성자에서 목록 가져오기 그것을 처리하는 방법?

:
그래요 목록<字符串>두번째 사다리꼴 컨트롤러존재하다 목록<名称>로 변환되는 이유
참가자(참가자) 이름만들 이유가 없다고 봅니다.

전의 코멘트 참가자 생성자 매개변수로 목록<字符串>생성자에서 이름 나는 판단하기 위해 창조라는 단어를 사용합니다 참가자그리고 이름 연결이 있으므로 참여자 생성자에서 이름만드는게 무리가 아니라는 말씀이신가요

:
안녕 헤나
말씀하신 것처럼 배우와 이름 사이에는 관계가 있고, 배우(넓은 의미의 사람)는 현실 세계에 대입해도 꽤 자연스럽게 이름이 떠오릅니다. 그리고 객체를 생성하는 방법은 여러가지가 있는데, 조금 더 복잡한 로직을 가진 객체 생성을 위해서는 생성자 대신 정적 팩토리 메소드를 사용하는 것을 선호합니다!


참가자는 이름과 관계가 있습니다.

조금만 생각해보면 이미 참가자목록<名称>멤버 변수로.
즉, 클래스는 개념적으로 연관된 상태로 서로 연결되어 있습니다.
public class Participants {

	private final List<Name> names;

	// ...
}
두 클래스가 관련이 없는 경우 참가자 생성자에서 이름이전보다 더 의존적이 되는 방법입니다.
하지만 현재 참가자이름의존성이 강하다 참가자 내부에서 이름문제가 되지 않아야 합니다.
따라서 다음과 같이 수정합니다. 제어 장치존재하다 이름로 전환하는 일을 담당합니다. 이름양도할 수 있다
(나는 목록을 만들고 사용 일류 컬렉션으로 Names. )
public class Participants {

    private final Names names;

    public Participants(final List<String> names) {
        validateParticipants(names);
        this.names = new Names(names);
    }
    
    // ...
}

public class Names {

    private final List<Name> names;

    public Names(final List<String> names) {
        validateNames(names);
        this.names = createNames(names);
    }

    private List<Name> createNames(final List<String> names) {
        return names.stream()
                .map(Name::new)
                .collect(Collectors.toList());
    }
    
}


null 체크, 선택 사항

null 검사를 위해 Optional을 사용하는 것으로 넘어갑시다.

:
학습 및 적용은 어떻습니까?

:
아래와 같이 null이 입력되면 빈 리스트를 반환하고 isEmpty() 메서드를 사용하도록 검증 로직을 변경합니다.

public Participants(final List<String> nameValues) {
    final List<String> names = Optional.ofNullable(nameValues).orElse(List.of());
    validateParticipants(names);
    this.names = new Names(names);
}

private void validateParticipants(final List<String> names) {
    if (names.isEmpty()) {
        throw new IllegalArgumentException(PARTICIPANTS_EMPTY_EXCEPTION);
    }
}​

:
목록에 null 값이 있는 경우 orElse(List.of())를 사용하여 빈 목록을 가져옵니다.
값이 필요한 경우 기본값이 있습니다.
선택적을 사용하여 다른 값으로 대체하여 이 null 검사를 확인합니다.

기존의 참가자 개체를 만들 때 매개 변수는 유효하지 않은나는 확인했다

다음 코드와 같이 if(이름 == null) 방법이 사용됩니다.

public class Participants {

    public Participants(final List<Name> names) {
        validateParticipants(names);
        this.names = names;
    }

    private void validateParticipants(final List<Name> names) {
        if (names == null) {
            throw new IllegalArgumentException(PARTICIPANTS_NULL_EXCEPTION);
        }

    // ...
}

이미 궁금해 하시는 분들이 계실텐데요.

이상에서 optional이 남용되었다고 할 수 있지만, optional이 잘 사용되고 있다고 보기는 어렵다.

아래와 같이 코드 쇼:

private String validateValue(final String value) {
	return Optional.ofNullable(value).orElse("DEFAULT");
}

단순히 가치를 얻으십시오 임의로 선택할 수 있는사용하고 있습니다

모으다 같은 경우 계속해서 비어 있는지 확인할 수 있습니다.

임의로 선택할 수 있는다시 포장하면 비용이 추가됩니다.

같게 참가자 내부 코드에서 모으다두번째 임의로 선택할 수 있는List.of()로 한 번 감싸서 반환합니다.

이 경우 null 여부를 직접 비교하는 것보다 List.of()를 리턴하는 오버헤드가 크기 때문에 상황에 따라 선택이 가능해야 한다.


수정 불가능한 목록

수정 불가능한 목록,
(얕은/깊은) 방어 카피

래더 게임에서는 참가자의 이름이 입력된 순서대로 출력되어야 합니다.

따라서 외부에서 데이터 순서를 변경하고 싶지 않습니다. 수정할 수 없는 목록이미 사용 된.

public List<String> getNames() {
    return names.stream()
            .map(Name::getValue)
            .collect(Collectors.toUnmodifiableList());
}

:
불변사용시 먼저 getName() API를 사용하는 다른 개발자의 단점은 무엇입니까?

갑자기 Unmodifiable이 무엇인지 명확하게 설명할 수 없어서 테스트 코드를 작성하면서 자세히 살펴보았습니다.

수집(collector.toUnmodifiableList())

  • 수집(수집기.toUnmodifiableList())정렬할 수 없습니다.
  • 수집(수집기.toUnmodifiableList())데이터를 추가할 수 없습니다.
  • 수집(수집기.toUnmodifiableList())데이터를 삭제할 수 없습니다.
  • 수집(수집기.toUnmodifiableList())개체에 액세스하고 수정할 수 있습니다.
  • 수집(수집기.toUnmodifiableList())방어적인 사본을 만드십시오.
  • 수집(수집기.toUnmodifiableList())심층 방어 복제는 수행되지 않습니다.

내가 사용 collect(Collectors.toUnmodifiableList()) 구현 코드는 다음과 같다는 것을 알 수 있습니다.

/**
 * Returns a {@code Collector} that accumulates the input elements into an
 * <a href="http://programming-hyena./List.html#unmodifiable">unmodifiable List</a> in encounter
 * order. The returned Collector disallows null values and will throw
 * {@code NullPointerException} if it is presented with a null value.
 *
 * @param <T> the type of the input elements
 * @return a {@code Collector} that accumulates the input elements into an
 * <a href="http://programming-hyena./List.html#unmodifiable">unmodifiable List</a> in encounter order
 * @since 10
 */
@SuppressWarnings("unchecked")
public static <T>
Collector<T, ?, List<T>> toUnmodifiableList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               list -> (List<T>)List.of(list.toArray()),
                               CH_NOID);
}

이것으로 테스트 코드를 작성했습니다.

class CollectorToUnmodifiableListTest {

    @Test
    void collectorsToUnmodifiable_정렬_할_수_없다() {
        final List<Integer> values = new ArrayList<>(List.of(1, 3, 2));
        final List<Integer> newValues = values.stream().collect(Collectors.toUnmodifiableList());

        assertThatThrownBy(() -> Collections.sort(newValues))
                        .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void collectorsToUnmodifiable_데이터를_추가_할_수_없다() {
        final List<Integer> values = new ArrayList<>();
        final List<Integer> newValues = values.stream().collect(Collectors.toUnmodifiableList());

        assertThatThrownBy(() -> newValues.add(1))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void collectorsToUnmodifiable_데이터를_삭제_할_수_없다() {
        final List<Integer> values = (new ArrayList<>());
        final List<Integer> newValues = values.stream().collect(Collectors.toUnmodifiableList());

        assertThatThrownBy(() -> newValues.remove(1))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void collectorsToUnmodifiable_내부_데이터를_변경_할_수_있다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));

        final List<Name> newNames = names.stream().collect(Collectors.toUnmodifiableList());
        newNames.get(0).setValue("하이에나");
        final Name name = newNames.get(0);

        assertThat(name).hasFieldOrPropertyWithValue("value", "하이에나");
    }

    @Test
    void collectorsToUnmodifiable_방어적_복사를_한다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = names.stream().collect(Collectors.toUnmodifiableList());
        names.remove(0);

        assertThat(newNames).hasSize(1);
    }

    @Test
    void collectorsToUnmodifiable_깊은_방어적_복사를_하지_않는다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = names.stream().collect(Collectors.toUnmodifiableList());
        newNames.get(0).setValue("하이에나");

        Name name = names.get(0);

        assertThat(name.getValue()).isEqualTo("하이에나");
    }

    static class Name {
        private String value;

        public Name(final String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(final String value) {
            this.value = value;
        }
    }
}

Collections.unmodifiableList()

  • Collections.unmodifiableList()정렬할 수 없습니다.
  • Collections.unmodifiableList()데이터를 추가할 수 없습니다.
  • Collections.unmodifiableList()데이터를 삭제할 수 없습니다.
  • Collections.unmodifiableList()개체에 액세스하고 수정할 수 있습니다.
  • Collections.unmodifiableList()방어적인 사본은 만들어지지 않습니다.
  • Collections.unmodifiableList()심층 방어 복제는 수행되지 않습니다.

Collections.unmodifiableList()수집(수집기.toUnmodifiableList()).에 비해 방어적인 복사, 추가, 삭제, 정렬이 없습니다.

현재 내 코드에는 방어적인 복사 및 정렬이 필요할 수 있으므로 Collections.unmodifiableList()사용할 이유가 없어 보입니다.

class UnmodifiableTest {

    @Test
    void unmodifiable_리스트는_정렬_할_수_없다() {
        final List<Integer> unmodifiableValues = Collections.unmodifiableList(new ArrayList<>());

        assertThatThrownBy(() -> Collections.sort(unmodifiableValues))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void unmodifiable_리스트는_데이터를_추가_할_수_없다() {
        final List<Integer> unmodifiableValues = Collections.unmodifiableList(new ArrayList<>());

        assertThatThrownBy(() -> unmodifiableValues.add(10))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void unmodifiable_리스트는_데이터를_삭제_할_수_없다() {
        final List<Integer> unmodifiableValues = Collections.unmodifiableList(new ArrayList<>());

        assertThatThrownBy(() -> unmodifiableValues.remove(10))
                .isInstanceOf(UnsupportedOperationException.class);
    }

    @Test
    void unmodifiable_리스트는_내부_데이터를_변경_할_수_있다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));

        final List<Name> unmodifiableNames = Collections.unmodifiableList(names);
        unmodifiableNames.get(0).setValue("하이에나");
        final Name name = unmodifiableNames.get(0);

        assertThat(name).hasFieldOrPropertyWithValue("value", "하이에나");

    }

    @Test
    void unmodifiable_리스트는_방어적_복사를_하지_않는다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = Collections.unmodifiableList(names);
        names.remove(0);

        assertThat(newNames).hasSize(0);
    }

    @Test
    void unmodifiable_리스트는_깊은_방어적_복사를_하지_않는다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = names.stream().collect(Collectors.toList());
        newNames.get(0).setValue("하이에나");

        Name name = names.get(0);

        assertThat(name.getValue()).isEqualTo("하이에나");
    }

    private static class Name {
        private String value;

        public Name(final String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(final String value) {
            this.value = value;
        }
    }
}


새로운 ArrayList<>()

그렇다면 new ArrayList<>()와 UnmodifiableList의 차이점은 무엇입니까?

  • 새로운 ArrayList<>()정렬할 수 있습니다.
  • 새로운 ArrayList<>()데이터를 추가할 수 있습니다.
  • 새로운 ArrayList<>()데이터를 삭제할 수 있습니다.
  • 새로운 ArrayList<>()개체에 액세스하고 수정할 수 있습니다.
  • 새로운 ArrayList<>()방어적인 사본을 만드십시오.

수집(수집기.toUnmodifiableList())그에 반해 추가, 삭제, 수정, 확인이 가능한 것이 하나 있습니다.

배열 목록다음 구현 코드가 있습니다.

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public ArrayList(Collection<? extends E> c) {
    Object() a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object().class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}

마찬가지로 테스트 코드를 작성하여 확인합니다.

class ArrayListTest {
    
    @Test
    void newArrayList_리스트는_정렬_할_수_있다() {
        final List<Integer> values = new ArrayList<>(List.of(1, 3, 2));
        final List<Integer> newValues = new ArrayList<>(values);
        Collections.sort(newValues);

        assertThat(newValues).containsExactlyElementsOf(List.of(1, 2, 3));
    }

    @Test
    void newArrayList_리스트는_데이터를_추가_할_수_있다() {
        final List<Integer> values = new ArrayList<>();
        final List<Integer> newValues = new ArrayList<>(values);
        newValues.add(1);
        
        assertThat(newValues).hasSize(1);
    }

    @Test
    void newArrayList_리스트는_데이터를_삭제_할_수_있다() {
        final List<Integer> values = (new ArrayList<>());
        final ArrayList<Integer> newValues = new ArrayList<>(values);
        newValues.add(0);
        newValues.remove(0);

        assertThat(newValues).isEmpty();
    }

    @Test
    void newArrayList_리스트는_내부_데이터를_변경_할_수_있다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));

        final List<Name> newNames = new ArrayList<>(names);
        newNames.get(0).setValue("하이에나");
        final Name name = newNames.get(0);

        assertThat(name).hasFieldOrPropertyWithValue("value", "하이에나");

    }

    @Test
    void newArrayList_리스트는_방어적_복사를_한다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = new ArrayList<>(names);
        names.remove(0);

        assertThat(newNames).hasSize(1);
    }

    @Test
    void newArrayList_리스트는_깊은_방어적_복사를_하지_않는다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = new ArrayList<>(names);
        newNames.get(0).setValue("하이에나");

        Name name = names.get(0);

        assertThat(name.getValue()).isEqualTo("하이에나");
    }

    private static class Name {
        private String value;

        public Name(final String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(final String value) {
            this.value = value;
        }
    }
}

수집(collector.toList())

아래 코드는 스트림을 사용하여 목록을 수신합니다.

이 경우 단순히 값을 가져와야 합니까?

public List<String> getNames() {
    return names.stream()
            .map(Name::getValue)
            .collect(Collectors.toList());
}

사실 이 경우에도 방어 복제가 발생합니다.

내부 코드를 보면 새로운 ArratList를 생성하고 있음을 알 수 있습니다.

/**
 * Returns a {@code Collector} that accumulates the input elements into a
 * new {@code List}. There are no guarantees on the type, mutability,
 * serializability, or thread-safety of the {@code List} returned; if more
 * control over the returned {@code List} is required, use {@link #toCollection(Supplier)}.
 *
 * @param <T> the type of the input elements
 * @return a {@code Collector} which collects all the input elements into a
 * {@code List}, in encounter order
 */
public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

테스트를 통해 확인해보자.

class CollectorToListTest {

    @Test
    void collectorsToList_정렬_할_수_있다() {
        final List<Integer> values = new ArrayList<>(List.of(1, 3, 2));
        final List<Integer> newValues = values.stream().collect(Collectors.toList());
        Collections.sort(newValues);

        assertThat(newValues).containsExactlyElementsOf(List.of(1, 2, 3));
    }

    @Test
    void collectorsToList_데이터를_추가_할_수_있다() {
        final List<Integer> values = new ArrayList<>();
        final List<Integer> newValues = values.stream().collect(Collectors.toList());

        newValues.add(1);

        assertThat(newValues).hasSize(1);
    }

    @Test
    void collectorsToList_데이터를_삭제_할_수_있다() {
        final List<Integer> values = (new ArrayList<>());
        final List<Integer> newValues = values.stream().collect(Collectors.toList());

        newValues.add(0);
        newValues.remove(0);

        assertThat(newValues).isEmpty();
    }

    @Test
    void collectorsToList_내부_데이터를_변경_할_수_있다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));

        final List<Name> newNames = names.stream().collect(Collectors.toList());
        newNames.get(0).setValue("하이에나");
        final Name name = newNames.get(0);

        assertThat(name).hasFieldOrPropertyWithValue("value", "하이에나");

    }

    @Test
    void collectorsToList_방어적_복사를_한다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = names.stream().collect(Collectors.toList());
        newNames.get(0).setValue("하이에나");
        names.remove(0);

        assertThat(newNames).hasSize(1);
    }

    @Test
    void collectorsToList_깊은_방어적_복사를_하지_않는다() {
        final List<Name> names = new ArrayList<>();
        names.add(new Name("헤나"));
        final List<Name> newNames = names.stream().collect(Collectors.toList());
        newNames.get(0).setValue("하이에나");

        Name name = names.get(0);

        assertThat(name.getValue()).isEqualTo("하이에나");
    }

    private static class Name {
        private String value;

        public Name(final String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(final String value) {
            this.value = value;
        }
    }
}

Collections.unmodifiableList(),

수집(수집기.toUnmodifiableList()),

새로운 배열 목록(),

수집(수집기.toList())

나는 4가지를 기억한다

Collections.unmodifiableList()방어적인 카피가 아니므로 주의하세요.

하지만 수집(수집기.toUnmodifiableList()) 사용하시는 분들은 방어용 카피임을 인지하시고 사용하셔야 합니다.

(얕고 깊은) 방어 카피

개체를 복사할 때 알아야 할 몇 가지 사항이 있습니다.
얕은 복사본과 깊은 복사본이 있습니다.
실제로 딥 카피가 만들어지지 않은 것을 테스트 코드에서 확인할 수 있습니다.

수집(수집기.toList()) 샘플 코드를 가져오겠습니다.

@Test
void collectorsToList_방어적_복사를_한다() {
    final List<Name> names = new ArrayList<>();
    names.add(new Name("헤나"));
    final List<Name> newNames = names.stream().collect(Collectors.toList());
    newNames.remove(0);

    assertThat(names).hasSize(1);
}

@Test
void collectorsToList_깊은_방어적_복사를_하지_않는다() {
    final List<Name> names = new ArrayList<>();
    names.add(new Name("헤나"));
    final List<Name> newNames = names.stream().collect(Collectors.toList());
    newNames.get(0).setValue("하이에나");

    Name name = names.get(0);

    assertThat(name.getValue()).isEqualTo("하이에나");
}

두 테스트 모두 이름을 복사하여 newNames를 만들고 newNames를 통해 데이터를 조작합니다.

첫 번째 테스트에서 이름의 데이터 수는 변경되지 않았습니다.

두 번째 테스트에서는 이름의 첫 번째 데이터 상태가 변경됩니다.

그 이유는 (shallow/deep) 복사 중에 얕은 복사를 하기 때문입니다.

얕은 복사

먼저 첫 번째 테스트를 수행해 보겠습니다.

이름에 “Henna”를 추가하십시오.


새 이름을 만들려면 이름의 방어적인 복사본을 만드십시오. (또는 새로운 ArrayList<>())


newNames의 첫 번째 요소를 삭제합니다.


이제 실제 데이터는 다음과 같습니다.

이름에는 “헤나”가 존재하지만

“Henna”는 newNames에 존재하지 않습니다.


이유는 collect(Collectors.toList())가 얕은 복사본을 만들고 이름과 newNames의 주소가 다르기 때문입니다.


얕은 복사로 인해 newNames에서 요소를 제거해도 이름에 영향을 주지 않습니다.


두 번째 테스트에서 newNames의 “henna”를 수정하면 이름에 영향을 미치는 이유는 무엇입니까?

딥 카피

같은 방식으로 두 번째 테스트를 살펴보겠습니다.

이름에 “헤나”를 추가하십시오.


동일한(얕은) 방어적 복사를 수행하여 새 이름을 만듭니다.

이때 “henna”는 동일한 주소를 가진 동일한 개체입니다.


newNames의 첫 번째 요소 “henna”를 “hyena”로 변경하면 개체의 상태가 변경됩니다.


마지막으로 “henna” 개체는 복사되지 않고

이름이 지정된 프레임만 복사됩니다.

이 문제를 해결하기 위해 Henna 개체를 복제할 수도 있습니다.


이제 newNames에서 “henna” 개체의 상태를 변경해도 이름의 “henna” 개체 상태에 영향을 주지 않습니다.


딥카피 테스트 코드는 다음과 같습니다.

@Test
void collectorsToList_깊은_방어적_복사를_하기_위해_내부_객체도_복사_한다() {
    final List<Name> names = new ArrayList<>();
    names.add(new Name("헤나"));
    final List<Name> newNames = names.stream()
            .map(Name::getValue)
            .map(Name::new)
            .collect(Collectors.toList());
    newNames.get(0).setValue("하이에나");

    Name name = names.get(0);

    assertThat(name.getValue()).isEqualTo("헤나");
}

이렇게 하면 다음과 같이 이름에 있는 “henna” 개체의 상태가 변경될 위험이 없습니다.

딥카피는 좋아보이지만 새로 생성하는 비용이 있으니 상황에 맞게 활용해야 합니다.


테스트 픽스처

일반적으로 사용되는 테스트 개체
테스트 픽스처

TDD 때문에 손이 아파요 테스트 더미우리는 계속 사용할 개체를 계속 수집합니다.

public class TestDummy {
    public static final Name NAME_HYENA = new Name("hyena");
    public static final Name NAME_ROSIE = new Name("rosie");
    public static final Participants PARTICIPANTS_SIZE_2 = new Participants(List.of(NAME_ROSIE, NAME_HYENA));
    public static final Height HEIGHT_VALUE_1 = new Height(1);
    public static final BooleanGenerator TEST_BOOLEAN_GENERATOR = new BooleanGenerator() {
            Deque<Boolean> deque = new ArrayDeque<>(List.of(true, false));
        @Override
        public boolean generate() {
            Boolean polled = deque.pollFirst();
            deque.addLast(polled);
            return polled;
        }
    };

    public static final LineCreator TEST_LINE_CREATOR = new LineCreator(TestDummy.TEST_BOOLEAN_GENERATOR);
}

:
이와 같이 자주 사용되는 별도의 테스트 개체를 TestFixture라고 합니다!
그러나 이제 모든 테스트 개체가 한 곳에 있으므로 TestFixture도 분리하지 않는 이유는 무엇입니까?
이후 다양한 피실험자들이 서로 달라붙어 구분하기 어려웠다.

잘 만들었다고 생각했는데 아쉬운 부분도 있었습니다.

보기도, 부르기도 힘든 상태입니다.

그래서 이름을 TestFixture로 지정하고 여러 객체로 분리했습니다.

:
클래스 이름 TestDummy를 …TestFixture로 변경하여 분리합니다.


TestFixture는 편의상 만들어졌지만 개체의 상태가 변경되면 잘못될 수 있습니다.
그래서 새 것을 만들고 메서드가 호출될 때 전송하여 작동하게 했습니다!

public abstract class NameFixture {
    public static Name getNameRosie() {
        return new Name("rosie");
    }

    public static Name getNameHyena() {
        return new Name("hyena");
    }

    public static Name getNameJayon() {
        return new Name("jayon");
    }
}

:
엄청난! 내 관심사는 TestFixture의 상태입니다. 매번 잘 만들어집니다.
하지만 메서드 이름을 getXXX 대신 createXXX 또는 createXXX로 어떻게 변경할 수 있습니까?

:
새 객체를 계속해서 반환한다는 의미를 유지하기 위해 메서드 이름을 createXXX로 변경했습니다!

메소드를 호출하면 새로운 객체가 반환되기 때문에 상태는 안전하고 메소드 이름도 createXXX로 바뀌어서 여기까지인 줄 알았습니다.

public abstract class NamesFixture {
    public static Names getNamesSize2() {
        return new Names(List.of("rosie", "hyena"));
    }

    public static Names getNamesSize3() {
        return new Names(List.of("rosie", "hyena", "jayon"));
    }
}

그러나 현재 코드는 너무 정적이고 다른 이름으로 생성하려면 메서드를 계속 작성해야 합니다.

:
글쎄요, 저는 TestFixture가 그것을 더 잘 사용하기 위한 일반적인 방법이어야 한다고 생각합니다. (이에 대한 정답은 없습니다)

public static Names createNames(int nameSize) {
    List<String> names = // TODO: name 값을 랜덤으로 'nameSize'만큼 생성
    return new Names(names)
}​

:
그 느낌을 염두에 두고 전체 테스트 픽스처를 수정하는 것은 어떻습니까?
이름 값이 임의적이지 않더라도 “name-1”, “name-2” 같은 값은 괜찮은 것 같습니다.

:
다음과 같이 치수를 입력하면 적당한 사이즈를 생성할 수 있도록 방법이 수정되었습니다!

public abstract class NamesFixture {
    public static Names createNames(final int size) {
        final List<String> nameValues = new ArrayList<>();
        for (int count = 0; count < size; count++) {
            nameValues.add("name" + count);
        }
        return new Names(nameValues);
    }
}​

이러한 방식으로 TestFixture의 구조를 점진적으로 개선했습니다. 매개변수를 수락하고 동적으로 데이터를 생성할 수 있으며 상태 안전합니다. 다만, 아직 반영되지 않은 아쉬운 부분은 제이온에서 설명을 드리고 있으니 이만 적어보겠습니다.

:
정말 의도한 느낌입니다~
다만 이 경우 new Name()을 사용하는 것과 큰 차이가 없기 때문에 기본 매개변수 값이 있는 메서드를 주로 사용한다.

public class MyClass {
    public void myMethod(String param1, int param2, boolean param3) {
        // Method implementation here
    }

    public void myMethod(String param1, int param2) {
        myMethod(param1, param2, false); // default value for param3
    }
    
    // Default value for param2 and param3
    public void myMethod(String param1) {
        myMethod(param1, 0, false);
    }
}​

:
이런 느낌으로 createName()을 해도 내부 기본값이 있으면 테스트 코드 작성이 수월해집니다!
다만, 이는 선택 사항이므로 헤나는 잘 생각해보고 반영 여부를 결정해야 합니다.

현재 NameFixture 코드는 다음과 같이 구현되어 있으며 기본값을 지정하는 것이 더 편리합니다.

public abstract class NameFixture {
    public static Name createName(final String value) {
        return new Name(value);
    }
}

이 글을 읽고 기본값을 지정하여 사용하시길 바랍니다!


기본 값 래핑

원시 값 래핑

래더 미션을 통한 진행

래더 결과를 얻으려면 다음 정보가 필요합니다.

  • 정수, 스텝 폭
  • int, 래더의 세로 크기
  • 정수, 참가자 위치

코드를 구현할 때 변수 이름을 어떻게 지어도 같은 타입이기 때문에 사용하기가 어렵습니다.

아래 그림과 같이 가로 방향과 세로 방향이 반대인 경우도 있었습니다.

public class LadderController() {
	
    public void run() {
    	// ...
        final int width = 3;
        final int height = 2;
        final Lines lines = new LineCreator(height, width);
    }
}

public class LineCreator {

    public Lines createLines(final int width, final int height) {        
        // ...
    }
}

이러한 불편함을 해결하기 위해 프리미티브 값을 래핑하여 Width, Height 객체를 생성했습니다.

public class Width {
    private final int value;

    public Width(final int value) {
        validatePositive(value);
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

public class Height {
    private final int value;

    public Width(final int value) {
        validatePositive(value);
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

잘못된 순서로 넣으면 컴파일 오류가 발생하고 안정성이 향상될 수 있습니다.

public class LadderController() {
	
    public void run() {
    	// ...
        final Width width = new Width(3);
        final Height height = new Height(2);
        final Lines lines = new LineCreator(width, height);
    }
}

public class LineCreator {

    public Lines createLines(final Width width, final Height height) {        
        // ...
    }
}

예를 들면 다음과 같습니다.

래더 시작 위치(int)에서 시작하여 래더 끝 위치(int)를 반환하는 논리가 있습니다.

이 경우 Position 객체도 생성되어 사용됩니다.

public class Position {
    private final int value;

    public Position(final int value) {
        this.value = value;
    }

    public Position move(final int value) {
        return new Position(this.value + value);
    }

    public int getValue() {
        return this.value;
    }
}
private Position findNextPosition(final Position column, final Position row) {
      // ...
}

이유 때문에 findNextPosition 메소드를 가져옵니다.

현재 방법에서는 세 가지 위치가 있습니다.

프리미티브 값으로 랩핑해도 같은 타입이라 또 문제가 될 수 있습니다.

  • 위치, 참가자 위치
  • 위치, 사다리 수평 위치
  • 위치, 사다리 수직 위치

이 때문에 위치를 의미하는 Position을 Column, Row, Position으로 다시 나누고자 합니다.

동일한 의미를 가진 여러 기본 값 래퍼 및 VO를 가질 수 있습니까?

위치를 표현할 때 Column, Row, Position 3개의 오브젝트를 사용하는 것이 훨씬 안전합니다.

그러나 동일한 기본 값을 사용하고 세 가지 모두 위치를 나타내는 것이 가능한지 궁금합니다.

:
정수를 둘러싼 여러 개체가 중복으로 간주될 수 있는지 궁금합니다.

작업을 수행하는 동안 너비, 길이, 색인과 같은 정수를 계속 사용해야 했습니다.
이 때문에 변수 이름만으로 정렬하는 것은 혼란스럽기 때문에 값 개체를 만들었습니다.

값 객체를 사용하기 때문에 정수를 직접 전달할 수 있는 부분에서 값 객체로 변환하여 전달해야 합니다.
같은 유형을 계속 사용한다면 구현에 많은 오용이 있을 것입니다. 다른 유형의 값 객체를 사용하여 이러한 부분을 더 정확하게 구현하는 것이 유리할 것 같습니다.

너비, 높이 및 위치에 대한 필드를 보면 int 유형의 필드가 있습니다.
사용하는 부분이 각각 다르기 때문에 그 구분은 분명하지만, 한 단계 더 나아가 Row, Column과 같은 객체를 만들고 싶습니다.

이런 식으로 동종 분야에서 지속적인 성장을 하고 있는 상황이 좋다고 할 수 있을지 모르겠습니다.

:
Width, Height 및 Position 값 개체의 원시 값을 래핑하는 이점을 잘 알고 있는 것 같습니다.
이 질문에 바로 답하기보다는 제 질문에 답하면서 생각해보고 싶습니다.
아, 물론 이 작업은 모든 기본 값을 래핑해야 하므로 Hena가 원하는 것처럼 값 개체로 래핑하는 것이 좋습니다!

아직 명확한 답을 찾지 못했어요

기존 Position 개체를 사용하기로 결정했습니다.

비슷한 의미를 가진 Column과 Row 객체를 만들어서 사용하면 언제 어디서 사용해야 할지 더 헷갈릴 수 있다고 판단했습니다.

LEVEL 1 래더 생성 퀘스트를 진행하면서 생각을 적어보니 한 페이지가 너무 길게 느껴졌다.

다음에는 제가 했던 리서치를 포스팅하고 회고전과 연결하도록 하겠습니다.