BEYOND SW 캠프 23기 21주차 회고

이지연·2026년 4월 21일
post-thumbnail

Week 21

기간: 2026.04.06 - 2026.04.12
커리큘럼: 최종 프로젝트 (HR 인사시스템 SaaS 플랫폼) - 목표·성과 관리 도메인 설계 및 구현


1. Facts — 무엇을 했나?

< 목표·성과 관리 모듈 도메인 설계 및 핵심 로직 구현 >

  • 지난주 ERD를 바탕으로 kpi_template → goal → goal_approval_bundle 흐름을 실제 Spring Boot 엔티티와 서비스로 구현했다.
  • KpiTemplate에 KPI 정의 목록(kpis_json)을 JSON 배열로 저장하고, 이를 Java 값 객체 KpiDefinitionKpiDefinitionListConverter로 타입 안전하게 변환하는 구조를 설계했다.
  • Goal 엔티티에 parent_goal_id 자기 참조, rollup_policy, rollup_source, achievement_pct, rolled_achievement_pct를 설계해 계층 구조 목표와 하위 → 상위 달성률 롤업 계산이 자동으로 이루어지도록 구현했다.
  • 목표 상태를 DRAFT → ACTIVE → COMPLETED / CANCELLED로 정의하고, 활성화·완료 단계에 선택적 승인 워크플로를 붙이는 GoalApprovalBundle 구조를 설계했다. 단일 목표뿐 아니라 KPI 일괄 생성 시 여러 목표를 하나의 번들로 묶어 배치 승인이 가능하도록 만들었다.
  • GoalApprovalPolicy enum(NONE / ACTIVATION_ONLY / COMPLETION_ONLY / BOTH)으로 템플릿 단위 승인 정책을 세분화해, 활성화와 완료 각각에 대해 독립적으로 승인 여부를 제어할 수 있게 설계했다.
  • GoalActivity로 목표 생성·수정·승인·진행률 변경 등 모든 변경 이력을 감사 로그로 추적하고, GoalComment에 이모지 리액션 기능까지 구현했다.
  • MeasureType(HIGHER_BETTER / LOWER_BETTER / TARGET_MATCH)에 따라 actualValue를 입력하면 달성률이 자동 계산되는 수식을 Goal.updateActualValue()에 구현했다.

2. Feelings — 어떻게 느꼈나?

  • ERD 단계에서 테이블로만 보던 것들이 실제 엔티티 관계와 비즈니스 로직으로 구현되면서, 설계가 틀린 곳은 코드에서 반드시 드러난다는 걸 직접 체감했다.
  • 특히 승인 워크플로를 설계하면서, "목표를 만든다"는 단순한 기능이 실제로는 상태, 정책, 권한, 이벤트, 알림이 맞물리는 복잡한 흐름임을 실감했다.
  • rollup_policy처럼 집계 방식을 데이터로 관리하는 구조를 보면서, 단순히 조회 로직을 짜는 것과 비즈니스 규칙 자체를 시스템이 이해하는 것 사이의 차이를 느꼈다.
  • 코드를 작성하면서 처음부터 완벽하게 설계할 수는 없고, 구현하다 보면 설계의 허점이 보이고, 그걸 개선하는 과정이 진짜 설계라는 생각이 들었다.

3. Findings — 무엇을 배웠나?

  • 목표 계층 구조에서 자기 참조 엔티티의 한계를 배웠다. parent_goal_id로 트리를 구성하면 조회가 간단하지만, 깊이(depth)를 계산할 때 부모를 거슬러 올라가며 반복 쿼리가 발생하는 N+1 문제가 생긴다. Materialized Path나 depth 컬럼을 엔티티에 직접 저장하는 방식이 성능상 더 적합함을 알게 됐다.
  • 달성률 계산은 측정 방식에 따라 수식이 달라진다는 점을 설계로 풀었다. 수치가 높을수록 좋은 지표(HIGHER_BETTER), 낮을수록 좋은 지표(LOWER_BETTER), 특정 목표값에 근접할수록 좋은 지표(TARGET_MATCH) 각각에 다른 공식이 필요했고, 이를 MeasureType enum으로 분기 처리하는 구조를 배웠다.
  • 레거시 호환 필드와 신규 필드를 동시에 유지하는 것은 생각보다 위험하다는 걸 체감했다. requireApproval(boolean)과 goalApprovalPolicy(enum)처럼 같은 개념을 두 필드로 관리하면, 하나를 변경할 때 다른 하나와 동기화가 깨지는 버그가 발생할 수 있다. 마이그레이션을 빠르게 완료하고 레거시 필드를 제거하는 것이 중요하다.
  • 상태 기반 설계에서는 각 상태 전이에 따른 부수 효과(롤업 재계산, 활동 기록, 이벤트 발행)를 명확하게 정의해야 한다는 점을 배웠다. activate(), complete(), cancel()처럼 상태 변경 메서드를 엔티티에 캡슐화하고, 부수 효과는 서비스 계층에서 일관되게 처리하는 패턴이 유지보수에 유리했다.
  • JSON 컬럼은 편리하지만 타입 안전성이 없다는 점을 다시 확인했다. KpiDefinition처럼 전용 값 객체 클래스와 JPA AttributeConverter를 함께 사용하면 직렬화·역직렬화를 중앙에서 관리할 수 있고, 컴파일 타임에 오류를 잡을 수 있다.

4. Future — 다음에 어떻게 활용할까?

  • 현재 computeDepth()의 N+1 쿼리 문제를 해결하기 위해 Goal 엔티티에 depth 컬럼을 추가하고, 목표 생성·부모 변경 시 자동으로 갱신되도록 개선할 계획이다.
  • requireApproval(boolean) 레거시 필드를 DB 마이그레이션으로 제거하고, goalApprovalPolicy만으로 단일 관리하도록 정리할 예정이다.
  • 다음 단계에서는 평가(Evaluation) 모듈과의 연결을 구체화할 예정이다. 지금은 goalSnapshotJson이 평가자에게 맥락 정보를 보여주는 용도로만 쓰이는데, 목표 달성률(achievementPct)이 평가 점수 계산에 직접 반영되는 구조로 개선해야 한다.
  • EvaluationDesign의 섹션에 SectionType(MANUAL / KPI_SCORE / PEER_FEEDBACK)을 도입해, KPI 섹션은 achievementPct를 자동으로 집계하고 수동 섹션은 평가자가 직접 입력하는 혼합 채점 구조를 설계할 계획이다.
  • 이후 권한 설계와 연결해 GoalVisibility, ownerType별 접근 제어가 실제 멀티테넌시 환경에서도 올바르게 동작하는지 검증할 예정이다.

🧩 마무리 한 줄

"목표를 만드는 것보다 목표가 어떤 상태로, 누구에게, 어떻게 연결되는지를 설계하는 게 진짜 성과관리 시스템이었다."

profile
Eazy하게

0개의 댓글