관리 메뉴

100세까지 코딩

[우테코] 종합 피드백 본문

백엔드/스프링 입문

[우테코] 종합 피드백

100세까지 코딩 2023. 11. 26. 21:46

1. 커밋 메시지를 의미 있게 작성한다

커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성한다.

 

2. git을 통해 관리할 자원에 대해서도 고려한다

.class 파일은 java 코드가 있으면 생성할 수 있다. 따라서 .class 파일은 굳이 git을 통해 관리하지 않아도 된다.

IntelliJ IDEA.idea 폴더, Eclipse.metadata 폴더 또한 개발 도구가 자동으로 생성하는 폴더이기 때문에 굳이 git으로 관리하지 않아도 된다.

 

3. 이름을 통해 의도를 드러낸다

 

4. 축약하지 않는다

의도를 드러낼 수 있다면 이름이 길어져도 괜찮다.

클래스와 메서드 이름을 한 두 단어로 유지하려고 노력하고 문맥을 중복하는 이름을 자제하자.

짧게 ship()이라고 하면 클라이언트에서는 order.ship()라고 호출

 

5. 공백 라인을 의미 있게 사용한다

공백 라인을 의미 있게 사용하는 것이 좋아 보이며, 문맥을 분리하는 부분에 사용하는 것이 좋다.

과도한 공백은 다른 개발자에게 의문을 줄 수 있다.

 

6. 의미 없는 주석을 달지 않는다

모든 변수와 함수에 주석을 달기보다 가능하면 이름을 통해 의도를 드러내고, 의도를 드러내기 힘든 경우 주석을 다는 연습을 한다.

 

7. Java에서 제공하는 API를 적극 활용한다

사용자가 2명 이상이면 쉼표(,) 기준으로 출력을 위한 문자열은 다음과 같이 구현 가능하다.

 

List<String> members = Arrays.asList("pobi", "jason");

String result = String.join(",", members); // "pobi,jason"

 

8. 배열 대신 Java Collection을 사용한다

Java Collection 자료구조(List, Set, Map )를 사용하면 데이터를 조작할 때 다양한 API를 사용할 수 있다.

예를 들어 List<String>"pobi"라는 값이 포함되어 있는지는 다음과 같이 확인할 수 있다.

 

List<String> members = Arrays.asList("pobi", "jason");

boolean result = members.contains("pobi"); // true

 

9. README.md를 상세히 작성한다

미션 저장소의 README.md는 소스코드에 앞서 해당 프로젝트가 어떠한 프로젝트인지 마크다운으로 작성하여 소개하는 문서이다.

해당 프로젝트가 어떠한 프로젝트이며, 어떤 기능을 담고 있는지 기술하기 위해서 마크다운 문법을 검색해서 학습해 보고 적용해 본다.

 

10. 기능 목록을 재검토한다

기능 목록을 너무 상세하게 작성하지 않는다.

클래스 이름, 함수 시그니처와 반환값은 언제든지 변경될 수 있기 때문이다.

구현해야 할 기능 목록을 정리하는 데 집중한다.

정상적인 경우도 중요하지만, 예외적인 상황도 기능 목록에 정리한다.

특히 예외 상황은 시작 단계에서 모두 찾기 힘들기 때문에 기능을 구현하면서 계속해서 추가해 나간다.

 

11. 기능 목록을 업데이트한다

시작할 때 모든 기능 목록을 완벽하게 정리해야 한다는 부담을 가지기보다 기능을 구현하면서 문서를 계속 업데이트한다. 죽은 문서가 아니라 살아있는 문서를 만들기 위해 노력한다.

 

12. 값을 하드 코딩하지 않는다

문자열, 숫자 등의 값을 하드 코딩하지 마라.

상수(static final)를 만들고 이름을 부여해 이 변수의 역할이 무엇인지 의도를 드러내라.

 

13. 구현 순서도 코딩 컨벤션이다

클래스는 상수, 멤버 변수, 생성자, 메서드 순으로 작성한다.

class A {
상수(static final) 또는 클래스 변수

인스턴스 변수

생성자

메서드
}

 

14. 변수 이름에 자료형은 사용하지 않는다

변수 이름에 자료형, 자료 구조 등을 사용하지 마라.

 

String carNameList = Console.readLine();

String[] arrayString = carNameList.split(",");

 

15. 한 함수가 한 가지 기능만 담당하게 한다

함수 길이가 길어진다면 한 함수에서 여러 일을 하려고 하는 경우일 가능성이 높다.

아래와 같이 한 함수에서 안내 문구 출력, 사용자 입력, 유효값 검증 등 여러 일을 하고 있다면 이를 적절하게 분리한다.

 

public List<String> userInput() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
String userInput = Console.readLine().trim();
String[] splittedName = userInput.split(",");
for (int index = 0; index < splittedName.length; index++) {
if (splittedName.length < 1 || splittedName.length > 5) {
throw new IllegalArgumentException("[ERROR] 자동차 이름은 1자 이상 5자 이하만 가능합니다.");
}
}
return Arrays.asList(splittedName);
}

 

16. 함수가 한 가지 기능을 하는지 확인하는 기준을 세운다

만약 여러 함수에서 중복되어 사용되는 코드가 있다면 함수 분리를 고민해 본다.

또한, 함수의 길이를 15라인을 넘어가지 않도록 구현하며 함수를 분리하는 의식적인 연습을 할 수 있다.

 

17. 테스트를 작성하는 이유에 대해 본인의 경험을 토대로 정리해본다

단지 기능을 점검하기 위한 목적으로 테스트를 작성하는 것은 아니다.

테스트를 작성하는 과정을 통해서 나의 코드에 대해 빠르게 피드백을 받을 수 있을 뿐만 아니라 학습 도구(학습테스트를 통해 JUnit로도 활용할 수 있다.

이런 경험을 통해 테스트에 대해 어떤 유용함을 느꼈는지 알아본다.

 

18. 처음부터 큰 단위의 테스트를 만들지 않는다

테스트의 중요한 목적 중 하나는 내가 작성하는 코드에 대해 빠르게 피드백을 받는 것이다.

시작부터 큰 단위의 테스트를 만들면 작성한 코드에 대한 피드백을 받기까지 많은 시간이 걸린다.

 

19. 큰 단위의 테스트

자동차경주를 시작해서 사용자가 이름, 진행 횟수를 입력하면, 게임을 진행한 후 그 결과를 알려준다.

작은 단위의 테스트

무작위 값이 4 이상이면 자동차가 전진한다.

무작위 값이 3 이하이면 자동차가 전진하지 않는다.

 

20. 비즈니스 로직과 UI 로직을 분리한다

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다.

public class Lotto {
private List<Integer> numbers;

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
public boolean contains(int number) {
}

// UI 로직
private void print() {
}
}

현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString() 통해 구현한다.

View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.

 

21. 연관성이 있는 상수static final 대신 enum을 활용한다

public enum Rank {
FIRST(6, 2_000_000_000),
SECOND(5, 30_000_000),
THIRD(5, 1_500_000),
FOURTH(4, 50_000),
MISS(0, 0);

private int countOfMatch;
private int winningMoney;

private Rank(int countOfMatch, int winningMoney) {
this.countOfMatch = countOfMatch;
this.winningMoney = winningMoney;
}
}

 

22. final 키워드를 사용해 값의 변경을 막는다

최근에 등장하는 프로그래밍 언어들은 기본이 불변 값이다. 자바는 final 키워드

public class Money {
private final int amount;

public Money(int amount) {
}

 

23. 객체의 상태 접근을 제한한다

인스턴스 변수의 접근 제어자는 private으로 구현한다.

public class WinningLotto {
private Lotto lotto;
private Integer bonusNumber;

public WinningLotto(Lotto lotto, Integer bonusNumber) {
this.lotto = lotto;
this.bonusNumber = bonusNumber;
}
}

 

 

24. 객체는 객체스럽게 사용한다

이 객체는 로직에 대한 구현은 하나도 없고, numbers에 대한 getter 메서드만을 가진다.

ex) 나쁜 예
public class Lotto {
private final List<Integer> numbers;

public Lotto(List<Integer> numbers) {
this.numbers = numbers;
}

public int getNumbers() {
return numbers;
}
}

public class LottoGame {
public void play() {
Lotto lotto = new Lotto(...);

// 숫자가 포함되어 있는지 확인한다.
lotto.getNumbers().contains(number);

// 당첨 번호와 몇 개가 일치하는지 확인한다.
lotto.getNumbers().stream()...
}
}

ex) 좋은 예
public class Lotto {
private final List<Integer> numbers;

public boolean contains(int number) {
// 숫자가 포함되어 있는지 확인한다.
}

public int matchCount(Lotto other) {
// 당첨 번호와 몇 개가 일치하는지 확인한다.
}
}

public class LottoGame {
public void play() {
Lotto lotto = new Lotto(...);
lotto.contains(number);
lotto.matchCount(...);
}
}

 

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

ex2)
public class Car implements Comparable<Car> {
...
public boolean isSamePosition(Car other) {
return other.position == this.position;
}

@Override
public int compareTo(Car other) {
return this.position - other.position;
}
...
}

public class Cars {
...
public List<String> findWinners() {
final Car maxPositionCar = findMaxPositionCar();
return findSamePositionCars(maxPositionCar);
}

private Car findMaxPositionCar() {
Car maxPositionCar = cars.stream()
.max(Car::compareTo)
.orElseThrow(() -> new IllegalArgumentException("차량 리스트가 비었습니다."));
}

private List<String> findSamePositionCar(Car maxPositionCar) {
return cars.stream()
.filter(maxPositionCar::isSamePosition)
.map(Car::getName)
.collect(Collectors.toList());
}

 

Collection 인터페이스를 사용하는 경우 외부에서 getter메서드로 얻은 값을 통해 상태값을 변경할 수 있다.

public List<Car> getCars() {
return cars;
} (x)

public List<Car> getCars() {
return Collections.unmodifiableList(cars);
} (o)

 

이처럼 Collections.unmodifiableList() 와 같은 Unmodifiable Collecion 을 사용해

외부에서 변경하지 못하도록 하는 게 좋다.

 

25 .필드(인스턴스 변수)의 수를 줄이기 위해 노력한다

public class LottoResult {
private Map<Rank, Integer> result = new HashMap<>();
private double profitRate;
private int totalPrize;
}

public class LottoResult {
private Map<Rank, Integer> result = new HashMap<>();

public double calculateProfitRate() { ... }

public int calculateTotalPrize() { ... }
}

 

26. 테스트 코드도 코드다

@DisplayName("천원 미만의 금액에 대한 예외 처리")
@ValueSource(strings = {"999", "0", "-123"})
@ParameterizedTest
void underLottoPrice(Integer input) {
assertThatThrownBy(() -> new Money(input))
.isInstanceOf(IllegalArgumentException.class);
}

 

테스트 하기 어려운 메서드

public class Car {

private static final int MOVABLE_LOWER_BOUND = 4;
private static final int RANDOM_NUMBER_UPPER_BOUND = 10;

private final String name;
private int position;

public void move() {
final int number = random.nextInt(RANDOM_NUMBER_UPPER_BOUND);

if (number >= MOVABLE_LOWER_BOUND) {
position++;
}
}

public void move(int number) {
if (number >= MOVABLE_LOWER_BOUND) {
position++;
}
}

 

27. private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다.

public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다.

하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다.

다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.