Skip to content

Conversation

@Shin-Jae-Yoon
Copy link
Member

📌 과제 설명

  • 콘솔로 구현
  • OOP 계산기 구현
    • 더하기
    • 빼기
    • 곱하기
    • 나누기
    • 우선순위 (사칙연산)
  • 테스트 코드 구현
  • 계산 이력을 맵으로 데이터 저장 기능
    • 애플리케이션이 동작하는 동안 DB 외에 데이터를 저장할 수 있는 방법 고민
  • 정규식 사용

👩‍💻 요구 사항과 구현 내용

  1. 입력과 출력을 추상화하고 그를 구현한 구현체를 콘솔에 주입시켰습니다.
  2. Calculator 클래스에서 입력을 받게 했다가 계산이라는 책임에서 멀어지는 것 같아서 Controller를 추가하여 연결시켰습니다.
  3. Enum 클래스를 활용하여 선택 가능한 메뉴를 정의하였고 예외처리를 진행했습니다.
  4. Calculator 클래스에서는 계산이라는 logic을 수행하고 Converter와 Evaulator가 실질적인 behavior를 진행합니다.
    • 연산자에 대한 책임은 Enum 클래스를 활용한 Operator에서 가지고 있습니다.
  5. 저장소를 구현할 때, HistoryRepository로 추상화하였고 이를 구현했습니다. 추가로, CalculationHistory 객체를 이용하여 저장시키도록 구현했습니다.
  6. 수식 계산마다 Pattern을 생성하는 비용이 크다고 생각하여, 따로 Regex enum 생성시 만들어지게 했습니다.
    • Pattern 생성 비용에 관한 근거는, Pattern.compile() 메서드를 살펴봤을 때 return new Pattern() 부분이었습니다.

😭 1차 자기주도적 리뷰 피드백

영수님 피드백

  1. 값의 검증에는 예외처리를 하지 않는 것이 좋습니다. (Expression 클래스의 isNumber() 메서드)
  2. Calculator 클래스에서 필드로 expression을 가지는 것에 의문이 듭니다.
  3. Expression 클래스가 너무 비대하고 특히 converToPostfix() 메서드를 최대한 쪼개봅시다.
  4. if-else 보단 switch를 이용하여 리팩토링 해봅시다.
  5. 최대한 3 depth를 넘지 않게 메서드를 리팩토링 해봅시다.
  6. 여러 객체가 참조하는 형태가 아니라면, static 사용에 관하여 고민해봅시다.
    • OutOfMemory에 관해서도 고려해봅시다.
    • 가능한 static을 사용하지 않고 코드를 짜봅시다.
  7. Regex는 상수를 모아놓기 위한 장소같아보이는데, 어떤 고민을 해봐야할까요? 생각해봅시다.

흑구님 피드백

  1. 현재 모두 구현인 것 같고, 확장에 너무 닫혀 있습니다.
  2. 궁금한 것을 설명하고 싶은 의도는 알겠으나, 코드리뷰 요청하는 입장에서 코드를 주석처리한 것은 반드시 없애봅시다.
  3. 인스턴스를 생성하지 못하게 하고 싶을 때, 방어적으로 코드 작성하는 것을 생각합시다.
  4. 1개의 콘솔로 입출력 하는 상황인데, InputConsoleOutputConsole이 마치 2개의 콘솔처럼 보입니다.
  5. 객체지향적 설계가 어렵다면, 저만의 방식인데, logic → behavior → data 를 생각해봅시다.
    • logic : data를 이용한 behavior의 전체적인 흐름
    • behavior : data를 이용하는 어떠한 행위
    • data : 말 그대로 데이터, 객체
    • logic은 behavior에, behavior은 data에 의존(사용)
    • 각각 interaction(상호작용)에 관해서도 생각해볼 것
  6. 무조건 메서드로 표현할 필요는 없습니다. 객체간 상호작용이 부족한 지 생각해봅시다.
  7. 무조건 적인 set 지양은 하지말고, 어떤 문제가 있는지, 어떤 이유 때문인지 알고 set을 지양해봅시다. 너무 무서워하지 마세요.

😤 피드백 기반 리팩토링

  • Console 단일화 및 구현체가 아닌 추상체에 의존하도록
  • Controller 클래스 리팩토링
    • logic → behavior → data 고려했는가?
    • 객체간 상호작용을 고려했는가?
  • Expression 클래스 리팩토링
    • 3 depth를 넘지 않는가?
    • 추상화를 고려하였는가?
  • Regex에 방어 코딩을 적용했는가?

✅ PR 포인트 & 궁금한 점

PR 포인트

  • Expression 클래스의 기능이 비대한 것 같아서 나눴습니다.
    • Converter, Evaluator 추상체를 만들고 그를 구현한 구현체를 활용했습니다.
    • Calculator 추상체를 구현한 구현체에서 조립하여 사용했습니다.
    • Parser도 추가했었는데, 맡는 책임이 Regex에서 역할과 비슷하여 없애고 이관했습니다.
  • 계산 결과는 수식을 감싸기 위하여 CalculationResult와 Expression vo를 만들었습니다.
  • Regex에서 방어적 코드를 작성했다가, Regex의 역할이 상수만 저장하는 것이라서 util과 맞지 않�다고 생각하여 enum으로 따로 뺐습니다.

궁금한 점

  1. logic behavior data를 최대한 고려해봤는데, 사실 아직도 감이 잘 잡히지 않습니다. 최대한 추상화해보려고 했는데, 역할이 너무 작나? 과한 추상화인가? 라는 고민이 계속 생겼습니다.
  2. 테스트 코드가 미흡하여, 영수님 강의를 듣고 감이 조금 잡혀서 작성해봤는데, 방식이 맞는지 모르겠습니다.
  3. 홍섭님 피드백을 봤을 때, 공학용 계산기와 사칙연산만 가능한 계산기를 고려해보려 했습니다.
    • 현재, Operator enum에 사칙연산이 모두 몰아두고 enum의 values()를 반복돌리는 메서드를 정의해놔서 enum에 정의되어 있다면 반환해주고 없으면 예외를 던지는 상태입니다.
    • enum에 기능만 추가하면 되기 때문에 공학용 계산기가 추가되는건 문제가 없습니다.
    • enum에 다른 기능을 추가하면 사칙연산만 가능한 계산기가 다른 기능도 사용할 수 있는 상황이 되어버립니다.
    • 이에 대하여 생각한 해결 방법은 아래와 같습니다.
      • 공학용 계산기 enum을 하나 더 만들 때, 인터페이스를 이용해 확장 가능한 Enum Type이 되도록 리팩토링 하는 것 입니다. 아이템38 - 인터페이스 활용 Enum Type 확장를 참고하였습니다.
      • enum을 없애고 FourArithmeticStrategy 라는 인터페이스를 구현하는 각각 사칙연산이 있고, 공학용 전략 인터페이스를 하나 만들어서 거기에 심화적인 기능을 추가하고, 기본 계산기에는 사칙연산 전략만 적용, 공학용 계산기에는 사칙연산전략 + 심화전략 적용 이렇게 가보려고 합니다.
      • 어떠한 방법이 더 괜찮을지 궁금합니다.

Comment on lines +20 to +21
public List<CalculationHistory> findAll() {
return new ArrayList<>(store.values());
Copy link

Choose a reason for hiding this comment

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

Map vs List

왜 List를 반환하셨는지 재윤님의 생각이 궁금합니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

사실, 관례적으로 List<> findAll()을 사용했던 것 같아서, 늦게나마 이유에 대하여 찾아보았고
아래와 같은 이유로 Map 보다는 List가 적절하다고 생각합니다.

  1. 계산 이력이라는 특성 상 순서대로 반환하고 싶었습니다. Map은 순서가 보장되지 않기 때문입니다.
  2. 특정 값에 접근하는 상황이라면 Map이 빨랐겠지만, 현재 상황에서는 List가 빨라서 입니다.
    • List는 인덱스가 있지만, Map은 인덱스가 없어서 iterator로 뽑아야합니다.

Comment on lines 25 to 28
@Override
public String toString() {
return id + " : " + expression + " = " + result;
}
Copy link

Choose a reason for hiding this comment

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

toString() 오버라이드 괜찮네요.

혹시 기왕 이런 객체를 만들었다면, toString()을 오버라이드 하지 않고 다른 추가 기능을 만들어줘도 되지 않을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

값을 담는 객체라고 생각하여 어떠한 로직을 넣기에 조금 꺼려져서 toString()을 오버라이드 하였는데,
계산 이력을 반환한다는 기능을 추가해보겠습니다.

@@ -0,0 +1,33 @@
package com.programmers.calculator.domain.vo;

Copy link

Choose a reason for hiding this comment

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

Expression이 왜 CharSequence를 구현하고 있는지 재윤님의 의도가 궁금합니다.

CharSequence가 무엇일까요?

length, charAt, subSequence가 Expression에 필요할까요~?

Copy link
Member Author

Choose a reason for hiding this comment

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

정규식에 넣어야하는 부분에서 요구타입이 CharSequence 였기에 구현했는데,
구현하면서도 Expression이 구현하는 것이 맞는가하는 의문이 있었습니다.
흑구님의 피드백에서도 나왔는데 String에서 CharSequence를 구현하고 있다는
기본적인 내용을 다시 한번 상기했습니다.

import java.util.List;

public class FourArithmeticCalculator implements Calculator {

Copy link

Choose a reason for hiding this comment

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

LGTM

public CalculationResult calculate(Expression expression) {
List<String> tokens = RegexEnum.parseToTokens(expression);
List<String> postfix = converter.convert(tokens);
return evaluator.evaluate(postfix);
Copy link

Choose a reason for hiding this comment

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

evaluator는 무슨 역할을 하는 친구인가요?

해석해보면 평가자..라는 의미같은데 그냥 위임한 정도인거같아요

이유가 있으실까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

Calculator의 calculate 메서드는 logic으로 판단하였고
계산에 대한 실질적인 behavior는 evaluator에서 진행하여 위임했습니다.

파싱한 expression을 converter로 후위표기식으로 변경한 이후
evaluator에서 적절한 후위표기식인지, 수식에 오류가 있지는 않는지를 판단하고
계산하는 역할이어서 나누게 되었습니다.

Comment on lines 17 to 46
@DisplayName("repository 정상적으로 저장되는지 확인")
@Test
void repository_right_work() {

//given
Expression expression1 = new Expression("1 + 2 * 3");
Expression expression2 = new Expression("3 + 2 * 3 / 3");
CalculationResult calculationResult1 = new CalculationResult(new BigDecimal(7));
CalculationResult calculationResult2 = new CalculationResult(new BigDecimal(5));

CalculationHistory result1 = new CalculationHistory(expression1, calculationResult1);
CalculationHistory result2 = new CalculationHistory(expression2, calculationResult2);

//when
repository.save(result1);
repository.save(result2);

//then
List<CalculationHistory> all = repository.findAll();
/**
* 1. size check
* 2. reposity.contains(r1, r2)
* 3. reposity.get(0) = 7
* 4. reposity.get(1) = 5
*/
assertThat(all.size()).isEqualTo(2);
assertThat(all).contains(result1, result2);
assertThat(all.get(0)).isEqualTo(result1);
assertThat(all.get(1)).isEqualTo(result2);
}
Copy link

Choose a reason for hiding this comment

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

@ParameterizeTest에 대해 알아보세요!

반복되는 코드의 중복을 줄일 수 있습니다.

CalculationResult operand2 = new CalculationResult(new BigDecimal(0));

// when
Assertions.assertThatThrownBy(() -> division.getFunction()
Copy link

Choose a reason for hiding this comment

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

static import 하셨으면 통일성 맞춰주는게 좋아요!

assertThat(operator.getSymbol()).isEqualTo(operatorChar);
}

@DisplayName("연산자가 아닌 값들을 파라미터로 넘겨주면, 예외가 발생")
Copy link

Choose a reason for hiding this comment

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

LGTM


import java.util.List;

public class CalculatorController {
Copy link

Choose a reason for hiding this comment

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

예외는 어떻게 처리하실 건지 재윤님 생각이 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

제가 던지기만 하고 예외처리를 제대로 하지 않았었네요.
처리하도록 하겠습니다 !!


import java.util.List;

public interface Converter {
Copy link

Choose a reason for hiding this comment

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

저는 뭔가 Convert가 inptu String을 Expression 객체로 변환시켜주는 역할인줄 알았어요!

내부 구현을 알 필요 없이 input을 넘기면 식 객체로 반환되게끔요!

postfix prefix infix 상관없이 인터페이스만 사용해서요!

Copy link
Member Author

Choose a reason for hiding this comment

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

Converter라는 이름은 오해의 소지가 있는 것 같아서 명확하게
NotationConverter와 같은 방식으로 변경해보겠습니다.

SUBTRACTION('-', 10, CalculationResult::subtract),
MULTIPLICATION('*', 100, CalculationResult::multiply),
DIVISION('/', 100, (o1, o2) -> {
if (o2.getValue().equals(BigDecimal.ZERO)) {
Copy link

Choose a reason for hiding this comment

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

o2가 만약 null이 들어오면 npe가 발생할 수 있을것같아요

Objects.equals에 대해 알아보세요 !

Copy link
Member Author

Choose a reason for hiding this comment

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

Objects.equals() 메서드를 통해 null이 들어오더라도 NPE를 방지하고
안전하게 동등성 비교를 수행하는 것을 알게 되었습니다. 알려주셔서 감사합니다 !!

  • 두 개의 객체가 모두 null인 경우, true를 반환하여 두 개의 null 객체는 동등하다고 판단
  • 두 개의 객체 중 하나만 null인 경우, false를 반환하여 null과 다른 객체는 동등하지 않다고 판단
  • 두 개의 객체가 모두 null이 아닌 경우, equals() 메서드를 사용하여 객체의 동등성을 검사

import java.math.BigDecimal;
import java.math.RoundingMode;

public class CalculationResult {
Copy link

Choose a reason for hiding this comment

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

호옹

import java.math.RoundingMode;

public class CalculationResult {
private BigDecimal value;
Copy link

Choose a reason for hiding this comment

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

값 객체이면서 바뀔 일이 없다면 final 고려 해보시는게 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

이 부분도 동일하게 final에 대하여 개념이 부족했던 것 같습니다.
(또, Expression에는 private final 했으면서 여기는 안한것이,,, 죄송함니다)

}

public String inputOption() {
output.write("\n\n선택 : ");
Copy link

Choose a reason for hiding this comment

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

System.lineseparator() 라는 메소드가 존재합니다~!

Copy link
Member Author

Choose a reason for hiding this comment

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

System.lineseparator()를 이용하여 OS 구분 없이 일정한 개행을
제공할 수 있다는 점을 알게 되었습니다.

추가로, String newLine = System.getProperty("line.separator");
같은 형태로 조금 더 깔끔하게 newLine을 추가할 수 있을 것 같아서 적용해보았습니다.

Copy link

@devYSK devYSK left a comment

Choose a reason for hiding this comment

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

재윤님 안녕하세요. 피드백 반영하여 개선하시느냐 고생 많으셨습니다.
꽤 많은 내용이 바뀌었는데 재미있는 부분이 많았고, 가독성도 좋은 편이라 읽기 쉬웠습니다.

의문인점은, Calculator, Evaulator, Converter의 관계가 조금 애매한거 같긴 해요!
과하게 나누었거나, 서로 뭔가 꼬여있는 듯한 느낌이 들어요. 이부분은 다시 한번 고민해보시면 좋을거같아요.

또한 예외 처리하는 부분이 없는데, 어디서 처리해야 하고 어떻게 하면 좀 더 깔끔하게 할 수 있을지 생각해보세요!

값 객체를 만드셔서 사용했는데, 어디서부터 값객체가 전달되어야 할까를 고민해 보시면 좋을거같습니다.
테스트 코드 꼼꼼하게 짜신거 같아서 보기 좋습니다.
리팩토링할때도 테스트 코드가 도움이 많이 될거에요!

logic behavior data를 최대한 고려해봤는데, 사실 아직도 감이 잘 잡히지 않습니다.
최대한 추상화해보려고 했는데, 역할이 너무 작나? 과한 추상화인가? 라는 고민이 계속 생겼습니다.

아직 감이 잘 안잡히실거라 생각합니다.
SOLID를 생각하면서 설계해보시면 도움이 될 것 같습니다.
추상화의 목적은 무엇인가? 어떤 문제를 해결하기 위한 것이지? 어떤 장단점이 있을까

테스트 코드가 미흡하여, 영수님 강의를 듣고 감이 조금 잡혀서 작성해봤는데, 방식이 맞는지 모르겠습니다.

저는 꼼꼼하게 잘 작성하신거 같습니다.
조금더 스킬풀하게 사용하려면 많이 사용해보시면 점점 늘 수 있다고 생각합니다.
이것저것 테스트 해보고, 다른 사람은 어떻게 테스트 했을까 보시면서 흡수해보세요!


홍섭님 피드백을 봤을 때, 공학용 계산기와 사칙연산만 가능한 계산기를 고려해보려 했습니다.

  • 현재, Operator enum에 사칙연산이 모두 몰아두고 enum의 values()를 반복돌리는 메서드를 정의해놔서 enum에 정의되어 있다면 반환해주고 없으면 예외를 던지는 상태입니다.
  • enum에 기능만 추가하면 되기 때문에 공학용 계산기가 추가되는건 문제가 없습니다.
  • enum에 다른 기능을 추가하면 사칙연산만 가능한 계산기가 다른 기능도 사용할 수 있는 상황이 되어버립니다.
  • 이에 대하여 생각한 해결 방법은 아래와 같습니다.
    * 공학용 계산기 enum을 하나 더 만들 때, 인터페이스를 이용해 확장 가능한 Enum Type이 되도록 리팩토링 하는 것 입니다. 아이템38 - 인터페이스 활용 Enum Type 확장를 참고하였습니다.
    * enum을 없애고 FourArithmeticStrategy 라는 인터페이스를 구현하는 각각 사칙연산이 있고, 공학용 전략 인터페이스를 하나 만들어서 거기에 심화적인 기능을 추가하고, 기본 계산기에는 사칙연산 전략만 적용, 공학용 계산기에는 사칙연산전략 + 심화전략 적용 이렇게 가보려고 합니다.
    * 어떠한 방법이 더 괜찮을지 궁금합니다.

두 가지 방법 모두 좋은거 같습니다. 상황에 따라 적절하게 사용할 수 있을것 같아요.
어떤 방법이 재윤님이 만드는 시스템의 목적과 목표에 더 부합하느냐 차이일것 같습니다.

인터페이스를 활용하여 enum을 확장하면 직관적이며 타입 안정성을 유지할 수 있지만, 모든 enum 상수가 동일한 동작을 가져야 하는경우에는 적합하지 않겠죠
예를들면, 제곱, 제곱근등의 연산이 추가될경우 기본 사칙연산에는 사용되지 않을 수 있으니까요

전략패턴을 사용하면 사칙연산이랑 공학용 계산기 전략을 분리하여 각각 다른 전략을 사용할 수 있지만,
서로 독립적인 클래스로 구현되어야 하므로 개발난이도와 코드 복잡성이 증가할 수 있다는 단점이 있다고 생각합니다.

@WooSungHwan
Copy link

영수님이 잘 답변해주셔서 제가 더 첨언할게 없네요;;;

  1. 일단 현재 진행했던 과제가 과할때까지 무언가를 추상화하고 역할을 부여하고 확장을 생각하고 구조를 개선하고 등등의 작업이 진행되어야 하니까요. 배움에 끝이 없드시 만족할때까지 생각해보시면 좋지 않을까 싶어요. 처음부터 완벽한 설계는 없고 역설적이게도 완벽한 설계는 없는법이라고 생각해요. 100이라는 목표를 향해 달려가는 99.999... 만이 있을 뿐이지 고민은 또다른 고민을 낳는 법이니까 스스로 만족하는 선이 저와 어떤 차이가 있었는지를 체감해보는 정도로 느껴보시면 좋을 것 같습니다.

  2. 충분합니다 ㅎㅎ 그 이상의 테스트코드는 아마 어려울거고 지금처럼 콘솔기반에서 input이 오는 경우보다는 api endpoint 단에서 판단했을 때 더 명확한 테스트 범위가 그려질거에요. 충분히 연습했다고 생각하시고 다음 과제에서의 상황에서 지금의 상황을 고려하여 작성해보시면 다른 케이스들을 발견해보실 수 있을 것 같습니다 ㅎㅎ

  3. 재윤님은 enum을 통해서 사칙연산의 연산기능을 수행하도록 구현하였잖아요? (물론 CaculationResult 통해서긴 하지만) 그 한계를 느껴보셨을거에요. 그 상황에 스스로가 대응하지 못했다면 구조를 개선을 해야합니다. 더 다양한 Operator를 담아야 할텐데... 뭐 제가 물론 공학용 계산기를 써본적은 없지만.. 기호가 좀더 많이 나오고.. (그럼 operator라는 추상적인 개념이 되버렸네요?) enum안에서도 카테고리가 분리될텐데 거기서 대응하기에 어떤게 좋을것이냐!
    일단 제가 답변듣기 원했던 방향은 컴포지션을 통해 사칙연산 계산을 하는 부분을 특정 기능으로 쓰는 공학용 계산기가 있고 사칙연산 계산을 공학용 계산기가 사용하는 식으로 생각했었어요. 기존의 사칙연산을 하는 logic을 행위를 하는 behavior로 추상화하고 해당 행위를 활용하는 logic 혹은 behavior(공학전용이겠죠?)를 설계하기를 바랬습니다.ㅎㅎ
    말씀하신 전략패턴은 그부분에 가까운것 같습니다. 하지만 그 안에 컴포지션을 통해 공학계산전략이 단순 공학계산만을 담당하느냐 사칙연산 전략을 의존하여 사칙연산과 공학전용 operation을 함께 제공하느냐에 달렸을 것 같습니다.

@Shin-Jae-Yoon
Copy link
Member Author

흑구님 영수님 안녕하세요. pre 기간 동안 너무 감사했습니다.
1차 피드백 이전 코드는 정말 불만족스러웠고 어떻게 짤 수 있을까
굉장히 많은 고민을 했는데, 1차 피드백과 중간 중간에 알려주신 힌트들을 통해
SOLID가 무엇인지, 특히 추상화란 무엇인지, 객체지향적 설계에 관한 고민을
처음으로 해본 것 같습니다.

강의에서 나온 코드를 따라치는 것이 아닌 직접적으로 짜본 경험은 이번이 처음인데
좋은 멘토님들 덕에 바른 길로 나아가는 것 같아서 좋았습니다. 테스트 코드에 관해서도
감을 아예 잡지 못했는데 이번을 경험으로 꼼꼼한 테스트가 어떤 것인지를 알게 되었습니다.

또, 한번씩 던져주시는 질문에 대한 고민을 해볼 때 지식들이 통합되는 과정이 즐거웠습니다.
잘 모르던 enum을 경험했고, 거기서 나아가 인터페이스를 활용하는 방법과 전략패턴에 대해서도,
흑구님이 말씀해주신 상속 말고도 컴포지션이 있는 것도 고민해보게 되었습니다.

앞으로도 logic -> behavior -> data와 추상화는 절대로 안까먹을 것 같습니다.
꼼꼼하고 친절한 리뷰 감사합니다 :)

@Shin-Jae-Yoon Shin-Jae-Yoon merged commit 58a3626 into prgrms-be-devcourse:Shin-Jae-Yoon Jun 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants