개괄
Jira나 Linear을 이용하다보면, 가끔 내가 해결한 티켓들을 그대로 고이 모셔 두기가 너무 아깝다.
같은 프로젝트를 하더라도, 내가 이 사람보다 월등히 일을 많이 해결했는데 인정받지 못하는 순간이 아쉬워서
어떤 프로젝트(또는 회사) 안의 SubTask를 만들고, 그 Task 안에서 Ticket을 발부할 때마다 티켓의 중요도와 난이도를 가지고 인사평가를 할 수 있는 프로젝트를 만들었다.
그런데 프로젝트 도중 내부 회의를 하다가 이상한 점을 발견했다.
그동안의 점수 로직은 0~5까지의 0.5단위의 점수를 매길 수 있는 중요도와 난이도를 그냥 합산한 수치로 계산했는데,
나는 어쩔때는 예상 밖의 예외에 집착하고 많은 엣지케이스를 실험해 보는 습관이 있어서
내가 짠 로직들에 대해 악감정을 가지고 어떻게 해야 개발자를 골려먹을 수 있을까에 대해 실험해 보았다.
결과적으로는 이 로직을 악용하기에 충분했다. 모든 난이도와 중요도를 5점으로 설정해 놓으면 모든 사원이 만점을 받고, 정상적으로 난이도와 중요도를 매긴 타 업체나 타 프로젝트들에 비해 이 사람이 어떤 능력을 가진 사람인지 예상하기 힘들었고, 편애를 받는 사람은 그 사람이 가진 티켓의 난이도나 중요도만 상위 직급에 있는(Subtask나 Project장) 사람이 올려놓으면 다른 사람의 질타를 받기 충분했다.
토의
그럼 이 문제를 어떻게 해결할까 하다가 대학교 2학년때 배운 선형대수가 떠올랐다.
바로 'Weight Sum(가중합)'을 이용하는 것이다.
가중합이라고 하면 이름만 어렵지 사실은 우리가 옛날에 배운 기초 수학이다.
먼저, 우리가 여태 사용하던 평균을 구하는 공식을 가지고 예를 들어 보자.
국어 | 수학 | 음악 | 평균 | |
영준 | 70 | 80 | 90 | 80 |
대현 | 100 | 100 | 40 | 80 |
영준이와 대현이는 공부를 열심히 했으나 둘 다 평균 80점을 받았다.
그런데 사실 대현이는 불만이 있었다. 수업 시수는 국어와 수학이 압도적으로 많은데, 비교과 내신인 음악 과목 때문에 결국 똑같은 평균 취급을 받아야 한다니..
대현이는 바로 교장선생님께 달려가서 이 부당함을 일렀고, 교장선생님은 이런 부당함을 없애기 위해 조치를 취했다.
바로 평균을 낼 때, 수업 시수를 고려한 평균을 내는 것이다.
국어 | 수학 | 음악 | 평균 | |
수업 | 3 | 5 | 2 | |
영준 | 70 | 80 | 90 | 79 |
대현 | 100 | 100 | 40 | 88 |
영준이는 ( 3 * 70 + 5 * 80 + 2 * 90 ) / (3+5+2) 인 평균 79점이,
대현이는 ( 3 * 100 + 5 * 100 + 2 * 40 ) / (3+5+2) 인 평균 88점이 나오게 됐다.
이렇게 영준이는 조금은 억울하지만 물론 쉬운 음악 과목을 날먹했으므로 억울함은 토로할 수 없었고,
대현이는 중요한 내신인 국어와 수학을 열심히 공부해서 좋은 평균을 냈으니 이보다 더 공정할 수는 없었다.
이렇게 둘은 공평한 점수를 얻게 되었다.
이것이 가중합 방식이고, 수업 시수는 가중치라고 생각하면 된다.
이 방식을 이용해 조금 더 공평한 점수를 구할 수 있다.
적용
이를 적용하여 나름 공평한 시스템을 설계할 수 있었다.
- 티켓과 그 티켓을 담고 있는 하나의 '일'인 Task에 대해서 얼마나 진행되었는지를 조사한다.
- 티켓이 완료되지 않았다면 이 티켓에 대해 점수를 줄 수 없어야 하고,
- 티켓을 처리하여 완료시간이 나왔다면, 전체 진행도에 비해서 이 티켓을 얼마나 빨리 끝냈는가를 체크한다.
- 진행도는 '분 단위'를 기준으로 계산한다.
- 그리고는 아까 계산한 평균처럼 점수 * 가중치 / 전체를 계산한다.
DTO로 매핑하기 위한 소스코드를 첨부한다. 매핑 프레임워크는 Mapstruct를 사용하였다.
@Named("calculateScore")
default double calculateScore(Ticket ticket) {
Task task = ticket.getTask();
if (ticket.getCompletedAt() == null) {
return 0;
} else {
double taskProgress = task.getExpiredAt() == null ? 0.0d : checkTicketDurationWithTask(ticket);
double difficultyWeight = 4;
double priorityWeight = 4;
double taskProgressWeight = 2;
return (ticket.getDifficulty() / (double)task.getTotalDifficulty()) * difficultyWeight +
(ticket.getPriority() / (double)task.getTotalPriority()) * priorityWeight +
taskProgress * taskProgressWeight;
}
}
/*
* 1. 티켓과 task의 expiredAt의 자유도를 고려한다.
*/
default double checkTicketDurationWithTask(Ticket ticket) {
if (ticket.getCompletedAt() == null || ticket.getCompletedAt()
.isAfter(ticket.getTask().getExpiredAt().plusDays(1).atStartOfDay())) {
return 0;
} else {
long now = ChronoUnit.MINUTES.between(ticket.getCreatedAt(), ticket.getCompletedAt());
long total = ChronoUnit.MINUTES.between(ticket.getCreatedAt(),
ticket.getTask().getExpiredAt().plusDays(1).atStartOfDay());
return now / (double)total;
}
}
쉽게 예를 들기 위해 가중치를 보이게 설정했으나, Property 파일에 담아서 암호화해줄 수 있다.
결론 및 더 발전할 점
이 방식을 이용해서 조금 더 공정한 점수 방식을 채택할 수 있었다.
이 글만 읽어보면 또 개인이 한 태스크에 한 티켓만 만들면 어떡하나요 라는 문제점을 제기할 수도 있지만,
B2B SaaS 프로젝트를 생각하고 만들었기에 1인이 어뷰징하는 일은 상관 없으며,
프로젝트나 태스크를 만들고 티켓의 난이도나 중요도를 설정하는 것은 각자의 역할을 잘 설정함으로서 해결하였다.
이를테면, 프로젝트의 장은 그 프로젝트 아래의 모든 태스크를 총괄할수 있으며, 태스크의 장은 그 태스크 안의 모든 티켓을 총괄할 수 있다. 또 그 태스크 예하에 라벨(팀)을 설정하여 이런저런 설정과 권한을 부여할 수 있다.
프로젝트를 조금 더 돌볼 시간이 있었더라면, 개인적으로는 Kafka를 이용해 티켓을 등록할 때마다 태스크 내의 참여인원들에게 SSE로 Noti를 줄 수 있다거나 스케쥴링을 이용해 태스크가 완료되지 않았더라도 실시간 점수를 계산해서 보여주는 시스템을 만들고 싶었다.
다음 진행할 사이드 프로젝트에서는 이를 보완하여 조금 더 안전한 시스템을 만들어볼 생각이다.
'설계' 카테고리의 다른 글
PK는 왜 Long을 쓸까? (0) | 2023.02.23 |
---|---|
TIL - 좋은 객체지향적 설계원칙, SOLID (0) | 2023.01.26 |
TIL - 좋은 객체지향적 설계원칙, SOLID(1) : SRP (0) | 2023.01.25 |
TIL - REST API : 누구나 읽기 쉽게 설계하기 (0) | 2023.01.18 |