머리를 정말 많이 쓴 한 주다. 도메인 공부도 하고, 코드에서의 도메인 로직 분리도 진행하고 도메인 구조도 엎었던 도메인 주간. <리뷰>라는 도메인을 파악하는 것도 정말 어려운데 현업에서는 더 복잡한 도메인을 풀어나가지 않을까? 문제를 풀어나가는 과정이 참 재미있었다. 개발이 더디게 진행되는 것은 그거대로 아쉽지만, 그만큼 도메인 쪽의 지식을 잘 잡아가는 것 같아서 도움닫기를 열심히 하고 있다는 생각으로 달려가고 있다 🏃
🪢 도메인 구조 다시 생각하기
기존에는 미래를 대비해(…) 핵심 기능과는 다소 동떨어지는 도메인도 얽혀 있었다. 구현은 까다로웠고, 양방향 의존관계를 구현하면서 NPE까지 맛봤다 😮💨 저번 주에 소개한 영상을 보고 ‘생명주기가 온전히 일치하는 도메인끼리만 연관관계를 맺자‘는 결론에 다다랐고, 이를 기반으로 다시 코드를 작성해나가기 시작했다. 서로 몰라도 되는 <리뷰>와 <키워드>에 대해서는 외래 키를 들게끔 하는 방식으로 진행했다. 연관관계를 끊으니 테스트도 쉬워졌다. 테스트하고자 하는 것을 위해 설정하는 의존성도 훨씬 줄었다. 아래 코드는 도메인 구조를 다시 생각한 전/후 코드 비교다.
public class Review { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "reviewer_id", nullable = false) private Member reviewer; // 핵심 기능에 '회원'은 없었다. @ManyToOne @JoinColumn(name = "reviewee_id", nullable = false) private Member reviewee; // 핵심 기능에 '회원'은 없었다. @ManyToOne @JoinColumn(name = "reviewer_group_id", nullable = false) private ReviewerGroup reviewerGroup; // 리뷰와 리뷰 그룹의 생명주기가 일치하지 않는다. @OneToMany(mappedBy = "review") private List<ReviewContent> reviewContents; @Embedded private Keywords keywords; // 리뷰와 키워드의 생명주기는 더더욱 일치하지 않는다 (키워드는 서비스에서 제공) // ... }
public class Review { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "review_group_id", nullable = false) private long reviewGroupId; // 그룹의 id만을 참조해 연관관계 끊기 @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinColumn(name = "review_id", nullable = false, updatable = false) private List<ReviewContent> reviewContents; // 리뷰와 리뷰 내용은 생명주기가 정확하게 일치하므로, 영구적인 통로를 만들어준다. // ... }
주석에도 적어두었지만, 전체적으로 정말 필요하지 않은 연관관계를 모두 끊었고, 이를 비즈니스 계층으로 옮겼다. 이렇게 생각한 이유는 몇 가지가 있는데, 그 중 하나가 도메인 로직과 비즈니스 로직의 분리다.
🗃️ 도메인 로직, 비즈니스 로직 분리
도메인은 우리의 관심사 개념을 코드로 나타낸 것이다. 어떻게 보면 정말 변하지 않을 속성이다. 이곳에 비즈니스 로직이 들어오는 순간 도메인의 발전은 더뎌진다. 켈리와 1기 리뷰어 안돌이 함께 이야기하는 것을 옆에서 훔쳐(?)본 적 있었는데, 좋은 예시가 있어서 다듬어 보았다.
카페는 커피 전문점으로 시작했어요. 카페 이름도 커피였지요. 카페가 해야할 일들이 커피로 들어오기 시작했어요. 어느새 커피는 5000원이면서, 10잔을 마시면 한 잔 무료인 음료가 되었어요.
날씨가 너무 더워지니 새로운 메뉴를 개발하려고 했어요. 아이스크림을 섞은 아포가토를 팔아야겠다고 생각했어요. 아포가토는 더운 날씨에 인기가 많을 테니, 가격도 올리고 열 잔 무료 행사도 진행하지 않으려고 해요.
바리스타는 지금 커피로는 아포가토를 만드는 것이 불가능하다고 합니다.
안돌의 도메인 로직과 비즈니스 로직의 비교
처음 들었을 때에는 가볍게 넘긴 내용이었는데, 요즘 들어 이전에 리뷰어분들이 해주신 이야기들이 무겁게 다가오고 있다. 우리 도메인으로 이야기하자면, <리뷰 그룹>에 <여러 명의 리뷰어가 중복으로 작성할 수 없다>라는 로직은 리뷰 그룹의 도메인 로직이 아니라는 이야기다. 만약 <롤링 페이퍼>라는 기획이 진행되고 이곳에서는 중복 작성 제한이 사라진다면 <리뷰 그룹>을 재사용할 수 있을까? 결국 좌절하며 <롤링 페이퍼>라는 도메인을 추가로 만들고 있었을 테다. 의존성은 복잡해지고 재사용할 수 없는 코드가 탄생한다. 개념이나 추상적인 것에 집중한 것이 아닌 구체화된 도메인 객체를 가지게 된다.
이른 타이밍에 운이 좋게 팀원들이 이 내용을 이해해서 정말 다행이었다. 잘못되었다고 생각이 들었지만 너무 나아갔다면 그것대로 너무 맥이 빠졌을 것 같다.
신규 인사 관리 시스템에 대하여 상의하는 도중 의뢰인이 다음과 같이 말했다고 해 보자. “직원의 기록은 해당 직원의 관리자와 인사팀만 열람할 수 있습니다.” 이 진술이 진짜로 요구 사항일까? 오늘은 그럴 수도 있다. 하지만 이 단언하는 진술 속엔 사업 정책이 포함되어 있다.
사업 정책인가, 요구 사항인가? 그 차이는 비교적 미묘하다. 하지만 이 차이가 개발자에게는 중대한 의미가 있다. 만약 요구 사항이 “인사팀에서만 직원 기록을 열람할 수 있다.”는 식으로 되어 있다면 개발자는 애플리케이션이 이 데이터에 접근할 때마다 인사팀인지 확인하는 코드를 작성할 것이다. 하지만 요구 사항이 “권한이 있는 사용자만이 직원 기록에 접근할 수 있다.”는 식이라면 개발자는 아마도 일종의 접근 관리 시스템을 설계하고 구현할 것이다. 그러면 정책이 바뀔 때(실제로 바뀐다) 시스템의 메타데이터Metadata만 업데이트하면 된다. 실제로 이런 식으로 요구 사항을 수집하면 자연스럽게 메타데이터를 지원하는 잘 분리된 시스템이 만들어질 것이다. 사실 여기엔 일반 원칙이 있다.정책은 메타데이터다.
실용주의 프로그래머
📝 도메인 분석: 좋은 피드백을 위해서
리뷰미는 개발자가 나를 더 잘 찾아나갈 수 있도록 돕는 상호 리뷰 서비스다. 피드백을 통해서 나를 찾기 위해서는 좋은 피드백을 받아야 한다. 이미 우테코에서도 회고나 설문을 통해 다양한 방식으로 피드백을 주고받고 있는데, 우리 도메인에서 이를 더 잘 활용할 수 없을까? 라는 생각이 들었다. 그런 중에 1, 2차 데모데이 팀원 피드백을 작성해야 했고, 우리는 이를 기회로 삼아보기로 했다.
리뷰를 주고받는 서비스다보니, 어떤 리뷰가 리뷰이에게 도움이 될까? 도 중요했고, 어떤 방식으로 설문지를 작성해야 리뷰어가 불편하지 않게 속마음을 털어놓을 수 있을까? 도 하나의 과제였다. 문제를 한 번에 푸는 건 항상 어려우니 🥲 하나씩 풀어가보기로 했다. 설문지의 내용은 아래와 같은 느낌이었다.
1. 팀원이 기여한 점 세 가지 골라 작성하기
설문 내용 재구성
2. 팀원과 함께해 좋았던 점 세 가지 쓰기
3. 팀원이 성장하기 위해 줄 수 있는 피드백이나, 함께할 수 있는 일
리뷰미 팀에서는 7명의 남은 팀원들에게 이 내용을 각자 작성하고, 리뷰어의 입장과 리뷰이의 입장에서 문항을 분석해보기로 했다. 따뜻한 말들이 오고가고 도움이 된 내용도 있고, 문항이 중복되어 같은 이야기가 오고가는 문제점도 발견했다.
이런저런 긴 회의 끝에 몇 가지를 정리해볼 수 있었다. 좋은 말을 듣는 것만이 좋은 피드백으로 이어지는 것은 아니겠지마는, 다른 사람이 바라보는 나의 장점을 발견하는 것만으로도 충분한 메타인지와 자신감으로 이어졌다. 이를 기반으로 질문을 주관식으로 할 지, 객관식 문항으로 구성할지도 고민했다. 카테고리를 나눠 꼬리질문을 할 것인가? 와 같은 구체적인 질문 구성을 다음 주에 진행해보기로 했다. 도메인 공부를 하느라 코드를 잡은 지 오래 됐다. 기획의 연장선이기도 한데, 기술적으로는 아직 더디지만 언젠가 추진력을 받을 수 있지 않을까? 🔥
👏 2.5차 데모데이
2차 데모데이에서 ‘핵심 기능’에 집중하지 못해 사용자가 직접 경험할 수 있는 사이클을 만드는 데에 실패했기에 포비, 준에게 미니 데모데이를 여는 것을 제안했고, 흔쾌히 받아들여주신 덕분에 한 주만에 핵심 기능을 다시 구현하고 피드백을 받아 보았다. 데모데이는 2주마다 열리는데 이 중 한 주를 모두 개발에 쏟을 수 있었다.
핵심 기능을 ‘리뷰 그룹 생성, 리뷰 작성, 리뷰 조회‘로 잡았으니 이를 외부 사용자가 직접 진행해볼 수 있도록 구현하는 것을 최우선으로 했다. 기존의 도메인 구조에서 필요없었던 부분들도 많이 쳐냈다. 한 주만에 실행해볼 수 있는 구현물이 나왔고, 이를 바탕으로 피드백을 받아볼 수 있었다. 어떤 부분이 모자란지, 어떤 부분은 좋았는지를 다방면으로 들었다.
우테코 코치들도 좋은 피드백을 위한 고민을 많이 했을 것이라고 생각했어서 질문 형식과 같은 피드백이 유용할 것이라고 봤다. 실제로 ‘열린 질문, 닫힌 질문’이나 ‘중복 답변’에 대한 피드백도 받아볼 수 있었다. 너무 많은 문항 수인지도 경계해보아야 할 점이다. 나아가 이런 것에 대한 피드백은 코치를 통해서 받아볼 수 있지만, 팀 내에서도 지속적인 설문을 통해 발전시켜나가야 한다는 느낌도 받았다. 기획, 개발 하기에도 바쁜데 중요하게 생각할 것이 계속 늘어난다. 안 까먹고 어떻게 잘 지킬 수 있을까 🤔
🏋🏻♀️ 이번 주는요
지속되는 회의가 나에겐 참 어렵다. 협업이라는 게 이런 거라면 정말 잘 하고 있다 싶을 정도로 많이 한다. 의미있는 회의를 계속 하느라 머리를 많이 쓰고, 오후가 될 때쯤이면 관자놀이가 시큰하고 피로하다. 이번 주는 잠도 많이 못 자서 졸 뻔한 적도 몇 번 있다. 몸 컨디션 지키면서 회의, 개발까지 진행하기에는 무리인 걸까 🤨 그래도 건강이 최고다 아자아자 🏃