🏴 우테코 프리코스 과제 2 - 자동차 경주
이번 일주일에만 과제 8개에 자격증시험, 중간고사까지...이번 학기가 특히 팀플도 많고, 버겁고 힘들다 OTL . 개인적인 공부를 많이 하고 싶은데 정말 바빠서 공부를 소홀히 했다. 이런 상태로 우테코를 하는게 맞나 많은 생각이 들었지만, 그래도 마음을 다잡고 계속 달려보자. 포기는 배추 셀 때나. 그래도 이제, 길었던 3주의 중간고사 기간은 지났으니, 코딩 공부를 더 열심히 해야겠다. 아자아자! 😊
🪜 지난 과제 피드백 & 회고
지난 과제에서는 일단 코드를 대충 완성한 후에 클래스 분리에 대하여 생각했다. 또한 기능 요구를 상세히 읽지 않아 테스트에서 에러가 발생했다. 따라서 이번에는 먼저 요구사항을 상세히 분석하고, 기능을 꼼꼼히 분석 후, 클래스 구조를 먼저 생각할 것이다.
또한 공통 피드백에서 특히 간과했던 점 & 보완해야 할 점들은 아래와 같다.
* git을 통해 관리할 자원에 대해서도 고려한다
.class 파일은 java 코드가 있으면 생성할 수 있다. 따라서 .class 파일은 굳이 git을 통해 관리하지 않아도 된다.
IntelliJ IDEA의 .idea 폴더, Eclipse의 .metadata 폴더 또한 개발 도구가 자동으로 생성하는 폴더이기 때문에 굳이 git으로 관리하지 않아도 된다.
앞으로 git에 코드를 추가할 때는 git을 통해 관리할 필요가 있는지를 고려해볼 것을 추천한다.
* PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
PR을 이미 한 번 보냈다면, 새로운 PR을 생성할 필요가 없다. 수정이 필요하다면 추가 커밋을 하면 자동으로 반영된다. 단, 미션 제출 기간 이후에는 추가 커밋을 하지 않는다.
* 배열 대신 Java Collection을 사용한다
Java Collection 자료구조(List, Set, Map 등)를 사용하면 데이터를 조작할 때 다양한 API를 사용할 수 있다.
예를 들어 List<String>에 "pobi"라는 값이 포함되어 있는지는 다음과 같이 확인할 수 있다.
그리고 미션 완료 후 다른이들의 코드를 보니 mvc패턴을 적용한 경우가 많았는데 유지 보수 측면에서는 좋겠지만 이렇게 간단한 프로젝트의 경우 클래스가 많아지고 복잡성이 증가해 굳이 적용할만한가 란 의문이 들었다🧐.
더불어, 디스코드 함께나누기 카테고리에서 미션 관련 여러 유용한 정보를 보며 어떻게 하면 좋은 코드를 짤까 고민했지만, 이를 종합적으로 고려하자니 머리가 터질 것 같았다. 그래서 그냥 매주 나만의 미션 목표를 정하고, 이를 위해 달려보기로 했다. 이번 주차의 목표는 최소한의 기능을 위한 함수 클래스 분리와 TDD개발이 목표이다!
사실 이제까지는 테스트 코드를 작성하는 과정은 굳이 왜 필요하나 싶었다. 일을 할때도 개발한 기능에 맞게 테스트를 구현하고 실행해보는 , 귀찮은 과정으로 인식했다. 그러나 TDD 방법론을 보고 이를 꼭 적용시켜 장점을 몸소 실감해보고 싶었다.
🚀 기능 요구 사항 분석
기능을 구현하기 전, 기능 목록을 만들어야한다. 일단 기능 요구 사항을 세갈래로 나누어 분석해보았다.
◼️게임 시작 시 사용자 입출력
게임 시작 시 "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)" 를 출력
사용자의 자동차 이름 입력 - 제한 : ,구분 / 이름은 5글자 이하
사용자로부터 이동을 시도할 횟수를 입력받기
잘못된 사용자 값 입력시 IllegalArgumentException
◼️자동차 이동
0~9 사이 무작위 값 생성
생성 값 4이상이면 이동 (-)
실행결과(이동 현황) 출력
◼️ 우승자 출력
누가 우승했는지 출력 - 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
그리고 TDD 방식을 위해, 먼저 위의 기능별로 클래스,클래스의 기능들을 분리해 정의해 보겠다. 여기서 각 클래스(함수)는 최소의 기능을 갖도록 유의하여 작성했다.
🍒 사전 준비
먼저 지난주 코드 리뷰에 따라 불필요한 부분을 빼기 위해 .gitignore를 수정했다.여기를 참고해 수정했다.
🍒 클래스 기능 명세
처음 적은 클래스 & 기능 명세 | 명세 수정 후 |
📝 Car.class - name: 자동차 이름 - position: 자동차 현재 위치 - carMove(): 랜덤 생성 값에 따라, 4 이상이면 position 이동 📝 Referee.class - raceStart(): 라운드 수만큼 게임 진행 - round(): 한 판 진행 - roundResults(): 회차별 결과 출력 - determineWinner(): 최종 우승자 결정 로직 - racingWinner(): 최종 우승자 반환 및 출력 📝 RacingGame.class - raceSetting(): 레이싱 게임 Car, Referee 객체 셋팅 - raceStart(): 레이싱 게임 진행 & 결과 반환 📝 InputValidator.class - validateInput(): 사용자 입력 값이 유효한지 검사 - validateCarNumber() : 두 개 이상의 차 입력 검사 - validateCarName(): 자동차 이름이 유효한지 검사 - validateRounds(): 시도 횟수 입력이 유효한지 검사 |
📝 Car.class - name : 자동차 이름 - position: 자동차 현재 위치 - carMove() : 랜덤 생성 값에 따라, 4 이상이면 position 이동 📝 Refree.class - playRound() : 한 판 진행 - determineWineer() : 최종 우승자 결정 로직 📝 RacingGame.class - prepareGame() : 레이싱 게임 설정, Car 및 Referee 객체 초기화 - startGame() : 레이싱 게임 진행 - printRoundResults() : 회차별 결과 출력 - printGameWinner() : 최종 우승자 반환 및 출력 📝 InputValidator.class - validateInput() : 사용자 입력 값이 유효한지 검사 - validateCarName() : 자동차 이름이 유효한지 검사 - validateRounds() : 시도 횟수 입력이 유효한지 검사 |
왼쪽은 처음 명세를 작성한 것이다. 작성하고보니, Refree에 복잡하고 많은 책임이 부여되고, raceStart()가 두번 등장하여 역할이 중복된 문제점이 보였다. 따라서 단일 책임 원칙에 따라, Referee 클래스는 게임의 규칙을 관리하고 RacingGame 클래스는 게임 흐름을 관리하도록 정확히 역할을 분리하였다.
📜 실제 구현기
🔘 기초 작업
1. 코드 컨벤션 & .gitignore 등록
저번 주차 피드백에 따라, 더욱 효과적인 깃 저장소 관리를 위해 여기를 참고해 .gitignore 를 수정하고 커밋했다.
🔘 코드 구현
(🔑 클래스 기능 테스트 작성 -> 해당 기능 코드 구현 TDD 방식으로 진행)
📑 InputValidatorTest.java
package racingcar;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class InputValidatorTest {
@Test
void 문자열_형식오류_예외발생_테스트() {
String invalidInput = "@@$";
assertThatThrownBy(() -> {
InputValidator.validateString(invalidInput);
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
void 차이름_5자이하_테스트() {
String validInput = "pbi";
boolean isValid = InputValidator.validateCarName(validInput);
assertTrue(isValid);
}
@Test
void 차이름_오류_예외발생_테스트() {
String invalidInput = "pobisportscar";
assertThatThrownBy(() -> {
InputValidator.validateCarName(invalidInput);
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void 시도입력_유효_테스트() {
String validInput = "5";
InputValidator validator = new InputValidator();
boolean isValid = validator. validateRounds(validInput);
assertTrue(isValid);
}
@Test
public void 시도입력_형식오류_예외발생_테스트() {
String invalidInput = "ㅇ";
InputValidator validator = new InputValidator();
assertThatThrownBy(() -> {
InputValidator.validateString(invalidInput);
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void 시도입력_음수_예외발생_테스트() {
String invalidInput = "ㅇ";
assertThatThrownBy(() -> {
InputValidator.validateString(invalidInput);
}).isInstanceOf(IllegalArgumentException.class);
}
}
📑 InputValidator.java
public class InputValidator {
public static boolean validateString(String input) {
if (!input.matches("[a-zA-Z0-9,가-힣]+")) { // "[a-zA-Z0-9]+"
throw new IllegalArgumentException("Invalid input format");
}
return true;
}
public static boolean validateCarName(String carName) {
if (carName.length() <= 5) {
return true;
} else {
throw new IllegalArgumentException("Car name should be 5 characters or less");
}
}
public static boolean validateRounds(String rounds) {
try {
int roundsInt = Integer.parseInt(rounds);
if (roundsInt < 0) {
throw new IllegalArgumentException("Rounds should be a positive number");
}
return true;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid input format for rounds");
}
}
}
먼저 가볍게 TDD의 시작을 Validator Test코드로 시작하여 감을 잡았다. 테스트 코드를 먼저 작성한다는게 생소해서 처음엔 당황했지만, 막상 테스트 코드 작성 후 코드를 작성하니 어떤함수를 짜야할지 명확히 알 수 있어 함수를 짜는 시간이 단축되었다. 또한 테스트를 먼저 작성함으로써, 빼먹을 수 있는 오류 처리를 미연에 방지할 수 있음을 느낄 수 있었다. 😃
InputValidator의 모든 메서드는 특정 인스턴스의 상태나 데이터에 의존하지 않기 때문에, static 메서드로 작성하였다. 이렇게하면 객체 인스턴스화 없이 사용할 수 있으며, 메서드를 호출할 때 클래스 이름으로 직접 접근할 수 있다!
더불어 InputValidatorTest 에서는 별도의 에러생성 메서드를 생성하여 중복되는 코드를 줄이고 간결히 하였다.
📑 InputValidatorTest.java (최종)
public class InputValidatorTest {
@Test
void 문자열_형식오류_예외발생_테스트() {
assertThrowsIllegalArgumentException(() -> InputValidator.validateString("@@$"));
}
@Test
void 차이름_5자이하_테스트() {
assertTrue(InputValidator.validateCarName("pbi"));
}
@Test
void 차이름_오류_예외발생_테스트() {
assertThrowsIllegalArgumentException(() -> InputValidator.validateCarName("pobisportscar"));
}
@Test
void 시도입력_유효_테스트() {
assertTrue(InputValidator.validateRounds("5"));
}
@Test
void 시도입력_형식오류_예외발생_테스트() {
assertThrowsIllegalArgumentException(() -> InputValidator.validateString("ㅇ"));
}
@Test
void 시도입력_음수_예외발생_테스트() {
assertThrowsIllegalArgumentException(() -> InputValidator.validateString("ㅇ"));
}
private void assertThrowsIllegalArgumentException(Executable executable) {
assertThrows(IllegalArgumentException.class, executable);
}
}
위와 같이 코드를 정리했다. 따로 Excutable을 파라미터로 받는 오류처리 함수를 만들어 코드를 단축했다.
🤔 여기서 잠깐! Excutable?
- Executable은 Java의 함수형 인터페이스 중 하나이며, 이 인터페이스는 Java 8부터 도입되었다.
- Executable은 실행 가능한 코드 블록을 나타낸다.
- 단일 추상 메서드인 void execute()를 정의한다. 즉, 람다 표현식이나 메서드 레퍼런스를 사용하여 구현할 수 있는 함수형 인터페이스이다.
- JUnit의 assertThrows 메서드는 Executable을 받아 특정 예외를 기대하며 해당 코드를 실행한다.
- 따라서 assertThrows 메서드는 코드 실행 블록 (Executable 함수)을 실행하고 해당 코드가 예외를 발생시키는지 확인한다.
java
Copy code
@FunctionalInterface
public interface Executable {
void execute() throws Throwable;
}
위와같은 방식들로 다른 기능들도 명세를 보고 해당 기능 테스트 -> 기능 구현을 쭉쭉 진행했다.
📑CarTest.java
public class CarTest {
@Test
void 차_이동_테스트() {
Car car = new Car("Car");
car.carMove(5);
assertEquals(5, car.getPosition());
}
@Test
void 차_이동_하지않는경우_테스트() {
Car car = new Car("Car");
car.carMove(2);
assertEquals(0, car.getPosition()); // 4 미만의 값이므로 위치 이동 X
}
}
📑domain/Car.java
public class Car {
private String name;
private int position;
public Car(String name) {
this.name = name;
this.position = 0;
}
public String getName() {
return name;
}
public int getPosition() {
return position;
}
public void setPosition() {
this.position = this.position + 1;
System.out.println("position = " + position);
}
public void setPosition(int num) {
this.position = num;
}
public void carMove(int randomNumber) {
if (randomNumber >= 4) {
setPosition();
}
}
}
위의 아주 간단한 테케에서도 에러가 나서 수정하였다! 테케가 fail이 떠서 확인해보니 carMove를 나도모르게 ++randomNumber로 한 것이다...하하 😅간단한 코드임에도 사람인지라 실수를 하는데, 이를 방지해주는게 TDD 의 장점이라는 생각이 들었다.
📑RefereeTest.java
public class RefereeTest {
@Test
public void 테스트_승자_결정() {
ArrayList<Car> cars = new ArrayList<>();
Car car1 = new Car("Car1");
Car car2 = new Car("Car2");
Car car3 = new Car("Car3");
cars.add(car1);
cars.add(car2);
cars.add(car3);
car1.setPosition(1);
car2.setPosition(2);
car3.setPosition(3);
Referee referee = new Referee(cars);
ArrayList<Car> winners = referee.determineWinner(cars);
assertEquals(1, winners.size());
assertTrue(winners.contains(car3));
}
@Test
public void 테스트_승자_동시우승_결정() {
ArrayList<Car> cars = new ArrayList<>();
Car car1 = new Car("Car1");
Car car2 = new Car("Car2");
Car car3 = new Car("Car3");
cars.add(car1);
cars.add(car2);
cars.add(car3);
car1.setPosition(1);
car2.setPosition(3);
car3.setPosition(3);
Referee referee = new Referee(cars);
ArrayList<Car> winners = referee.determineWinner(cars);
assertEquals(2, winners.size());
assertTrue(winners.contains(car2));
assertTrue(winners.contains(car3));
}
}
📑domain/Referee.java
package racingcar.domain;
import camp.nextstep.edu.missionutils.Randoms;
import java.util.ArrayList;
public class Referee {
private ArrayList<Car> cars;
public Referee(ArrayList<Car> cars) {
this.cars = new ArrayList<>(cars);
}
// 각 자동차마다 값을 랜덤으로 뽑아 Car.position 값 변경
public ArrayList<Car> playRound(ArrayList<Car> cars) {
for (Car car : cars) {
int move = Randoms.pickNumberInRange(0,9);
if (move >= 4) {
car.carMove(car.getPosition() + 1);
}
}
return cars;
}
public ArrayList<Car> determineWinner(ArrayList<Car> cars) {
int maxPosition = 0;
for (Car car : cars) {
maxPosition = Math.max(maxPosition, car.getPosition());
}
ArrayList<Car> winners = new ArrayList<>();
for (Car car : cars) {
if (car.getPosition() == maxPosition) {
winners.add(car);
}
}
return winners;
}
}
📑RacingGameTest.java
public class RacingGameTest {
@Test
public void 게임진행_최종_테스트() {
RacingGame racingGame = new RacingGame();
// 자동차 생성 및 게임 초기화
Car car1 = new Car("Car1");
Car car2 = new Car("Car2");
racingGame.prepareGame(new ArrayList<>(Arrays.asList(car1, car2)));
// 게임 시작
racingGame.startGame(5);
// 결과 출력
racingGame.printRoundResults();
// 최종 우승자 확인
String winner = racingGame.printGameWinner();
assertNotNull(winner);
}
}
📑domain/ RacingGame .java
public class RacingGame {
private ArrayList<Car> cars;
private Referee referee;
public void prepareGame(ArrayList<Car> carList) {
this.cars = carList;
this.referee = new Referee(cars);
}
public void startGame(int rounds) {
if (cars == null || referee == null) {
throw new IllegalStateException("Game objects not initialized.");
}
for (int i = 0; i < rounds; i++) {
this.cars = referee.playRound(cars);
printRoundResults();
}
System.out.println("\n실행 결과");
printGameWinner();
}
public void printRoundResults() {
if (referee == null) {
throw new IllegalStateException("Game not started. No round results to display.");
}
for (int i = 0; i < cars.size(); i++) {
System.out.println(cars.get(i).getName() + " : " + "-".repeat(cars.get(i).getPosition()));
}
}
public String printGameWinner() {
if (referee == null) {
throw new IllegalStateException("Game not started. No winner to display.");
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("최종 우승자 : ");
ArrayList<Car> winners = referee.determineWinner(cars);
StringJoiner joiner = new StringJoiner(", ");
for (Car winner : winners) {
joiner.add(winner.getName());
}
String winnersString = joiner.toString();
stringBuilder.append(winnersString);
System.out.println(stringBuilder);
return stringBuilder.toString();
}
}
📑RacingController.java
public class RacingController {
public RacingController() {}
public String carNamePrompt(){
System.out.println("경주할 자동차 이름을 입력하세요. (이름은 쉼표(,)로 구분)");
String carNamesInput = readLine();
validateString(carNamesInput);
System.out.println("carNamesInput = " + carNamesInput); // tmp
return carNamesInput;
}
public String roundPrompt(){
System.out.println("시도할 회수는 몇 회인가요?");
String roundsInput = readLine();
validateRounds(roundsInput);
System.out.println("roundsInput = " + roundsInput);
return roundsInput;
}
}
📑Application.java
public class Application {
public static void main(String[] args) {
RacingGame racingGame = new RacingGame();
// TODO: 프로그램 구현
System.out.println("실행 잘 되나요??");
RacingController racingController = new RacingController();
String carNamesInput = racingController.carNamePrompt();
String[] carNames = carNamesInput.split(",");
ArrayList<Car> cars = new ArrayList<>();
for (String carName : carNames) {
validateCarName(carName);
cars.add(new Car(carName));
}
int rounds = Integer.parseInt(racingController.roundPrompt());
// 게임 준비
racingGame.prepareGame(new ArrayList<>(cars));
// 게임 시작
racingGame.startGame(rounds);
// 최종 우승자 확인
String winner = racingGame.printGameWinner();
System.out.println("winner = " + winner);
}
}
여기까지가 1차 최종 코드이다! 더불어 코드를 작성하며 오류가 발생한 몇몇 부분도 수정해 주었다.
그리고 Promt문구와 에러메시지는 따로 상수로 파일을 분리했다.
🔧 일급 컬렉션 적용해보기 (코드 리팩토링)
더 수정할것을 찾아보던 중,누군가 지식나눔방에 일급컬렉션에 관련하여 올린 것을 보게되었다. 오호, 가만보니 본 프로젝트에 이 개념을 적용하면 더욱 클린한 코드를 만들 수 있을것 같았다! 🤔
일급 컬렉션이란 Collection을 Wrapping하면서, 그 외 다른 멤버 변수가 없는 상태를 말한다. 이는 비지니스에 종속적인 자료구조를 가질 수 있고, 불변성을 보장하며, 상태와 행위를 함께 관리할 수 있는 등의 여러 장점을 가진다 한다.
예를 들어, Cars 클래스는 ArrayList<Car>를 Wrapping하며, 자동차 목록에 관련된 기능을 제공하고 해당 기능을 관리하는 역할을 수행하는 것이다.
📑Cars.java
public class Cars {
private ArrayList<Car> cars;
public Cars(ArrayList<String> carNames) {
this.cars = createCarListFromNames(carNames);
}
private ArrayList<Car> createCarListFromNames(ArrayList<String> carNames) {
ArrayList<Car> cars = new ArrayList<>();
for (String carName : carNames) {
validateCarName(carName);
cars.add(new Car(carName));
}
return cars;
}
public Car getIndex(int i) {
if (i < 0 || i >= cars.size()) {
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + cars.size());
}
return cars.get(i);
}
public ArrayList<Car> getCars() {
return cars;
}
public int size() {
return cars.size();
}
}
나는 그래서 위와 같이 Cars 를 만들어 ArrayList<Car> cars를 랩핑해주었다.그리고 이와 관련한 부분들을 모두 Cars로 수정해주었다.
// 유저 인풋 값 받고 차 준비
String[] carNames = carNamesInput.split(",");
ArrayList<String> carNamesList = new ArrayList<>(Arrays.asList(carNames));
Cars cars = new Cars(carNamesList);
이렇게 직접 해보니 일급컬렉션의 장점이 와닿았다. 아까의 경우 이름값이 유효한지 검사후 ArrayList<Car> cars 에 일일히 담았는데, 이를 하나의 규칙이 있는 자료구조 Cars로 만들어 Cars 클래스 내부에서 유효한지 검사하는 식으로 코드를 수정하여, 위와 같이 코드를 깔끔히 작성할 수 있었다.
TDD로 개발하니 테스트 통과와 오류잡기가 수월하다...!
그런데, 이거 참 쉽지않다 ...
이번 과제를 진행하며 TDD 방식을 적용해보고 리팩토링하는 과정에서 여러 가지를 배웠다. 사실 TDD 개발론이 이론적으로는 장점이 아주 많고 그렇게 하는 것이 맞아보이지만, 생각보다 먼저 테스트 코드를 작성한다는 것이 쉽지않고, 시간이 많이 걸리는 과정인 것같다. 하지만 개발을 진행할땐 테스트 코드를 목표로 코드를 작성하기에 확실히 에러없는 코드를 작성할 수 있다는 장점이 있는 것 같다.
참고로 나의 경우는 리팩토링을 하며 테스트 코드는 수정하지않는다는 원칙을 여러번 깨버렸기에 ... 완벽하게 TDD 개발론을 수행했다고는 할 수 없어 아쉬움이 남는다. (그래도 시도에 의의를 ... 😊)
리팩토링을 거치며 코드가 위와 많이달라졌기에,최종 코드는 아래를 참고하자!
https://github.com/hyeonjeong-ko/java-racingcar-6/tree/hyeonjeong
GitHub - hyeonjeong-ko/java-racingcar-6: 우테코 프리코스 두번째 과제 java-racingcar-6
우테코 프리코스 두번째 과제 java-racingcar-6. Contribute to hyeonjeong-ko/java-racingcar-6 development by creating an account on GitHub.
github.com
👩💻 과제수행에 참고한 것들
테스트 코드 그게 뭔데? 중요한거야?(feat. TDD)
테스트 코드를 짜는게 중요하다는 말은 많이 들어보았다. 하지만 실제로 개발을 하면서 테스트 코드를 짜본적이 한번도 없다. 이 글을 통해 테스트 코드 작성의 중요성을 이해하고, 작성에 대한
nakhonest.tistory.com
[TIL] Java 테스트 코드 작성법
테스트 코드를 왜 작성해야 할까 🤔 테스트 코드를 작성하는 이유는 잘 작동하는, 깔끔한 코드를 얻기 위함이다. 테스트를 쉽게 하기 위해서는 어플리케이션 코드를 테스트하기 쉽게 짜야하는
velog.io
[Java] JUnit5 과 AssertJ 로 단위 테스트 작성하기
Java 프로그래밍을 할 때 JUnit5와 AssertJ로 간단한 단위 테스트를 하는 법을 알아보자.
velog.io
일급 컬렉션 (First Class Collection)의 소개와 써야할 이유
최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코
jojoldu.tistory.com
생소했던 일급 콜렉션을 공부하며
자동차 경주 미션을 진행하던 중, Car 객체를 담는 List를 생성하는 클래스를 짰는데 리뷰어님으로부터 이러한 코멘트를 받았다.자료구조를 포함한 네이밍보다 Cars 라는 이름을 사용하는 것이 좋
velog.io
'개발프로젝트' 카테고리의 다른 글
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 02- 테스트 (0) | 2023.12.20 |
---|---|
[개발프로젝트] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 chapter 01 (0) | 2023.12.19 |
utc4 (2) | 2023.11.15 |
utc3 (0) | 2023.11.07 |
utc1 (1) | 2023.10.24 |