강의) 네오 1단계 자경 피드백
1단계에서 겪을 수 있는 피드백관련 강의
📜 제목으로 보기
- final 키워드로 값의 변경을 막아라
- 객체를 객체스럽게 사용해라
- 인변의 접근제한자는 private으로 고정 -> 객체상태에 대한 접근은 제한무조건>직접>
- (내부)랜덤요소는 단위 테스트하기 힘들다.
- 인변 수는 최소화한다.
- 비즈니스 로직과 UI 로직의 분리
- Collection 활용 로직 처리
- 기본 api를 이용해서 다짜지 말자
- 경계값을 테스트한다.
- 테스트 픽스처 생성
- 특정상태를 만들어주기 위한 반복코드 -> 생성자로 상태 바로 만들기
- getter없이 구현 가능?
- 테스트를 위한 코드는 구현코드에 분리
final 키워드로 값의 변경을 막아라
값은 final로 재할당금지 -> 재사용 금지
-
main 메소드 template 인
psvm
의 인자에도final
을 붙혀준다. - 메서드 내부에서 코드 중간에서 가변 변수를 수정할 때, 위에 재할당 등의 수정이 되었었나 확인해야한다.
-
final
로 재할당(재사용)이 불가능한불변 값
사용하다가,변수의 재사용
이 필요할 경우 새 변수를 만들어서 쓰자.
-
-
모든 지역변수, 인자에 final을 붙이자.
final 객체의 내부 속성변화 메서드 -> 변한 속성으로 만든 새 객체로 응답
-
객체의 경우는
final
로는 완벽한 불변을 못만든다.final 값
와 달리final 객체
는 메소드 등에 의해내부가 변할 수 있다.(포인터의 포인터에서 껍데기 포인터1)
-
객체를 불변으로 만든 법
상태변화를 일으키는 기능들은 새로운 객체를 반환
하도록 정의하자.-
상태변화 시켰던 메서드의 void returnType에
해당 객체를 응답(return)
하도록 수정public void move() { this.position++; } public Car move() { this.position++; }
-
변화된 상태
로 new생성자()로 호출하여 -> 새로운 객체를 반환시킨다.public Car move() { return new Car(this.position + 1); } public Car(final int position) { this.position = position; }
-
객체, 컬렉션(껍데기포인터1)등은 응답시 copy해서 응답하라
my) 포인터1의 포인터2
에 의해 변경될 수 있으니, 껍데기 포인터1 자체를 copy해서 응답해라
컨밴션 중 하나가 된다.
-
final
을 붙이는 것은 컨벤션이며 팀 안에서는 따르는게 좋을 수 도 있다.- 내 생각에 타당하지 않으면 얘기를 꺼내서 바꿀 수 있다.
- 컨벤션이라면, 비효율적이라도 그대로 가는 경우가 많다.
- 디자인 패턴 역시, 효율적이거나 좋은 코드라서 뿐만 아니라,
커뮤니케이션을 줄이는 컨밴션
이기 때문에 좋기도 하다.
- 디자인 패턴 역시, 효율적이거나 좋은 코드라서 뿐만 아니라,
참고 글
객체를 객체스럽게 사용해라
- 아래와 같이 getter와 setter만 있는 객체를 구현하지 않는다.
-
setPosition()
같은 setter 대신move()의 역할
로 분리해야한다. - getter()는 안쓰는게 좋은데, setter()는 100% 확정적으로 안쓰는게 좋다.
- setter() == 생성자 == pushed 로 외부에서 주입은 X
- 특히 불변객체의 경우는
속성 변화 -> 새 객체로 응답
해야하니, setter란 개념이 없다.
-
view에서 사용하는 getter()는 필수적이다.
- view에서 getter()안쓰기 위해 toString() 쓰는 것은… 좀 아니다.
getter()쓰지마라
라는 말은도메인에서
getter() 쓰는 대신, 객체에 메세지를 던지면서 협력하라는 뜻이다.- 객체를 직접 사용하지말고, View에서는 dto로 값만 꺼내써라
-
Q.
view에서 cars.get(0).getPosition()은 가능한가요?
-
my) 디미터법칙 위반이다 -> 책임을 2번째 놈에게 위임해야됌
-
index 0 은 무엇을 말하는지 모른다.
-
가져오려는 이유가 무엇인지 부터 물어보고 싶다.
- 우승자를 구하려는 이유라면 메세지를 던지는 식으로 바꿔보자.
cars.getWinners()
- 우승자를 구하려는 이유라면 메세지를 던지는 식으로 바꿔보자.
-
.get
+.get
이 이어지는 경우가 있는데 의심을 해봐야한다.-
점 1개로 줄이도록 해보자.
cars.getFirst().getPosition(); cars.getFirstPosition();
-
첫번째 포지션 -> 의미가 애매하다 -> 위너로 바꾸자.
cars.getWinners();
-
-
객체.getXXX()대신 new XXX( 객체 )를 던지는 클래스로 만드는 방법도
-
**getter()대신
객체를 생성자로 던져 class를 만들어볼까?
**new Winner(cars);
객체를 던졌어도, 객체.setter() 등의 주입 대신 역할위임을 하자.
public class Racing {
...
public int run(Car car) {
int num = random.nextInt(11);
int position = car.getPosition();
if (num >= 4) {
position++;
}
car.setPosition(position);
return position;
}
}
-
car.setPosition(position);
대신에 car내부로 역할이 위임
되어서 내부에서 했으면 좋겠다.
인변의 접근제한자는 private으로 고정 -> 객체상태에 대한 접근은 제한무조건>직접>
- getter()도 지금 허용할까말깐데.. 인변에 접근은 절대 안시킨다.
- 그렇다면
public
,protected
는 왜 있는 걸까?- 과거에는 의도가 있어서 만들었는데, 요즘에는 필요없다고 느껴졌을 수 가 있음.
- 과거에는 왜 여러 접근제한자를 만들었을까?
-
public으로 열여
있으면, getter/setter 동시허용이나 마찬가지다.- 어디서든 접근하면 ->
불안해야 한다.
이 역할만 하면 좋겠다.
변수를 public으로 열어두면, 변했는지 안했는지 직접 보고 확인해야하는 비용
이 든다.변수를 public으로 열어두지말고, method를 열어주어 캡슐화된 속성변화
를 만들어내자.
- 어디서든 접근하면 ->
(내부)랜덤요소는 단위 테스트하기 힘들다.
-
내부에
random
은 테스트하기 힘들다.
밖에서 받아오는 메서드로 변경한다.
-
테스트하기 힘든
내부
랜덤은 일단밖에서 주어지는 파라미터
로 변경 한다.- 파라미터 추출 단축키
ctrl+shit+P
public void move() { if (getRandomNo() >= 4) { this.position++; } } car.move();
public void move(final int number) { if (number >= 4) { this.position++; } } car.move(car.getRandomNo()); // 자동으로 메서드의 인자도 바뀐다.
- 파라미터 추출 단축키
-
문제점: 밖에서 받아오는 랜덤이
- 단일객체.move( 밖 )
- 일급컬렉션.move( 밖 )
- cars.forEach( car -> car.move( 밖 ) ) -> ..
- 일급컬렉을 사용하는 클래스에서도 ( 밖 )
- 어디 밖까지 빼서 받아올지가 끝이 없다 -> 적절한 경계를 잡아준다.
-
질문:
- car.move ( 밖 )도 테스트 했는데, cars.move ( 밖 ) 도 테스트해야할까?
- 마음의 평화가 중요함. 자기가 생각해 봐야함.
- 리뷰어에게는
설계가 담근 readme의 링크
와 같이 보낸다.
- car.move ( 밖 )도 테스트 했는데, cars.move ( 밖 ) 도 테스트해야할까?
받아오는 인자를 전략메소드를 가진 인터페이스로 변경하여 -> 랜덤외에 구상 전략객체들도 입력가능하게 만든다.
public interface MovableStrategy {
boolean isMove();
}
인변 수는 최소화한다.
-
최대 2개 정도로…
-
기존 인변으로 만들어낼 수 있는 것은, 변수로 선언하지 않는다.
public class RacingGame { private List<Car> cars; private List<String> winners;
-
my) 인변으로 둔다 -> 바깥에서(or default값으로) 받아와 알고있고, 의존해야하는 것
- 그게 아니라면, 시간이 좀 걸리더라도, 기존 인변들도 만든다.
- 관리포인트를 늘리면 좋은가? 그냥 그때그때 실시간으로 만드는게 좋은 것 같다.
- 현대에는 메모리/컴퓨팅 비용
성능
«< 인력의유지보수성
- DB를 이용하는 시점에서 변수 -> 성능을 위한 캐싱을 하기도 한다.
### setter는 사용을 자제해라 -> 생성자에서 초기화 후 더이상 변경안한다.
- 인스턴스 변수는
생성자 초기화 후
에 불변을 유지하기 위해 setter는 사용을 자제한다. - setter의 문제점은?
- 역할을 하지 못하게 된다.
- 어디서든 변경되어 -> 내 의도와 다르게 동작한다.
- 유지보수성이 떨어진다.
비즈니스 로직과 UI 로직의 분리
-
View관련 코드는 도메인에 넣지말라는 소리
-
90%는 도메인에 View관련 코드를 넣었었다.
public class Car { private int position; ... private void print(int position) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < position; i++) { sb.append("-"); } System.out.println(sb.toString()); } }
-
-
리뷰를 받으면서 점점 분리한다.
-
왜 분리할까? 그냥 출력하도록 도메인에서 메서드 만들면 되는거 아닌가?
- getter쓰지말라고 해서 -> 객체.역할부여()로 출력역할 만들었더니
- 역할부여는 도메인 && getter없이 하는게 맞지만,
변경이 자주 일어나는 view로직을 예외
다.
-
View관련 로직
-
출력관련 역할부여(책임)를 도메인에서 안시킨다. 아예 정의하지말고 도메인은 객체만 넘겨라.
출력 책임은 객체가 가지게 하지마라
-
<변경 빈번> UI
변경 -><변경없어야할 소중한, 중요도가 높은> 도메인
에 여파를 일으키면 큰일이 나기 때문
-
dto로 getter로 값만 꺼내쓰게 할 것이다
-
Collection 활용 로직 처리
-
Car 목록에서 최종 우승자를 구하는 로직이다. 이 코드를 Collection 기능을 사용해 어떻게 리팩터링할 것인가?
public class ResultView { private Cars cars = null; private String getTopRankedCar(List<Car> carList) { String topCarString = ""; cars = new Cars(carList); int maxPosition = getMaxPosition(carList); for (int i = 0; i < carList.size(); i++) { if (cars.getPosition(i) == maxPosition) { topCarString += cars.getCarName(i) + ", "; } } return topCarString.substring(0, topCarString.length() - 2); } private int getMaxPosition(List<Car> carList) { int maxPosition = 0; cars = new Cars(carList); for (int i = 0; i < carList.size(); i++) { if (maxPosition < cars.getPosition(i)) { maxPosition = cars.getPosition(i); } } return maxPosition; } }
기본 api를 이용해서 다짜지 말자
Collection을 활용해 로직을 구현할 때 직접 구현하려 하지 말고 먼저 Collection API를 통해 해결할 수 있는 방법이 있는지 찾는다. 방법을 찾았는데 해결 방법을 찾지 못하는 경우만 직접 구현한다.
경계값을 테스트한다.
-
다 하는게 아니라 경계값만 테스트
- 모든 테스트 X
@Test public void 랜덤숫자가_4이상일때만_움직인다() { Car car = new Car(); assertFalse(car.shouldMove(0)); assertFalse(car.shouldMove(1)); assertFalse(car.shouldMove(2)); assertFalse(car.shouldMove(3)); assertTrue(car.shouldMove(4)); assertTrue(car.shouldMove(5)); assertTrue(car.shouldMove(6)); assertTrue(car.shouldMove(7)); assertTrue(car.shouldMove(8)); assertTrue(car.shouldMove(9)); }
테스트 픽스처 생성
-
테스트를 반복적으로 수행할 수 있게 도와주고 매번 동일한 결과를 얻을 수 있게 도와주는 ‘기반이 되는 상태나 환경’을 의미한다. 여러 테스트에서 공용으로 사용할 수 있는 테스트 픽스처는 테스트의 인스턴스 변수 혹은 별도의 클래스에 모아 본다.
- 중복제거를 위해 하면 된다.
- junit의 @beforEach를 사용하면 된다. 테스트 픽스쳐의 일종이다.
public class RacingGameTest { private final String[] testNames = {"a", "b", "c"}; private final int testPosition = 5; private final String resultSamePositionString = ", b"; private final String resultWinnersString = "a, b"; RacingGame racingGame; private final Car firstWinner = new Car(testPosition, "a"); private Car secondWinner; private final List<Car> cars = new ArrayList<Car>(); ... }
과정 실습
-
추후 미션이 진행 되면, 복잡해지고 객체가 많아질 경우, 코드를 분리해야됨.
@Test void move() { final Car car = new Car("재성"); }
-
비슷한 환경의 테스트를 복사해서 case를 나눈다고 가정하자.
@Test void move_go() { final Car car = new Car("재성"); } @Test void move_stop() { final Car car = new Car("재성"); }
-
객체의 생성이 반복된다면,
Generate > Setup method
를 선택해서@BeforeEach
를 생성하자.@BeforeEach void setUp() { } @Test void move_go() { final Car car = new Car("재성"); } @Test void move_stop() { final Car car = new Car("재성"); }
-
공통되는 객체생성코드를 beforeEach로 뺀다
@BeforeEach void setUp() { final Car car = new Car("재성"); } @Test void move_go() { } @Test void move_stop() { }
-
ctrl+.(show context action)
을 눌러서split declation and assignment
를 통해-
변수 선언은 변수재활용을 위해 클래스변수로
- 클래스변수에 넣고 매번 초기화한다 -> 변수 재활용한다 -> 변수 재할당한다 -> final은 안된다.
-
할당만
beforeEach에서매 테스트마다 새로운 것
으로 분리한다.
@BeforeEach void setUp() { final Car car; car = new Car("재성"); }
class ApplicationTest { //final Car car; // 매 테스트마다의 재활용 = 재할당 Car car; @BeforeEach void setUp() { car = new Car("재성"); }
-
변수 선언은 변수재활용을 위해 클래스변수로
-
픽스쳐가 많아지면, 공통이 맞는지 확인한다.
특정상태를 만들어주기 위한 반복코드 -> 생성자로 상태 바로 만들기
-
과도하게 반복해서 만든다?
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); ... } }
- 단순반복의 경우, 반복문+메서드 호출로 해결할 수도 있지만, 그렇지 않은 경우가 많다.
- 상태를 만들기 위해 복잡한 과정이 많은 경우, 상태를 바로 만들어주는 생성자를 만들 수 있다.
- 여기선, move()테스트가 아니라 우승자를 찾는게 목적이기 때문에, 목적 달성을 위해, 다른 것은 바로 상태를 만드는 생성자를 만들수 도 있음.
- 현재는 move()호출방식에 따라 테스트가 의존하여, 변하면 테스트코드가 깨질 수 도 있다.
- 테스트를 위해 생성자를 만들지만, 그게 진짜 테스트만을 위한 코드인지 생각해봐야한다.
public Car(final String name, final int position) { this.name = name; this.position = position; } List<Car> cars = new ArrayList<>(); // Car car1 = new Car("pobi"); // Car car2 = new Car("crong"); // Car car3 = new Car("honux"); Car car1 = new Car("pobi", 2); Car car2 = new Car("crong", 3); Car car3 = new Car("honux", 1); // car1.move(); // car1.move(); // car2.move(); // car2.move(); // car2.move(); // car3.move(); cars.add(car1); cars.add(car2); cars.add(car3);
-
이 코드가 프로덕션 코드에서 사용되지 않더라도, 객체관점+객체역할에서 봤을 때, 테스트를 위한 기능이 아닐 수 있다.
- 객체 관점에서 자연스러우면 괜찮다.
- position default값을 지정해주는 관점에서 괜찮다.
-
테스트만을 위한
메서드추가
는 문제가 더 큰데, 생성자는객체를 만드는 방법
일 뿐이므로 생성자는 많아도 괜찮다.- 충분히 납득이 가능하다.
-
생성자는 여러개 만들면,
인자 많은 것을 주생성자
로 취급하고 인자 적은 생성자 -> 위쪽에 위치하고default값을 this에 넣어주는 부생성자
// 부 생성자는 주 생성자(name, position)보다 더 앞에 위치하며, 뒤쪽의 주 생성자를 this( , )로 사용한다. public Car(final String name) { this(name, 0); } public Car(final String name, final int position) { this.name = name; this.position = position; }
-
생성자가 너무 많아지거나,
같은 시그니쳐
or이름을 주고 싶을 때
-> 정팩메로 분리 고려
getter없이 구현 가능?
-
setter/getter 메서드를 사용하지 말라는 것은 핵심 비지니스 로직을 구현하는 도메인 객체를 의미한다. 도메인 Layer -> View Layer, View Layer -> 도메인 Layer로 데이터를 전달할 때 사용하는 DTO(data transfer object)의 경우 setter/getter를 허용한다.
-
아예 없이
toString
으로 구현하는 분들도 있지만,도메인 Layer
에서 사용하는 getter를 만들지마라 ==비즈니스 로직
를 위한 getter를 이용하지마라-
도메인 대신 DTO 구현시 getter/setter는 허용한다.
- DTO라도
setter
는생성자에서 초기화
하는 것으로 대신한다. - view에서는 열어야한다.
-
-
접근제한자는 최소한으로 열고 시작한다.
-
getter로 받아와서 하지말고,
역할을 부여해서 시키는 것
-> 그래야협력
한다.
객체안에서 toDTO를 만들지마라
-
내부에서
toDto()
를 만든다?- 모델에서 DTO를 알면 왜 안좋을까?
DTO는 뷰에 종속적인 객체 -> view처럼 변경이 많다 -> 도메인에 영향을 줌
객체 안에서 DTO를 생성하지마라
- 객체는 밥줄이라 아껴야하고, DTO는 막쓴다.
테스트를 위한 코드는 구현코드에 분리
- 테스트용 편의 메서드를 구현 코드에서 구현하지마라
생성자로 주입하는 건 괜찮고, setter는 안좋냐?
- 똑같은 내부 코드(행위)보다는 역할이나 시점을 봐야한다.
-
setter
가 하나라도 존재하면내부 상태변화 기능.move()
을 따로 가지는 객체지만,객체를 믿을 수 없게 된다.
- 기능으로 내부 변하는 것도 좀 그런데… 언제든지 변화시키는 믿을 수 없는 객체가 된다.