글을 작성하기 시작한 때는 3월 초이다. 그러나 사다리 미션의 회고를 제대로 작성하지 못한 채 블랙잭 미션이 시작되었고, 정신없이 미션을 수행하다 보니 어느 덧 3월 중순이 되어있었다. ☹
더는 미룰 수 없어서, 사다리 타기 미션의 마침표를 작성하려 한다.
사다리 타기 2단계 미션을 진행하며 1단계 미션과 비교해서 달라지고 싶었던 점은 TDD의 장점을 이해하는 것이다. 당연히 꼭 이해할 필요는 없다. 그저 자연스럽게 "이래서 TDD를 쓰는구나!"를 느끼고 싶었지만 1단계 미션에서는 이를 느끼지 못해서 아쉬웠었다.
미션 구현 요약
하지만 2단계 미션이 끝나도 여전히 TDD의 장점은 와닿지 않았다. 조금 더 사이즈가 큰 미션을 수행하게 된다면 알게 될까?
이번 미션의 객체 설계도이다.
이번 미션의 가장 큰 부분이었던 사다리 이동을 여러 객체가 협력하도록 하여 구현하였다. 나는 객체지향 프로그래밍을 최대한 사용하고 싶어서, 이 객체의 역할이 맞나? 단일 책임 원칙을 잘 지켰나? 등의 고민을 많이 하였다.
그 결과 꽤나 마음에 드는 코드가 탄생했고, 리뷰어 현구막과 이야기를 나누면서 부족한 부분을 리팩토링 하였다.
사다리 게임 실행 미션을 진행하며 고민한 것들
역시 모르는 것이 있을 때는 크루들과 이야기해보는 게 최고다. 같은 고민을 하는 크루들이 주변에 널리고 널린 것이 얼마나 감사한 일인지 많이 느끼는 요즘이다.
테스트 코드를 위해 프로덕션 코드의 수정
사다리를 랜덤으로 생성하고 싶었다. 하지만 테스트 코드를 작성하기 위해서는 사다리를 내 마음대로 생성할 필요가 있었고, 그래서 인터페이스로 LadderGenerator를 정의했다. 이 Generator를 내 입맛에 맞게 구현하여 구현체를 만들면 테스트가 쉽겠지?
pobi honux crong jk
|-----| |-----|
| |-----| |
|-----| |-----|
| |-----| |
|-----| |-----|
꽝 5000 꽝 3000
나는 이런 사다리를 만들고 테스트하고 싶으니까, 이런 사다리를 만들어주는 구현체 ZigzagLadderGenerator를 정의해보자. 엇, 그런데 이 구현체가 잘 만들어진건가? 사다리가 잘 만들어졌는지도 테스트해봐야지.
이런 생각의 흐름에 따라 아래 코드를 작성하였다.
public class ZigzagStepStatusGeneratorTest {
@Test
void generateStepStatus() {
Ladder ladder = Ladder.of(new Height(5), new Width(3), new ZigzagStepStatusGenerator());
List<Line> lines = List.of(
createLine(CONNECTED, DISCONNECTED, CONNECTED),
createLine(DISCONNECTED, CONNECTED, DISCONNECTED),
createLine(DISCONNECTED, CONNECTED, DISCONNECTED),
createLine(CONNECTED, DISCONNECTED, CONNECTED),
createLine(DISCONNECTED, CONNECTED, DISCONNECTED),
createLine(CONNECTED, DISCONNECTED, CONNECTED)
);
Assertions.assertThat(ladder).isEqualTo(lines);
}
private Line createLine(StepStatus... stepStatuses) {
return Arrays.asList(stepStatuses)
.stream()
.map(Step::from)
.collect(collectingAndThen(toList(), Line::new));
// TODO: Line의 생성자를 public으로 변환
}
}
하지만, createLine 함수를 보면 Line의 생성자를 사용해서 직접 사다리를 생성해주고 있었다. 그럼 필수적으로 Ladder 클래스의 생성자를 public으로 열 수 밖에 없는데 🤔🤔 어떻게 하면 좋으려나, 테스트를 하지 말아야 하나 끙끙 고민하고 있었다.
그 찰나에 켈리가 머리를 싸매고 있는 나를 발견하고 말을 걸어주었고, 테스트 작성을 위해 생성자를 개방해도 된다는 인사이트를 던져주고 갔다. 테스트 코드는 테스트하는 용도일 뿐인데, 본 코드에 영향을 줘도 괜찮은가? 하는 생각에 현구막에게 질문을 했다. 나는 사실 현구막이 안된다. 지양하라. 라고 할 줄 알았는데, 아래와 같이 답변이 왔다.
결론적으로 이유가 있는 생성자 개방은 괜찮다 ! 라는 것이다. (속 시원 .. ) 하지만 protected 등의 접근 제어자를 적극 활용해서 최대한 덜 개방하도록 하는 것이 좋다.
최근에 이런 말을 들었다.
생성자가 많을수록, 메서드가 적을수록 응집도가 높다.
생성자가 많으면 다른 객체와 쉽게 협력할 수 있다. 다른 객체와 협력이 쉬워지게 되면 외부에서 이 생성자를 적극 활용할 수 있고, 객체의 쓰임이 증가하면서 협력이 원활해지는 것이다. 반면 메서드가 많다는 것은 객체가 가지고 있는 역할이 많아 다른 객체로 충분히 구분될 수 있던 것이 하나의 객체에서 수행되고 있을 수 있으므로, 응집도가 낮다는 것이다.
생성자는 외부에서 객체의 인터페이스를 제공해주는 역할을 한다.
접근 제어자를 적극 활용해서 응집도를 높이자!
VO의 사용에 대한 고찰
1단계 미션 때 현구막에게 VO에 대한 질문을 받았다.
VO가 뭐지? 정확히 몰랐던 나는 지폐에 대한 예시를 통해 VO에 대해 쉽게 이해할 수 있었다.
이때 조금 오해했던 부분은, VO가 무조건 좋은 것이구나! 불변성과 동등성을 항상 보장해야겠다! 라고 생각했던 것이다. 그래서 충분히 가변될 수 있는 객체도 VO를 유지하기 위해 값을 변경한 객체를 새로 반환해주는 방식으로 값의 변경을 구현했다.
그러다 보니, 가변을 가정하고 설계한 객체임에도 불변 객체로 선언하게 되면 일일이 객체가 재할당되어야 한다는 불편함이 생겼다. 객체를 = 연산자로 재할당하게 되면 해당 Scope를 벗어나면 변경된 값이 일정하게 유지되지 않았다. 그래서 그냥 원본 객체를 변경하는 것이 좋겠다, 라는 생각이 들었고 고민 끝에 가변적인 객체에는 VO를 사용하지 않았다.
VO를 사용해보고, 사용해보지 않았을 때 느낀 장단점은 아래와 같다.
장점
스스로 유효성을 검증하기 때문에 검증 로직이 간단해진다.데이터의 불편성을 보장한다.
단점
객체의 상태를 변경해야 하는 경우 대입 연산자로 재할당해야하며, 원본 객체를 변경하기 어렵다.
함수형 인터페이스
(축) 우아한테크코스 미션 시작 이래로 처음으로 함수형 인터페이스를 사용하였다. (축)
프리코스 때 로또 미션에서 한 번 사용했던 것 같은데, 그 이후로 사용하지 않아서 가물가물 했다.
이번 사다리 미션의 Position의 상태에 따라 서로 다른 동작을 하는 것이 구현하기 복잡하다고 느꼈다.
기존에 아래와 같이 if문으로 분기해주면서 move를 구현했어야 했는데, 함수형 인터페이스를 사용하면서 아주 간단히 코드를 개선할 수 있었다.
public void move(Direction direction) {
if(direction.equals(Direction.LEFT)) {
moveLeft();
return;
}
if(direction.equals(Direction.RIGHT)) {
moveRight();
return;
}
moveStraight();
}
하지만 함수형 인터페이스를 사용하면 아래와 같이 한 줄의 코드로 변경될 수 있다는 점이 큰 메리트이다.
direction.move(position);
앞으로 상태를 확인하고 상태에 따라 서로 다른 동작을 하는 예제가 많이 생길 것이다. 함수형 인터페이스의 사용을 더 익히고 공부해보자.
내부 클래스
정적 내부 클래스와 비정적 내부 클래스의 공통점과 차이점을 알고 있는가?
이번에 미션을 수행하면서, 프리코스 때 자주 사용했던 내부 클래스를 스리슬쩍 사용해보았다. 이에 대해 현구막에게 정적 클래스에 대한 질문을 받게 되었고, 정적 내부 클래스와 비정적 내부 클래스의 공통점과 차이점을 비교해보았다.
결과적으로 내가 사용한 Validator라는 내부 클래스는 정적 클래스로 선언해야한다는 것이다. 왜냐하면 인스턴스화 될 필요가 없으니까 🤗 자바의 세상은 멀고도 험하다.
객체에게 메시지 보내기
마지막으로 현구막에게 새로운 클린 코드의 기준을 배우게 되었다.
객체에 메시지를 보내라 라는 흔한 클린 코드에서 하는 말이 와닿은 적이 없었는데 실질적으로 어떤 사이드 이펙트가 있을 지 자세하게 설명을 들을 수 있어서 클린 코드를 작성하는 나만의 기준을 세우게 되었다.
객체에게 메시지를 보내는 이유는 협업을 위해서라고 할 수 있다. 그 이전에 캡슐화를 먼저 말하면, 객체가 어떤 상태를 가지는 지 외부에서 알 필요는 없다. 아니, 알지 않는게 좋다. 왜냐하면 외부에서 이를 알고 있게 되는 순간 외부에서도 이 객체의 변경과 추가에 대한 책임을 가지게 된다. 결과적으로 연쇄적으로 책임이 옮겨지면서 유지보수가 어려운 스파게티 코드가 탄생한다. 그러면 나는 개발자 내 신뢰를 잃고 퇴직당할 수 있다 ㅋㅋ
그리고, 하나의 객체를 변경시키기 위해 많은 클래스 파일을 수정하게 되면 merge conflict가 생길 위험도 커진다. 이렇게 구체적인 사례를 통해 객체에게 메시지를 보내라라는 말에 이유를 체감하게 되니, 어떻게 코드를 작성해야할 지 감이 올 수 있었다.
미션을 마무리하며
사다리 미션을 끝내고 느낀 점.
뭔가 시간이 좀 지나고 다른 미션을 하다가 이 부분을 쓰고 있는데, 사다리 미션에서 배운 건 굉장히 온보딩스러운 부분이었다는 것을 느낀다. 블랙잭 미션, 체스 미션을 하다보니 사다리 미션에서 배웠던 것들은 기본 중에 기본이었고 여기서 많은 이야기를 나누며 내 코드 철학을 다소 다지고 갈 수 있어서 다행인 것 같다.
사다리 미션은 TDD를 목표로 수행한 미션이지만, 사실 TDD보다는 어떻게 리뷰어와 효과적으로 소통할 수 있을 지, 객체지향적인 코드는 과연 무엇인지, 그리고 그 모든 관념들이 결론적으로 무엇을 위한 것인지 생각해보는 계기가 되었다.
브라운 코치가 했던 말이 기억난다.
우아한테크코스에 오기 전에 공부했던 모든 것들을 잊어라. 책에서 봤던 지식도 다 잊어라. 그냥 미션의 요구사항만 만족시키기 위해 코드를 작성하고, 리뷰어와 이야기하면서 내가 이 부분에서 어떤 문제를 겪었고 이를 해결하기 위해 어떤 것을 적용해야하는 지 공부해라.
이 말을 듣고 많은 깨달음을 얻었다. 나는 굉장히 관념적으로 코드를 작성하고 있었구나. 이 관념이라는 것이 참 위험한 것 같다. 내가 왜 이걸 쓰는 지도, 무슨 영향이 있는지도 모르고 프로덕션을 만들고 있었다니, 만약 개인 프로젝트가 아니라 실제 회사 프로젝트라면 나비효과처럼 좋지 않은 파장이 일어날 수 있는데.
그래서 리뷰어가 하는 말에도, 크루가 하는 말에도, 코치가 하는 말에도, 심지어 내가 습관적으로 하는 말에도 의심을 품기로 했다. 진짜 그런가? 내가 와닿는 이유가 아니라면, 모두가 틀렸다고 해도 나한테는 맞을 수도 있다고 생각한다.
나의 아주 작은 성장을 위해 기여해준 현구막 리뷰어님과 브라운 코치, 그리고 소중한 우테코 크루들, 그리고 나 자신에게 감사하다.
'우아한테크코스 > 레벨1' 카테고리의 다른 글
[회고] 우아한테크코스 6기 백엔드 레벨1 5주차 회고 (2) | 2024.03.24 |
---|---|
[CleanCode] 1단계 ♠️블랙잭♠️ 페어 프로그래밍 & 리팩토링 회고 (2) | 2024.03.24 |
[회고] 우아한테크코스 6기 백엔드 레벨1 4주차 회고 (4) | 2024.03.11 |
[회고] 우아한테크코스 6기 백엔드 레벨1 3주차 회고 (0) | 2024.03.03 |
[TDD] 1단계 `사다리 생성 미션` 페어 프로그래밍 & 코드 리뷰 회고 (4) | 2024.03.03 |