[우테코 6기 프리코스] 2주차 피드백 정리

별의개발자커비·2023년 11월 2일
1

우테코 도전기

목록 보기
29/37

개요

2주차 미션이 끝난 후 유독 빠뜨린 점이 많이 보였던 이번주였다. 이 덜렁이..🥲
그래 할 수 있는 실수를 지금 미리 다 하고 있는 거지💪 앞으로 안하면 된다!
그렇게 어떤 아쉬운 점과 피드백이 있었고 어떻게 반영했는지 정리해보려고 한다!


📬 공통 피드백

띵동. 2주차 행운의 메일이 도착했습니다!

1. README.md를 상세히 작성한다

해당 프로젝트가 어떠한 프로젝트이며, 어떤 기능을 담고 있는지 기술하기 위해서 마크다운 문법을 검색해서 학습해 보고 적용해 본다.__피드백 문서 중

2. 기능 목록을 재검토한다

너무 세세한 부분까지 정리하기보다 구현해야 할 기능 목록을 정리하는 데 집중한다.
클래스 이름, 함수(메서드) 시그니처와 반환값은 언제든지 변경될 수 있기 때문이다.__피드백 문서 중

3. ✅ 기능 목록을 업데이트한다

4. ✅ 값을 하드 코딩하지 않는다

  • 2주차에 다 상수화를 한다고 했는데 리뷰를 받으며 0 이런 숫자를 빠뜨린 걸 발견했다.
    다음주에 상수화 점검을 체크리스트에 넣고 점검하기!

5. ✅ 구현 순서도 코딩 컨벤션이다

class A {
    상수(static final) 또는 클래스 변수

    인스턴스 변수

    생성자

    메서드
}

6. 변수 이름에 자료형은 사용하지 않는다

앗 2주차의 내 프로젝트에서 본 것 같은데... 여기 이렇게 썼었네...ㅎㅎ

왜 안되는 걸까? 그리고 그렇다면 어떻게 대체할 수 있을까? 고민 필요!🚨

고민결과

PlayerMoveGroup, PlayerMoveCollection 등을 쓸 수 있겠다!

7. ✅ 한 함수가 한 가지 기능만 담당하게 한다

8. ✅ 함수가 한 가지 기능을 하는지 확인하는 기준을 세운다

9. 테스트를 작성하는 이유에 대해 본인의 경험을 토대로 정리해본다

단지 기능을 점검하기 위한 목적으로 테스트를 작성하는 것은 아니다. 테스트를 작성하는 과정을 통해서 나의 코드에 대해 빠르게 피드백을 받을 수 있을 뿐만 아니라 학습 도구(학습테스트를 통해 JUnit 학습하기.pdf)로도 활용할 수 있다. 이런 경험을 통해 테스트에 대해 어떤 유용함을 느꼈는지 알아본다.__피드백 문서 중

나의 경우 이번주차에서는 철저히 TDD 기반으로 시도해보기로 한 경우이기에,
테스트를 작성하는 이유는 필요한 기능이 있을 때 테스트 코드를 우선 작성한 후 그것이 통과하는 프로덕션 코드를 만드는 순서이기 때문이다.

10. 처음부터 큰 단위의 테스트를 만들지 않는다

테스트의 중요한 목적 중 하나는 내가 작성하는 코드에 대해 빠르게 피드백을 받는 것이다. 시작부터 큰 단위의 테스트를 만들게 된다면 작성한 코드에 대한 피드백을 받기까지 많은 시간이 걸린다. 그래서 문제를 작게 나누고, 그 중 핵심 기능에 가까운 부분부터 작게 테스트를 만들어 나간다.__피드백 문서 중

이 부분은 나름 지키고 있다고 생각한다! 이번주에도 놓지 않고 가야겠다!


📌 기초적인 실수 하지 말기

  • 상수화 빠뜨린 것 없는지 점검
  • of, from 네이밍, 중복 점검
  • 패키지 분류 잘 되어있는지 점검

# 📌 패키지 구조 기준 정하기 저번주 패키지 분류를 제대로 하지 않고 제출한 실수를 바탕삼아 상수는 종류별로 어떤 건 도메인에 어떤 건 유틸에 넣을지 정리해보았다!
  • controller
  • domain
    • constant: RaceConstant, RandomConstant, DistanceConstant, PlayerNameConstant
  • dto
  • util :
    • validator : ValidatorFactory, Validator(I), CarValidator, PlayerNameValidator
    • convertor: Converter
    • RegexPattern, Seperator
  • view: InputView, outputView

📌 CLI로 커밋해보기, add도 수동으로 해보기

그동안은 인텔리제이의 add는 자동 add를 썼고, commit은 git 메뉴탭을 이용해서 즉, GUI 방식으로 했었다.
그러다보니 한 commit에 포함되지 않아야하는 여러 변경사항을 한꺼번에 commit하는 일도 많이 있었다.
이렇게 실수를 줄이고 더 작은 단위의 add, commit을 하려면 CLI 방식으로 해볼 필요가 있다고 느꼈다! 다음주는 저 git 메뉴탭은 없는 기능이다.. 생각하고 CLI 방식으로 해보자!

  • git status
    : 변경된 파일 확인
  • git add -A(또는 add .)
    : 변경된 전체 파일을 한번에 반영
  • git commit -m "메시지"
    : 작업한 내용을 메시지에 기록
  • git add -p
    : 부분별 add, 더 작은 단위 hunk는 's

📌 디미터의 법칙을 지켜주는 comparable!

이 코드는 디미터의 법칙을 위반한 코드지만

이 코드는 위반하지 않은 코드다. 왜 아래는 위반하지 않은 것일까?

아하! 이런 방식이었구나!


이렇게 comparable를 상속받으면 getter 없이도 비교하기가 쉬워진다!

이번 경우는, Integer::compareTo로 충분히 리팩토링 가능!

일급컬렉션을 돌아야한다면 comparable 상속받아 구현하면 됨!


📌 커밋 메시지 단위, 형식 정하기

피드백을 받지는 않았지만 개인적으로 1주차에 비해 개선이 없던 것 같아 상당히 아쉬웠던 부분이었다. 두 번 실수는 없다! 이 기회에 제대로 정리해서 다음주에는 꼭 적용해보려고 한다!

정리해본 포스팅 : 링크텍스트


📌 반복 테스트는 repeatedTest!

오호 이런 어노테이션도 있구나!


📌 DTO에 record 클래스!

레코드란? 불변의 데이터를 쉽게 생성할 수 있는 새로운 유형의 클래스!

'불변의 데이터'가 딱 필요한 DTO에 적용하면 되겠다!

단, 리스트를 getter로 반환할 때 unmodifiableList 적용이 자동으로 되지는 않기때문에 overide 해줘야 할 것 같다!

원래 이랬던 dto가 이렇게 간단해졌다!


📌 테스트에서 예외던지는 메소드의 분리

이렇게 예외를 던지는 메소드를 분리하는 방법을 발견해서 나도 적용해보았다.
다만 걸리는 점은,
1. assertJ를 최대한 활용하고 싶은데 Throwable 타입은 assertThrow로 구현이 되어서 그럴 수 없다는 점
2. 여러 메소드의 exception이 아니라 한 메소드씩 exception 메소드가 만들어진다.

앞으로 적용해볼지는 고민을 해봐야겠다.


📌 테스트 코드가 길더라도 가독성, given이 명시적이면 괜찮다!

다른 분들의 코드를 보는데 테스트 코드가 꽤나 길었지만 생각보다 가독성이 괜찮았다!
given - when - then의 형식에 의해 잘 갖춰져있다면 괜찮겠다는 생각이 들었다!


📌 테스트를 위한 상수 모음 객체

테스트를 위해 자주 쓰일 것 같은 상수부터 메소드, 함수형 인터페이스의 람다식까지 중복되는 부분을 상수, 메소드로 분리해준 것을 발견했다! 유레카!

나도 적용해보았다!


📌 assert.extracting()!

assertThat과 함께 쓸 수 있는 extracting이라는 메소드를 발견했다.
다음에 유용하게 써봐야겠다!


📌 trim으로 입력값 공백 제거 처리!

Console.readLine().trim()
이렇게 입력된 값에 대해 공백을 제거하여 받는 코드도 간간히 있었다.

그런데 여기에는 고민이 필요할 것 같다.
왜냐하면 공백을 포함해서 받는 것을 예외 상황으로 볼 수도 있기에 일단 이런 방법이 있다는 것만 기록해두려고 한다!


📌 리스트를 받아 객체 생성을 컨트롤러에서 했어?

  1. 컨트롤러의 중개 역할만
  2. 의존성이 생기지는 않는지 측면에서 생각해보자

1번. (내 방식) 컨트롤러에서 dto를 받아 거기어 get으로 풀어서 Player를 생성하고 List<Player> 로 담아서 Players 객체로 생성하게 보낸다.

  • 단점
    컨트롤러의 역할이 많아보인다. -> Player를 생성하는 역할을 하게 됨

2번. dto를 get으로 풀어서 Players 객체로 보낸다. Players 객체의 부생성자 부분에서 Player를 생성하고 List<Player> 로 담아서 주생성자로 보낸다.

적어놓고 보니 2번 방법이 의존성 문제도 더 추가되지 않으면서 컨트롤러의 중개역할에 충실할 수 있기에 적용해보아야겠다는 생각을 했다! 리팩토링 가자!

리팩토링 과정에서의 고민


위의 방식처럼 컨트롤러의 의존성을 줄여보려했으나 역시 문제가 있었다.
그래서 3가지 방식으로 해보고 장단점을 비교해보고 결론을 내려봤다.

1번방법

public class GameController {
    private final MoveDecider moveDecider;

    public GameController(final MoveDecider moveDecider) {
        this.moveDecider = moveDecider;
    }

    public void start() {
        Players players = createPlayers(); 
    }

    private Players createPlayers() {
        PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
        List<Player> players = playerNamesDto.playerNames().stream()
                .map(Player::from)
                .toList();
        return Players.from(players);
    }
}
----
public record PlayerNamesDto(List<String> playerNames) {
    public static PlayerNamesDto from(final List<String> playerNames) {
        return new PlayerNamesDto(playerNames);
    }

    @Override
    public List<String> playerNames() {
        return Collections.unmodifiableList(playerNames);
    } 

2번 방법

public class GameController {
    private final MoveDecider moveDecider;

    public GameController(final MoveDecider moveDecider) {
        this.moveDecider = moveDecider;
    }

    public void start() {
        Players players = createPlayers(); 
    }

    private Players createPlayers() {
        PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
        return Players.from(playerNamesDto);
    }
}
----
public class Players {
    private final List<Player> players;

    private Players(final List<Player> players) {
        this.players = players;
    }
 
    public static Players fromPlayerNames(PlayerNamesDto playerNamesDto) {
        List<Player> players = playerNamesDto.getPlayers();
        return new Players(players);
    }
}
----
public record PlayerNamesDto(List<String> playerNames) {
    public static PlayerNamesDto from(final List<String> playerNames) {
        return new PlayerNamesDto(playerNames);
    }

    @Override
    public List<String> playerNames() {
        return Collections.unmodifiableList(playerNames);
    }

    public List<Player> getPlayers() {
        return playerNames.stream()
                .map(Player::from)
                .toList();
    }
}

3번방법

public class GameController {
    private final MoveDecider moveDecider;

    public GameController(final MoveDecider moveDecider) {
        this.moveDecider = moveDecider;
    }

    public void start() {
        Players players = createPlayers(); 
    }
 
    private Players createPlayers() {
        PlayerNamesDto playerNamesDto = InputView.InputPlayerNames();
        List<Player> players = DtoConverter.getPlayers(playerNamesDto);
        return Players.from(players);
    }
----
public class DtoConverter {
    public static List<Player> getPlayers(PlayerNamesDto playerNamesDto) {
        return playerNamesDto.playerNames().stream()
                .map(Player::from)
                .toList();
    }
}

방법 별 장,단점 비교

1번 방법

  • 컨트롤러가 변환 역할까지 한다. 단일 책임 원칙에 벗어남.
  • 간단한 상황에서는 코드의 간결성을 유지할 수 있다.

2번 방법

  • Player가 PlayerNamesDto에 의존성이 생긴다.
  • 변환 코드가 분리되고 재사용성이 높아질 수 있다.

3번 방법

  • 별도의 변환 클래스 DtoConverter를 만들어 써야한다.
  • 각 클래스가 자신의 주요 역할에 집중할 수 있게 되며, 변환 로직의 변경이 필요한 경우 해당 클래스만 수정하면 되므로 유지보수가 쉽다.

결론

이 경우에는 외부 변환 클래스를 쓰는 것이 최선이다!


📌 상수 모음 : static 모음 클래스 vs enum 방식?

상수, 구분자, 예외메시지 등을 객체화해서 모아 놓았었는데 (물론, 이 방식의 개선이 필요하다)
이렇게 상수 모음을 객체에 모아넣을 때 2가지 방식을 쓰곤했다.

1번은 static 변수로 모아놓고 바로 불러쓰는 방식

2번은 아래처럼 enum으로 만들어서 getMessage해서 쓰는 방식

두 방식은 어떤 장단점이 있고, 각각 언제 써야할까?

1. enum 방식

  1. 성능
  • 각 열거형 값이 필드의 값들을 갖고 있기 때문에 약간의 메모리 오버헤드가 발생할 수 있다.
  • 두 가지 접근 방식 간의 성능 차이는 무시할정도로 미미하다.
  1. 장점
  • 여러 속성 관리 가능 : NAME_SEPARATPR.getName 또는 getSeperator 등 여러 속성을 관리하거나
  • 관련 메소드 생성 가능 : NAME_SEPARATPR.split() 이런식으로 관련된 메소드를 생성해 쓸 수 있다.
  • 유지보수: 열거형 내에 캡슐화하는 방식으로 상수화했기때문에 더 읽기 쉽고 유지보수하기 쉽다.
  1. 단점: 호출 방식의 길이가 긺
  • LOTTO_PRICE.getMessage() 이런식으로 getter가 매번 붙여야한다.

2. static 변수 모음 클래스

  1. 성능
  • 약간 더 메모리 효율적일 수 있다.
  • 단, 두 가지 접근 방식 간의 성능 차이는 무시할정도로 미미하다.
  1. 장점: 호출 방식이 단순함.
  • LOTTO_PRICE 이런식으로 바로 사용할 수 있다.
  1. 단점: 여러 속성이나 메소드를 만들어 줄 수는 없음

결론

어차피 둘 다 프로그램이 실행될 때 생성되고 그 후로는 재생성되지 않고 항상 동일한 인스턴스를 참조한다. 즉, 성능과 메모리 측면에서 둘은 유사한 동작을 하며 클래스 레벨에서 공유됨.

그렇다면 용도에 맞게 쓰면 되겠다!

  • 단순히 숫자 리터럴은 여러 속성을 가질 필요가 없으니 호출 방식이 단순한 static 변수 모음 클래스로 사용하면 되겠고,
  • 구분자 같은 경우는 이름, 구분자, split 메소드 등의 여러 속성과 메소드를 가져야하기에 enum 방식을 쓰면 되겠다!

📌 InputView에서 왜 원시값이 아니라 dto로 반환해?

이러한 코멘트를 다른 분의 리뷰에서 발견하고 나도 고민이 들었다.
그러게 왜 나는 원시 타입이 아닌 DTO로 보내는 거지?

그리하여 이번 기회에 정리해보았다!

내가 그냥 list나 원시값으로 view에서 컨트롤러로 넘겨줄 수 있지만 dto로 넘겨주는 이유

  1. 넘겨주는 값들의 무결성을 보장하기 위해서
  • dto의 유효성 검사를 통과한 데이터이기에 외부에서는 믿고 쓸 수 있음.
  • 레코드 방식이기때문에 데이터가 왔다갔다 하는 과정에서 불변성이 보장됨
  1. 여러 개의 데이터를 넘겨줄 수 있음.
  • 만약 입력받는 것이 이름, 색깔이라면 하나의 리턴값으로 리턴할 수 없었을텐데, dto를 사용하면 dto로 포장해서 한번에 보낼 수 있음!

📌 줄바꿈 자바 기본 메소드!

이 메소드는 printf와 같이 줄바꿈이 안되었을 때 사용 가능하다!(빈 라인을 삽입하는 것과는 다름)
따라서, 기존에 빈 라인을 삽입하는 메소드를 만들어 쓰는 것과 혼용하여 적절하게 쓰면 될 것 같다!


📌 스트림의 skip 메소드?

skip이라는 메소드는 또 처음 보는데 한 번 알아봐야겠다!


📌 OutputView에 대해 nsTest 없이 출력 검증하는 법

생각해보니 그동안 outputView에 대한 검증을 안했다. 출력 검증하는 좋은 방법을 리뷰다니다 발견하여 적용해보았다!

작동 방식은 아래와 같다!


📌 정규식 사용 vs 세세한 예외 처리

보다보니 정규식을 사용한 경우와, 간간히 세세한 예외처리로 대체한 경우 이렇게 나뉘었다.
그러다보니 생각이 들었다. 각각의 장단점이 뭐고, 난 왜 정규식 사용으로 통일했을까?

  1. 정규식 사용
  • 장점
    • 주어진 입력이 한글, 영어, 숫자, 공백은 허용하고 특수문자만 허용하지 않는다면? 이런 경우에는 세세한 예외로 처리하기 어렵다. 이럴 때, 위의 사항을 한번에 확인할 수 있다는 장점이 있다!
  • 단점
    • 만약 구분자가 ,가 아니라 다른 것으로 변경된다면? 마지막에 ,를 허용한다면?
      그렇다면 정규식을 다시 수정해야하는 일이 발생하는데 정규식 사용이 미숙한 나로서는 유지보수가 직관적이지 않다
  1. 세세한 예외 처리
  • 장점
    • 변경사항이 생겨도 해당 예외 메소드만 변경 또는 제거하면 된다.
  • 단점
    • 생길 수 있는 형식의 예외 상황들에 대한 검증 메소드를 모두 만들어야한다.

결론

2주차의 검증 방식에서는 정규식 하나로 형식에 대한 검증을 모두 마치게 구현했는데 다시 돌아보니,
정규식 하나가 너무 많은 일을 하게 했던 것 같다는 생각이 든다.
만약 한글, 영어, 숫자, 공백은 허용하고 특수문자만 허용하는 등의 세세한 예외를 처리하기 어려운 경우, 큼직큼직한 형식 검증은 검증 메소드를 생성하고, 이러한 작은 단위의 형식 검증(문자 타입 등)은 정규식을 활용하는 등의 적절한 혼용이 필요하겠다!


📌 메소드 순서 정리하기!

아래의 순서를 프리코스 커뮤니티에서 찾게되었고 거기에 내가 조금 더해서 정리해보려고 한다!

아래의 형식이 기본이나, 서로 호출하는 메소드는 가까이에 둔다!

  • static 변수
  • 멤버변수
  • 주생성자
  • 부생성자
  • static 메소드
  • public 메소드
  • private 메소드
  • overide hashcode, equals
  • getter를 가장 아래

📌 함수형 인터페이스에는 어노테이션!

함수형 인터페이스에는 어노테이션 @FunctionalInterface을 붙여서 해당 타입에 어긋난 부분 확인 등을 컴파일러의 도움을 받을 수 있다고 한다!


📌 정말 정규식 모음 객체가 필요할까?


마지막에 남은 정규식은 numeric 확인 정도였는데 아니나 다를까 해당 부분의 리뷰를 통해 질문을 던지게 되었다. 이게 객체화해서 필요한 정도인가?

다시 돌아본 내 답은, 이번 프로그램 사이즈 정도에서는 그렇지 않다. 였다.
더 많은 입력이 있고, 중복된다면 객체화가 맞겠지만
(예: 숫자가 여러번 입력되는데 다 다른 Validator 에서 검증이 일어나 숫자 확인 정규식이 여러번 필요할 때)

결론: 호출 위치와 횟수를 생각해서 객체화하자, 무작정 하지 말자.


📌 정말 에러메시지의 상수화, 객체화가 필요할까?

모든 문자열을 포장해야한다는 생각이 있었고, 에러 메시지는 당연히 포장해야한다고 생각했다. 하지만 이 피드백을 보고 머리를 댕- 하고 맞은 것 같았다.

스스로 생각해보았다.

  1. 객체화가 가독성에 도움이 되는가? NO

    • 심지어 해당 검증 클래스 안에 있지 않기 때문에 에러메시지 객체로 가서 확인해야한다.
  2. 객체화가 유지보수에 도움이 되는가? NO

    • 만약 playerName에 대한 에러메시지를 수정하고 싶다면, errorMessage 클래스에 들어가서 또 playerName의 해당하는 에러 메시지를 찾아야햔다.
    • 차라리 에러메시지를 객체별로 만들어 관리한다면 모르겠지만!
  3. 객체화를 안하고, 그렇다면 에러메시지의 상수화 자체는 가독성에 도움이 되는가? NO

    • 파악하기 쉽게 네이밍을 했으나, 직접 문구가 표출되는 것보다 이해가 빠를 수는 없다.
  4. 상수화가 유지보수와 관리에 도움이 되는가? 재사용성이 높은가? YES or NO

    • 하나의 예외 메시지가 여러 검증 메소드에서 중복으로 쓰인다면 그렇다고 할 수 있다.
    • 하지만 에러메시지를 예외 상황에 맞게 상세하게 나눈다면 한 에러메시지는 해당 검증 메소드에서만 쓰인다고 할 수 있다.
    • 이런 경우에는 상수화를 했지만 한번밖에 쓰이지 않는, 상수화가 필요하지 않다고 할 수 있다.

결론

  1. 객체화의 경우,
    에러메시지를 하나의 객체에 넣는 객체화라면 가독성과 유지보수 모두에 도움이 되지 않는다.
  2. 나아가 상수화 역시,
    예외 상황별로 다른 에러 메시지를 지향한다면 상수화가 필요없고, 스트링 그대로 메소드에 넣는 것이 효율적일 수 있겠다.

다시 요렇게 객체화, 상수화를 풀어서 넣어주니 보기 편하다! 클래스 구조도 복잡하지 않다!


📌 컨트롤러에서 판단하지 않는지 더 확인!

그래서 이렇게 바꿨다! 생각도 못한 부분!


📌 Factory 라는 클래스명 막쓰지 말기

이번 미션 중 랜덤 번호를 받아 이동여부를 반환하는 클래스명을 MoveFactory라고 지었는데
클래스명의 suffix로 Factory를 사용하는 케이스에 대해 찾아보면 좋을 것 같다는 피드백을 받고 찾아보았다.

그랬군... 객체 생성 로직이 캡슐화하는 역할이 있는 경우를 지칭하는데 난 막 썼군..!
변경 완료!


📌 컨텍스트의 중복 제공 확인!

이러한 변수명과 클래스명의 중복이 간간히 있었던 것 같다.
중복 확인하기!


📌 부생성자 네이밍에서 속성의 의미를 더하기

사실 완전히 이해가 가지는 않아서 추천해주신 강의를 추후에 들어보면서 이해해야할 것 같다!


📌 의인화로 책임에 대해 고민해보자!

책임을 나눌 때 의존관계를 살펴보는 것과 함께 이렇게 리뷰어분이 이야기하신 것 처럼 의인화해서 얘기해보면 부자연스러운 관리인지를 확인할 수 있을 것 같다!


📌 유틸 클래스의 private 생성자 추가!

InputView 나 유틸 클래스 등 객체를 생성하지 않고 사용하는 클래스들은,
생성자를 추가하지 않으면 되는 걸까? NO!
그러면 암시적 생성자가 어차피 존재하여 객체를 생성할 수 있게된다.

즉, 위와 같은 클래스들에는 private 생성자 추가하는 것을 잊지 말기!


📌 jupiter보다 assertJ 활용하기!

🎯 프로그래밍 요구 사항에 보면
JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
라고 적혀있다.

사실 이에 대한 리뷰를 받기 전까지는 jupiter와 AssertJ를 크게 구분하지 않고 사용하고 있었는데 이에 대한 리뷰를 받으면서 명확히 구분되어 인식하게 되었다.

나는 좀 더 짧으니까! 더 가독성이 좋겠지! 하는 생각으로 jupiter를 많이 사용하고 있었는데

리뷰어님이 언급한 이유
1. AssertJ 가 jupiter 구현 보다 가독성이 높고, 2. 더 보편적인 방식임
를 생각해보니 요구 사항에서 괜히 권장한 게 아니었겠구나 라는 생각을 하게되었다.

assertThat()을 보다 사용하고 assertEqauls 등을 불필요하게 사용하는 것 줄여보자!


📌 stream.iterate에 대한 이해

stream.iterate라는 걸 쓴 코드를 봤는데 흥미로워서 일단 검색 결과를 첨부해놓는다.
다음에 더 찾아보고 써봐야지!

📌 테스트에 faker를 만들 수 있겠구나! 랜덤, 입력 등에!

선호님

https://jojoldu.tistory.com/676

0개의 댓글