재밌었다면 재밌었고, 힘들었다면 힘들었던 우아한테크코스 프리코스 1주차가 어제부로 끝났다.
오늘 바로 2주차 과제가 주어지는데, 짬을 내서 과제가 나오기 전 1주차 후기 및 공부한 부분을 적어보고자 한다.
1주차 과제를 진행할 때나, 끝나고 나머지 크루 분들의 코드를 볼 때도
나보다 훨씬 뛰어난 사람이 많다는 것을 알 수 있었다.
처음에는 상당히 좌절하고 힘들었지만, 나보다 뛰어난 크루분들의 코드를 흡수해서
나도 더 발전해야겠다는 의지가 생기게 되었다.
이제 1주차 진행 시 힘들었던 점과 공부한 부분을 기록해보겠다!
힘들었던 점
1. 메소드, 변수명 네이밍
알맞은 영어 어휘가 생각나지 않아서 너무 시간을 많이 썼다.
2. 2번 문제 로직 구현
2번 문제 로직 구현이 쉽지 않았는데, 검색해보니 정규식을 사용하면 됐다.
하지만, 정규식도 생소해서 해당 조건에 맞는 정규식을 짜기가 힘들었다.
여기서 검색의 중요성을 느꼈다.
단순하게 검색을 하는 것이 아니라 검색을 '잘' 하는 것이 중요하다고 느꼈다!
처음에도 검색을 했었지만 검색 키워드가 올바르지 않아서 풀이가 뜨지 않았었다.
이러한 검색을 '잘' 하는 능력이 부족해서 시간낭비를 했다고 생각한다.
(물론 혼자 힘으로 풀었으면 더 좋았겠지만..)
3. 6, 7번 문제 로직 구현
중복 리스트가 있어서 stream 사용 시 너무 애먹었다.
이번에도 최대한 처음부터 클린코드로 짜려다보니 시간이 너무 오래걸렸다.
바로 클린코드가 생각이 나지 않는다면, indent가 많아지더라도 구현부터 완료해보자.
공통 피드백 중 내가 지키지 못했던 것들
1. 공백도 코딩 컨벤션이다.
공백 관련해서 컨벤션이 따로 있을 줄은 상상하지 못했다.
지금까지 코딩을 해오면서도 고민을 해보긴 했지만 그때마다 찾아보지는 않고, 멋대로 했었던 것 같다.
그래서 이번 1주차에도 공백 관련 컨벤션을 지키지 못했다.
내 코드
private static void validateRecommendAlgorithm(String userId, List<List<String>> friendRelationships, List<String> visitors) {
if(validateUserLengthFail(userId)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + USER_ID_LENGTH_EXCEPTION_MESSAGE);
}
if(validateUserIdTypeFail(userId)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + USER_ID_REGEX_EXCEPTION_MESSAGE);
}
if(validateFriendRelationshipsSizeFail(friendRelationships)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + FRIEND_RELATIONSHIPS_SIZE_EXCEPTION_MESSAGE);
}
if(validateUserIdsFail(friendRelationships)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + USER_IDS_SIZE_EXCEPTION_MESSAGE);
}
if(validateVisitorsSizeFail(visitors)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + VISITORS_SIZE_EXCEPTION_MESSAGE);
}
}
👉 if문 뒤에 공백 없이 바로 소괄호가 오는 것을 볼 수 있다.
if, for, while, catch 같은 제어문은 공백을 삽입하고 소괄호를 시작해야한다.
좋은 예
if (maxLine > LIMITED) {
return false;
}
2. IDE의 코드 자동 정렬 기능 활용
이전에 IDE에 코드 자동 정렬 기능이 있다는 걸 배웠지만, 자주 쓰지 않아서 까먹고 있었다.
항상 코드가 완성 되면 ⌥ + ⌘ + L으로 코드 자동 정렬을 하는 습관을 들이자.
3. Java에서 제공하는 API를 적극 활용
공통 피드백에서 메소드를 직접 구현하기 전에 Java API에서 제공하는 기능인지 먼저 검색 후,
제공하지 않을 경우에 직접 구현하라고 되어 있었다.
생각해보면, 복잡한 로직의 경우에는 검색을 통해 알아봤지만,
간단한 로직의 경우에는 Java가 제공하는 API가 있는지 검색조차 안 해보고
간단하니 직접 구현해서 사용하곤 했었다.
예시로, 리스트에서 사용자 출력 시 2명 이상일 때 쉼표로 구분하여 출력하는 기능은
Java에서 제공하는 String.join()을 통해 아래와 같이 간단하게 구현 없이 사용할 수 있다.
List<String> members = Arrays.asList("pobi", "jason");
String result = String.join(",", members); // "pobi,jason"
공부한 부분
1. List -> HashSet 사용, HashSet 사용법
자료 구조 중 거의 List만 사용해봐서 처음에 관성적으로 List로 설계했다가,
중복 값을 제거해야된다는 것을 깨닫고, stream.distinct()로 중복을 제거하려고 생각했었다.
그때, 자료 구조 중에 중복을 허용하지 않는 자료 구조는 없을까? 해서 검색한 후에, HashSet을 찾게 되었다.
관성적으로 List만 사용하던 나에게 생각의 폭을 넓혀준 것 같다.
HashSet 초기화 및 기본 기능은 다음과 같이 사용하면 된다. (메소드들은 List와 거의 같다)
※ 저장 순서는 비선형구조이므로 보장되지 않는다.
* 초기화
HashSet<String> names = new HashSet<>();
* 값 추가
names.add("ksh");
* 값 삭제
names.remove("ksh");
* 모든 값 삭제
names.clear();
2. static 메소드는 외부에서 호출할 것이 아니라면 private으로 선언하자
public class Problem {
public static String solution(String momWord) {
validateWord(momWord);
return convertWord(momWord);
}
private static String convertWord(String word) { ... }
private static void validateWord(String momWord) { ... }
}
※ 개발자가 의도한 올바른 Problem 클래스 사용법
👉 외부에서 Problem.solution 만을 호출하여, solution() 안의
validateWord() 와 convertWord()를 solution() 안에서 호출되도록 설계
이러한 설계일 때, validateWord() 와 convertWord() 를 private이 아닌 public으로 선언하면,
Problem.converWord() , Problem.validateWord() 등 개발자가 의도하지 않은 대로 실행될 수 있다.
따라서, static이더라도 외부에 호출되지 않게 설계한 메소드들은 private을 붙여서 접근을 막자!
3. stream은 Collection의 값 추가, 변경, 삭제가 되지 않는다.
private static List<String> getUserFriends(String userId, List<List<String>> friends) {
friends.stream()
.filter(friend -> friend.contain(userId))
.forEach(friend -> friend.remove(userId))
...
}
👉 stream.filter()로 userId를 포함하는 리스트를 가져오고, 그 리스트에서 userId 요소를 지우는 과정을 한꺼번에 수행하기 위해
위처럼 코드를 짰는데, friend.remove(userId) 부분에서 UnsupportedOperationException 예외가 발생했다.
구글링 해보니, 이러한 예외가 발생하는 이유는 Collection을 new 로 초기화시켜주지 않았는데
Collection의 메소드들(add, remove, ...)를 사용하면 지원하지 않는 연산 예외를 발생시키는 것이었다.
stream() 은 초기화를 하지 않고 요소를 뽑아내서 반복시켜주는 역할이기 때문에,
그 과정에서 Collection의 메소드들을 사용하면 예외를 발생시키는 것이다.
물론, .collect(Collectors.toList()) 를 사용해서 List로 반환하더라도,
마찬가지로 초기화한 List가 아니기 때문에 Collection의 메소드들을 사용하지 못한다!
Collection의 값을 추가, 변경, 삭제하려면 stream() 밖에서 진행해야한다.
4. 조건식에 부정문 대신 긍정문을 사용하자
리팩토링 전 - 부정문 사용
private static void validateCryptogram(String cryptogram) {
if(!validateCryptogramType(cryptogram)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + CRYPTOGRAM_TYPE_EXCEPTION_MESSAGE);
}
}
private static boolean validateCryptogramType(String cryptogram) {
return (Pattern.compile(CRYPTOGRAM_TYPE_REGEX).matcher(cryptogram).matches());
}
👉 조건문에 호출되는 검증 메소드에 부정을 없애고(검증을 통과하면 true),
조건문에서 검증 메소드를 호출하고 부정문을 사용하여 검증을 통과하지 못한 요소를 예외처리하고 있다.
이렇게 하면, 조건문 if(!validateCryptogramType(cryptogram)을 딱 봤을 때 부정문이 들어갔기 때문에 의미가 헷갈리게 된다.
따라서, 아래와 같이 리팩토링하는게 좋다.
리팩토링 후 - 긍정문 사용
private static void validateCryptogram(String cryptogram) {
if(validateCryptogramTypeFail(cryptogram)) {
throw new IllegalArgumentException(EXCEPTION_MESSAGE_PREFIX + CRYPTOGRAM_TYPE_EXCEPTION_MESSAGE);
}
}
private static boolean validateCryptogramTypeFail(String cryptogram) {
return (!Pattern.compile(CRYPTOGRAM_TYPE_REGEX).matcher(cryptogram).matches());
}
👉 조건문에 호출되는 검증 메소드에 부정을 넣고(검증 실패가 true),
조건문에서 검증 메소드를 호출할 때 부정문을 사용하지 않고 호출한다.
호출되는 검증 메소드는 부정문을 사용하여 의미가 명확하지않지만, 대신에 메소드 네이밍에 Fail 을 추가하여 의미를 명확하게 나타냈다.
이렇게 호출되는 검증 메소드에 부정을 넣고 메소드 네이밍으로 실패 시 true인 의미를 명확하게 하고,
조건식에는 부정문을 없애는 것이 훨씬 가독성에 좋다.
5. 자료 구조를 static으로 선언할 때는 final을 붙이자
* static
private static Map<Integer, Integer> moneyMap = new LinkedHashMap<>();
...
moneyMap = new HashMap<>(); (OK -> 재할당 가능)
👉 moneyMap을 LinkedHashMap으로 초기화하고 이후 코드에서 사용할 의도로 static을 붙여줬는데,
이후 코드에서 위처럼 HashMap으로 재할당이 가능해진다.
따라서, 개발자는 LinkedHashMap으로 설계했는데, 코드 진행 시 HashMap으로 변경되는 것이다.
이러한 상황을 방지하기 위해 재할당을 막는 키워드 final 을 붙여서 사용하자!
* static final
private static final Map<Integer, Integer> moneyMap = new LinkedHashMap<>();
...
moneyMap = new HashMap<>(); (X -> final이 있기 때문에 재할당 불가능)
6. git commit 시 메시지에 scope도 작성하자
※ AngularJS 커밋 컨벤션
커밋 메시지 헤더
<type>(<scope>): <short summary>
│ │ │
│ │ └─⫸ 명령문, 현재 시제로 작성합니다. 대문자를 사용하지 않으며, 마침표로 끝내지 않습니다.
│ │
│ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
│ elements|forms|http|language-service|localize|platform-browser|
│ platform-browser-dynamic|platform-server|router|service-worker|
│ upgrade|zone.js|packaging|changelog|dev-infra|docs-infra|migrations|
│ ngcc|ve
│
└─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
The <type> and <summary> fields are mandatory, the (<scope>) field is optional.
이때, 나의 경우에는 1주차 커밋 메시지를 scope를 빼고 작성해버렸다.
물론, scope는 필수가 아니라 선택 사항이지만 가독성을 위해 넣는 것이 좋을 것 같았다.
1주차에는 문제가 1번부터 7번 총 7개가 나왔는데, 커밋 당시 생각은
1번~7번을 구분할 메시지를 안 적어줘도,
직접 문제 파일에 들어가서 History를 보면 상관없겠다’고 생각하고 적어주지 않았다.
그렇게 Pull Request를 한 후에 scope를 왜 써야하는지 깨닫게 되었다.
Pull Request에서는 이렇게 파일 별이 아니라 커밋별로 나오기 때문에,
scope를 쓰지 않아서 문제를 구분하지 않으니 어떤 문제를 커밋했는지 한 눈에 알기가 힘들었다.
다음 주차부터는 꼭 커밋 시 scope도 작성해야겠다.
* 기존 내가 했던 커밋 방식 (scope 제외)
feat : 암호를 해독하는 기능 메소드 추가
* scope를 추가한 커밋 방식
feat(Problem1) : 암호를 해독하는 기능 메소드 추가
참고 링크
[GitHub] 깃 커밋 메세지 컨벤션 (Git Commit Message Convention)
'우아한테크코스 5기 프리코스' 카테고리의 다른 글
우아한테크코스 5기 1차 합격 & 최종 코딩테스트 후기 (7) | 2022.12.18 |
---|---|
우아한테크코스 프리코스 4주차 후기 (0) | 2022.11.23 |
우아한테크코스 프리코스 3주차 후기 & 공부한 부분 (0) | 2022.11.16 |
우아한테크코스 프리코스 2주차 후기 & 공부한 부분 (4) | 2022.11.09 |
우아한테크코스 5기 프리코스 시작! 프리코스 시 지킬 나만의 규칙 (0) | 2022.11.02 |