[인프런 워밍업 클럽 2기 클린코드 & 테스트 코드] 2주차 발자국

[인프런 워밍업 클럽 2기 클린코드 & 테스트 코드] 2주차 발자국

해당 글은 [인프런 워밍업 클럽 2기 클린 코드 & 테스트 코드]에 참가하여 박우빈님<Readable Code: 읽기 좋은 코드를 작성하는 사고법> 강의를 듣고 작성한 글입니다.

 

이번주 강의 요약

  1. 객체 지향 적용하기

    1. 상속은 부모와 자식 간의 결합이 높다. -> 확장과 변경이 어려움

    2. 조합과 인터페이스를 사용하자!

    3. VO는 불변성, 동등성, 유효성 검증이 필수

    4. 일급 컬렉션은 컬렉션을 객체로 다룬다.

      1. 컬렉션만을 유일하게 변수로 가진다.

      2. 가공 로직을 추가할 수 있음!

      3. 일급 시민과 유사

      4. Enum은 상태와 행위를 한번에 정의한다.

         

     

  2. 코드 다듬기

    1. 대부분 코드로 풀어내고, 그럼에도 전달해야 할 정보가 있을 때 주석을 쓰자!

    2. 변수는 사용 순서로, 메서드는 public, private 순서로.

      1. 상태 변경 -> 판별 -> 조회 순

     

  3. StudyCafe 리팩토링!

    1. 추상화 레벨 잘 맞춰주기

    2. 무지성 getter, setter NO! 이왕이면 객체에 메시지를 보내는 형태로 해결하자.

    3. 컬렉션이 의미가 있고, 가공 로직이 필요한 경우 일급 컬렉션을 쓰자!

    4. 외부에서 데이터를 가져올 때,

      1. 방법에 대해 초점을 맞추지 않는다. (기존에는 파일을 읽는 행위가 클래스로 만들어져 있었음)

      2. 어떤 데이터가 필요한 지에 대한 추상을 적용하자! (Provider(규격)를 두고 필요한 것(구현체)을 제공.)

     

  4. 기억하면 좋은 조언들

    1. 어떤 코드를 이해하려고 할 때, 모든 방법을 동원하자. -> 도메인 지식을 늘리고 선대의 의도 파악하는 것이 중요!

    2. 완벽한 기술은 없다!

 

🏃 미션

  1. Day7

다양한 리팩토링 조건이 있었는데 잘... 안됐다.. 중복을 제거하고 추상화 레벨을 맞추는 것만 해보려고 노력했다.

일단 run()에 모든 로직이 모여 있어서 공동 로직을 추출했다.

  • 이용권 목록 파일 읽어와서 선택한 이용권 타입에 맞는 이용권 리스트 반환

  • 이용권 리스트 출력

  • 이용권 선택

public void run() {
        try {
            outputHandler.showWelcomeMessage();
            outputHandler.showAnnouncement();

            //이용권 타입 선택
            outputHandler.askPassTypeSelection();
            StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction();

            //이용권 목록 파일 읽어오기
            StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler();
            List<StudyCafePass> studyCafePasses = studyCafeFileHandler.readStudyCafePasses();

            //이용권 타입에 맞는 이용권 리스트 반환
            List<StudyCafePass> filteredPasses = filterStudyCafePassesByType(studyCafePasses, studyCafePassType);

            //이용권 선택
            outputHandler.showPassListForSelection(filteredPasses);
            StudyCafePass selectedPass = inputHandler.getSelectPass(filteredPasses);

            if (studyCafePassType == StudyCafePassType.HOURLY) {
                outputHandler.showPassOrderSummary(selectedPass, null);
            } else if (studyCafePassType == StudyCafePassType.WEEKLY) {
                outputHandler.showPassOrderSummary(selectedPass, null);
            } else if (studyCafePassType == StudyCafePassType.FIXED) {
                List<StudyCafeLockerPass> lockerPasses = studyCafeFileHandler.readLockerPasses();
                StudyCafeLockerPass lockerPass = lockerPasses.stream()
                    .filter(option ->
                        option.getPassType() == selectedPass.getPassType()
                            && option.getDuration() == selectedPass.getDuration()
                    )
                    .findFirst()
                    .orElse(null);

                boolean lockerSelection = false;
                if (lockerPass != null) {
                    outputHandler.askLockerPass(lockerPass);
                    lockerSelection = inputHandler.getLockerSelection();
                }

                if (lockerSelection) {
                    outputHandler.showPassOrderSummary(selectedPass, lockerPass);
                } else {
                    outputHandler.showPassOrderSummary(selectedPass, null);
                }
            }
        } catch (AppException e) {
            outputHandler.showSimpleMessage(e.getMessage());
        } catch (Exception e) {
            outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다.");
        }
    }

스터디 카페 타입에 맞는 이용권 리스트를 반환하는 로직은 메서드로 추출했다.

private List<StudyCafePass> filterStudyCafePassesByType(List<StudyCafePass> studyCafePasses, StudyCafePassType studyCafePassType) {
        return studyCafePasses.stream()
            .filter(studyCafePass -> studyCafePass.getPassType() == studyCafePassType)
            .toList();
}

그리고 현재 분기문이 스터디 카페 타입에 따라 나눠지는데,

  • 공통적인 부분 : 선택한 이용권 내역 출력

  • 고정권일 때, 사물함을 추가적으로 받는다.

잠깐 출력 메서드인 outputHandler.showPassOrderSummary()을 알아보자!

public void showPassOrderSummary(StudyCafePass selectedPass, StudyCafeLockerPass lockerPass) {
    ....
}

이 메서드는 선택한 이용권과 사물함 여부를 인수로 받는다.

즉, 공통적으로 이 메서드를 쓰고

  • 시간권, 주간권일 때는 lockerPassnull

  • 고정권이고 사물함을 쓸 때만 lockerPass 값을 넣자.

run()

public void run() {
        try {
            outputHandler.showWelcomeMessage();
            outputHandler.showAnnouncement();

            //이용권 타입 선택
            outputHandler.askPassTypeSelection();
            StudyCafePassType studyCafePassType = inputHandler.getPassTypeSelectingUserAction();

            //이용권 목록 파일 읽어오기
            StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler();
            List<StudyCafePass> studyCafePasses = studyCafeFileHandler.readStudyCafePasses();

            //이용권 타입에 맞는 이용권 리스트 반환
            List<StudyCafePass> filteredPasses = filterStudyCafePassesByType(studyCafePasses, studyCafePassType);

            //이용권 선택
            outputHandler.showPassListForSelection(filteredPasses);
            StudyCafePass selectedPass = inputHandler.getSelectPass(filteredPasses);

            //이용권 내역 출력
            showPassOrderDetails(selectedPass);
        } catch (AppException e) {
            outputHandler.showSimpleMessage(e.getMessage());
        } catch (Exception e) {
            outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다.");
        }
}

showPassOrderDetails()

private void showPassOrderDetails(StudyCafePass selectedPass) {
        StudyCafeLockerPass lockerPass = getLockerPassIfFixed(selectedPass);
        outputHandler.showPassOrderSummary(selectedPass, lockerPass);
}

getLockerPassIfFixed()

private StudyCafeLockerPass getLockerPassIfFixed(StudyCafePass selectedPass) {
        if (selectedPass.getPassType() == StudyCafePassType.FIXED) {
            //사물함 목록 파일 읽기
            List<StudyCafeLockerPass> lockerPasses = studyCafeFileHandler.readLockerPasses();

            //고정권 타입 조건에 맞는 사물함 반환
            StudyCafeLockerPass lockerPass = lockerPasses.stream()
                    .filter(option ->
                            option.getPassType() == selectedPass.getPassType()
                                    && option.getDuration() == selectedPass.getDuration())
                    .findFirst()
                    .orElse(null);

            //사물함이 있다면, 사물함 선택 여부를 입력 받기
            boolean lockerSelection = false;
            if (lockerPass != null) {
                outputHandler.askLockerPass(lockerPass);
                lockerSelection = inputHandler.getLockerSelection();
            }

            //사물함을 선택한다면
            if (lockerSelection) {
                return lockerPass;
            } else {
                return null;
            }
        }
        return null;
}

이용권 타입이 고정권일 때,

  • 사물함을 선택한다면 lockerPass 반환

  • 사물함을 선택하지 않는다면 null 반환

이용권 타입이 고정권이 아니라면 null 반환

getLockerPassIfFixed() 내부가 너무 복잡하므로 추상화를 하기로 했다.

  • FileHandler는 변하지 않고 공통적으로 쓰이므로 맨 위로 private final로 빼주었다.

  • 고정권 타입에 맞는 사물함을 반환하는 부분을 메서드로 추출

  • 사물함 선택 여부 메서드로 추출

getLockerPassIfFixed()

private StudyCafeLockerPass getLockerPassIfFixed(StudyCafePass selectedPass) {
        if (selectedPass.getPassType() == StudyCafePassType.FIXED) {
            //사물함 목록 파일 읽기
            List<StudyCafeLockerPass> lockerPasses = studyCafeFileHandler.readLockerPasses();

            //고정권 타입 조건에 맞는 사물함 반환
            StudyCafeLockerPass filterdLockerPass = filterLockerPassByTypeAndDuration(lockerPasses, selectedPass);

            //사물함이 없다면, null 반환
            if (filterdLockerPass == null) {
                return null;
            }

            //사물함을 선택한다면
            if (askForLockerUsage(filterdLockerPass)) {
                return filterdLockerPass;
            }
        }
        return null;
}

filterLockerPassByTypeAndDuration()

private static StudyCafeLockerPass filterLockerPassByTypeAndDuration(List<StudyCafeLockerPass> lockerPasses, StudyCafePass selectedPass) {
        return lockerPasses.stream()
                .filter(option ->
                        option.getPassType() == selectedPass.getPassType()
                                && option.getDuration() == selectedPass.getDuration())
                .findFirst()
                .orElse(null);
}

askForLockerUsage()

private boolean askForLockerUsage(StudyCafeLockerPass filterdLockerPass) {
        outputHandler.askLockerPass(filterdLockerPass); 
        return inputHandler.getLockerSelection();
}

그리고 run()에는 행위별로 나뉘어져 있으면 좋겠다고 생각해서

  • 안내메시지 출력

  • 이용권 타입 선택

  • 이용권 선택

  • 이용권 내역 출력

으로 정리했다.

 

public class StudyCafePassMachine {

    private final InputHandler inputHandler = new InputHandler();
    private final OutputHandler outputHandler = new OutputHandler();
    private final StudyCafeFileHandler studyCafeFileHandler = new StudyCafeFileHandler();

    public void run() {
        try {
            //스터디 카페 안내 메시지
            outputHandler.showWelcomeMessage();
            outputHandler.showAnnouncement();

            //이용권 타입 선택
            StudyCafePassType studyCafePassType = getPassTypeFromUser();

            //이용권 선택
            StudyCafePass selectedPass = getPassFromUser(studyCafePassType);

            //이용권 내역 출력
            showPassOrderDetails(selectedPass);
        } catch (AppException e) {
            outputHandler.showSimpleMessage(e.getMessage());
        } catch (Exception e) {
            outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다.");
        }
    }

    private StudyCafePassType getPassTypeFromUser() {
        outputHandler.askPassTypeSelection();
        return inputHandler.getPassTypeSelectingUserAction();
    }

    private StudyCafePass getPassFromUser(StudyCafePassType studyCafePassType) {
        List<StudyCafePass> studyCafePasses = studyCafeFileHandler.readStudyCafePasses();
        List<StudyCafePass> filteredPasses = filterStudyCafePassesByType(studyCafePasses, studyCafePassType);
        outputHandler.showPassListForSelection(filteredPasses);
        return inputHandler.getSelectPass(filteredPasses);
    }

    private List<StudyCafePass> filterStudyCafePassesByType(List<StudyCafePass> studyCafePasses, StudyCafePassType studyCafePassType) {
        return studyCafePasses.stream()
                .filter(studyCafePass -> studyCafePass.getPassType() == studyCafePassType)
                .toList();
    }
    
    private void showPassOrderDetails(StudyCafePass selectedPass) {
        StudyCafeLockerPass lockerPass = getLockerPassIfFixed(selectedPass);
        outputHandler.showPassOrderSummary(selectedPass, lockerPass);
    }

    private StudyCafeLockerPass getLockerPassIfFixed(StudyCafePass selectedPass) {
        if (selectedPass.getPassType() == StudyCafePassType.FIXED) {
            List<StudyCafeLockerPass> lockerPasses = studyCafeFileHandler.readLockerPasses();
            
            StudyCafeLockerPass filterdLockerPass = filterLockerPassByTypeAndDuration(lockerPasses, selectedPass);
            
            if (filterdLockerPass == null) {
                return null;
            }
            
            if (askForLockerUsage(filterdLockerPass)) {
                return filterdLockerPass;
            }
        }
        return null;
    }

    private static StudyCafeLockerPass filterLockerPassByTypeAndDuration(List<StudyCafeLockerPass> lockerPasses, StudyCafePass selectedPass) {
        return lockerPasses.stream()
                .filter(option ->
                        option.getPassType() == selectedPass.getPassType()
                                && option.getDuration() == selectedPass.getDuration())
                .findFirst()
                .orElse(null);
    }

    private boolean askForLockerUsage(StudyCafeLockerPass filterdLockerPass) {
        outputHandler.askLockerPass(filterdLockerPass);
        return inputHandler.getLockerSelection();
    }

}

🗒️ 회고

  1. 드디어 완강했다! 그런데 반쯤은 이해 못해서 무조건 복습해야 할 것 같다..ㅎㅎ

  2. 미션을 하면서 너무 헤매서 괴로웠다! ㅜㅜ 나는 지금껏 뭘 들은거지... 싶었다. 강의자님이 하신 리팩토링을 보면서 내 코드가 너무 바보같고 한심했다..... 꼭 복습해서 완벽히 내 것으로 만들고 싶다!

  3. 아무튼 이번 클린 코드 강의를 통해서 지금까지 내가 작성했던 코드가 얼마나 엉망진창이었는지 알게 됐다..... 그래도 내가 지금 제대로 하는 건지, 이게 맞는 건지 항상 의문이 있었는데 나아갈 방향이 생겨서 좋다.

  4. 다음주부터 시작하는 테스트 코드도 화이팅! 밀리지 않고 열심히 듣기~!!🥰🥰🥰🥰

댓글을 작성해보세요.

채널톡 아이콘