TDD, 클린코드 학습내용 정리

TDD

TDD와 단위테스트는 다름.
TDD: 테스트 먼저 짜고 프로덕션 코드를 구현하는것.

시작전 To do 리스트를 작성.

to do 리스트는 언제든지 프로젝트를 진행해나가면서 변경될 수 있다.

최초 실패하는 코드를 작성

그 후 프로덕션 코드를 고쳐 테스트를 통과하도록 해라

테스트에서 먼저 이뤄지라

테스트코드에서부터 프로덕트 코드를 만들어 나가는것에 익숙해져야함.
실제로 없는 함수를 테스트 코드에서 호출하고 이 후 프로덕션에서 만든다.

메세지를 던지자

ball.getNothing() == 0 을 ball.isNotingng() 로 하듯이..

Random 값을 테스트

객체관계가

RacingMain -> RacingGame -> Car 라고 하자.

Car 내부에 move라는 함수가 있는데 이는 random 값을 기반으로 도는 함수라 테스트하기가 쉽지않다. Car move안에 random으로 동작하는 부분으로 인해 RacingGame, RacingMain 또한 테스트하기가 쉽지않아짐.
random 생성하는 부분을 RacingMain으로 올리면 RacingGame, Car는 테스트가 가능해진다.

원래 최 상위 RacingMain 같은 부분은 테스트하기가 쉽지 않은 부분이긴 함.

랜덤값에 의존하는 레거시코드를 못바꿀 때 테스트하는 방법.
사진 참고(private였던 getRandom을 protected로 바꾸고 getRandom을 오버라이딩해서 테스트 진행)

테스트는 경계값으로 테스트한느것이 좋다.

move 작동방식이 계속변경된다면 해당메소드를 인터페이스로 뽑아내는것도 좋은방법임.

게터/세터는 되도록 쓰지마라(dto 제외)
객체와 객체를 비교하는것이 객체지향적인 사고이다.
equals를 만들어줘야하겠지?

기본형 타입에 대해서도 일급객체를 만들어서 써라.
int position이 아닌 Postion postion으로 해서
position++; 보단 postion.increase(); //메세지를 전달

CQRS

CQRS는 Command and Query Responsibility Segregation(명령과 조회의 책임 분리)을 나타냅니다. 이름처럼 시스템에서 명령을 처리하는 책임과 조회를 처리하는 책임을 분리하는 것이 CQRS의 핵심입니다. 이제 명령과 조회에 대해 정의할 필요가 있습니다. CQRS에서 명령은 시스템의 상태를 변경하는 작업을 의미하며 조회는 시스템의 상태를 반환하는 작업을 의미합니다. 정리하면, CQRS는 시스템의 상태를 변경하는 작업과 시스템의 상태를 반환하는 작업의 책임을 분리하는 것입니다.

상수 값의 경우 static final, 변수 이름은 대문자
private static int RANGE = 9;
private final int MIN_COUNT = 1;

상수의 위치는? 상수, 클래스 변수, 인스턴스 변수, 생성자 순으로 위치한다

1
2
3
4
5
6
7
8
public class Car {
private int moveIndex;
private int carNumber;
private String carName;
private static final int INIT_POSITION = 0;
private static int autoIncrease = 0;
[...]
}

공백 라인을 의미있게 사용해라. 문맥을 분리하는 부분에 사용한다.
리팩토링의 단위가 될 수 있따.

space도 고려한다

1
2
3
4
5
for (int i=10; i<1000; i++) {
assertTrue(checkMove(2, i));
assertTrue(checkMove(20, i));
assertTrue(checkMove(200, i));
}

equals, hashcode는 맨아래 놓는게 관례임

이름은 가능한 구체적이어야 한다. 모호하거나 하나 이상의 목적으로 사용될 수 있는 일반적인 이름은보통 나쁜 이름이다

적당한 이름
numTeamMembers, teamMemberCount
numSeatsInStadium, seatCount
teamPointsMax, pointsRecord

Total, Sub, Average, Max, Min, Record, String, Pointer 등의 한정자를 사용해야 한다면, 이름의 끝이 이런 수정자를 입력하는 것이 좋다

좋은 예
revenueTotal
expenseTotal
revenueAverage
expenseAverage

클린코드 가이드함수(메소드)

-작게 만들어라.
함수를 만드는 첫 번재 규칙은 ‘작게’다. 함수를 만드는 두 번째 규칙은 ‘더 작게’다.

-한 가지만 해라.
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만 해야 한다.

-함수 당 추상화 수준은 하나로
함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장이 동일한 추상화 수준에 있어야 한다.
코드는 위에서 아래로 이야기처럼 일해야 좋다.

-서술적인 이름을 사용하라
이름이 길어도 괜찮다.
이름을 정하느라 시간을 들여도 괜찮다.
이름을 붙일 때는 일관성이 있어야 한다.

-함수 인수
함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개이고, 다음은 2개이다.
3개는 가능한 피하는 편이 좋다.
4개 이상은 특별한 이유가 있어도 사용하면 안된다
인수가 2 ~ 3개 필요한 경우가 생긴다면 인수를 독자적인 클래스를 생성할 수 있는지 검토해 본다.

1
2
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

-side effect를 만들지 마라.
side effect는 많은 경우 예상치 못한 버그를 발생시킨다.
명령과 조회를 분리하라.
함수는 뭔가를 수행하거나 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다.
개체 상태를 변경하거나 아니면 개체 정보를 반환하거나 둘 중 하나다.
-오류 코드보다 예외를 사용하라.
오류 처리도 한 가지 작업이다.
함수는 ‘한 가지’ 작업만 해야 한다. 오류 처리도 ‘한 가지’ 작업에 속한다.
그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
try/catch 블록은 원래가 추하다. 코드 구조에 혼란을 일으키며, 정상적인 동작과 오류 처리 동작을 뒤섞는다. try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.

1
2
3
4
5
6
7
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}

인스턴스 변수의 수를 최소화한다.

인스턴스 변수의 수를 최소화할 수 있는 방법을 찾는다.
인스턴스 변수에 중복이 있는지를 확인하고 제거할 수 있는 방법을 찾는다
인스턴스 변수는 2개가 적절
아래 코드에서 winners는 cars에서 추출하는것으로서 중복으로 봐도 무방함.
cars가 변경되면 winners도 변경되어야 하는데 이는 버그유발 가능성을 높이는것.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RacingGame {
private List<Car> cars;
private List<String> winners;
public List<Car> move(int time) {
int curCountOfWin = 0;
for(Car car : cars){
countOfMove(time, car);
curCountOfWin = max(car.getCarPosition(), curCountOfWin);
}
for(Car car : cars){
setWinners(curCountOfWin, car);
}
return cars;
}
[...]
}

setter, geeter를 최대한 지양하라(dto는 써도 됨)

car.setPosition(car.getPosition)) 대신 car.increase()
전자는 c언어의 구조체처럼 car을 쓰는것임
즉 car로 메세지를 보내라!
인스턴스를 초기화한 후에 값을 변경할 수 있는 setter 메소드를 생성하지 않는다. 가능하면 생성자를 사용해 초기화한다.
상태 데이터를 get하지 말고 메시지를 보내라
객체의 데이터를 꺼내 로직을 구현하면 중복 코드가 발생한다.
객체에 메시지를 보내 상태 데이터를 가지는 객체가 일하도록 하라.
private void addMaxCarPostion(GameResult result, int maxCarPosition, Car car) {
if(maxCarPosition == car.getCarPostion()) {
result.addWinner(car);
}
}

좋은 이름 짓기 위한 연습 방법

JDK, Spring 프레임워크와 같이 유명한 오픈소스 코드를 많이 읽는다.
동의어, 유사어 사전을 활용해 문맥에 맞는 이름을 찾으려 노력한다.
구현하는 프로그래밍 도메인에 대한 지식을 쌓기 위해 노력한다. 도메인 지식을 높아질 수록 좋은 이름을 지을 가능성이 높아진다

비지니스 로직과 UI 로직의 분리

비지니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다.
단일 책임의 원칙에도 위배된다

어느 부분을 테스트할 것인가?

어느 정도의 테스트가 적정한가? 너무많이는 필요없다, 필요한것만!
경계 값을 기준으로 테스트

Test Fixture 생성

Fixture란 테스트를 실행하기 위해 필요한 것으로 테스트를 실행하기 위해 준비해야할 것들을 의미한다.
테스트의 인스턴스 변수는 각 Test Case에서 공통으로 필요한 Fixture만 위치, 나머지는 각 TestCase에 로컬 변수로 구현한다.
@BeforeEach는 각 Test Case에서 중복으로 사용하는 Fixture만 초기화해야 한다

테스트를 위해 생성자를 추가하는것도 괜찮다.

우승자 구하는 로직을 테스트하기 위해 Test Fixture 준비

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RacingGameResultTest {
@Test
public void check_ranking_if_correct() {
List<Car> cars = new ArrayList<>();
Car car1 = new Car("pobi");
Car car2 = new Car("crong");
Car car3 = new Car("honux");
car1.move();
car1.move();
car2.move();
car2.move();
car2.move();
car3.move();
cars.add(car1);
cars.add(car2);
cars.add(car3);
[...]
}
}

Test Fixture를 위해 Car(String name, int position) 생성자를 추가한다면

1
2
3
4
5
6
7
public class RacingGameResultTest {
@Test
public void check_ranking_if_correct() {
List<Car> cars = Arrays.asList(new Car("pobi", 2), new Car("crong", 3), new Car("honux", 1));
[...]
}
}

MVC 기반으로

1
2
3
4
5
6
7
8
9
10
11
12
public class RacingMain {
public static void main(String[] args) {
String carNames = InputView.getCarNames();
int tryNo = InputView.getTryNo();
RacingGame racingGame = new RacingGame(carNames, tryNo);
while(!racingGame.isEnd()) {
racingGame.race();
ResultView.printCars(racingGame.getCars());
}
ResultView.printWinners(racingGame.getWinners());
}
}

자료 전달 객체 -dto

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 Data Transfer Object(DTO)라고 한다.
자바에서 DTO의 일반적인 형태는 ‘자바 빈(java bean)’ 구조다.

클래스는 작아야 한다.

클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야 한다. 두 번째 규칙도 크기다. 더 작아야 한다.
단일 책임 원칙(Single Responsibility Principle, SRP)은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.
클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미다.
응집도(cohesion) -클래스는 인스턴스 변수 수가 작아야 한다.
응집도를 유지하면 작은 클래스 여럿이 나온다.
큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다. 그러면서 프로그램에 체계가 더 잡히고 구조가 더 투명해진다.

변경하기 쉬운 클래스

요구사항은 변화기 마련이다. 따라서 코드도 변하기 마련이다.
구현 클래스에 의존하게 되면 테스트가 어려우며, 변화에 빠르게 대응하기 힘들다. 변화에 따르게 대응하려면 DIP 원칙을 지키는 습관을 가져야 한다.
DIP(Dependency Inversion Principle) 원칙은 클래스가 상세한 구현이 아니라 추상화(인터페이스)에 의존해야 한다는 원칙이다.
테스트가 가능할 정도로 시스템 결합도를 낮추면 유연성과 재사용성도 더 높아진다.

디미터 법칙

디미터 법칙은 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 것이다.
다음 코드는 디미터 법칙을 어기는 것으로 보인다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

서비스레이어에 있는것을 도메인 객체로 이동해라.

테이블과 도메인 객체(엔티티)는 1:N으로 구성되어야 함.
즉 테이블 하나에 엔티티 하나로 제한하지 마라

https://velog.io/@neity16/4-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%99%80-JPA-%ED%99%9C%EC%9A%A9-3-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B5%AC%EC%A1%B0-Service-Repository-%EA%B0%9C%EB%B0%9C
상품도메인 개발부분 참고

요약한 규칙

-한 메서드에 오직 한 단계의 들여쓰기만 한다.
-else 예약어를 쓰지 않는다. if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
-모든 원시 값과 문자열을 포장한다.
-한 줄에 점을 하나만 찍는다.
-줄여 쓰지 않는다(축약 금지).
-모든 엔티티를 작게 유지한다.
-3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
-일급 컬렉션을 쓴다.
Map을 사용하는 경우 Map의 모든 인터페이스를 노출하지 않을 수 있게됨
-getter/setter/프로퍼티를 쓰지 않는다.
-메소드의 라인 수를 15라인이 넘지 않도록 구현한다.
-try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.
-한 줄에 점을 하나만 찍는다. (디미터 법칙을 지키는 것을 의미)
-모든 엔티티를 작게 유지한다.
-3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
-게터/세터/프로퍼티를 쓰지 않는다.

-4개 이상은 특별한 이유가 있어도 사용하면 안된다. 인수가 2 ~ 3개 필요한 경우가 생긴다면 인수를 독자적인 클래스를 생성할 수 있는지 검토해 본다.

1
2
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

-기본형 타입에 대해서도 일급객체를 만들어서 써라.
int position이 아닌 Postion postion으로 해서
position++; 보단 postion.increase(); //메세지를 전달
-클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
Customer, WikiPage, Account, AddressParser 등이 좋은 예다.
Manager, Processor, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않는다.
-메소드 이름은 동사나 동사구가 적합하다.
postPayment, deletePage, save 등이 좋은 예다.
접근자, 변경자, 조건자는 자바 빈 표준에 따라 값 앞에 get, set, is를 붙인다.
생성자를 중복해 정의할 때는 정적 팩토리 메소드를 사용한다. 메소드를 인수를 설명하는 이름을 사용한다.

Share