본문 바로가기

카테고리 없음

[우아한테크코스 4기] 프리코스 3주차 미션

우아한 테크코스 4기 3주차 미션과제는 '자판기'를 구현하는 것이었습니다.

 

README.MD


2주차 피드백 중 아래와 같은 내용이 있었습니다.

너무 세세한 부분까지 정리하기보다 구현해야 할 기능 목록을 정리하는 데 집중한다

 

항상 README를 작성할 때 마다 시간을 너무 많이 소요하는 것은 아닌지에 대한 고민이 많았는데, 위 패드백을 바탕으로 이번에는 심플하게 작성하려고 노력하였습니다.

 

## 🚀 기능 요구사항

반환되는 동전이 최소한이 되는 자판기를 구현한다.

- 자판기가 보유하고 있는 금액으로 동전을 무작위로 생성한다.
  - 자판기가 보유하고 있는 금액은 `10원` 부터 시작.
  - 자판기가 보유하고 있는 금액은 `10원`으로 나누어 떨어져야 함.

- 상품명, 가격, 수량을 입력하여 상품을 추가할 수 있다.
  - 예외사항 : 상품끼리는 `;` 기준으로 분리할 수 있어야 한다.
  - 예외사항 : 상품명 가격 수량은 대괄호 `[]`내에 존재해야 한다.
  - 예외사항 : 상풍명과 가격 수량은 `,`로 구분 가능해야 한다.
  - 예외사항 : 상품 가격은 `숫자`이어야 한다.
  - 예외사항 : 상품 가격은 `100원`부터 시작.
  - 예외사항 : 상품 가격은 `10원`으로 나누어 떨어져야 함.
  - 예외사항 : 상품 수량은 `숫자`이어야 한다.
  - 예외사항 : 상품 수량은 `1개 이상`이어야 한다.
  - 예외사항 : 상품명 `중복 불가`.

- 투입 금액 입력
  - 예외사항 : 투입금액이 `숫자`인지 확인
  - 예외사항 : 투입금액이 `양의 정수`인지 확인

- 구매할 상품명 입력
  - 예외사항 : `존재하는 상품명`인지 확인

- 남은 금액이 남은 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.

- 잔돈 반환
  - 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려준다.
  - 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
  - 반환되지 않은 금액은 자판기에 남는다.
  - 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.

한번에 작성하기 보다는 계속해서 추가, 추가 하면서 살아있는 README를 작성하려고 노력하였습니다. (빠진 부분은 없게끔..)

작성하고 나니 예외 사항이 대부분인 것 같네요..ㅎ

 

기능 구현


1. 불필요한 static 메서드 사용 지양

Money.java

 

자판기에 넣은 돈을 관리하는 Money 객체에 대한 것입니다.

 

package vendingmachine.domain;

public class Money {
    private static int amount = 0;

    public static void add(int money) {
        amount += money;
    }

    public static void spend(int cost) {
        amount -= cost;
    }

    public static int amount() {
        return amount;
    }
}

자판기에 돈을 넣었을 때 사용하는 add 기능

자판기에 넣은 돈으로 상품을 구매했을 때 사용하는 spend 기능

마지막으로 현재 남은 구매자의 돈을 확인할 때 사용하는 amount 기능이 존재합니다.

 

static 메서드이기 때문에 인자를 주고 받을 필요 없이 바로 접근이 가능하다는 점 때문에 코드가 깔끔해지는 것을 확인할 수 있었습니다.

또한 자체도 객체간의 관계를 생각할 필요가 없기 때문에 구현하는 난이도 또한 엄청 줄어들었습니다.

 

하지만 몇가지 문제점으로 인하여 자판기 돈에 관련된 객체는 static 메서드를 사용하지 않기로 결정하였습니다.

 

1. static 메서드로 이루어진 클래스는 객체라고 할 수가 없다고 판단하였습니다.

예를 들어, Math라는 클래스를 생각하면 좀 더 이해가 쉬울 것 같습니다.

Math라는 함수는 sqrt, sum 기능을 Math.sqrt(), Math.sum()으로 호출할 수 있는데요.

수학적인 연산과 관련된 메소드들을 Math라는 클래스 아래로 모아주는 장점이 있습니다. (grouping)

제가 만들려고 했던 Money라는 객체의 컨셉과는 사뭇 달라진다고 판단하였습니다.

 

2. 유지 보수 관리적인 측면에서 더욱 난이도를 어렵게 만든다고 판단하였습니다.

객체 간의 관계가 존재하지 않기 때문에, 내가 아닌 다른 사람들이 Money라는 클래스를 본다면 의미를 바로 파악하기 어려울 것이라고 판단하였습니다.

VendingMachine 속에 속한 Money라면 '아! 자판기 내에 존재하는 돈을 의미하는구나' 라고 파악을 할 수 있는 반면 

독자적인 클래스로 Money만 존재한다면 의미가 모호하여 다른 개발자에게 유지 보수 난이도를 높일 수 있는 부분이 될 수 있다고 생각하였습니다.

Money라는 클래스 이름을 좀 더 역할이 분명하게 변경하는 방법도 존재하지만 어디까지나 한계가 있을 것이라고 판단하고 좀 더 최선의 방법이라고 생각한 객체화를 시키는데 주력하였습니다.

 

2. 코인 클래스의 사용

Coin.java

 

까다로웠던 부분은 제한사항에 적힌 Coin 클래스를 사용하라는 것이었습니다.

package vendingmachine;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum Coin {
    COIN_500(500),
    COIN_100(100),
    COIN_50(50),
    COIN_10(10);

    private final int amount;

    Coin(final int amount) {
        this.amount = amount;
    }
}

코인 클래스를 살펴보면, 500원 100원 50원 10원이라는 변수를 가지고 있습니다.

자판기의 잔액을 의미하는 enum이라고 볼 수 있습니다.

이것을 어떻게 사용하길 원하였는지 의도를 생각해 보았지만 정확히 파악하기가 쉽지 않았습니다.

 

잔돈을 enum으로 어떻게 잘 사용할 수 있을까,,,

저는 처음 잔돈을 셋팅하는 용도로만 사용을 하였습니다.

 

프로그램 요구사항을 살펴보면 pickNumberInList를 활용하라고 나타나 있으며

Random 값 추출은 camp.nextstep.edu.missionutils.Randoms의 pickNumberInList()를 활용한다.

 

테스트 코드를 살펴보면 pickNumberInList  메서드로 리스트에서 값을 뽑을 때 마다 100원, 100원, 100원, 100원, 50원이 나오게 구성을 해놓았습니다.

void 기능_테스트() {
        assertRandomNumberInListTest(
            () -> {
                run("450", "[콜라,1500,20];[사이다,1000,10]", "3000", "콜라", "사이다");
                assertThat(output()).contains(
                    "자판기가 보유한 동전", "500원 - 0개", "100원 - 4개", "50원 - 1개", "10원 - 0개",
                    "투입 금액: 3000원", "투입 금액: 1500원"
                );
            },
            100, 100, 100, 100, 50
        );
    }
    

public static void assertRandomNumberInListTest(
        final Executable executable,
        final Integer value,
        final Integer... values
    ) {
        assertRandomTest(
            () -> Randoms.pickNumberInList(anyList()),
            executable,
            value,
            values
        );
    }
    
    
private static <T> void assertRandomTest(
        final Verification verification,
        final Executable executable,
        final T value,
        final T... values
    ) {
        assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
            try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
                mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
                executable.execute();
            }
        });
    }

 

pickNumberInList 메서드를 이용하기 전에 {500, 100, 50, 10} 이라는 리스트를 만들어야 하는데 이때 Coin이라는 enum 에 amount라는 새로운 메서드를 사용하여 만들기로 생각하였습니다.

 

package vendingmachine;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum Coin {
    COIN_500(500),
    COIN_100(100),
    COIN_50(50),
    COIN_10(10);

    private final int amount;

    Coin(final int amount) {
        this.amount = amount;
    }

    // 추가 기능 구현
    public static List<Integer> amounts() {
        return Arrays.stream(values())
            .map(coin -> coin.amount)
            .collect(Collectors.toList());
    }

    public int getAmount() {
        return amount;
    }
}

 

3. Setter 사용을 지양

 

자판기에는 상품 정보, 잔돈, 고객이 넣은 돈을 인스턴스 변수로 가지고 있게 설계하였습니다.

이번 문제는 아래와 같은 단계로 설계가 되었습니다.

잔돈을 입력 받음 -> 잔돈의 현황 출력 -> 상품 정보를 입력 받음 

 

처음 자판기 클래스는 아래와 같이 잔돈을 생성자에서 값을 입력 받고, 잔돈 현환을 출력한 뒤 상품 정보는 setter로 입력하는 형태로 구현을 하였습니다.

 

main() {
	prepareChange();
	prepareMerchandise();
}

private void prepareChange() {
    try {
        vendingMachine = new VendingMachine(InputView.requireChanges());

    } catch (IllegalArgumentException exception) {
        OutputView.printErrorMessage(exception.getMessage());
        prepareChange();
        return;
    }
    OutputView.printChangesState(vendingMachine.getChanges());
}

private void prepareMerchandise() {
    try {
        vendingMachine.setMerchandise(InputView.requireVendingMachineMerchandiseInfo());
    } catch (IllegalArgumentException exception) {
        OutputView.printErrorMessage(exception.getMessage());
        prepareMerchandise();
        return;
    }
}

구현을 완료한 뒤 살펴보니, 만약 다른 사람들이 코드를 사용할 때 자판기 클래스에 상품목록을 setter로 입력하지 않는다면, 제대로 된 동작을 기대하기가 어려운 문제가 발생하였습니다. (setter는 최대한 지양하도록 합시다!)

 

위 문제를 피하기 위해 생성자로 잔돈과 상품목록을 모두 받도록 재구현을 하였습니다.

main() {
	Changes changes = prepareChange();
    	prepareMerchandise(changes);
}

private Changes prepareChange() {
    Changes changes;
    try {
        changes = new Changes(InputView.requireChanges());
    } catch (IllegalArgumentException exception) {
        OutputView.printErrorMessage(exception.getMessage());
        changes = prepareChange();
        return changes;
    }
    OutputView.printChangesState(changes.changes());
    return changes;
}

private void buyTargetMerchandise() {
    try {
        vendingMachine.buyMerchandise(InputView.requireMerchandise());
    } catch (IllegalArgumentException exception) {
        OutputView.printErrorMessage(exception.getMessage());
        buyTargetMerchandise();
        return;
    }
}

처음에 받은 잔돈은 Changes 클래스에 미리 담아 놓고, 상품 목록을 받을 때 자판기를 생성하도록 구현을 하였습니다.

자판기를 생성할 때, 잔돈과 상품목록을 모두 넣도록 강제를 했습니다.

이로써 위에서 자판기에 상품목록을 넣지 않으면 문제가 발생할 수 있었던 상황을 조기에 방지할 수 있게 되었습니다.

 

4. 원시값 포장

Merchandise.java

 

상품 클래스입니다.

상품 클래스는 상품의 이름, 상품의 수량, 상품의 가격등을 가지고 있습니다.

 

package vendingmachine.domain;

public class Merchandise {

    private final String name;
    private final int cost;

    private int count;

    public Merchandise(String merchandise) {

        String[] merchandiseInfo = merchandise
            .substring(1, merchandise.length() - 1)
            .split(",");

        validStartEndformat(merchandise);
        validMerchandisFormat(merchandiseInfo);
        validCost(merchandiseInfo[1]);
        validCount(merchandiseInfo[2]);

        this.name = merchandiseInfo[0];
        this.cost = Integer.parseInt(merchandiseInfo[1]);
        this.count = Integer.parseInt(merchandiseInfo[2]);
    }

    private void validCount(String stringCount) {
        int count = 0;
        try {
            count = Integer.parseInt(stringCount);
        } catch (Exception exception) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 숫자로 이루어져야 합니다.");
        }

        if (count <= 0) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 1개 이상 존재해야 합니다.");
        }
    }

    private void validCost(String stringCost) {
        int cost = 0;
        try {
            cost = Integer.parseInt(stringCost);
        } catch (Exception exception) {
            throw new IllegalArgumentException("[ERROR] : 가격은 숫자로 이루어져야 합니다.");
        }

        if (cost < 100) {
            throw new IllegalArgumentException("[ERROR] : 상품 가격은 100원 이상이어야 합니다.");
        }
        if (cost % 10 != 0) {
            throw new IllegalArgumentException("[ERROR] : 상품 가격은 10원으로 나누어 떨어져야 합니다.");
        }
    }

    private void validStartEndformat(String merchandise) {
        if (!merchandise.startsWith("[") && !merchandise.endsWith("]")) {
            throw new IllegalArgumentException("[ERROR] : 상품명과 가격 수량을 대괄호( [] ) 내에 입력해 주시기 바랍니다");
        }
    }

    private void validMerchandisFormat(String[] merchandiseInfo) {
        if (merchandiseInfo.length != 3) {
            throw new IllegalArgumentException("상품명과 가격 수량을 ,로 구분하여 입력해 주시기 바랍니다.");
        }
    }

    public String name() {
        return name;
    }

    public int cost() {
        return cost;
    }

    public int count() {
        return count;
    }

    public void buy() {
        this.count--;
    }
}

상품 클래스를 자세히 살펴보니 메서드가 너무 많습니다.

현재는 상품의 이름, 수량, 가격등이 정확한 범위의 정확한 타입으로 왔는지 확인 및 상품이 판매되었을 때 상품의 수량을 감소시키는 역할만 하고 있지만 점점 기능이 많아 질 수록 복잡해 질 것이라고 예상할 수 있습니다.

 

상품 클래스의 어깨가 너무 무겁습니다.

객체 생활 체조 원칙에서 말한 `원시값 포장`을 진행한 결과를 확인해 보겠습니다.

 

Merchandise.java

 

package vendingmachine.domain;

public class Merchandise {

    private final MerchandiseName merchandiseName;
    private final MerchandiseCost merchandiseCost;
    private final MerchandiseCount merchandiseCount;

    public Merchandise(String merchandise) {

        String[] merchandiseInfo = merchandise
            .substring(1, merchandise.length() - 1)
            .split(",");

        validStartEndformat(merchandise);
        validMerchandisFormat(merchandiseInfo);

        merchandiseName = new MerchandiseName(merchandiseInfo[0]);
        merchandiseCost = new MerchandiseCost(merchandiseInfo[1]);
        merchandiseCount = new MerchandiseCount(merchandiseInfo[2]);
    }
    
    private void validStartEndformat(String merchandise) {
        if (!merchandise.startsWith("[") && !merchandise.endsWith("]")) {
            throw new IllegalArgumentException("[ERROR] : 상품명과 가격 수량을 대괄호( [] ) 내에 입력해 주시기 바랍니다");
        }
    }

    private void validMerchandisFormat(String[] merchandiseInfo) {
        if (merchandiseInfo.length != 3) {
            throw new IllegalArgumentException("상품명과 가격 수량을 ,로 구분하여 입력해 주시기 바랍니다.");
        }
    }

    public String name() {
        return merchandiseName.name();
    }

    public int cost() {
        return merchandiseCost.cost();
    }

    public int count() {
        return merchandiseCount.count();
    }

    public void buy() {
        merchandiseCount.minus();
    }
}

기존 보다는 조금 더 깔끔해진 것 같은가요??

생성자에서는 merchandise 전체 포맷에 대한 확인을 진행함으로써 역할을 좀 더 명확히, 직관적으로 알 수 있게 되었습니다.

나머지 원시값들은 어떻게 되었는지 확인해 보도록 하겠습니다.

 

MerchandiseCount.java

 

package vendingmachine.domain;

public class MerchandiseCount {
    private Integer count;

    public MerchandiseCount(String count) {
        validCount(count);
        this.count = Integer.parseInt(count);
    }

    private void validCount(String stringCount) {
        int count = 0;
        try {
            count = Integer.parseInt(stringCount);
        } catch (Exception exception) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 숫자로 이루어져야 합니다.");
        }

        if (count <= 0) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 1개 이상 존재해야 합니다.");
        }
    }

    public void minus() {
        this.count --;
    }

    public int count() {
        return count;
    }
}

 

MerchandiseCost.java

 

package vendingmachine.domain;

public class MerchandiseCost {
    private final Integer cost;

    public MerchandiseCost(String cost) {
        validCost(cost);
        this.cost = Integer.parseInt(cost);
    }

    private void validCost(String stringCost) {
        int cost = 0;
        try {
            cost = Integer.parseInt(stringCost);
        } catch (Exception exception) {
            throw new IllegalArgumentException("[ERROR] : 가격은 숫자로 이루어져야 합니다.");
        }

        if (cost < 100) {
            throw new IllegalArgumentException("[ERROR] : 상품 가격은 100원 이상이어야 합니다.");
        }
        if (cost % 10 != 0) {
            throw new IllegalArgumentException("[ERROR] : 상품 가격은 10원으로 나누어 떨어져야 합니다.");
        }
    }

    public int cost() {
        return cost;
    }
}

 

MerchandiseName.java

 

package vendingmachine.domain;

public class MerchandiseName {
    private final String name;

    public MerchandiseName(String name) {
        this.name = name;
    }


    public String name() {
        return name;
    }
}

 

객체의 상태와 관리 검증을 한 곳에서 진행하게 되었습니다.

그리고 상품이름에 대한 추가적인 기능 또는 검증할 것들이 생기면 MerchandiseName 쪽으로 가서 구현을 하면 되겠습니다.

아주 좋습니다.

 

위 클래스는 상수를 추출해 내고 마무리 하도록 하겠습니다.

 

4. 상수화

 

상수들을 private static final 접근 제한자를 사용하면서, 중복 방지 및 메모리적 이득을 얻을 수 있습니다.

 

변경 전

package vendingmachine.domain;

public class MerchandiseCount {
    private Integer count;

    public MerchandiseCount(String count) {
        validCount(count);
        this.count = Integer.parseInt(count);
    }

    private void validCount(String stringCount) {
        int count = 0;
        try {
            count = Integer.parseInt(stringCount);
        } catch (Exception exception) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 숫자로 이루어져야 합니다.");
        }

        if (count <= 0) {
            throw new IllegalArgumentException("[ERROR] : 상품 수량은 1개 이상 존재해야 합니다.");
        }
    }

    public void minus() {
        this.count --;
    }

    public int count() {
        return count;
    }
}

 

변경 후

package vendingmachine.domain;

public class MerchandiseCount {

    private static final String MERCHANDISE_COUNT_IS_INTEGER = "[ERROR] : 상품 수량은 숫자로 이루어져야 합니다.";
    private static final String MERCHANDISE_COUNT_IS_MORE_THAN_ONE = "[ERROR] : 상품 수량은 1개 이상 존재해야 합니다.";
    
    private Integer count;

    public MerchandiseCount(String count) {
        validCount(count);
        this.count = Integer.parseInt(count);
    }

    private void validCount(String stringCount) {
        int count = 0;
        try {
            count = Integer.parseInt(stringCount);
        } catch (Exception exception) {
            throw new IllegalArgumentException(MERCHANDISE_COUNT_IS_INTEGER);
        }

        if (count <= 0) {
            throw new IllegalArgumentException(MERCHANDISE_COUNT_IS_MORE_THAN_ONE);
        }
    }

    public void minus() {
        this.count --;
    }

    public int count() {
        return count;
    }
}

혐오 스러운 작명 센스입니다.

많은 코드들을 보면서 더욱 발전하도록 하겠습니다.ㅜㅜㅜ

 

프리코스를 진행하면서..


 Commit을 신경써라

커밋을 사용하다가 문득 잘 쓴 커밋이 왜 중요할까??

이게 어떤 도움을 줄까? 라는 생각이 들어 검색을 하였는데, 커밋을 제대로 하지 않아 후회하는 블로그가 종종 보였습니다.

예전에 반영한 변경사항을 되돌리고 싶다면, 그때 변경했던 것들이 무엇인지 하나하나 찾아서 원래대로 덮어쓰기 하는게 아니라, revert를 사용한다고 합니다.

하지만 문제가 생겼을 때 이전에 기능 별로 깔끔하게 커밋 하지 않았다면, 복구하기까지 많은 시간적, 정신적인 문제가 발생할 것이라고 보입니다. 이런 문제가 발생하기 전에 앞으로는 더욱 꼼꼼히 커밋을 하도록 노력하겠습니다.

 

  • 테스트 추가 -> test 커밋
  • 기능 구현 -> feat 커밋
  • 리팩토링 -> refactor 커밋
  • 오류 수정 -> fix 커밋

 

객체에 메시지를 보내라 + getter 사용을 줄여라

"객체에 메시지를 보내라"

'메시지를 보내는게 좋은거니까 사람들이 그렇게 하라고 말씀해 주시는 거겠지' 라고 생각만 하면 무지성 코딩만 하였습니다.

이번에는 그 의미를 곰곰히 생각해 보는 시간을 가져 보았습니다.

객체에 메시지를 보내는 것은 다른 말로 객체 내부 데이터 노출을 막는 방법입니다.

 

객체 내부 데이터 노출을 막기 위해 우리는 인스턴스 변수의 접근 제한자를 private으로 막은 대신 필요한 경우 getter를 사용하고 있습니다.

인스턴스 변수를 private으로 막는 이유는 혹시나 잘못된 코드로 인하여 직접적으로 값을 변경하였을 경우 모든 결과가 달라지는 치명적인 문제가 생길 수 있기 때문입니다. (setter 사용을 지양하는 이유와 같다고 생각합니다.)

 

그러면 getter는?? getter는 값을 수정 안하는데??

(물론 getter를 사용할 때도 어떤 값을 가지고 와서 어떻게 사용하느냐에 따라서 값을 변경할 수도 있습니다.)

 

getter를 사용하는 대신 메세지를 보내는게 더욱 객체 지향적인 프로그래밍 방법이라고 생각합니다.

최근 들어서 느낀 객체 지향 프로그래밍의 가장 큰 장점은 상태와 관리를 한 곳에서 하기 때문이라고 생각합니다.

이로 인하여, 어떤 코드를 수정할 때 어디만 수정하면 되겠다 라는 것이 직관적으로 나도, 상대방도 떠 올릴 수 있기 때문에 유지 보수 적인 측면에서 큰 이득을 가지고 온다고 생각합니다.

 

제가 그 동안 객체를 객체스럽게 사용하지 못하고 데이터 덩어리처럼 사용하는 경우가 많았다는 걸 깨달았습니다.

이번 프리코스를 진행할 때 객체 지향적인 프로그래밍을 하려고 많이 노력하였는데, 처음보다는 더욱 객체지향적인 프로그래밍에 익숙해 진 것 같아서 만족스럽습니다.

 

MVC 패턴 + 비지니스 로직과 UI 로직을 분리하라

MVC 패턴을 적용해 View와 Domain이 서로 의존성을 가지지 않고, 독립적인 개발이 가능하도록 하였습니다.

UI를 출력하는 로직은 철저하게 View의 책임으로 두었습니다.

 

비즈니스 로직과 UI를 분리하면서 좀 더 책임이 분명한 객체들을 만들 수 있었습니다.

레이어를 나누며 결합도가 낮아지는걸 경험했을 뿐 아니라 도메인, MVC, 서비스 레이어 등 여러 아키텍처를 조금씩 공부하면서 기능 구현 뿐 아니라 설계의 본질이라고 생각되는 "코드를 어떻게 배치할 것인가"를 고민하는 즐거운 시간이었습니다.

 

동작하게 만들고, 남들이 읽기 쉬운 코드를 작성하도록 하라.

프로그래밍은 기본적으로 협업하며 진행할 경우가 많을 것이라고 생각합니다.

이번 프리코스는 어떻게 코드를 작성해야 모두가 이해하기 쉬운 코드가 될까? 라는 생각을 많이 하면서 진행을 하였습니다.

코드가 어떻게 동작하면 될 지 저만의 스토리를 만들고 그 스토리를 바탕으로 코드를 구현함으로써, 남들이 읽기 쉬운 코드를 만들기 위해 많은 노력을 하였습니다. 

어디에 어떤 코드를 어떻게 배치하느냐에 따라 더욱 직관적인 코드가 되는걸 살펴보다 보니 시간이 금방 가버리는 신기한 경험을 할 수 있었습니다.

 

테스트 코드

이번 프로젝트에서 테스트 코드의 중요성을 느꼈습니다.

난이도가 높은, 구현해야 하는 것이 많은 프로젝트 일 수록 코드 수정이 많아 졌습니다.

많은 코드들을 수정하다 보니 '아 이거 어디서 잘 못 되는 것은 아닐까?' 라는 생각이 들었습니다.

 

그때마다 우아한 테크 코스에서 준비해준 테스트 코드들을 돌리고 이상 없다는 초록불을 볼 때 마다 안도감을 느꼈습니다.

'TDD를 기반으로 코드를 구현하라' 이 말이 구현하는데 속도만 늦추고 어떤 장점이 있을지 모르겠다고 생각을 하였는데 이번 프로젝트에서야 체감을 하게 되었습니다.

 

앞으로는 테스트 코드를 기반으로 하여 구현을 하도록 더욱 노력하도록 하겠습니다.

 

지금까지의 노력이 앞으로도 내 주위에 항상 함께 하기를

 

 

Reference


https://johngrib.github.io/wiki/law-of-demeter/#디미터-법칙을-위반한-코드---기차-충돌

 

디미터 법칙 (The Law of Demeter)

객체는 그것이 내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만 가지고 의사 결정을 내려야 한다

johngrib.github.io

https://jojoldu.tistory.com/412

 

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코

jojoldu.tistory.com