오늘은 사뭇 진지함이 묻어나오는 포스트가 되길 바란다.
주어진 코드를 개선해나가는 과정을 적는 것으로 2주차의 회고를 대체한다.
🚗 RacingCar

모든 레이싱카에는 이름이 있다.
그리고 그가 치열하게 달리며 살아온 거리가 있다.
이를 코드로 표현하면 다음과 같다.
public class RacingCar {
public final String name;
private int distance;
public RacingCar(String name) {
this.name = name;
}
public void move() {
int randomValue = Randoms.pickNumberInRange(0, 9);
if (randomValue >= 4) {
this.distance++;
}
}
}
인생사 새옹지마라 하였던가
나아가고 싶어도 마음대로 할 수 없는 우리의 '카'
마치 모두가 우테코에 합격할 수 없다는 것처럼 내겐 전해져 온다.
😫장난은 그만 치고, 그래서 제대로 굴러가는 거 맞아?
그럼요 제대로 동작합니다
@Test
void move() {
int dice = 4;
var racingCar = new RacingCar("mia");
racingCar.move();
// ... ??
}
... 아니?! 분명 제대로 동작하긴 할텐데...
테스트를 어떻게 하지?
⚠️구현에는 정답이 없습니다.
이후의 내용들은 초기 설계를 기준으로 평가된 내용이며,
지극히 주관적인 견해임을 미리 밝힙니다.
private int
🔨 현재의 설계가 어떤 결핍을 가지고 있는지 파악해보자.
첫째, 현재의 설계는 move를 실행했을 때, 그 결과에 따른 상태 변화를 확인하기 위해 private 필드를 직접 참조해야 한다.
둘째, 현재의 설계는 무작위의 값에 의존하여 그 결과가 결정된다.
⚡이 문제를 해결하기 위한 방법은?
첫째, move를 실행했을 때, 그 결과를 반환한다.
둘째, 전달한 인수를 바탕으로 move를 실행한다.
와~ 쉽네~ 그럼 이렇게 만들면 되는거 아니야?
public class RacingCar {
public final String name;
private int distance;
public RacingCar(String name) {
this.name = name;
}
public boolean move(int randomValue) {
if (randomValue >= 4) {
this.distance++;
return true;
}
return false;
}
}
@Test
void move() {
int dice = 4;
var racingCar = new RacingCar("mia");
assertTrue(racingCar.move(dice));
}
🔨 다시, 현재의 설계가 어떤 결핍을 가지고 있는지 파악해보자.
기존의 설계에서 move()는 RacingCar의 고유한 행동 방식으로 파라미터 없이 잘만 동작했었다.
이제 추가적인 파라미터가 필요해졌다는 것은 곧 캡슐화에 실패했다는 뜻이다.
개발자는 캡슐화의 실패 여파로 전달되는 무작위의 값을 검증하는 불필요한 코드를 추가해야 한다.
⚡ 다시, 이 문제를 해결하기 위한 방법은?
앞선 두 조건에 마지막 조건을 추가하는 것이다.
첫째, move를 실행했을 때, 그 결과를 반환한다.
둘째, 전달한 인수를 바탕으로 move를 실행한다.
셋째, move의 파라미터를 제거한다.
⁉️ 파라미터 없이 인수를 어떻게 전달하라고
public boolean move() {
int randomValue = Randoms.pickNumberInRange(0, 9);
if (randomValue >= 4) {
this.distance++;
return true;
}
return false;
}
문제 상황을 정리해보자.
파라미터를 제거했기 때문에, 다시 난수값을 함수 내부에 적을 수 있었다.
만약, 당신이 가장 빠른 방식으로 이 문제를 해결하고자 한다면,
접근 제한자 package-private를 사용하는 것을 제안한다.
package-private
현재의 함수는 무작위의 값을 생성하고, 그 값을 평가해서 분기를 거친다.
우리는 이미 값을 들고 있는 것을 가정하고 있으므로 forward(int) 함수를 내장시켜본다.
이렇게 하면 손쉬운 테스트가 가능해진다.
public class RacingCar {
public final String name;
private int distance;
public RacingCar(String name) {
this.name = name;
}
public boolean move() {
int randomValue = Randoms.pickNumberInRange(0, 9);
return forward(randomValue);
}
boolean forward(int dice) {
if (dice >= 4) {
this.distance++;
return true;
}
return false;
}
}
@Test
void forward() {
int dice = 4;
var racingCar = new RacingCar("mia");
assertTrue(racingCar.forward(dice));
}
🔨 다시, 현재의 설계가 어떤 결핍을 가지고 있는지 파악해보자.
기존의 설계에서 move()는 하나의 독립적인 함수로 잘만 동작했었다.
이제 추가적인 파생 함수가 필요해졌다는 것은 이론적으로는 파생 함수의 파생 함수도 필요할 수 있다는 뜻이다.
더 이상 문제가 없다면 상관없지만, 이 방법은 근본적으로는 함수의 파생을 막을 수 없는 해결 방식이다.
⚡ 다시, 이 문제를 해결하기 위한 방법은?
앞선 세 조건에 마지막 조건을 추가하는 것이다.
첫째, move를 실행했을 때, 그 결과를 반환한다.
둘째, 전달한 인수를 바탕으로 move를 실행한다.
셋째, move의 파라미터를 제거한다.
넷째, 별도의 내장 함수에서 난수 값을 가져온다.
⁉️ 그런다고 뭐가 달라져?
달라진다. 달라지는 것은
외부 클래스의 정적 메서드를 통해 들어오던 통제할 수 없던 값을
클래스라는 우리의 제어권 안으로 들여오는 것이다.
protected pickRandom() 함수를 작성하고,
테스트에서는 이 클래스를 상속하여 메서드를 오버라이딩한다.
public boolean move() {
int randomValue = pickRandom();
return forward(randomValue);
}
protected int pickRandom() {
return Randoms.pickNumberInRange(0, 9);
}
static class StubRacingCar extends RacingCar {
public StubRacingCar(String name) {
super(name);
}
@Override
protected int pickRandom() {
return 4;
}
}
@Test
void move() {
var racingCar = new StubRacingCar("mia");
assertTrue(racingCar.move());
}
⁉️ 그럼 모든 값에 대해 매번 상속해야 해?
좀 더 개방된 사고를 해보자.
이제 우리의 제어권에 온 테스트 클래스는 마음대로 사용하면 된다.
(사실 여기서도 기준이 더 있지만, 글이 너무 길어져서 멈추려고 한다.)
class RacingCarTest {
static class StubRacingCar extends RacingCar {
public String name = "name";
public int random = 4;
public StubRacingCar() {
super("name");
}
@Override
protected int pickRandom() {
return random;
}
}
private final StubRacingCar racingCar = new StubRacingCar();
@Test
void moveSuccess() {
racingCar.random = 4;
assertTrue(racingCar.move());
}
@Test
void moveFail() {
racingCar.random = 3;
assertFalse(racingCar.move());
}
}
⁉️ 특정한 거리에 도달한 상태에 대한 테스트는?
🎉축하한다. 다시 처음으로 돌아왔다.
처음 우리의 문제는 void move() 의 동작 성공을
private int distance 필드에 대한 명시적인 확인 대신,
boolean forward(int) 를 통한 간접적 확인으로 대체했다.
그렇게 문제를 피했지만, 언젠가는 특정한 거리에 있는 것이 중요해질 수 있다.
3에서 4로 성공적으로 이동하는 것이 보고 싶나?
그렇다면...
짜잔 ✨접근 제한을 package-private 또는 protected로 느슨하게 풀어주면 됩니다~ 😀
(농담 반 진담 반)

이거 다 뻘소리 아닌가요?
현재의 포스트에서는 외부 라이브러리를 통해 무작위 난수를 받아 움직이는 것의 어려움과, 마지막에서는 이를 어떻게 확인할 것이냐의 총 2가지 문제를 다루고 있다. 난수는 별도의 클래스를 통해 의존성을 주입받고, 테스트에서는 해당 의존성을 재정의하거나 유사 인터페이스를 구현하는 것으로 해결할 수 있다. 이는 단순히 int 형식의 파라미터를 받는 해당 글의 내용과는 다른 이야기이다. 해당 글에서는 단순히 책임을 상위 호출자에게 전가한 것에 불과하다고 생각해서 좋은 해결책이라 생각하지 않는다. 또는 Stub, Mock 등의 Test double을 작성하는 식으로 해결할 수 있다.
이렇게 해결할 수 있지만, 어떤 방식을 선택하든 작성해야 할 코드의 양과 복잡도가 증가한다. 또 중간에 구조 변경이 발생하면, 해당 테스트가 무의미해질 수도 있다. 그래서 그러한 결정을 지연할 수 있는 방법, 그리고 코드와 최대한 결합시킨 상태에서 가장 쉽게 접근할 수 있는 방법들부터 논하고 싶었다.

또한 이러한 부분들은 피드백을 통해 제재를 받았는데, **결과적으로 최대한 캡슐화를 지향하자**는 말은 누구나 할 수 있다. 이것이 분명 베스트 프랙티스는 아니지만, 막상 TDD를 기반으로 플로우를 이어나갈 때 현재의 테스트 문제를 지연시키면서, 구현의 유효성을 확보할 수 있는 한시적으로 효율적인 방법이라는 데에는 변함이 없다. 현재의 상황에 대한 문제를 인식하고, 리팩토링 과정에서 고치는 것이 우리의 다음 스텝이 되어야 한다.
🤔 개발에 정답은 [업ː따]
개발에 정답은 없다.
최우선 과제와 기한, 그리고 우리의 상황만 있을 뿐이다.
그 가치를 달성하기 위한 최적의 방법을 선택하는 것.
가능하면 넓은 가능성 속에서 더 좋은 선택을 하기 위해 우리는 배우는 것이고,
그 선택이 무엇이든 당신이 가치를 만들 수 있다면 그 선택은 옳다
2주차 회고는 이렇게 마무리 짓도록 하겠습니다.
읽어주셔서 감사합니다!
'Learn > 우테코' 카테고리의 다른 글
| [프리코스 3주차 회고] 돌고 돌아 절차지향 (1) | 2025.11.07 |
|---|---|
| [프리코스 1주차 회고] 아 쓰var지 말라고 (2) | 2025.10.23 |
| [프리코스 1주차 회고] 너의 해석은? (3) | 2025.10.21 |