Skip to content

Conversation

@discphy
Copy link
Owner

@discphy discphy commented Mar 10, 2025

📅 미션 일자

  • Day 7

✅ 미션 내용

  • 스터디 카페 이용권 선택 시스템 리팩토링 연습

💭 리뷰

  • 이미 강의를 수강하기 전, 강의를 수강하면서 또 복습차원에서 총 3번의 리팩토링 경험이 있어 우빈님이 작성해주신 코드와 거의 유사한 것 같습니다.
  • 그래서 추가적으로, 리팩토링이 필요한 부분을 추가 리팩토링 하였습니다. 🙃

✨ 추가 리팩토링

하단에 코멘트 남기겠습니다!

  1. Reader 클래스들의 많은 책임
    • CSV 파일 읽는 유틸성 객체 분리
    • Read 객체 클래스 추출
  2. InputHandler, OutputHandler의 OCP 적용
    • StudyCafePassType의 타입이 추가가 되거나 삭제가 되면 영향을 받는 부분의 코드들을 인터페이스로 분리하여 StudyCafePassType 내에서 구현하였습니다.
  3. Provide 예외 클래스 작성
    • AppException 성격이랑 다른 것 같다고 생각되어 Provider에서 발생하는 예외 클래스를 별도로 생성하였습니다.

@discphy discphy self-assigned this Mar 10, 2025
Comment on lines 3 to 9
public class ProvideException extends RuntimeException {

public ProvideException(String message) {
super(message);
}

}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppException 성격이랑 다른 것 같다고 생각되어 Provider 인터페이스에서 발생하는 예외 클래스 ProvideException를 별도로 생성하였습니다.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[우빈님 리뷰]

우빈님께서 작성하신 AppException은 해당 프로그램에서 발생할 수 있는 최상위 예외 클래스 의도를 가지고 있다. ProvideException으로 만든 것은 괜찮은데 구체적으로 AppException의 상속을 받아야 한다.

커스텀 예외 만들 때 마다 catch를 다 잡아주어야 한다.

추가로, "이용권을 제공받을 수 없습니다." 예외 메세지는 사용자 친화적이지 않은 것 같다. 누가 보는지가 중요하다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 커스텀 예외를 만들어서, 저에게도 해당되는 리뷰이네요..!

덕분에 리마인드 합니닷

Comment on lines 5 to 8
public interface PassTypeFormatter {

String format(StudyCafePass pass);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍이 모호하지만.. 😂

OutputHandler에서 display() 메서드의 분기문의 OCP를 적용하기 위해 인터페이스를 작성하였고,
StudyCafePassType 클래스에서 인터페이스를 구현하고 있습니다.

Comment on lines 9 to 17
public class ConsoleInputHandler implements InputHandler {

private static final Scanner SCANNER = new Scanner(System.in);

@Override
public StudyCafePassType getPassTypeSelectingUserAction() {
String userInput = SCANNER.nextLine();
return StudyCafePassType.findBy(userInput);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StudyCafePassType 클래스 내에서 사용자 입력값으로 타입을 찾는 메서드로 리팩토링 하였습니다.

Comment on lines 82 to 85
private String display(StudyCafePass pass) {
StudyCafePassType passType = pass.getPassType();
return passType.format(pass);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위와 마찬가지로 StudyCafePassType 내부에서 포맷팅하는 로직을 구현하여 OCP를 적용하였습니다.

Comment on lines +14 to +26
public static final String CSV_FILE_NAME = "locker.csv";

@Override
public StudyCafeLockerPasses provide() {
try {
List<String> allLines = CsvFileReadSupport.readAllLinesBy(CSV_FILE_NAME);
ReadLockerPasses readSeatPasses = ReadLockerPasses.ofLines(allLines);

return readSeatPasses.toPasses();
} catch (IOException e) {
throw new ProvideException(e.getMessage());
}
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 메서드 내에 많은 책임이 있다고 생각되어..

CSV 파일을 읽고 라인을 반환하는 유틸성 클래스인 CsvFileReadSupport를 분리하였으며,
읽은 라인의 내용을 읽어 StudyCafe*Passes 객체로 반환하는 VO 클래스 개념을 도출하였습니다.

Comment on lines 10 to 44
public enum StudyCafePassType implements PassTypeSelectable, PassTypeFormatter {

HOURLY("시간 단위 이용권") {
@Override
public boolean selected(String userInput) {
return "1".equals(userInput);
}

@Override
public String format(StudyCafePass pass) {
return String.format("%s시간권 - %d원", pass.getDuration(), pass.getPrice());
}
},
WEEKLY("주 단위 이용권") {
@Override
public boolean selected(String userInput) {
return "2".equals(userInput);
}

@Override
public String format(StudyCafePass pass) {
return String.format("%s주권 - %d원", pass.getDuration(), pass.getPrice());
}
},
FIXED("1인 고정석") {
@Override
public boolean selected(String userInput) {
return "3".equals(userInput);
}

@Override
public String format(StudyCafePass pass) {
return String.format("%s주권 - %d원", pass.getDuration(), pass.getPrice());
}
};
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스 내부에서 사용자 입력값출력값에 사용하는 인터페이스를 구현함으로써 오버 엔지니어링이 된 것 같은 느낌이 드네요.. 🤦‍♂️

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[우빈님 리뷰]

오버 엔지니어링이기보다 PassType은 중요한 도메인 모델인데, Input에서만 의미를 가지는 사용자 선택지가 침투하고 있다.
사용자 선택 방법이 "a", "b", "c"로 바뀐다면?
단순히 입력 방식을 바꿨을 뿐인데 무료 도메인 모델이 수정되어야 하는 엄청난 사태가 발생한다.
항상 구조화를 하는 것이 정답은 아니다.
Output format도 마찬가지이다.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

책임이 우선이다. 적절한 책임의 분배가 결합도를 낮추고 응집도를 높이는 것이다.

Comment on lines +37 to +39
public StudyCafeLockerPasses toPasses() {
return StudyCafeLockerPasses.of(passes);
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급 컬렉션을 적용하기 위해toPasses() 메서드를 생성했는데
Read~라는 네이밍을 가진 클래스에 많은 책임이 부여된 것 같아 네이밍이 모호한 것 같습니다.

좋은 방법이 있을까요..? 🧐

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[우빈님 리뷰]

좋은 설계 방식이였다. 테스트 코드 작성도 가능하다.
많은 책임이라고 생각하지 않는다.

그와 별개로 CSV라는 방식에 종속되어있다. CSV형식이 다른 방식으로 바뀌었을 때 같이 바뀌어야 하는 부분이 CSV_SPLITTER 부분이다. 의도한 것 이라면 상관없다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants