너무 늦은 4주차 프리코스 회고

4주차 미션은 프리코스 동안 가장 어려운 도전이었습니다. 회고를 적기엔 늦었지만, 당시의 고민과 배운 점을 기록하며 앞으로의 방향을 정리하고자 합니다.

4주차 목표와 구현 목록

4주차에는 모든 의존성을 관리하는 Config 클래스를 설계하고, 1~3주차 동안 순수 Java로 구현한 기능들을 조합하여 4주차 미션을 완성하는 것이 목표였습니다.

4주차 구현 목록

더보기

구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템을 구현한다.

  • 사용자가 입력한 상품의 가격과 수량을 기반으로 최종 결제 금액을 계산한다.
    • 총구매액은 상품별 가격과 수량을 곱하여 계산하며, 프로모션 및 멤버십 할인 정책을 반영하여 최종 결제 금액을 산출한다.
  • 구매 내역과 산출한 금액 정보를 영수증으로 출력한다.
  • 영수증 출력 후 추가 구매를 진행할지 또는 종료할지를 선택할 수 있다.
  • 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
    • Exception이 아닌 IllegalArgumentException, IllegalStateException 등과 같은 명확한 유형을 처리한다.

재고 관리

  • 각 상품의 재고 수량을 고려하여 결제 가능 여부를 확인한다.
  • 고객이 상품을 구매할 때마다, 결제된 수량만큼 해당 상품의 재고에서 차감하여 수량을 관리한다.
  • 재고를 차감함으로써 시스템은 최신 재고 상태를 유지하며, 다음 고객이 구매할 때 정확한 재고 정보를 제공한다.

프로모션 할인

  • 오늘 날짜가 프로모션 기간 내에 포함된 경우에만 할인을 적용한다.
  • 프로모션은 N개 구매 시 1개 무료 증정(Buy N Get 1 Free)의 형태로 진행된다.
  • 1+1 또는 2+1 프로모션이 각각 지정된 상품에 적용되며, 동일 상품에 여러 프로모션이 적용되지 않는다.
  • 프로모션 혜택은 프로모션 재고 내에서만 적용할 수 있다.
  • 프로모션 기간 중이라면 프로모션 재고를 우선적으로 차감하며, 프로모션 재고가 부족할 경우에는 일반 재고를 사용한다.
  • 프로모션 적용이 가능한 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 필요한 수량을 추가로 가져오면 혜택을 받을 수 있음을 안내한다.
  • 프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제하게 됨을 안내한다.

멤버십 할인

  • 멤버십 회원은 프로모션 미적용 금액의 30%를 할인받는다.
  • 프로모션 적용 후 남은 금액에 대해 멤버십 할인을 적용한다.
  • 멤버십 할인의 최대 한도는 8,000원이다.

영수증 출력

  • 영수증은 고객의 구매 내역과 할인을 요약하여 출력한다.
  • 영수증 항목은 아래와 같다.
    • 구매 상품 내역: 구매한 상품명, 수량, 가격
    • 증정 상품 내역: 프로모션에 따라 무료로 제공된 증정 상품의 목록
    • 금액 정보
      • 총구매액: 구매한 상품의 총 수량과 총 금액
      • 행사할인: 프로모션에 의해 할인된 금액
      • 멤버십할인: 멤버십에 의해 추가로 할인된 금액
      • 내실돈: 최종 결제 금액
  • 영수증의 구성 요소를 보기 좋게 정렬하여 고객이 쉽게 금액과 수량을 확인할 수 있게 한다.

미션 구현 중 큰 실수...

이번에 미션 구현하면서 가장 큰 실수는 이 요구사항이였습니다.

더보기

입력

  • 구현에 필요한 상품 목록과 행사 목록을 파일 입출력을 통해 불러온다.
    • src/main/resources/products.md과 src/main/resources/promotions.md 파일을 이용한다.
    • 두 파일 모두 내용의 형식을 유지한다면 값은 수정할 수 있다.

이 요구사항을 제출하기 하루전에 확인하는 큰 실수를 해버렸던 기억이 강하게 남아있습니다...

진짜 제출하기 24시간동안 손에 땀을 흘리면서 코드 구현을 했습니다. 다행히도 대부분의 요구사항을 만족하는 코드를 작성하였지만, 기존의 테스트 코드는 쓸데가 없어져 버려 의미가 사라져버렸습니다.

테스트 코드가 다 날라갔...

이번 미션을 하면서 다시 한번 깨달은 점은 요구사항을 정확하게 인지하여 정리하자... 입니다.

어찌되었든 어떻게든 기능 구현에 성공했지만 그렇기에 코드가 개판이 되어버렸습니다.

하지만 이번에 이렇게 코드를 급하게 구현하면서 알게된 점은 요구사항을 정확히 인지하고, 코드 설계 전 철저히 검토해야 한다는 점입니다.

Config란?

4주차 미션에서 새롭게 도입된 설계는 Config 클래스였습니다.
Config 클래스는 각 객체의 의존 관계를 주입하는 책임을 가집니다.

Config를 사용하는 이유

  • 코드 유연성 확보: 의존 관계가 외부에서 주입되므로 수정이 쉬움.
  • 코드 가독성 향상: 의존성 관리와 로직 실행을 분리.

Config 코드 예시

public class Config {
    private final FileLeader leader;
    private final UserRequest userRequest;

    public Config() {
        this.leader = new FileLeader();
        this.userRequest = createUserRequest();
    }

    public UserRequest getUserRequest() {
        return userRequest;
    }

    private UserRequest createUserRequest() {
        return new UserRequest(new CallBackTemplate(), createController(), createViewHandler());
    }

    private ConvenienceController createController() {
        Promotions promotions = new Promotions(leader.loadPromotionsFromFile("./src/main/resources/promotions.md"));
        Products products = new Products(leader.loadProducts("./src/main/resources/products.md", promotions));
        return new ConvenienceController(products, promotions);
    }

    private ViewHandler createViewHandler() {
        return new ViewHandler(new CallBackTemplate(), new ApiHandler(new Validator()), new Input(), new Output());
    }
}

이렇게 Config 클래스는 의존성을 정의하고 필요한 객체를 생성하여, 로직 코드에서 의존성 관리 부담을 줄여줍니다.

4주차 회고

이번 미션은 시간에 쫓기며 코드를 작성해야 했던 어려운 경험이었습니다.
하지만 동작하지 않는 아름다운 코드보다는 동작하는 덜 아름다운 코드가 낫다는 사실을 다시금 느꼈습니다.

앞으로는 요구사항을 꼼꼼히 분석하고, 나만의 방식으로 체계적으로 정리하는 습관을 들이고자 합니다.
또한, 시간에 쫓겨도 테스트 코드와 요구사항 검토를 놓치지 않는 개발자가 되기를 목표로 합니다.

3주차 프리코스 목표

이번 3주차에는 Call-Back Template 디자인 패턴 구현하는 것과 서버 - 클라이언트 형태의 코드로 설계하는 것을 목표로 잡았다.

일단 Call-Back Template란 무엇이며, 어떤 것을 해결하고자 나온 디자인 패턴일까?

Callback Template: 모듈화의 필요성과 구현

개발 중 반복적이지만 모듈화하기 어려운 코드가 종종 등장합니다. 대표적으로 try-catch 문과 같은 예외 처리가 그렇습니다.

public Api<MoneyDto> purchaseMoney() {
    while(true) {
    	try {
            String money = input.getPurchaseMoney();
            return apiHandler.transformMoneyDto(money);
        } catch (IllegalArgumentException e){
            output.viewExceptionMessage(e.getMessage());
        }
    }
}

이처럼 try-catch 문은 반복되지만, 내용이 달라 모듈화가 어렵습니다. 이를 해결하기 위한 방법이 바로 Callback Template입니다.

public class CallBackTemplate {
    public <T> T retry(Supplier<T> callback, Consumer<MyException> exceptionHandler) {
    while (true) {
        try {
            return callback.get();
        } catch (MyException e) {
            exceptionHandler.accept(e);
        }
    }
}

이 코드에서 Supplier<T>는 함수형 인터페이스 중 하나로, 특정 기능을 추상화하여 람다 표현식으로 구현할 수 있도록 합니다.

함수형 인터페이스란?

함수형 인터페이스는 단 하나의 추상 메서드만 가지는 인터페이스로, Java 8에서 람다 표현식과 함께 도입되었습니다. 이는 개발자가 모듈화된 코드를 간결하게 작성할 수 있도록 해줍니다.

추가 설명: 함수형 인터페이스는 오라클에 따르면 Object의 메서드를 제외하고 하나의 추상 메서드만 가져야 하며, 이를 통해 람다 표현식 사용이 가능해집니다. 이에 대한 자세한 설명은 별도의 글로 다루겠습니다.

Callback Template 사용법과 장단점

Callback Template는 아래와 같이 람다식을 활용해 호출됩니다.

public Api<MoneyDto> purchaseMoney() {
    return retry.retry(() -> {
                String money = input.getPurchaseMoney();
                return apiHandler.transformMoneyDto(money);
             },
             e -> output.viewExceptionMessage(e.getMessage())
    );
}
  • 장점: 반복 코드를 줄여 가독성을 높이고, 코드 오류를 줄입니다.
  • 단점: 모듈화가 성능 저하를 초래할 수 있으며, 람다식에 익숙하지 않은 개발자에게는 가독성이 떨어질 수 있습니다. 하지만 성능이 극도로 중요한 경우가 아니라면, 이 정도의 저하는 거의 무시해도 됩니다.

 

클라이언트 - 서버 관계와 API 설계

클라이언트와 서버의 관계를 정의하는 것은 API 설계에서 핵심입니다. 클라이언트는 요청을 하고, 서버는 이 요청을 받아 비즈니스 로직을 처리하여 응답을 반환합니다.

지난주 MVC 패턴 구현 시 고민했던 주제는 캐싱 주체였습니다. 일반적으로 Controller가 캐싱을 담당하는 것은 역할상 어색하다고 느꼈습니다. 이에 따라 UserRequest라는 클래스를 생성하여 캐싱을 전담하게 하고, 컨트롤러는 비즈니스 로직과 데이터 흐름을 관리하는 구조로 수정했습니다

3주차 회고

이번 3주차 프리코스에서는 Callback Template클라이언트-서버 구조를 구현하며 아직 더 공부가 필요하다는 것을 실감했습니다. 특히, 함수형 인터페이스와 같은 개념을 단순히 이해하는 것과 실제로 구현하는 것의 차이를 느낀 주이기도 했습니다. 이 과정에서 개발은 단순한 지식이 아닌 능력이라는 것을 다시 한번 깨달았습니다.

 

다음 4주차에는 Config를 도입하여 DI를 적용하는 클래스를 구현하는 것을 목표로 삼고 있습니다. 이를 통해 보다 유연하고 확장 가능한 구조를 만들어보고자 합니다.

 

3주차 TDD 결과

TDD 결과

3주차 깃 주소

https://github.com/woowacourse-precourse/java-lotto-7/pull/242

참고문헌

Infa 블로그 - 함수형 인터페이스

Oracle - Functional Interface

 

2주차 프리코스 목표

이번에는 MVC패턴 형식으로 코드를 설계하고 구현하는 것을 목표로 잡았다.

지금까지 서버를 개발할 때, 무의식적으로 MVC 패턴을 이용해서 개발을 해왔는데, 왜 많은 개발자들이 이 패턴을 사용해서 개발하는지는 잘 몰랐었다. 이번 기회에 MVC로 개발하는 이유와 더불어 MVC로 개발할 때 단점은 무엇인지 공부해보았다.

여기서 말하는 MVC패턴은 무엇일까?

 

MVC Pattern

Model - View - Controller라는 크게 3가지의 개념으로 코드를 설계하고 구현하는 방법론을 의미한다. Model, View, Controller 3가지의 층을 나누어 각자의 책임과 기능을 정의하고, 이에 맞는 코드를 구현하여 좀 더 읽기 쉬우며, 객체 지향적으로 코드를 설계할 수 있다.

 

Model

여기서 Model은 핵심 기능이 구현된 층이다. 핵심 기능들은 다 여기서 진행된다고 보면 된다.

해당 코드는 다음과 같다.

 

public class Car {

    private final String name;
    private int distance;

    protected Car(String name) {
        this.name = name;
        distance = 0;
    }

    public static Car of(String name) {
        return new Car(name);
    }

    public void moveCar(Integer randomNumber) {
        if(randomNumber >= 4) {
            distance++;
        }
    }

    public String getCarname() {
        return name;
    }

    public Integer getCarDistance() {
        return distance;
    }
}

View

이 서비스를 사용하는 사용자가 보는 인터페이스가 구현된 층이다. 사용자는 View를 통해 서비스를 이용하게 된다.

해당 코드는 다음과 같다.

 

public class Input {

    private final String COMMA = ",";

    protected Input() {
    }

    public static Input of() {
        return new Input();
    }

    public String getStringCarnames() {
        System.out.println(자동차_이름_입력.getMessage());
        return getInput();
    }

    private String getInput() {
        return readLine();
    }
}

Controller

Model과 View 사이에서 코드를 이어주는 중계하는 역할을 가지고 있다. 따라서 View에서 기능 요청이 들어오면 해당 기능을 수행하는 Model로 연결해주는 기능을 가지고 있다.

해당 코드는 다음과 같다.

 

public class GameController {
    private final ViewHandler viewHandler;
    private final ApiHandler apiHandler;

    public GameController() {
        viewHandler = ViewHandler.of();
        apiHandler = ApiHandler.of();
    }

    public void playGame() {
        Api<InputDto> inputDto = viewHandler.inputHandler();
        Integer totalRound = inputDto.getData().getTotalRound();
        Race race = createRace(inputDto.getData());

        game(race, totalRound);

        List<CarDto> winnerCars = transformCarsDto(race.getWinnerCars());
        viewHandler.outputHandler(apiHandler.transformResultDto(winnerCars));
    }

    private Race createRace(InputDto inputDto) {
        List<String> carnames = inputDto.getCarnames();
        return Race.of(carnames);
    }

    private void game(Race race, Integer round) {
        IntStream.range(0, round).forEach(oneRound -> {
            oneRoundGame(race);
            List<CarDto> carDtos = transformCarsDto(race.getCars());
            viewHandler.outputHandler(apiHandler.transformRoundDto(carDtos, oneRound));
        });
    }

    private void oneRoundGame(Race race) {
        race.getCars().forEach(oneCar -> {
            oneCar.moveCar(pickNumberInRange(0, 9));
        });
    }

    private List<CarDto> transformCarsDto(List<Car> cars) {
        return cars.stream().map(CarDto::new).toList();
    }
}

 

위 코드 같은 경우, 유저의 요청을 컨트롤러에서 직접 처리하고 있다.

따라서 기본적인 Controller 역할에서 좀 더 기능이 추가되었다고 보는것이 맞는것 같다.

 

MVC Pattern의 장점

그럼 이 MVC 패턴을 이용해서 코드를 구현한 경우 어떤 장점이 있을까??

내가 생각하는 장점은 다음과 같다.

1. 클래스의 기능과 범위를 명확하게 정의할 수 있다.

2. 협업이 용이하다.

3. 모듈화를 하기 쉽다.

 

첫번째, 클래스의 기능과 범위를 명확하게 정의할 수 있다.

위는 사실 Layer Architecture가 적용되었다면 모두 해당하는 장점인 것 같다. 각 층에서 진행해야하는 책임과 기능이 한정되어 있다 보니각 층에 벗어나는 기능이 정의된다면 리펙토링을 통해 재정의하면서 클래스의 기능과 범위가 명확해진다.

 

두번째, 협업이 용이하다.

대부분 Spring 서버 개발자들이 MVC 패턴을 기반으로 서버를 개발하고 있으므로, 각 클래스에서 해야하는 기능에 대해서 서로 잘 알고 있다. 따라서 협업 시 필요한 기능이나 리뷰가 필요한 기능을 바로 찾아 의논할 수 있다.

 

세번째, 모듈화를 하기 쉽다.

이건 첫번째와 연결되는 장점인데, 각 클래스에서 많이 사용하는 메서드들을 따로 모아서 모듈화를 할 수 있다. 이렇게 모듈화를 해놓으면 코드를 구현할 때, 개발자의 실수를 줄일 수 있으며 중복되는 코드를 제거할 수 있다.

 

MVC Pattern의 단점

위의 장점과 반대로 단점은 무엇일까?

내가 생각하는 단점은 다음과 같다.

1. Trade Off

2. 코드의 추상화

 

첫번째, Trade Off

이렇게 층을 나누게 된다면 서버 내부에서도 많은 메서드들을 거쳐서 로직이 수행되게 되고, 당연히 이에 따른 DTO 변환같은 추가적인 연산이 필요하게 된다. 따라서 서버의 부하가 더 심해지는 결과를 가지고 오게 된다.

 

두번째, 코드의 추상화

여러개의 층을 나누고, 그 층에 맞는 코드를 작성했기 때문에 개발자는 바로 이 코드가 어떤 유저의 요청에 사용되는지 한눈에 알 수 없다.

따라서 코드를 읽어보면서 이해하는 시간이 추가적으로 걸리게 된다. 물론 서버가 확장되면 될수록 오히려 층을 나누는 것이 개발자가 이해하기 쉬워지나, 간단한 프로젝트나 서버에서 이정도의 추상화를 진행하게 된다면 오히려 가독성이 떨어지게 된다고 생각한다.

 

이처럼 대부분의 디자인 패턴은 무조건 옳다라는 것은 없는거 같다. 각 프로젝트에 필요한 디자인 패턴을 차용해 코드를 개발하는 것이 개발자의 자세라고 생각한다.

 

2주차 회고

이번에 MVC 패턴으로 프리코스를 개발하면서 꽤 높은 수준의 추상화가 이루어졌음을 느꼈다. 이로 인해 한 번에 이해하기 쉽지 않은 코드가 완성되었다. 하지만 MVC로 개발하는 순간 이렇게 높은 추상화가 필수적이라고 생각한다. 이런 높은 추상화를 Spring같은 프레임워크가 대신 해준다는 것을 구현하면서 깨달았다.

 

다음 3주차에는 MVC에 모듈화를 같이 진행하여, 코드의 중복성을 줄이는 것을 시도해보고자 한다.

 

2주차 TDD 결과

TDD 결과

Test코드가 대부분의 메서드를 충족시키고 있는 것 같다. 이정도면 내 개인적으로 만족한다.

 

2주차 깃 주소

https://github.com/woowacourse-precourse/java-racingcar-7/pull/223

프리코스 목표

저는 이번 프리코스 목표를 TDD로 잡았습니다.

지금까지 프로젝트를 진행할 때, 기능을 설계 및 개발하는 것을 목표로 진행했었습니다.

그리고 실제 코드 돌아가는 것은 직접 서버를 돌려보면서 진행하다보니 많은 테스트를 진행하지 못하였고, 후 고도화 과정에서 많은 리펙토링을 진행하게 되더라구요.

그래서 이번에 프리코스에 참여하는 겸 처음으로 TDD를 천천히 적용시키면서 테스트 코드를 작성하는 연습을 진행할려고 합니다.

TDD란?

TDD(Test Driven Development)는 어떤 기능을 구현하기 전에 테스트를 먼저 구현하고, 기능을 구현하는 것을 의미합니다.

처음에 이 소리를 들으면 이게 무슨말이고, 어떻게 코드를 짜는건지 잘 몰랐었습니다.그래서 많은 정보를 찾아보니 Red, Green, Refactoring이라는 과정이 있다고 하는데, 제가 이해한 바로 천천히 알아가봅시다.

 

Red (테스트 코드 작성)

Red과정은 테스트 코드를 작성하는 겁니다.어떤 기능이 있다면, 그 기능에 들어가는 파라미터와 타입이 존재하겠죠?이를 테스트 코드에 작성하는 것을 의미합니다. 당연하게도 기능을 구현 안했기 때문에 테스트 동작은 실패로 나오게 됩니다.

이 과정을 Red라고 합니다.

class ValidatorTest {
	
    private Validator validator = Validator.of();

    @Test
    void 구분자_검증_확인() {
        
        String userInput = "//; \n1;2;3\n";
        assertThat(validDelimiters(userInput)).isTrue();
    }
    
    Boolean validDelimiters(String inputData) {
		
        return false;
    }
}

 

Green(기능 개발)

자 위에 나와있던 것처럼 들어갈 파라미터와 타입을 정의한 메서드의 기능을 개발하고, 테스트를 통과하도록 하는 과정입니다.

원래 개발하는 것처럼 비지니스 로직이 들어가는 것이죠

class ValidatorTest {
	
    private Validator validator = Validator.of();

    @Test
    void 구분자_검증_확인() {
        
        String userInput = "//; \n1;2;3\n";
        assertThat(validDelimiters(userInput)).isTrue();
    }
    
    Boolean validDelimiters(String inputData) {
		
        return inputData.contains("//");
    }
}

 

Refactor(리펙토링)

지금까지 개발한 기능을 실제 돌아갈 Main 파일 아래 클래스로 올리는 과정입니다.

해당 객체 안에 테스트를 통과한 기능을 조립하는 과정이라고 이해하면 될 것 같습니다.

 

public class Validator {

    protected Validator() {
    }

    public static Validator of() {
        return new Validator();
    }

    public Boolean validDelimiters(String inputData) {
        return inputData.contains("//");
    }
}

 

1주차 회고

이번에 'TDD로 개발해야겠다!!' 라는 생각에 사로잡혀서 프리코스에서 제시한 요구사항을 놓치게 되었습니다.

확실하게 요구사항에 적합하는 테스트를 모두 만든 후, 기능개발을 하는 쪽으로 2주차를 진행해볼려고 합니다.

 

1주차 깃 주소

https://github.com/raccoon-coding/java-calculator-7/tree/raccoon

+ Recent posts