1~2 레벨의 객체지향에 대한 생각 변화 회고

이 글의 목차4개
  1. 레벨 1
  2. 내게 객체지향이란
  3. 레벨 2
  4. 먼저 납득해야만 배울 수 있을까?

레벨 1

객체지향이 유지보수에 좋다는 말은 정말 많이 듣는다. 책임을 나누고, 의존성을 줄이고, 불변 객체를 사용하면 좋다는 설명도 할 수 있다. 그런데 왜 내가 지금 그렇게 작성해야 하는지는 잘 모르겠다. 레벨1 인터뷰를 준비하며 그 마음을 꽤 솔직하게 적었다.

왜 많은 계층을 도입하고 클래스를 나누었나 하는 질문에 대한 내 솔직한 생각은 ‘다들 그랬으니까, 그렇게 하라고 장려하니까.’ 정도이다.

배운 것이 없었다는 뜻은 아니다. 배운 이유를 말할 수 있었지만, 그 이유가 아직 내 경험은 아니었다.

내게 객체지향이란

블랙잭, 장기 미션의 제약조건은 축구의 규칙처럼 느껴졌다. 축구에서 왜 손을 쓰면 안 되느냐고 묻는다면, 그것이 축구이기 때문이라는 답이 돌아올 것이다. 내게는 객체지향 설계도 비슷했다.

동작하는 프로그램을 만들 수 있는데 왜 이렇게까지 나눠야 할까. 지금 더 간단하게 작성할 수 있는 방법을 두고, 일부러 어려운 규칙을 지키는 기분이었다.

블랙잭을 만들 때는 빠르게 구현하는 데 집중했다. 테스트도 실패한 위치를 빨리 찾을 수 있으면 좋겠다고 생각했다. 그 과정에서 테스트 전에 전역 상태를 비우거나, 카드가 스스로 무작위 값을 고르거나, DTO가 도메인을 품고 있는 코드들을 돌아보게 되었다.

하나씩 설명을 들으면 왜 문제가 될 수 있는지 이해는 됐다. 그런데 이해했다고 해서 처음부터 그런 코드를 피하고 싶어지는 것은 아니었다.

내가 편하다고 생각한 방식과 권장되는 방식의 차이를 직접 비교해보면 됐을지도 모른다. 인터뷰 답변을 쓰다가 나도 그 생각을 했다. 모두가 좋다고 하는 것을 그냥 따르는 것은 싫지만, 내 생각을 증명할 만큼 부지런하지도 않았던 것 같다고 적었다. 조금 부끄러운 문장이지만, 당시의 나를 꽤 잘 설명한다.

블랙잭 회고에서는 더 날카롭게 나를 돌아보기도 했다. 귀찮은 동작이 많고 성능도 안 좋아지는 것 같은데 왜 굳이 이렇게 해야 하느냐고 냉소적으로 봤다고 적었다. 그것이 성능을 중요하게 생각하는 개발자여서가 아니라, 배움을 귀찮게 여기는 태도였던 것 같다고 했다. 납득하고 싶다는 마음과 배우기 싫다는 마음을 나도 잘 구분하지 못했던 것 같다.


레벨 2

레벨2에서는 새로운 것들이 한꺼번에 들어왔다. 테스트를 어떻게 작성할지, 여러 변경을 어디까지 트랜잭션으로 묶을지 고민했다. 그러는 동안 도메인의 책임은 충분히 고민하지 못했다. 코드가 동작하고 새로 배운 기술을 적용하는 데에도 신경 쓸 것이 많았다.

그런데 기능이 하나씩 추가되자 같은 판단을 여러 곳에서 해야 했다. 코드는 늘어나고 중복되었고, 어디를 고쳐야 하는지 점점 헷갈렸다.

리뷰를 거쳐 남은 두 사이클의 코드를 함께 보면 그 고민의 흔적이 보인다. 예약 대기 사이클1의 서비스에는 검증과 저장, 순번 조회가 이어져 있었다.

@Transactional
public WaitingResponse save(WaitingRequest body, long memberId) {
    slotService.validateSlot(body.date(), body.timeId(), body.themeId());
    long slotId = slotService.resolveSlotId(body.date(), body.timeId(), body.themeId());

    validateReservedByMemberNotExists(memberId, slotId);
    validateWaitingByMemberNotExists(memberId, slotId);
    validateWaitingTargetExists(slotId);

    Waiting waiting = waitingRepository.save(body.toDomain(memberId, slotId));
    long waitingOrder = waitingRepository.countBySlotIdAndIdLessThanEqual(slotId, waiting.getId());

    return WaitingResponse.of(waiting, waitingOrder);

다음 사이클의 코드에는 같은 슬롯에 속한 대기들을 모아 순서를 찾는 WaitingLine이 등장한다. 그 안의 메서드는 이렇게 생겼다.

public long orderOf(Waiting waiting) {
    for (int index = 0; index < waitings.size(); index++) {
        if (hasSameId(waitings.get(index), waiting)) {
            return index + 1L;
        }
    }
    throw new IllegalArgumentException("대기열에 존재하지 않는 대기입니다.");
}

두 코드는 같은 메서드를 그대로 옮긴 전후 비교는 아니지만, 그래도 내가 어디에 책임을 두려고 고민했는지는 보인다. 처음에는 조회 결과를 서비스에서 조립하던 문제가, 나중에는 대기열 자체에 물어볼 수 있는 문제가 되었다.

6월 5일 최종 회고에는 이렇게 적었다.

레벨 1에서는 “하라고 하니까” 도메인의 책임에 대해 고민하고, 설계했지만, 레벨 2에서 나의 부실한 도메인으로 인해 기능이 추가될 때마다 많은 코드들이 생겨나고 그것이 중복되고 뒤죽박죽이어서 관리하기 힘들다는 것을 체감했다.

먼저 납득해야만 배울 수 있을까?

레벨1의 모든 제약조건을 뒤늦게 정답으로 받아들이게 된 것은 아니다. 어디까지 나누는 것이 적절한지, 어떤 책임을 같은 곳에 두어야 하는지는 여전히 어렵다.

다만 예전의 나는 장점을 이해하면 필요성도 곧바로 느껴야 한다고 생각했던 것 같다. 느껴지지 않으면 그 규칙을 의심하거나, 내가 제대로 배우지 못했다고 생각했다.

하지만 설명을 알아듣는 시점과 그 설명이 절실해지는 시점은 달랐던 것 같다. 레벨1에서 들었던 말이 레벨2의 불편을 겪고 나서야 내 말이 되었다.

아직 필요성을 느끼지 못하는 것을 배우는 일도 그래서 완전히 헛된 것은 아니라고 생각하게 되었다.

전체 글 보기