본문 바로가기
우아한테크코스/Level 1

[우아한테크코스 1주차] 감자 생존 일지🥔

by kriorsen 2024. 2. 26.

📌 페어 프로그래밍 


네비게이터와 드라이버로 역할을 나누어서 15분마다 스위치 하는 방식으로 페어 프로그래밍을 진행했다. 전날 맛보기로 한 계산기 페어 프로그래밍 미션에서 시간 제한으로 인해 구현을 제대로 완성하지 못해 본 미션에 대한 걱정이 조금 있는 상태로 레벨1을 시작했다. 걱정이 무색하게 미션을 시작한 첫날부터 페어와 정말 재미있게 토론하면서 코드를 완성했던 것 같다.

 

📌 [1단계] - 자동차 경주 구현


미션 제출에서 의외로 고생을 좀 했는데, 제출 브랜치 이름도 틀리고 git 충돌도 내고, 말 그대로 우당탕탕이었다. 첫 미션 제출 때 실수를 많이 한 덕에 2주차 미션에서는 훨씬 수월하게 작업했다. 특히 PR에 리뷰어에게 전달할 내용을 작성하는 과정에서 소프트 스킬도 성장했던 것 같다.

 

리뷰어 분께서 남겨주신 코멘트에는 질문에 대한 명확한 답 대신 더 깊은 고민을 할 수 있는 질문들이 적혀 있었다.

 

  • 어떨 때 getter 메서드를 사용해야할까요?
  • 어떤 점이 코드를 복잡하게 하는 요인이라고 생각할까요?
  • getter 메서드를 사용한 경우의 장단점과 getter 메서드를 사용하지 않았을 때의 장단점은 무엇일까요?

 

Q1. 어떨 때 getter 메서드를 사용해야할까요?
prviate한 클래스의 필드를 접근하기 위해 사용합니다.현재 자동차 경주 미션에서는 우승자 판별과 우승자 이름 출력을 위해 Car의 distance와 name 조회 과정에서 getter가 사용될 수 있습니다.

Q2. 어떤 점이 코드를 복잡하게 하는 요인이라고 생각할까요?
getter가 없는 것으로 인해 필드의 상태 정보가 필요한 모든 로직들이 Car 클래스 내부에 정의되어야 해서 Car의 코드가 복잡해졌다고 생각합니다.

Q3. getter 메서드를 사용한 경우의 장단점과 getter 메서드를 사용하지 않았을 때의 장단점은 무엇일까요?
getter 메서드를 사용한 경우
장점: Car의 상태를 외부에서 조회할 수 있게 되어서 테스트 코드 작성하기에도 쉽고, Car와 관련된 비즈니스 로직을 수행하는 클래스에서 편리하게 참조를 할 수 있습니다.
단점: getter를 무분별하게 사용하면 Car가 수행해야 할 역할을 다른 클래스가 침범할 수 있습니다.

getter 메서드를 사용하지 않은 경우
장점: 객체 내부 상태를 외부로부터 은닉해서 캡슐화가 더 잘 이루어지고 Car의 책임이 명확해집니다.
단점: Car의 필드에 대한 모든 로직을 다 내부에서 수행해서 Car가 가진 메서드가 너무 많아지고 유지보수가 어렵습니다.

 

질문에 대한 고민을 깊이 해봤을 때, 결국 getter 자체를 지양해야 되는 것이 아니라, 무분별한 getter 사용으로 인해 클래스의 책임이 불명확해지는 것을 경계해야 한다는 것을 깨달았다. 그래서 getter의 사용 여부 대신 책임을 해치지 않는 선에서 올바르게 getter를 사용하는 방법을 더 고민하기로 결심했다.

 

// Car.java
public static int findMaxDistance(List<Car> cars) {
    return cars.stream()
            .mapToInt(car -> car.movedDistance)
            .max()
            .getAsInt();
}

 

MaxDistance를 구하는 것이 Car의 책임이 아니라고 생각했고, Cars라는 일급 컬렉션을 정의한 후 Car에 getDistance() 메서드를 추가하여 가장 먼 거리를 구하는 책임을 Cars에게 넘기도록 수정했다.

 

public class Cars {

    private final List<Car> cars;

    public Cars(List<Car> cars) {
        validateUniqueCarNames(cars);
        this.cars = Collections.unmodifiableList(cars);
    }

    public static Cars createCarsByName(List<String> carNames) {
        List<Car> cars = carNames.stream()
                .map(Car::new)
                .toList();

        return new Cars(cars);
    }
    
    // 기타 로직

    private int findMaxDistance() {
        return cars.stream()
                .mapToInt(Car::getMovedDistance)
                .max()
                .orElseThrow(IllegalArgumentException::new);
    }
}

 

📌 [2단계] - 자동차 경주 리팩터링


   // RandomOilGeneratorTest.java
   void testGenerate() {
        assertThat(RandomOilGenerator.generate())
                .matches((oil) -> oil <= 9 && oil >= 0);
    }

 

1단계 리뷰에서 위 코드는 Random이기 때문에 테스트가 운이 좋아 통과한 것인지 아닌지 판단하기 힘들지 않냐는 피드백을 받았다. RandomOilGenerator에 대해 아무런 테스트도 하지 않는 것보다는 범위에 대한 테스트 시도라도 하는 것이 낫다고 생각해서 해당 테스트를 작성했었다. 그러나 케이스를 통과해도 코드의 정확성을 보장할 수 없다는 점이 테스트의 의미를 많이 퇴색시켰다. 결론적으로 나는 랜덤한 값을 생성하는 부분과 값의 유효 범위를 검증하는 부분을 분리해서 테스트하기 쉬운 OilGenerator에 대한 테스트 케이스만 작성하는 방식으로 코드를 작성하고 2단계 첫 리뷰를 요청했다.

 

public abstract class OilGenerator {

    public static final int MIN_OIL_AMOUNT = 0;
    public static final int MAX_OIL_AMOUNT = 9;

    public int generateValidOil() {
        int oil = generate();
        if (oil < MIN_OIL_AMOUNT || oil > MAX_OIL_AMOUNT) {
            throw new OilOutOfRangeException();
        }
        return oil;
    }

    protected abstract int generate();
}

 

class OilGeneratorTest {

    @DisplayName("유효하지 값이 생성되면 예외를 던진다")
    @ParameterizedTest
    @ValueSource(ints = {10, 11, -1, 100})
    void testGenerateInvalidOil(int oil) {
        OilGenerator generator = new TestOilGenerator(oil);
        assertThatThrownBy(generator::generateValidOil)
                .isInstanceOf(OilOutOfRangeException.class);
    }

    @DisplayName("유효한 값이 생성되면 예외를 던지지 않는다")
    @ParameterizedTest
    @MethodSource("generateValidOilValues")
    void testGenerateValidOil(int oil) {
        OilGenerator generator = new TestOilGenerator(oil);
        assertThatCode(generator::generateValidOil)
                .doesNotThrowAnyException();
    }

    static Stream<Integer> generateValidOilValues() {
        return IntStream.rangeClosed(MIN_OIL_AMOUNT, MAX_OIL_AMOUNT)
                .boxed();
    }

    static class TestOilGenerator extends OilGenerator {

        private final int fixedOil;

        public TestOilGenerator(int fixedOil) {
            this.fixedOil = fixedOil;
        }

        @Override
        protected int generate() {
            return fixedOil;
        }
    }
}

 

 

두 번째 미션을 진행하고 다시 이 코드를 보니, 사용자의 입력이 아닌 내부 코드 문제로 인해 프로덕션 코드에 검증 로직을 추가한 것이 좋은 설계는 아닌 것 같다. 하지만 적어도 예측 불가능한 RandomOilGenerator에서 범위를 벗어난 값을 생성했을 때 확실하게 예외를 발생시킨다는 점에서 의의가 있다고 생각한다.

 

매번 테스트 결과가 다를 수 있기 때문에 가치가 없는 건지, 정확성은 보장할 수 없어도 오류를 잡아낼 가능성이 있다는 점에서 의미가 있는 건지 잘 모르겠어요.

 

PR에 남긴 질문에 대해 리뷰어분께서는 정답이 없는 문제이고 랜덤한 부분과 핵심 비즈니스 로직을 분리하여 테스트를 한 점으로 충분하다고 말씀해 주셨다. 그리고 추가적으로 랜덤에 대해 1회가 아닌 반복을 통해 혹시 모를 에러를 잡아낼 수 있도록 테스트를 작성하는 편이라고 하셔서 반복 테스트를 추가적으로 작성헀다.

 

class RandomOilGeneratorTest {

    @DisplayName("유효 범위 내에서 숫자를 생성한다")
    @Test
    void testGenerate() {
        OilGenerator randomOilGenerator = new RandomOilGenerator();
        int trial = 10_000;

        for (int count = 0; count < trial; count++) {
            assertThatCode(randomOilGenerator::generateValidOil)
                    .doesNotThrowAnyException();
        }
    }
}

 

💭 첫 번째 미션을 마치며


프리코스를 포함해서 혼자 거의 세 번 정도 구현해 본 미션인데도 불구하고 연극 준비와 병행하며 해서 꽤 버거웠던 것 같다. 첫 미션을 수행하면서 캠퍼스에 적응도 어느 정도 하고, 페어 프로그래밍에 대한 감도 잡아갔다. 우테코에 와서 정말 내가 걱정이 많은 성격이라는 걸 느꼈는데, 앞으로는 불안을 떨치고 현재 내가 할 수 있는 것에 집중하도록 멘탈 관리를 잘 해야 할 것 같다. 🥔