숫자 야구의 일주일이 지나가고 새로운 미션이 도착했다. 새로운 미션에 앞서서 6기 예비생들끼리 디스코드에서 서로의 pr 주소를 올리며 각자의 숫자 야구 코드를 리뷰해주는 시간을 가졌다. 현업에서 7~8이 코드 읽기고 나머지가 코드 작성이라 할만큼 코드를 읽는게 중요하다는 걸 알았고 최대한 많은 분들의 코드를 읽으며 리뷰를 진행했다. 문법을 이해하지 못할 정도로 고수의 향기가 나는 분도 계셨고 내 기준에서 흠 잡을데 없이 잘 짜신 분도 계셨다. 간단한 숫자 야구일지라도 정말 많은 사람들이 다양하게 코드를 작성했고 배울 점이 많았던 시간이었다. 매 주차가 끝날때마다 리뷰하는 시간을 소중히 여기고 잘 활용해야겠다는 생각이 들었다. 점점 실력이 느리지만 꾸준히 쌓이면서 코드 보는 실력도 늘어나길 기대해본다.
아! 2주차 미션에 들어가기에 앞서 하나만 더 얘기하자면 정말 기본적인 실수로 인해 당황을 했다. git clone을 메인 repository로 하는 바람에 여기에 꾸준히 커밋을 했다. Public archive 상태여서 pr이 안됐음에도 곧 되겠지 라는 안일한 생각을 가졌었다. 쎄함을 감지한 나는 우테코 사람들에게도 물어봤고 그 결과 실수임을 알게 되었다. 구현을 완료하고 1차 리펙토링까지 끝낸 상황, 아까운 커밋들을 모두 날리게 됐다. 다시는 이런 실수를 안하고자 장문의 커밋 메세지와 함께 마음을 다잡았다.
이제 정말 2주차로 들어가서, 이번 주제는 자동차 경주였다. 프로그래밍 요구 사항은 나머지는 모두 같고 추가된 사항이 있었는데 아래와 같다.
3항 연산자는 원래 잘 쓰지 않고 함수가 한 가지 일만 하도록 하는건 1주차부터 꾸준히 생각해오던 경우였지만 남은 2가지가 문제였다. indent depth 를 2까지만 허용한다는 것은 아래 코드가 마지노선 들여쓰기라는 뜻이다.
private static void validateInputRange(int[] guess) {
for (int num : guess) {
if (num < Constants.MIN_NUMBER || num > Constants.MAX_NUMBER) {
throw new IllegalArgumentException("입력은 1에서 9 사이의 숫자여야 합니다.");
}
}
}
빡빡할 수 있는 조건이지만 최대한 일을 분산화할 수 있도록 하는데에 있어서 어느정도 강제성이 생기기 때문에 그만큼 더 좋은 코드가 나올 수 있다는 뜻이다.
그리고 테스트 코드를 직접 구현해보는 미션이 주어졌다. 가장 난관은 이것으로 예상됐고 역시나 예상대로였다. 주어진 test/java/study 를 포함해서 구글링, ChatGPT 까지 동원해서 공부를 이어가며 코드를 작성했다. TDD 즉, 테스트 주도 개발, 단위 테스트, 통합 테스트 등 평소 경험이 거의 없었던 테스트에 대해 공부하다보니 일단 문법 측면에서 어려움이 있었다. JUnit5와 AssertJ를 처음 사용해봤고 TDD를 위해 꼭 익숙해져야 겠다고 다짐했다.
과제 진행 요구 사항에서는 한가지가 추가되었는데, 커밋 메시지 컨벤션 가이드를 참고하라는 것이었다. 1주차 숫자 야구 코드리뷰 때도 어떤 분이 리뷰로 말씀해주시기도 했고 나도 다른 분들의 커밋 메시지를 보면서 커밋 메시지도 컨벤션이 있다는걸 알게되었다. 2주차부터는 코드 컨벤션과 함께 커밋 메시지 컨벤션도 같이 생각하며 프리코스를 진행하고자 한다.
기능 요구 사항은 아래와 같다.
코드 회고
이번에 과제를 수행하면서(아직 완벽히 다 한건 아니지만) 안쓰던 방식들을 써보기도 하고 기존에 두루뭉실하게 제대로 알고 있지 못했던 사실들이나 알았어도 까먹은 사실들에 대해 다시 한번 깨닫고 공부해보는 시간을 가졌다. 저번 1주차 리뷰때는 완성 코드를 먼저 보여주고 고민이나 실수들을 회고했었는데 이번에는 반대로 해볼까 한다. 이게 맞는 순서 같기도 하다. 먼저 아래는 자동차 이름 입력 예외 처리를 해주는 코드 첫 구현 단계이다.
for (String input : inputArr) {
boolean pass = true;
if (input.length() <= 5) {
pass = false;
}
if (!pass) {
throw new IllegalArgumentException("자동차 이름이 5자 이하가 아닙니다: " + input);
}
if (input.matches("^[a-zA-Z0-9]*$")) {
pass = false;
}
if (!pass) {
throw new IllegalArgumentException("특수문자가 포함된 문자열이 입력되었습니다: " + input);
}
if (!checkDuplication.add(input)) {
pass = false;
}
if (!pass) {
throw new IllegalArgumentException("중복된 이름이 입력되었습니다: " + input);
}
carNames.add(input);
}
위 코드를 작성 후 보자마자 기능별로 세분화 시키고 싶은 생각이 바로 들었던걸 보면 조금은 성장했을지도..? 라는 생각을 해본다. 세분화시켜 리펙토링한 코드는 나중에 완성된 코드에서 확인할 수 있다. 또한 평소 잘 쓰지않던 정규 표현식에 대해서도 공부하게 되었다.
정규표현식 ^[a-zA-Z0-9]*$은 다음과 같이 구성된다 ->
- ^ : 문자열의 시작을 나타내는 메타문자
- [a-zA-Z0-9]* : 이 부분은 문자와 숫자로 이루어진 0개 이상의 문자열을 나타냄
- [a-zA-Z0-9] : 이 부분은 대문자 A부터 Z, 소문자 a부터 z, 그리고 0부터 9까지의 숫자 중 하나를 의미합니다. 즉, 어떤 문자가 이 범위에 속하는지 확인
- * : 0회 이상 반복을 의미합니다. 따라서 이 부분은 문자와 숫자로 이루어진 문자열을 나타냄
- $ : 문자열의 끝을 나타내는 메타문자
그리고 처음에는 무작위 숫자를 추출 후 바로 4 이상인지 판단하려했지만 최대한 기능을 세분화하여 한가지 일만 하도록 하는게 낫다고 생각하여 각각 따로 코드를 작성하였다. 각각 다른 메서드에서 기능을 제공함으로써 최대한 한가지 일을 하도록 하는 조건을 만족하려 노력했다.
한편 어김없이 indent depth 가 2가 넘어가는 순간이 찾아왔고 2가 넘지않도록 코드를 변경하는 과정에서 새로운 joiner에 대해서도 공부하게 되었다. 아래는 depth가 3인 경우의 코드이다.
System.out.print("최종 우승자 : ");
for (int i = 0; i < carGoingCount.size(); i++) {
if (maxCount == carGoingCount.get(i)) {
System.out.print(carNames.get(i));
if (i < carGoingCount.size() - 1) {
System.out.print(", "); // 마지막 요소가 아닌 경우에만 쉼표 추가
}
}
}
depth 가 2를 초과해서 아래와 같이 코드를 변경했다.
StringJoiner joiner = new StringJoiner(", ");
for (int i = 0; i < carGoingCount.size(); i++) {
if (maxCount == carGoingCount.get(i)) {
joiner.add(carNames.get(i));
}
}
System.out.print("최종 우승자 : " + joiner.toString());
전체 피드백에서 Java에서 제공하는 API를 적극 활용하고 Java Collection을 사용하라고 하셨는데 말씀대로 사용했더니 코드가 더 간결해졌다. 무엇보다 depth 조건을 지킬 수 있다는게 당장의 나에게 가장 큰 메리트였다. 한 번 사용에서 그치는게 아니라 앞으로도 잘 활용해서 코드의 간결성과 가독성을 증가시키겠다는 생각을 해본다.
아래와 같이 값을 변경하기 위해 set을 사용했는데 IndexOutOfBoundsException오류가 발생했다. 생각해보니 처음에는 초기값이 없는 상태에서 set을 하니 발생하는 오류였다.
private static void judgeCarGoing(ArrayList<Integer> randomNumbersList, ArrayList<Integer> carGoingCount) {
for (int i = 0; i < randomNumbersList.size(); i++) {
if (randomNumbersList.get(i) >= 4) {
carGoingCount.set(i, carGoingCount.get(i) + 1);
}
}
}
따라서 예외적으로 아래와 같이 초기화를 미리 진행해줬다.
ArrayList<Integer> carGoingCount = new ArrayList<>();
for (int i = 0; i < carNames.size(); i++) {
carGoingCount.add(0);
}
평소 알고리즘 문제를 풀때도 this로 생성자를 기계적으로 생성해서 사용했던 나는 정확하게 알고있지 않았다. 하지만 이번 코드 작성을 하며 흐름을 파악하는데에 있어서, 코드를 작동시키는데에 있어서 반드시 제대로 이해하고 넘어가야겠다고 생각을 했다.
public void initialize(ArrayList<String> carNames) { //매개변수는 이 메서드에서만 유효함
this.carNames = carNames; //Race 내부 다른 메서드에서도 사용할 수 있도록 멤버 변수에 할당
/*
* arraylist 에서 set 메서드로 접근하기 위해 실시하는 초기화
*/
carGoingCount = new ArrayList<>(carNames.size());
for (int i = 0; i < carNames.size(); i++) {
carGoingCount.add(0);
}
}
위 코드에서 this.carNames = carNames; 문장은 꼭필요한 부분이다.
initialize 메서드는 GameController 클래스에서 호출되며, carNames라는 파라미터를 받는다. 이 파라미터는 메서드 내부에서 지역 변수로 사용될 것이다. 그러나 Race 클래스의 멤버 변수 carNames는 initialize 메서드가 실행된 이후에도 Race 클래스 내 다른 메서드에서 사용될 필요가 있다.
따라서 this.carNames = carNames; 문장은 파라미터로 전달된 carNames를 Race 클래스의 멤버 변수인 carNames에 할당하여 해당 변수를 다른 메서드에서 사용할 수 있도록 하는 역할을 한다.
그리고 static 정적 메서드 접근이 좋을지, 객체 생성 접근이 좋을지에 대해 생각해보게 되었다. 이 말부터 고민이 있었는데 좀 더 정확히는 객체 생성 접근이 아니라 객체의 인스턴스 접근이 맞는 것 같다. 클래스의 타입으로 선언되었을 때 객체라고 부르고, 그 객체가 메모리에 할당되어 실제 사용될 때 인스턴스라고 부른다. 즉 static 메서드를 통한 클래스 이름으로 호출할지, 객체의 인스턴스를 생성해서 호출할 지에 대해 생각을 해봤다.
static 메서드는 말그대로 메서드 앞에 static이 붙은 메서드이며 인스턴스 생성 없이 호출이 가능한 메서드다. static 메서드와 인스턴스 메서드의 차이는 인스턴스 변수 사용 유무로 나뉜다. 아래는 두 방법에 대한 비교 작성이다.
- 인스턴스 메소드
- 인스턴스 생성 후, '참조 변수.메소드 명()'으로 호출한다.
- 인스턴스 멤버와 관련된 작업을 한다.
- 메소드 내에서 인스턴스 변수를 사용할 수 있다.
- 인스턴스 변수를 이용해서 작업을 하므로 변수의 묶음인 객체를 생성해야 인스턴스 메소드를 호출할 수 있다.
ex) Exam ex = new Exam(); ex.example = "예시";
- static 메소드(클래스 메소드)
- 객체 생성 없이 '클래스 명.메소드 명()'으로 호출이 가능하다.
- 인스턴스 멤버와 관련되지 않은 작업을 한다.
- 메소드 내에서 인스턴스 변수를 사용할 수 없다.
- 인스턴스 메소드와 달리 인스턴스 변수가 필요 없으므로 객체를 생성하지 않고도 호출할 수 있다.
ex) Math.random()
간단하게 말하자면 인스턴스 변수를 사용하지 않는 경우에 static을 붙여서 정적 메서드 접근을 하면 된다. 그럼 인스턴스 변수는 뭘까?
public class test {
int iv; // 인스턴스 변수
static int cv; // 클래스 변수
void method() {
int lv; // 지역 변수
}
}
인스턴스 변수는 인스턴스가 생성될때 생성되기 때문에 인스턴스 변수의 값을 읽어오거나 저장하려면 인스턴스를 먼저 생성해야한다. 인스턴스 별로 다른 값을 가질 수 있으므로, 각각의 인스턴스마다 고유의 값을 가져야할 때는 인스턴스 변수로 선언한다.
반대로 클래스 변수는 모든 인스턴스가 공통된 값을 공유하게 된다. 공통적인 값을 가져야할 때 클래스 변수로 선언하며 public을 붙이면 같은 프로그램 내에서 어디서든 접근할 수 있는 전역 변수가 된다.
지역변수는 메서드 내에서 선언되며 메서드 내에서만 사용할 수 있는 변수다. 메서드가 끝나면 소멸되어 사용할 수 없다.
그런데 제출할 때의 내 코드는 전부 인스턴스 생성 후 접근하는 방식을 택했다. 당시의 나는 큰 오류를 범한게 클래스 파일마다 선언된 인스턴스 변수를 기준으로 삼았어야 했는데 메서드 내에서 또다른 메서드를 호출하면 인스턴스 방식으로 접근해야 한다고 생각했다. 지금 생각해보면 왜 그랬지는 모르겠지만 추측하건데, 너무 많은 글들을 주의깊게 읽지 않았던 탓이라 생각되고 스스로 반성하는 시간을 가져본다. 다행히 이렇게 회고록을 쓰면서 정확히 알게 됨에 다행이라고 생각한다.
다음으로는 객체를 생성한다면 내부생성이 좋은지 외부에서 생성된 것을 받아오는게 좋은지에 대해 고민하게 되었다.
class Engine {
void start() {
System.out.println("Car's engine has started.");
}
}
class Car1 { // 객체 내부 생성
private Engine engine = new Engine();
...
}
class Car2 { // 외부 객체 받아오기
private Engine engine;
Car(Engine engine) {
this.engine = engine;
}
...
}
객체를 내부에서 생성하는 방식의 장단점:
장점:
- 캡슐화와 초기화 제어: 객체를 내부에서 생성하면 해당 객체를 외부에서 직접 초기화하거나 조작할 수 없으므로 객체의 캡슐화가 강화된다.
- 단순성: 객체를 내부에서 생성하면 클라이언트 코드가 객체를 생성하고 초기화할 필요가 없으므로 사용자 편의성이 높아진다.
단점:
- 유연성 부족: 내부에서 객체를 생성하면 특정 타입의 객체에 고정된다. 이로 인해 다른 종류의 객체를 사용하려면 클래스의 수정이 필요할 수 있다.
- 테스트 어려움: 객체를 내부에서 생성하면 테스트 중에 객체를 대체하기 어려울 수 있다.
외부에서 생성된 객체를 받아오는 방식의 장단점:
장점:
- 유연성: 외부에서 객체를 주입받으면 다른 종류의 객체로 손쉽게 교체할 수 있다. 이는 객체의 다형성을 활용하는 데 유용하다.
- 의존성 관리: 객체의 의존성을 명시적으로 관리하며, 이로 인해 객체 간의 결합도가 낮아진다.
- 테스트 용이성: 객체의 의존성을 주입하면 테스트 중에 가짜(Mock) 객체를 주입하여 테스트를 수행할 수 있어 테스트 용이성을 향상시킨다.
단점:
- 복잡성: 외부에서 객체를 받아오는 방식은 추가적인 코드 작성 및 설정이 필요할 수 있다. 이로 인해 코드의 복잡성이 증가할 수 있다.
- 오용 가능성: 잘못된 객체나 잘못된 설정이 주입될 수 있으며, 이로 인해 오류가 발생할 가능성이 있다.
- 의존성 주입 관리 어려움: 의존성 주입은 객체의 생성 및 관리에 추가 작업을 필요로 하므로 초기 설정 및 관리가 어려울 수 있습니다.
사실 이 부분에 대해서는 아직 코드에 어떻게 정확하게 적용해야할지 잘 모르겠다. 앞으로도 계속 생각해봐야할 부분인 것 같다.
아래는 MVC 패턴을 적용한 전체 코드다. 1주차 때 파일들을 분류했으면 좋겠다는 리뷰 피드백을 받았고, MVC 패턴도 적용해봐야겠다는 생각은 하고 있어서 이번 미션에서 적용하게 되었다.
전체 코드
https://github.com/JaeMin-1/java-racingcar-6/tree/JaeMin-1
GitHub - JaeMin-1/java-racingcar-6
Contribute to JaeMin-1/java-racingcar-6 development by creating an account on GitHub.
github.com
테스트 코드 리뷰는 프리코스 2주차 미션 (2)에서 다루도록 하겠다.
글 작성에 도움을 주신 목록 ->
https://itmining.tistory.com/20
https://gmlwjd9405.github.io/2018/09/17/class-object-instance.html
'우테코 프리코스' 카테고리의 다른 글
프리코스 3주차 미션 (0) | 2023.11.07 |
---|---|
2주차 코드에 대한 코드 리뷰 (1) | 2023.11.03 |
프리코스 2주차 미션 (2) (0) | 2023.11.03 |
테스트 코드 : NoSuchElementException 오류 (0) | 2023.10.31 |
프리코스 1주차 미션 (0) | 2023.10.22 |