큰 그림: 이 코드는 “정답"이 아니라 “다리”
“스프링 환경에서도 객체지향을 지키라"고 했더니 다들 갈피조차 못 잡는 것 같아서, 직접 빌드해 공개한 코드입니다. 그래서 모범 답안이 아니라 갈피만 잡으라고 던진 다리예요. 오늘 보고 다시는 보지 말라는 말이 핵심입니다. 따라 하면 억지로 끼워넣게 되고 부작용이 더 큽니다.
관통하는 메시지
1. 트레이드오프 영역에선 머무르지 말고 의도를 박고 가라. 오늘 질문의 절반은 “왜 이렇게 했나요?“였고 답은 거의 “선택의 영역”, “의도의 차이”, “기획자랑 합의했다"였습니다. 가벼워 보이지만 IT는 학문이 깊지 않아서 대부분의 결정이 진짜로 트레이드오프예요. 정답 찾느라 머무르는 시간이 가장 비쌉니다. 양쪽 장단점을 다 알고 한쪽을 택해 의도를 박았으면 그게 정답에 가깝습니다. 면접·리뷰에서 “편해서 했어요"는 약한 답이고, “이런 장단점이 있는데 이런 이유로 택했다"가 무게를 실어줍니다.
2. 이상적 코드가 채택 안 되는 이유는 보통 “의도 변질 비용”. Clock 인터페이스로 시간을 추상화한 사례 — 이론적으론 그게 더 객체지향적입니다. 하지만 안 쓰는 이유는 경험상 후임이 내 의도를 잘못 읽어 더 나쁜 코드로 변질시키는 일이 잦았기 때문이에요. 추상화 전에 “이 의도를 모르는 사람이 들어와 어떻게 오용할 수 있는가?“를 자문하라는 겁니다. 추상화 안 한 코드에 죄책감 갖지 마세요.
3. 객체지향의 장점은 미션 규모에선 직관으로 안 온다. 응집도·결합도가 직관으로 오려면 변화가 잦고 문제가 나한테 직접 떨어지는 환경이 필요합니다. 대안은 (a) “테마마다 시간을 가진다면?” 같은 가상 변화 던져보기 — 객체지향이면 “테마만 가면 된다"가 바로 보임, (b) 혼자 프로젝트 빠르게 운영하며 피드백받기.
주제별 설계 의사결정
도메인 구조 — RoomEscape가 핵심 도메인(애그리거트 루트)이고 테마·예약은 거기서 기인한 객체들. 부정적으로 보면 갓클래스, 긍정적으론 파사드인데 서비스 규모가 작아서 이렇게 나온 것. 결제 같은 도메인이 붙으면 자연스럽게 분리될 구조라고 봅니다. 테마를 ID가 아니라 객체로 참조한 이유는 “자주 안 바뀌고, 무겁지 않고, 변경 시 예약이 영향받고, 내부에서 많이 참조"하기 때문 — 의존성 단점은 거의 없고 장점은 많다고 판단.
Repository = 도메인 — 레포지토리는 도메인이고 구현체가 인프라. 일부러 이름에서 ‘Repository’를 떼서 1급 컬렉션처럼 바라보게 했습니다. findById(Optional)와 getById(예외) 편의 메서드는 사용하는 쪽 중복 제거용. default 메서드 철학엔 안 맞지만 효용성 때문에 씀.
어셈블러 — 변환 작업만 담당. 실무에선 여러 테이블에서 데이터를 조합하는 노가다성 코드가 서비스에 섞이면 읽기 힘들어져서 분리합니다. 객체 그래프가 끊어져 있어도 응답 시 합쳐 보낼 정보를 여기서 처리.
에러 처리 — 도메인 커스텀 예외 → switch로 HTTP 매핑. 도메인을 완전히 분리하는 게 장점, 도메인과 HTTP 의존성이 섞이는 게 단점. switch는 컴파일 타임에 누락을 잡을 수 있어 괜찮다고 봄. BadRequest/Conflict 분리도 “선택의 영역"이지만, Conflict로 분리하면 개발자가 한 번 더 생각하게 되는 의도를 담을 수 있음. ProblemDetail은 스프링 표준을 따라 프론트와 약속한 형태를 보장하려고.
ID와 데이터 관점 (특히 의찬님 관심사)
도메인이 ID를 갖는 문제엔 정석이 없습니다. 대가들도 “ID 있으면 객체 아니다 / ID도 객체다 / 그냥 ID다"로 다 다르게 말해요. 의도 차이일 뿐입니다. 인터페이스 관점에선 ID 부여는 도메인 규칙이 아니라 구현체(JDBC 자동생성, 인메모리)에 달린 일. 네오 본인은 깊게 생각 안 하고 “어차피 ID 쓸 거고 번거로워서” 그냥 쓴다고. 다만 면접에선 “편해서"가 아니라 장단점을 붙여 답하라고 했습니다.
더 중요한 포인트는 현업은 객체지향보다 데이터적 사고를 훨씬 많이 한다는 것. 팔로워 1명과 저스틴 비버를 같은 플로우로 처리하는 건 어색하고, 데이터 기반으로 의사결정해 아키텍처가 나온다. OOP와 데이터 관점을 계속 왔다 갔다 하게 된다는 거예요(파워리프터가 무게마다 데드리프트 자세 바꾸는 비유). 실용주의로 가면 Table=Entity=도메인이 되고, 그렇게 갈 거면 더티체킹 문제를 걷어낸 Spring Data JDBC를 추천(JPA는 싫어함).
테스트
- 인수 테스트(전체 사이클 검증)와 컨트롤러 테스트(컨트롤러 단만), 통합 테스트, 서비스 단위 테스트는 목적이 다르다. 필요하면 둘 다 한다.
- 서비스 테스트를 안 쓴 건 서비스가 그냥 넘겨주기만 해서.
- 시간 테스트에 2099년 같은 먼 미래값을 쓴 건 OK — 테스트 형태는 요구사항(미래/하루 후/당일)과 작성 시점에 영향받음. 테스트는 애플리케이션 의사결정을 따라간다.
- 슬라이스 테스트가 안 와닿으면 억지로 공부하지 마라. 그 단위로 안정감을 얻는 사람이 짜는 것. 그 시간에 더 중요한 걸 학습하고, 필요해지면 학습한 사람한테 설득당하면 됩니다.
TDD 워크플로우
인수 조건을 인수 테스트로 박음(스펙) → 도메인 설계 → 도메인 단위 테스트로 통과시키며 진행 → 도메인이 조합되면 인수 테스트 하나씩 GREEN → 최종 목표는 전체 인수 테스트 GREEN. AI와 작업할 때 줄글 프롬프트보다 테스트로 인수 조건을 박아두면 AI가 거기까지 알아서 도달해서 일치도가 훨씬 높다고 합니다.
설계 시작은 의인화로
새 기능(예: 결제)을 만들 때 습관적으로 컨트롤러부터 가지 말고, “이 기능 쓰는 사람이 누구지 → 그 사람의 행동은 → 의인화 → 코드” 순서로. 기술적 부분(프론트 요청, DB)은 트리거로만 보고, 컨트롤러·어셈블러 같은 안티커럽션 영역에서 흡수시킵니다. 도메인은 외부에 절대 영향받지 않는 형태로 두고, 더러워져도 되는 곳으로 다 보내라는 것. 요구사항도 “그 사람의 메시지"일 뿐이니 내가 해석해 내 것으로 만들고, 절대 안 바뀔 핵심은 도메인으로·바뀔 부분은 추상화하는 게 개발자의 역량.
마무리 한마디
IT는 학문이 얕아서 연차가 큰 의미가 없고, 한 가지(TDD, 테스트 더블, 도메인 모델링, 동시성, HTTP 표준 등)만 깊게 파면 지구에서 가장 잘 아는 사람이 될 수 있다 — 그게 무기가 된다고 했습니다. 다들 깊이·근본이 없어서 트레이드오프라고 말하는 거고, 그게 곧 베테랑을 추월할 기회라는 거예요. 결론은 자신만의 생각을 만들고, 자신감을 갖고, 우겨라. 코드 참고는 Spring Pet Clinic이나 여러 언어 오픈소스, 심지어 쓰레기 코드까지 보며 “나라면 어떻게"를 생각하는 게 낫고, 크루 코드만 보면 스펙트럼이 좁아 매몰된다고 덧붙였습니다.