우테코 5기 프리코스 2주차 회고
우테코 프리코스 2주차 회고
공통 피드백
1주차가 끝이 나고 공통 피드백이 왔다.
- 요구사항을 정확히 준수한다
- 커밋 메시지를 의미 있게 작성한다
- git을 통해 관리할 자원에 대해서도 고려한다
- Pull Request를 보내기 전 브랜치를 확인한다
- PR을 한 번 작성했다면 닫지 말고 추가 커밋을 한다
- 이름을 통해 의도를 드러낸다
- 축약하지 않는다
- 공백도 코딩 컨벤션이다
- 공백 라인을 의미 있게 사용한다
- space와 tab을 혼용하지 않는다
- 의미 없는 주석을 달지 않는다
- IDE의 코드 자동 정렬 기능을 활용한다
- Java에서 제공하는 API를 적극 활용한다
- 배열 대신 Java Collection을 사용한다
비록 1:1이 아닌 공통 피드백이지만 누군가에게 이렇게 상세한 피드백을 듣는다는 게 처음이라 너무 좋아 습득할 때까지 계속 반복하여 읽었다. 1주 차에 직접 컨벤션들을 찾아보고 정리를 한 게 있어 좀 더 수월하게 읽혔던 것 같다.
피어 리뷰 스터디
1주 차가 끝난 뒤 깃허브에 우테코 커뮤니티가 생성되었는데 그중 핵심 기능은 피어 리뷰라고 생각한다. 하지만 지금 프리코스 참가자만 무려 3000명 정도 되기 때문에 그냥 올린다면 수많은 글에 묻혀갈 확률이 높다고 생각했고 그래서 피어 리뷰 스터디를 구하게 되었다. 다행히 1주 차가 끝난 다음날 바로 스터디를 구하는 사람들이 있어 참가하게 되었고 해당 스터디 내용은 아래처럼 진행하게 되었다.
code review ref
- https://github.com/woowacourse/woowacourse-docs/tree/code-review/codereview
- https://soojin.ro/review/review-comments
코드 리뷰를 하고 나니 사람들마다 코드 작성 방법(코딩 스타일, 구현 방법)이 다 달랐던 것 같고 각자 방법마다 배울 점도 있었고 아쉬운 점도 있었기 때문에 그런 부분에 대해서 코드 리뷰도 하고 서로 리뷰를 하다가 토론할 주제가 있으면 서로 의견을 주고받기도 했다.
그중 두 가지 주제를 보자면
1. 부정문을 지양하라
우리 코드들에서 if로 조건을 확인할 때 아래처럼 !(부정문)로 쓰여진게 꽤 있었는데 사람은 부정문을 보면 익숙하지 않기 때문에 한 번 더 생각해야 되서 지양해야 된다는 컨벤션이 있었다.
if (!isValid)
return -1;
로직...
return result;
위에 대한 의견으로 두 가지가 나올 수 있었는데, 첫 번째는 아래처럼 설계하여 긍정문을 사용하는 것
if(isValid) {
로직
return result;
}
return -1;
두 번째는 부정조건문을 사용하는 예외 경우가 있었다.
- Early Return을 사용할 때
- Form Validation할 때
- 보안 또는 검사하는 로직에서
나는 원래 두 번째로 했기 때문에 이번 2주 차에서는 첫 번째로 한번 해보자 생각했고 아래처럼 수정하게 되었다.
음.. 지금까지 두 번째로만 사용해와서 그런가 뭔가 고치고도 어색했던 것 같다.
2. 중간 변수가 선언돼야 될까?
우리 코드들을 보면 어떤 것에 반환을 받으면 가독성을 위해 아래의 예처럼 중간 변수를 선언해 주고 있었다.
User user = getUser()
nickname = getNickname(user);
return nickname;
하지만 이전에 박재성 님의 우테코 세미나에서 이런 변수 중복 없이 바로 넣었던 기억이 있어 어차피 이름을 명확하게 지어서 이미 가독성이 확보된 상태에서 추가적으로 뭔가 더 해줄 필요가 없지 않을까 의견을 전달했다.
그리고 좀 더 확실하게 하기 위해 클린 코드 책에서도 추가적인 내용을 공부하였는데 중복을 없애라는 표현이 있었고 두 번째 사진에서 세 번째 사진처럼 중복을 제거하는 걸로 보아 좀 더 납득할 수 있었다.
지난 주 목표
지난 주에 아래 두 가지 목표를 세웠었다.
- 컨벤션, 클린코드에 대해 더 공부하고 익숙해지기
- 객체 지향에 대한 공부
컨벤션, 클린코드에 대해 더 공부하고 익숙해지기
1주 차를 진행하면서 컨벤션에 익숙하지 않았기 때문에 1주 차 코드를 보면 같은 컨벤션이라도 뒤죽박죽이다. 예를 들어 어느 곳은 1줄짜리에도 중괄호를 했고 어느 곳은 하지 않았다. 또는 boolean 반환하는 메서드도 어느 곳은 isXXX 가 되있고 어느 곳은 안 되어있다. 이 역시 코드 리뷰에서 지적 당했다ㅠㅠ
그래서 이번 2주 차에는 이런 컨벤션 부분에서는 실수를 하지 않기 위해 1주 차 컨벤션 다시 복습, 피어 리뷰에서 지적된 것 공부, 공통 피드백, 2주차 우테코 에서 제공한 컨벤션을 추가 정리하고 나서 코드 구현에 들어가게 되었다.
확실히 이렇게 하고 코드를 구현하니깐 익숙해진 것인지 컨벤션을 계속해서 들여다 볼일이 잘 없어서 시간이 단축되었고 1주 차 보다 좀 더 수월하게 코드를 구현한 것 같다.
객체 지향에 대한 공부
1주 차에서 설계를 하는데 객체 지향에 대해 부족한 게 많은 것 같아 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴이라는 책을 보고 추가 공부를 하게 되었다. 그중 2주 차를 할 때 중점적으로 참고한 것에 대해 간단하게 정리해 보자면
객체 지향 설계 과정
객체 지향 설계란 다음의 작업을 반복하는 과정이라 할 수 있다.
- 제공해야 할 기능을 세분화하고, 그 기능을 알맞은 객체에 할당한다.
- A. 기능을 구현하는데 필요한 데이터를 객체에 추가한다. 객체에 데이터를 먼저 추가하고 그 데이터를
- B. 기능은 최대한 캡슐화해서 구현한다.
- 객체 간에 어떻게 메시지를 주고받을 지 결정한다.
- 과정1과 과정2를 개발하는 동안 지속적으로 반복한다.
예를 들어 아래와 같은 기능 목록이 있으면
- 파일에서 데이터 읽기
- 데이터를 암호화하기
- 파일에 데이터 쓰기
이들 기능을 제공할 객체 후보를 찾고 각 객체가 어떻게 연결되는지 그려볼 수 있다.
객체의 크기는 한 번에 완성되기 보다는 구현을 진행하는 과정에서 점진적으로 명확해진다. 위 그림에서 암호화 객체는 실제로는 다음의 두 기능을 함께 제공하고 있다.
- 흐름 제어 (데이터 읽고, 암호화하고, 데이터 쓰고)
- 데이터 암호화
처음에는 이것이 불명확한 경우가 많다. 구현을 진행하는 과정에서 암호화 알고리즘을 변경해야 할 때, 데이터 암호화 기능과 흐름 제어가 한 객체에 섞여 있다는 것을 알게 될 수도 있다. 또는, 암호화 기능만 테스트하고 싶은데, 흐름 제어 기능과 암호화 기능이 섞여 있어서 암호화 기능만 테스트하는 것이 힘들 때 알게 될 수도 있다.
구현 과정에서 이렇게 한 클래스에 여러 책임이 섞여 있다는 것을 알게 되면 아래와 같이 객체를 만들어서 책임을 분리하게 된다.
이처럼 객체 설계는 한 번에 완성되지 않고 구현을 진행해 나가면서 점진적으로 완성된다. 이는 최초에 만든 설계가 완벽하지 않으며, 개발이 진행되면서 설계도 함께 변경된다는 것을 의미한다. 따라서 설계를 할 때에는 변경되는 부분을 고려한 유연한 구조를 갖도록 노력해야 한다.
이를 기반으로 2주차 숫자 야구 게임에서는 처음에는 Computer, User가 생성되었고 그리고 구현하는 과정에서 결과 객체인 Ball 객체와 흐름제어 객체인 Game 객체가 점진적으로 구현되었다.
private 테스트 코드?
그리고 점진적으로 구현되는 과정에서 클래스마다 각 메소드를 테스트하는 과정을 거치면서 진행하게 되었는데 1주 차에서도 테스트 코드를 작성하며 진행했기 때문에 큰 어려움은 없었다. 하지만 어떤 기능을 나타내는 메소드가 private인 경우 테스트를 할 수 없었기 때문에 이를 public으로 해야 되나 아니면 Java의 Reflection이라는 걸 이용해서 해야 되나 아니면 그냥 넘어가야 되나 하고많은 고민을 했다.
그냥 모든 메소드를 테스트하려면은 public으로 하면 편하겠지만 그렇게 되면 접근제어자라는 의미가 없다고 생각했다. 그리고 Reflection을 사용하더라도 private 메소드에 대한 테스트는 깨지기 쉬운 테스트가 되고 테스트에 대한 비용을 증가시키는 요인이 될 수 있다고 한다. 또한 리플렉션 자체 역시 컴파일 에러를 유발하지 못하므로 최대한 사용을 자제해야 된다.
그래서 테스트 같은 경우 우선 public 메소드를 대상으로 진행하게 되었고 성공하는 케이스뿐 아니라 실패하는 케이스까지 모두 나누어 테스트를 진행하여 private 메소드까지 커버할 수 있는 테스트 코드를 작성하게 되었다.
2주차 진행 과정
위에 과정들을 하고 2주차를 시작했기 때문에 상당히 바쁜 시간이였던 것 같다. 그리고 코드를 구현하기 전까지 추가 컨벤션, 라이브러리 분석, 구현 기능 목록까지 작성했기 때문에 구현을 시작하기 까지 시간이 상당히 걸렸다.
- 우테코에서 제공한 컨벤션에 맞춰 추가 컨벤션 작성
- 라이브러리 분석
- 구현 기능 목록 작성
- 코드 구현
- 리팩터링 및 제출
1. 추가 컨벤션 작성
1주차에서는 직접 찾아 정리한 것이기 때문에 2주차에는 우테코에 제공한 컨벤션에 맞춰 추가적으로 작성했다.
참고:
- https://github.com/woowacourse/woowacourse-docs/tree/main/styleguide/java
- https://google.github.io/styleguide/javaguide.html
- https://github.com/JunHoPark93/google-java-styleguide
임포트는 다음과 같은 단계를 따른다: 만약에 static과 non-static이 둘 다 있다면, 개행을 하고 두 개의 블럭으로 나눈다. 그 이외에는 개행이 있으면 안된다. static 임포트는 static 중처버 클래스에 사용되지 않는다. 그것들은 일반적인 임포트를 사용한다. 괄호는 if, else, for, do, while 구문에 쓰이는데 몸체가 없거나 한 줄의 구문에도 괄호가 쓰인다. 괄호는 비어있지 않은 블럭과 block-like construct에서 Kernighan과 Ritchie 스타일(Egyptian brackets)을 따른다. Java 코드의 열 제한은 120자입니다. “문자”는 유니코드 코드 포인트를 의미합니다. 패키지명은 전부 소문자로 단순히 서로 뭍여서 연속된 단어로 이루어져 있다. (언더스코어 없음) 예를들어 com.example.deepspace같은 형식이다. com.example.deepSpace혹은com.example.deep_space 는 잘못되었다. 클래스 이름은 UpperCamelCase 이다. 클래스 이름은 전형적으로 명사나 명사 구이다. 예를들어, Character 혹은 ImmutableList 처럼 말이다. 인터페이스의 이름은 명사나 명사구가 될 수 있다. 예를들어 List. 그러나 가끔은 형용사나 형용사구가 대신 쓰이기도 한다 (예를들어 Readable) 테스트 클래스들은 테스트하려는 클래스의 이름이 앞에오고 끝에 Test를 붙여준다. 예를들어 HashTest 혹은 HashIntegrationTest 함수 이름은 lowerCamelCase 이다. 함수 이름은 전형적으로 동사 혹은 동사 구이다. 예를들어, sendMessage 나 stop이다. 언더스코어는 JUnit 테스트에서 논리적 컴포넌트를 분리시키기 위해 각각을 lowerCamelCase로 변경시켜 나올수 있다. 하나의 전형적인 패턴은_ 이다. 예를들어 pop_emptyStack. 테스트 메소드를 작성하는 하나의 정확한 방법은 없다. 상수는 CONSTANT_CASE를 사용한다: 모두 대문자이고 각 단어는 하나의 언더스코어로 구분하는 형식. 하지만 정확히 상수는 무엇인가? 상수는 static final 필드 인데 그것은 변경될 수 없고 그것들의 메소드는 부작용이 보여서는 안된다. 이것은 원시타입, 문자열 그리고 불변 타입, 불변타입의 불변 컬렉션을 포함한다. 만약 어떤 인스턴스의 상태가 바뀐다면 그것은 상수가아니다. 상수가 아닌 필드 이름은 (static 같은) lowerCamelCase로 작성한다. 이러한 이름들은 전형적으로 명사나 명사구이다. 예를들어, computedValues 혹은 index. 파라미터 이름은 lowerCamelCase 이다. public 메서드에서 한개의 문자를 가진 파라미터는 피해야 한다. 지역변수는 lowerCamelCase 이다. 심지어 final 이나 불변, 지역변수는 상수로 간주되어서는 안되고 상수 스타일로 기술해서도 안된다. @Override가 사용가능할 때 이 애노테이션을 붙인다. 이것은 클래스가 슈퍼 클래스의 메서드를 오버라이딩을 하는 것을 나타내기도하고 인터페이스의 메서드를 구현하는 것을 나타낼 수도 있다. 예외: 부모 함수가 @Deprecated가 되면 @Override를 생략할 수 있다. 아래 명시되있는 것말고 예외를 잡고 아무것도 안하는 것은 거의 있을 수 없다. (전형적인 반응은 로그를 남기는 것 혹은 불가능하다고 간주되면 AssertionError로 다시 던져준다) 정말로 캐치블럭에서 아무것도 하지 않는것이 정당하다면 주석을 남기는것으로 정당화한다. 예외: 테스트에서 예외를 잡는 부분은 expected, 혹은 expected로 시작하는 이름을 지으면서 무시할 수 있다. 다음 예제는 테스트에서 예외가 나오는게 확실한 상황에서 사용되는 대중적인 형식으로 주석이 필요가 없다. 깃 컨벤션의 경우 저번과 같기 때문에 생략
추가 컨벤션(Convention)
자바 컨벤션(Java Convention)
순서와 공간
클래스에는 static 임포트를 하지 않는다.
괄호는 선택사항에서도 쓰인다.
비어있지 않은 블럭: K & R 스타일
열 제한: 120
패키지 이름
클래스 이름
함수 이름
상수 이름
상수가 아닌 필드의 이름
파라미터 이름
지역변수 이름
@Override: 항상 사용한다
예외 잡기: 생략 하지 말것
try {
int i = Integer.parseInt(response);
return handleNumericResponse(i);
} catch (NumberFormatException ok) {
// 숫자가 아니다; 괜찮으니 그냥 넘어간다
}
return handleTextResponse(response);
try {
emptyStack.pop();
fail();
} catch (NoSuchElementException expected) {
}
추가 요구사항
깃 컨벤션
2. 라이브러리 분석
이번에 프로그래밍 요구사항 중에 camp.nextstep.edu.missionutils 에서 제공하는 Random, Console API를 사용하여 구현해야 되기 때문에 어떻게 사용해야 될지, 어떤 함정이 있을지 알아보기 위해 먼저 Random, Console에 대해 분석을 하였다.
그리고 마지막에 ApplicationTest에서도 기존의 Assertions를 사용하지 않고 직접 만들어 사용한 Assertions에 대해서도 궁금증이 생겨 추가적으로 분석했다.
sourceClosed : 자원이 종료되었는지 확인 변수(Boolean is true if source is done) ThreadLocalRandom : 자바7에서 추가된 기능으로 스레드 별로 난수 생성을 할 수 있는 랜덤 클래스, current() 라는 정적 메서드를 통해 객체를 얻도록 되어 있다. assertTimeoutPreemptively() : Executable을 실행해 TIMEOUT이 지나는 순간 테스트를 종료해 테스트가 성공한지 확인 왜 MockedStatic을 썼을까 궁금했다. Mockito는 final과 static 메서드를 mocking 하는걸 지원하지 않음 mocking은 이 static mock이 생성된 쓰레드에만 영향을 미치며 다른 쓰레드에서 이 객체를 사용하는 건 안전하지 않다. 이 객체의 ScopedMock.close()가 호출되면 static mock이 해제된다. 이 객체가 닫히지 않으면 static mock 객체는 시작 쓰레드에서 활성 상태로 유지된다. 따라서 예를 들어 JUnit 규칙이나 확장을 사용해 명시적으로 관리되는 경우가 아니면 try-with-resources 문 안에서 이 객체를 만드는 것이 좋다. 특정한 값이 아닌 임의이 값에 대해 실행하고 싶을 때 ArgumentMatchers를 이용해 인자 값을 지정하면 된다. Matchers 클래스는 anyList()뿐 아니라 anyInt(), anyString(), anyLong() 등 다양한 메서드를 제공한다. 참고:
라이브러리 분석
🔍 라이브러리 분석
Console 분석
readLine()
getInstance()
isClosed()
Randoms 분석
pickNumberInList()
pickNumberInRange()
pickUniqueNumbersInRange()
shuffle()
validateNumbers()
validateRange()
validateCount()
Assertions 분석
assertSimpleTest()
assertRandomTest()
assertRandomNumberInListTest
assertRandomNumberInRangeTest
assertRandomUniqueNumbersInRangeTest
assertShuffleTest
3. 구현 기능 목록 작성
컴퓨터 랜덤 숫자 생성 사용자 입력 숫자 비교 결과 출력 게임 종료시 추가 게임 여부 입력
구현 기능 목록
🚀 구현할 기능 목록
4. 코드 구현
https://github.com/woowacourse-precourse/java-baseball/pull/886
5. 리팩터링 및 제출
그렇게 코드를 구현하고 나서 다시 코드를 보며 좀 더 명확히 할 수 있는 건 없는지 혹은 안 지킨 컨벤션은 없는지 확인하며 추가적으로 리팩터링하여 최종적으로 제출하게 되었다.
- 좀 더 명확히 할 수 있는 것 메소드로 분리하여 명확히 하기
- 변수명 체크
- 메소드 분리했을 때 파라미터로 넘어 온 것들은 이름이 대충 지어져있는 것 수정
- for문에서도 i, j 같은 변수명 의미있는 변수명으로 수정
- 불필요한 것 제거
정리 및 후기
코드를 구현하기 전에 피어 코드 리뷰 스터디, 객체 지향 공부, 추가 컨벤션 정리, 라이브러리 분석 등 상당히 많은 시간을 써서 비교적 시작을 늦게 한거 같아 불안한 마음이 있었지만 확실히 이렇게 사전에 미리 공부하고 설계한 결과 구현할 때 수월하게 진행이 되어 시간이 부족하지 않았다.
다시 한번 구현 전 좋은 설계에 대한 중요성을 깨닫는 시간이었다. 하지만 아직 내가 프로젝트에 사용하던 구조인 MVC와는 상당히 거리가 멀었고 클래스를 분리했지만 여전히 Game 클래스에서 실행 흐름과 입출력 기능에 관해 동시 책임을 가지고 있었기 때문에 유연하지 않은 것 같았다.
그렇기 때문에 다음 주 목표로는 MVC 패턴에 대해 공부해서 도입해 볼 예정이다. 직접 프로젝트에서 스프링의 도움을 받아 자주 접해봤지만 직접 구조를 처음부터 설계하고 구현할 생각에 약간 두렵기도 하다. 하지만 동시에 설레는 이 마음은 무엇일까… ㅋㅋㅋ 또, 다음 주에 나는 어떻게 성장해있을지 궁금해진다.
우테코 2주 차 부터는 깃허브 커뮤니티가 생겼는데 위에서 말한 피어 리뷰 말고도 아고라, 학습 컨텐츠, 주간 회고록 카테고리가 있다. 많은 양의 좋은 글들이 올라오는데 뭔가 빠르게 지나가는 느낌이 들어 묻히는 글도 많은 것 같아 아쉽다.
말하는 것은 지식의 영역이고 듣는 것은 지혜의 영역이라고 한다. 말하는 것만큼 듣는 것도 중요하다고 최근에 좀 더 느끼게 되어서 시간이 날 때마다 올라온 글들을 읽고 의견을 남기는데 이는 상대에게도 독자가 생기게 되어 좋고 나에게도 상대방의 지식과 경험을 습득할 수 있어 좋은 것 같다.
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.