이 글은 지난 6월 휘갈겨뒀던 회고를 정리한 글입니다. 즉, 제목의 '지난 반년'은 2024년 1월~6월을 의미합니다.
회고를 이 표현으로 시작하려는 이유는, 이 글을 만들어낸 Insight가 지난 반년간의 경험이 없었더라면 존재할 수 없었음을 강조하고 싶기 때문이다.
예전 함께 PS(백준)를 하던 친구에게 자주 듣던 말이다. 당시 나는 알고리즘을 떠올리고 나름의 증명을 거치면 코드를 제출하지 않고 문제 풀이를 멈췄다. 구현하는게 귀찮았고, 그다지 중요하지 않다고 여겼다. 그런 나를 보던 친구는 많이 답답했겠지.
사실 아직도 메인은 알고리즘을 떠올리는 능력이라고 생각하지만, 더 이상 코드 작성을 무시하지 않는다. 다른 사람에 비해 유독 느린 구현 시간에 주눅드는 경험을 반복하면서 문제를 느꼈고, 구현 능력을 향상하기 위해서는 코드를 직접 작성해 보는게 중요함을 깨달았기 때문이다.
보통 이런 질문을 던지면 항상 돌아오는 답변이 있다.
"그건 상황마다 다릅니다. 직접 해보는게 중요합니다."
그렇다. 가설이 있다면 실험해서 결과를 지켜봐야 한다. 개발자란 그래야 한다.
객체지향이란 뭘까? 과감하게 말하건대 현시대의 개발이란 객체지향이라고 생각한다. 몇 년 전 주목받던 함수형 패러다임도 결국 객체지향 속으로 섞여 들었다. 객체지향만큼 사람 생각처럼 동작하는 모델이 없다.
요즈음에는 입문 언어가 다양하지만, 그래도 아직 많은 사람들이 Java로 프로그래밍에 입문한다고 생각한다. 그리고 Java로 입문한 많은 사람들이 Object보다 Class에 집중하는 함정에 빠진다. 난 JS로 입문했고, Prototype에 집중해야 했던 만큼 이러한 함정을 피했다고 생각했다. Class가 아닌 객체에 집중하고 있으니 자연스럽게 객체지향을 실천하는 중이라고 믿었다.
"객체지향의 사실과 오해"라는 책이 있다. 한때 이 책을 읽은 친구가 나에게 내용을 열정적으로 공유해줬다. 들으며 내심 너무 당연한 내용을 얘기한다고 생각했다. 내가 객체지향을 잘 하진 않지만, 특별히 놓치는 내용은 없다고 생각했다. 하지만 돌이켜보면 막상 코드를 작성해 보지도, 예제를 찾아보지도 않았다. 그저 적당한 이론을, 어디선가 주워들은 글귀를 머릿속에 떠올렸을 뿐이다.
GDSC 멤버로서 Google Solution Challenge에 참가하면서 팀원으로부터 많이 배웠다. 그리고 얻은 것 중 가장 값진게 무엇이었냐 묻는다면, 내가 객체지향을 아예 모르고 있었다는 깨달음이라고 생각한다.
시작은 단순했다. 동일한 날짜에 Record
중복 등록을 막아야 했다.
// in `PlantService`
fun registerRecord(platnId, record) {
val findPlant = PlantRepo.findById(plantId)
// here!!
PlantRepo.findRecordByDate(LocalDate.now())?. { throw }
findPlant.registerRecord(record)
}
Feedback: "날짜 중복 여부를 Domain
에서 확인하는게 더 좋지 않을까요? Service
에서 확인하시는 이유가 있나요?"
사실, 이때까지는 별생각이 없었다. Service
와 Domain
의 역할에 대해서는 나중에 다시 고민하게 되지만, 당시 나에게는 그저 "그럴 수도 있겠구나" 정도였다.
어쨌거나, 코드를 다음과 같이 수정했다. 그래그래 책임이 Domain
에 있으니 이렇게 작성하면 되겠지!
// in `Plant`, `Aggregate Root`
fun registerRecord(record) {
// here!!
this.records.find { it.date == LocalDate.now() } ?. throw
this.records.add(record)
}
Feedback: "records
가 너무 수동적이네요. 객체의 state 말고 behavior에 집중하는건 어떤가요?"
하나의 리뷰가 나를 뒤흔들었다. 이 순간 무언가 잘못됐다고 느꼈다. 객체지향이란 프로그램을 능동적인 객체의 상호작용으로 바라보자는 패러다임이다. 결국 내가 만든 프로그램은 객체지향이라 부를 수 없었다. 팀원에게 너무 못난 코드를 보여줬단 생각에 쥐구멍에라도 숨고 싶었고, 부랴부랴 코드를 수정했다.
// in `Plant`, `Aggregate Root`
fun registerRecord(record) {
// here!! Let `records` do it!
if (this.records.isAlreadyRegisteredAt(LocalDate.now()) throw
this.records.add(record)
}
// in `PlantRecords`, First Class Collection
fun isAlreadyRegisteredAt(date: LocalDate) =
this.itmes.find { it.date == date } != null
Feedback: "items
도 객체입니다. 능동적일 수 있어요!"
이날 난 살짝 정신을 놨다. 충격에서 벗어나기가 힘들었다. 문제를 구체적으로 짚어주고 해결 방향을 제시했는데도 불구하고 동일한 실수를 반복하다니?
// in `PlantRecords`, First Class Collection
fun isAlreadyRegisteredAt(date: LocalDate) =
this.items.find { it.registeredAt(date) } != null
(Approve 받은 최종본)
학교에 객체지향 개발 방법론 강의가 열렸다. 객체지향에 부족함을 느끼던 내게 흥미가 이는 강의였다. 당연히 수강했고, 강의 과제를 수행하며 이런저런 고민을 경험했다.
다만 결론이 내 예상과는 달랐다. 이 내용에 대해서는 조금 있다 적겠다.
동아리 친구가 객체지향 스터디를 함께하자고 했다. 그 유명한 "로또"나 "숫자 야구"같은 과제를 가져다가 각자가 코드를 작성해 보고 서로 리뷰를 하자고 했다. 솔직히 만만한 마음으로 참여했는데, 로또 구현에 한 달 넘게 걸리더라.
이 스터디를 시작하며 목표는 크게 2가지였다.
그런데 둘 다 썩 마음에 들지 않았다.
OOAD 강의를 수강하며 Client & Architecture Risk를 빠르게 발견하고 대응하자는 전략에 공감하면서도 은연중 "저게 된다고?"란 의문을 늘 안고 있었다. (물론 로또 과제에서는 요구사항이 변하지 않으며 기술 스택도 결정되어 있기에 위 사항과 거리가 있지만) 코드를 작성하며 느낀 건, 역시 무언가 지나치게 이상적이라는 것이다.
내가 바로 코드를 작성했더라면 과제 수행에 한 달이 걸렸을까? 그리고 작성한 코드 없이 머릿속으로만 준비하는 문서가 정확하기는 할까?
물론 요구사항에 대한 명세는 중요하다. 매우 중요하다. 이 부분에 대해서는 배운 내용에 여전히 깊게 공감한다. 하지만 구현에 대해서는 생각이 다르다.
나는 테스트조차 큰 범위에서 작은 범위로 작성해야 한다고 생각한다. E2E
(Acceptance) Test를 작성하고, 필요하다면 Integration
Test를, 역시 필요해지는 순간에 Unit
Test를...
구현이 진행되기 전 구체적인건 프로그램의 개괄적 구조이지 컴포넌트 하나하나의 세부 구조가 아니다. 그러한 세부 구조는 개발을 진행하며 완성할 수 있다고 생각한다.
딱 요 정도가 개발 전 설계로 충분하지 않을까?
위에서 소개했던 1월의 경험을 바탕으로 객체의 state가 아닌 behavior에 집중하고자 했다. 그런데 문득 이상함을 느낀다. 객체 간의 결합이 너무 강해지기 시작한 것이다.
public class Draw {
private final WinningNumbers winningNumbers;
private final BonusNumber bonusNumber;
...
public Prize compare(Lotto lotto) {
int winningCount = this.winningNumbers.compare(lotto);
boolean hitBonus = this.bonusNumber.belonged(lotto);
return Prize.from(winningCount, hitBonus);
}
...
}
Draw
는 로또 결과 산출을 담당한다. 추첨 결과(WinningNumbers
, BonusNumber
)와 Lotto
에 의존 관계가 있다.
public class WinningNumbers {
private final List<Integer> numbers;
...
public int compare(Lotto lotto) {
int result = 0;
for (Integer number : this.numbers) {
if (lotto.has(number)) {
result++;
}
}
return result;
}
...
}
위 코드에서 알 수 있듯이, WinningNumbers
객체도 Lotto
와 의존 관계가 있다.
객체와 능동적으로 상호작용하기 위해서는 해당 객체와 의존 관계를 형성해야 하지 않는가? 그렇다면 의존 관계를 끊어내기 위해서는 언젠가 State에 직접 접근하고 Aggregation
과 Aggregation
을 이어줄 객체가 있어야 하지 않는가? 비로소 Domain
과 Service
의 역할 경계를 고민한다.
"이직해도 맨날 같은 일만 하니까 지겹다."
이미 취업한 친구의 배부른(?) 불평이다. 무슨 말이냐 물어보니 도메인은 이직해도 잘 안 바뀐다고 하더라. 가령 결재를 담당하던 사람은 다른 회사 가서도 결재 업무를 맡는다던가? 첫 경력이 중요한 이유 중 하나라더라.
객체지향이라고 표현하지만 결국 도메인 주도 개발로 봐도 크게 다르지 않다고 생각한다. 객체마다 책임과 역할을 부여하겠다는 말이니까. 그런데 이러한 책임과 역할은 결국 현실의 추상화이고, 현실 개념에 대한 높은 이해를 필요로한다.
핵심이 무엇이고, 어떤게 잔가지며, 그래서 무엇을 중심으로 요구사항이 변할 수 있는가? 요구사항이 바뀐다는건 결국 현실이 변한다는 의미다. 모든 변화는 사실 프로그램 밖에 있다.
결국 결론은 "도메인 전문가가 되자!"... 일 순 없다. 그게 어디 쉽겠는가. 그리고 이렇게 얘기하지만, 프로그래밍도 만만하게 볼 일이 아니다. 두 가지를 모두 하기에 인생은 너무 짧다.
그럼 결국 대화가 중요하지 않을까? 도메인 전문가와 얘기하고, 우리의 시각을 공유하고, 상대의 설명을 이해할 수 있는 능력이 가장 중요하지 않을까? 이렇게 내향인은 또다시 웁니다
동아리 운영을 위한 Admin Page를 만드는 중이다. 개발진 간 회의의 주 내용도 "그래서 우리 프로그램이 어떤 방향으로 발전할 것인가?"였고, 당시 우리가 바라보던 프로그램 확장 방향성에 맞춰서 도메인을 설계했다.
하지만 시간이 흐르고, 막상 자주 사용하는 기능과 사용하지 않는 기능, 그리고 개선이 필요한 부분을 보니 개발 방향이 당시 예측과 많이 달랐다. 아직 관련 논의를 시작하지는 않았지만, 나는 빠른 시일 내로 기존 도메인 구조를 개편하자는 제안을 할 계획이다.
결국 프로그램을 상당 부분 갈아엎어야 할지도 모른다. 어떻게 보면 변경 대처가 잘 안된 셈이다. 과연 우리가 미숙했던건 프로그래밍 실력이었을까? 아니면 도메인(기획)이었을까? 물론 "둘 다!"가 정답이겠지만, 둘 중 무엇이 더 아쉬웠냐 묻는다면 잘 모르겠다.