Intro.
대망의 프리코스 1주차 온보딩 미션을 완료했습니다.
https://github.com/woowacourse-precourse/java-onboarding
GitHub - woowacourse-precourse/java-onboarding: 온보딩 미션을 진행하는 저장소
온보딩 미션을 진행하는 저장소. Contribute to woowacourse-precourse/java-onboarding development by creating an account on GitHub.
github.com
문제는 코딩테스트 형식과 비슷했으며, 모두 7문제로 이루어져 있습니다.
미션 마감 기한은 1주이며, 제출 가능 시간은 종료일 전날부터 제출이 가능합니다.
특히 이번 미션에서 지켜야 할 제약 조건은,
'특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.'
를 지키는 것이 가장 중요하다고 생각되어 7개의 미션들로 이를 연습해보자는 생각으로 임했습니다.
이번 미션에서 제가 집중했던 것들, 배웠던 것들 그리고 느낀점을 쭉 이야기해보고자 글을 작성하게 되었습니다.
온보딩 미션에서 집중하고, 배우고, 느꼈던 것들
1. 클린 코드를 향한 노력들
- else 키워드를 사용하지 않기
- 줄여쓰기 금지
- getter/setter 사용하지 않기
- 자바 코드 컨벤션 지키기 https://naver.github.io/hackday-conventions-java/
캠퍼스 핵데이 Java 코딩 컨벤션
중괄호({,}) 는 클래스, 메서드, 제어문의 블럭을 구분한다. 5.1. K&R 스타일로 중괄호 선언 클래스 선언, 메서드 선언, 조건/반복문 등의 코드 블럭을 감싸는 중괄호에 적용되는 규칙이다. 중괄호
naver.github.io
2. 네이밍(naming) : 이름 짓기
3. 함수 분리
- 모든 클래스와 메서드의 들여쓰기를 오직 한 단계만 허용하기
4. IntelliJ에서 Git 관리하기
5. Git 커밋과 푸시는 신중하게
else 키워드를 쓰지 않기
사실 줄곧 else, else if 와 같은 키워드를 많이 써 왔는데, 대체 else를 쓰지 않아야 하는 이유는 무엇일까요?
그리고 else를 쓰지 않는다면, 어떻게 대체할 수 있을까에 대한 고민으로 프리코스를 시작했던 것 같습니다.
https://tecoble.techcourse.co.kr/post/2020-07-29-dont-use-else/
else 예약어를 쓰지 않는다
The ThoughtWorks Anthology의 더 나은 소프트웨어를 향한 9단계: 객체지향 생활 체조 중 규칙…
tecoble.techcourse.co.kr
그러던 중 테코블 포스트에서 해답을 얻게 되었습니다.
우선 else 키워드를 사용하지 않으면 코드의 가독성 측면에서도 이점이 있지만, else 키워드를 통해 반전된 로직을 작성하게 되는 위험이 있으며, 이는 자칫 한 메서드에 두 가지 이상의 기능을 하는 코드로 이어질 수도 있다는 점입니다.
else 대신에 if문을 사용하여 early return을 하는 구조로 개선시키는 것이 하나의 해결책이 될 수 있다고 말합니다.
getter/setter 사용하지 않기
사실 setter보다 getter를 사용하지 않고서 코드를 짜는 것이 굉장히 어렵다는 것을 느꼈습니다.
이 함수에서 다른 값을 받아와야 할 일이 생겼는데, getter가 없다고 생각하니 조금은 답답하기도 했습니다.
결론적으로 이를 해결한 방법은 어떤 함수에서 그 값이 필요한 이유를 생각해 보고, 객체에게 메세지를 보내는 식으로 해결할 수 있었습니다.
package onboarding;
import java.util.List;
class Problem1 {
public static final int DRAW = 0;
public static final int POBI_WIN = 1;
public static final int CRONG_WIN = 2;
public static final int EXCEPTIONS_OCCURED = -1;
private static int pobiScore;
private static int crongScore;
private static Pages pobiPages;
private static Pages crongPages;
public static int solution(List<Integer> pobi, List<Integer> crong) {
try {
pobiPages = Pages.of(pobi);
crongPages = Pages.of(crong);
} catch (IllegalArgumentException e) {
return EXCEPTIONS_OCCURED;
}
pobiScore = Pages.makeScore(pobiPages);
crongScore = Pages.makeScore(crongPages);
return findWinnerAndMakeAnswer(pobiScore, crongScore);
}
private static int findWinnerAndMakeAnswer(int pobiScore, int crongScore) {
if (pobiScore > crongScore) {
return POBI_WIN;
}
if (pobiScore < crongScore) {
return CRONG_WIN;
}
return DRAW;
}
static class Pages {
private static final int SIZE_OF_PAGES = 2;
private static final int FIRST_PAGE_NUMBER = 1;
private static final int LAST_PAGE_NUMBER = 400;
private final List<Integer> pages;
private Pages(List<Integer> pages) {
this.pages = pages;
}
public static Pages of(List<Integer> pages) {
validate(pages);
return new Pages(pages);
}
private static void validate(List<Integer> pages) {
validateListSize(pages);
validateContinuousPages(pages);
validateFirstOrLastPages(pages);
}
private static void validateListSize(List<Integer> pages) {
if (pages.size() != SIZE_OF_PAGES) {
throw new IllegalArgumentException();
}
}
private static void validateContinuousPages(List<Integer> pages) {
if (pages.get(0) % 2 == 0 || pages.get(0) + 1 != pages.get(1)) {
throw new IllegalArgumentException();
}
}
private static void validateFirstOrLastPages(List<Integer> pages) {
if (pages.get(0) == FIRST_PAGE_NUMBER || pages.get(1) == LAST_PAGE_NUMBER) {
throw new IllegalArgumentException();
}
}
private static int makeScore(Pages pages) {
return Math.max(makeScoreByAddition(pages.pages), makeScoreByMultiplication(pages.pages));
}
private static int makeScoreByAddition(List<Integer> pages) {
return Math.max(calculateScoreByAddition(pages.get(0)), calculateScoreByAddition(pages.get(1)));
}
private static int makeScoreByMultiplication(List<Integer> pages) {
return Math.max(calculateScoreByMultiplication(pages.get(0)), calculateScoreByMultiplication(pages.get(1)));
}
private static int calculateScoreByAddition(int number) {
int total = 0;
while (number != 0) {
total += (number % 10);
number /= 10;
}
return total;
}
private static int calculateScoreByMultiplication(int number) {
int total = 1;
while (number != 0) {
total *= (number % 10);
number /= 10;
}
return total;
}
}
}
온보딩 1번 미션을 해결한 코드입니다.
우선 내부 클래스로 Pages를 정의하여 펼친 페이지의 정보를 담고 있는 컬렉션을 관리하도록 하였습니다.
최종적으로 펼친 페이지 정보를 이용해 점수를 계산하여 승자를 가려야 하는 문제입니다.
처음에는 점수를 계산하기 위해 포비와 크롱 Pages 객체에게 무작정 상태값을 제공해달라고(get) 로직을 짰던 것 같습니다. 하지만, 이렇게 되면 Pages객체 스스로 상태값을 변경할 수 없게 되며, 외부에서 이를 이용하여 접근하기도 더욱 쉬워진다는 단점이 있다는 것을 깨달았습니다. 곰곰이 생각해 보니, 결국 점수를 계산하기 위해 Pages 객체의 상태값이 필요한 것이니, Pages 객체에게 점수를 계산하도록 하고, 해당 객체에게 메세지를 보내 결과값만 받아온다면 Pages 객체의 상태값을 get하지 않고도 승자를 가리는 데에 전혀 문제가 없다는 것을 알게 되었습니다.
이를 바로 해당 코드에 반영하여 승자를 가리는 findWinnerAndMakeAnswer() 메서드만 solution 메서드에 위치시키는 방식으로 코드를 개선할 수 있었습니다.
네이밍에 관하여..
사실 이번 미션의 5할 이상은 네이밍에 관한 고민만 했을 정도로, 생각보다 변수명, 함수명을 보고 명확한 의도가 드러나도록 짓는 것이 정말 어려웠습니다. 이는 우테코 1주차 공통 피드백에도 언급되었을 정도로, 무릇 개발자라면 반드시 고민하고 극복해야 하는 주제가 아닐까 생각됩니다. 특히 별도의 주석 없이도, 단순히 코드만 보고서 이를 파악할 수 있을 정도로 명확하게 네이밍하는 것을 목표로 진행해 보았습니다.
온보딩 2번 문제를 해결하던 중, 저는 다음과 같은 고민을 하게 되었습니다.
함수의 이름을 지을 때 함수의 목적을 드러내는 것 vs 이름을 보고 함수의 기능을 한 눈에 파악할 수 있게 하는 것
둘 중에 무엇에 중점을 두어야 할까? 라는 고민을 줄곧 해왔던 것 같습니다.
예를 들어, cryptogram을 해독해서 결과를 도출하는 함수의 이름을
전자처럼 짓는다면 : decodeCryptogram()
반대로 후자처럼 짓는다면 : deleteContinuousAndDuplicatedLetters()
를 예시로 들 수 있겠습니다.
처음에는, 후자의 방식을 따라 코딩하였습니다. 전자처럼 네이밍을 하게 된다면 함수가 어떤 역할을 하는 지 모르니
내부 로직을 직접 확인해서 파악해야 하는 불편함이 있기 때문입니다.
하지만, 때로는 로직을 감춰야 할 순간도 분명 있을 것이라는 생각이 들었습니다.
고민 끝에 결론 내린 답은 다음과 같습니다.
public한 메서드의 경우에는 전자와 같이 추상화된 이름을 사용하는 것이 좋고,
직접 로직을 담고 있는 메서드는 private하게 선언하고, 이름에 해당 로직을 명확하게 드러낼 수 있도록 하자
네이밍에 관한 고민은 앞으로도 끝없이 해 나가야 할 숙제라고 생각합니다. 앞으로도 우테코 프리코스 미션을 통해서 더욱 좋은 코드에 가까워질 수 있도록 고민을 지속해 나갈 예정입니다.
함수 분리 : 한 메서드에 오직 한 단계의 들여쓰기만 허용
네이밍과 더불어 정말 지키기 어려웠던 숙제가 아닐까 생각합니다. 이번 2주차 미션 메일에서 2주차 미션에서 집중해야 할 포인트는 함수의 분리, 테스트 코드 작성이라고 전달받았습니다.
결과적으로, 1주차 미션의 모든 코드에서 오직 한 단계의 들여쓰기를 유지하는 것에 성공하였습니다.
이를 실천하고 나니, 하나의 메서드에서 오직 하나의 기능만 수행하도록 유지하는 것이 정말 수월해졌다고 느꼈습니다.
이와 더불어 코드의 가독성도 향상되었고 보다 더 명확한 코드를 작성할 수 있음을 체감하게 되었습니다.
IntelliJ를 이용한 Git 관리
여지껏 git의 버전 관리를 git bash만을 이용해 왔는데, 이번 기회에 IntelliJ에서 git을 사용하는 방법을 익힐 수 있었습니다.
IntelliJ에서는 따로 Git 메뉴 탭을 만들어 두었을 정도로, 이를 잘 이용하면 정말 손쉽게 버전 관리를 할 수 있습니다.
먼저 코드의 변경사항이 발생하면,
위 사진 좌측 메뉴 탭에 커밋(commit)에서 해당 변경사항을 확인할 수 있었습니다.
또한 그 아래에는 커밋 메세지를 직접 입력할 수 있었으며, 가장 하단의 커밋 및 푸시 버튼을 이용하면
제가 작업하던 branch에 손쉽게 커밋을 날릴 수 있었습니다.
이번 우테코 미션에서는 fork해온 레포지토리에서 제 github 이름으로 새로운 branch를 만들어 사용하도록 되어 있습니다.
이 또한 IntelliJ 메뉴 탭에서 새 브랜치.. 탭을 클릭하여 원하는 이름으로 branch를 생성하는 기능을 이용하면 간단합니다.
이렇게 새로운 branch의 생성과 동시에, 우측 하단을 확인해 보시면 제가 만든 branch로 자동 checkout 하는 모습도 확인할 수 있었습니다.
Git 커밋과 푸시는 신중하게
이번 온보딩 미션에서 기능 단위로 커밋을 할 때, 작성한 커밋 메세지를 변경하고 싶었던 적이 있습니다.
처음에는 그냥 쉽게 변경할 수 있을 줄 알았는데, 그 방법을 찾아보니 -f 옵션을 적용하여 강제로 푸시한 내용을 변경해야 함을 알게 되었습니다. 특히 협업을 할 때, 공동 repo에 강제로 커밋해야 하는 상황은 정말 최악의 상황이 아닐까 생각됩니다. 앞으로는 커밋과 푸시를 하기 전에 좀 더 신중하게 체크하고, 해당 메세지를 고민하는 데에 더욱 많은 시간을 들여서 작성하자고 다짐하는 계기가 되었던 것 같습니다.
이번 온보딩 미션을 마친 여러분들도 정말 고생하셨습니다.
우아한테크코스 프리코스를 시작하며 정말 제대로 몰입하는 기분이 듭니다. 다음 미션들도 점차 어려워지겠지만,
이 과정 속에서도 제가 반드시 성장할 수 있으리라 믿고 여러분과 함께 극복해 나아가는 시간이 되기를 소망합니다.
다음 미션도 화이팅입니다!!
'Daily > 회고' 카테고리의 다른 글
[회고] 대학생 미팅 서비스 - weave 프로젝트 회고 (3) | 2024.11.17 |
---|---|
[우아한테크코스] 프리코스 2주차 숫자 야구 미션을 마치며 (0) | 2022.11.08 |
2022 2분기 회고, 요새 드는 생각들 (1) | 2022.07.14 |