자바로 다시 계산해보는 휴가 계산법

기존의 휴가 계산 방식의 문제점


최근 사내에서 틈틈히 업무관리 시스템을 분석 / 재개발을 하고 있었습니다

기존에 사용하던 시스템에는 다음과 같은 문제점이 있었습니다

  • #1 큰 핵심로직이 자바 코드와 DB 코드(함수/프로시저) 분리
  • #2 매직 넘버의 나열 (1,2,3 등..)
  • #3 디컴파일로 복원하여 버전 관리되고 있는 코드들
  • #4 서로다른 두 얼굴의 항목 (프론트와 백엔드 로직)

#1 큰 핵심로직이 자바 코드와 DB 코드(함수/프로시저) 분리

예를들어 업무시스템에서 가장 까다로운 부분 중 하나는 잔여일수를 계산하는 공식이었습니다

이 잔여일수를 계산하는 공식이 자바 코드와 DB의 프로시저/함수 등의 코드 등으로 파편화되어 있었습니다

처음 각 휴가의 항목을 나누는 부분은 자바에서

그 나누어진 항목을 프로시저에서 임시테이블을 생성해서 처리하는 등의 복잡한 과정을 거치고 함수를 거쳐서 어떤 가공을 하였다가

최종적으로 자바에서 합계 등을 더하는.. 굉장히 복잡해 보이는 계산 과정이었습니다..

필자는 데이터를 가져오는 부분만 DB에 역할을 부여하고 실질적인 계산은 Java에서 돌리는 것으로 바꾸려 합니다

#2 매직 넘버의 나열 (1,2,3 등..)

사용자들은 휴가/반차 등에 대하여 휴가의 코드값이 CODE_1인지 2인지 반차가 5인지 6인지 등을 알 필요가 없습니다

그러나 운영자는 해당 코드명이 어떤 코드값과 대응되는지를 알아야 대응을 하거나 분석을 합니다

이 코드가 enum이나 내부 final staic 변수 등으로 전혀 관리가 되어 있지 않기 때문에

enum으로 변경하려 합니다

#3 디컴파일로 복원하여 버전 관리되고 있는 코드들

이 부분은 정확히는 버전 관리쪽에 가까운 얘기이지만 본래 이 코드는 사내 자체의 코드가 없고

서버 코드를 디컴하여 관리되고 있었습니다

이제 개발만 되면은 형상관리가 되니 이부분은 자연스럽게 해결이 될 것으로 보입니다

#4 서로다른 두 얼굴의 항목 (프론트와 백엔드 로직)

예를들어 휴가관리에서 전혀 사용되지 않는 항목이 있습니다

실제와 다르지만 편의상 경조휴가라고 하겠습니다

이 경조휴가를 프론트단에서부터 쭈욱 로직을 따라가보면

백엔드에서는 경조휴가가 아닌 공가의 개념으로 사용되고 있었습니다

그리고 최근 2년동안 사용된 결과를 보면은 전혀 사용되고 있지 않던 항목입니다

이런 불필요한 부분도 상의하에 없애기로 합니다

코드로 보는 개선한 휴가 관리 코드


이제 코드로 보겠습니다

매직 상수 제거


import static vacation.DateTimeUnit.*;
import static vacation.PlusMinus.*;

public enum Vacation {

      V01(1_000_001L, "휴가", DAY, MINUS)
    , V02(1_000_002L, "반차", HALF_DAY, MINUS)
    , V03(1_000_003L, "외출", HOUR, MINUS)
    , V04(1_000_004L, "조퇴", HOUR, MINUS)
    , V05(1_000_005L, "지각", SEC, MINUS)
    , V06(1_000_006L, "공가", DAY, NEUTRAL)
    , V07(1_000_107L, "대체휴일", DAY, PLUS);

    final Long id;
    final String name;
    final DateTimeUnit dateTimeUnit;
    final PlusMinus plusMinus;

    Vacation(Long id, String name, DateTimeUnit dateTimeUnit, PlusMinus plusMinus) {
        this.id = id;
        this.name = name;
        this.dateTimeUnit = dateTimeUnit;
        this.plusMinus = plusMinus;
    }

    @Override
    public String toString() {
        return "Vacation{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", dateTimeUnit=" + dateTimeUnit +
                '}';
    }
}

휴가는 매직넘버 대신에 enum 으로 관리됩니다

enum 생성자에는 코드ID, 코드명, 시간타입, +/- 등이 있습니다

예를들어

V01(1_000_001L, "휴가", DAY, MINUS)

의 경우에는 휴가를 갈 경우 하루이며, minus 계산을 한다는 뜻입니다

V02(1_000_002L, "반차", HALF_DAY, MINUS)

공가의 경우에는 HALF_DAY로, 4시간을 minus 계산합니다

V07(1_000_107L, "대체휴일", DAY, PLUS);

대체휴일의 경우는 하루를 plus합니다

public enum PlusMinus {

    PLUS(1), MINUS(-1), NEUTRAL(0);

    final int term;

    PlusMinus(int term) {
        this.term = term;
    }

}

plus의 경우는 +, minus의 경우는 - 이다

public class VacationRequest {

    private final static int HOUR_SECOND = 3600;
    private final static int MINUTE_SECOND = 60;

    Vacation vacation;
    LocalDateTime startDateTime;
    LocalDateTime endDateTime;

    // 시작 일시와 끝 일시가 같은 경우
    public VacationRequest(Vacation vacation, LocalDateTime startDateTime) {
        this.vacation = vacation;
        this.startDateTime = startDateTime;
        this.endDateTime = startDateTime;
    }

    public VacationRequest(Vacation vacation, LocalDateTime startDateTime, LocalDateTime endDateTime) {
        this.vacation = vacation;
        this.startDateTime = startDateTime;
        this.endDateTime = endDateTime;
    }

    // 휴가/부재의 종류에 다라 다른 연산 적용
    public int second() {

        int second = vacation.dateTimeUnit.getSecond();
        int term = vacation.plusMinus.term;

        switch (vacation) {
            case V03: // 외출
            case V04: // 조퇴
                int timeInterval = (endDateTime.getHour() * HOUR_SECOND + endDateTime.getMinute() * MINUTE_SECOND)
                        - (startDateTime.getHour() * HOUR_SECOND + startDateTime.getMinute() * MINUTE_SECOND);
                return timeInterval * term;
            case V05: // 지각
                timeInterval = (startDateTime.getHour() * HOUR_SECOND + startDateTime.getMinute() * MINUTE_SECOND + startDateTime.getSecond())
                        - (9 * HOUR_SECOND);
                return timeInterval * term;
        }
        return second * term;
    }

    // toString

휴가 요청을 해주는 class 입니다

이것은 실제 테이블과 연결이 될 객체입니다

public VacationRequest(Vacation vacation, LocalDateTime startDateTime)
public VacationRequest(Vacation vacation, LocalDateTime startDateTime, LocalDateTime endDateTime)

생성자가 두 개가 있는경우는 당일치기 휴가나 공가처럼 하루 단위로 부재인 경우를 짧게 입력받기 위해서 제공합니다

이제 second() 매서드를 보겠습니다

case V03: // 외출
case V04: // 조퇴
    int timeInterval = (endDateTime.getHour() * HOUR_SECOND + endDateTime.getMinute() * MINUTE_SECOND)
            - (startDateTime.getHour() * HOUR_SECOND + startDateTime.getMinute() * MINUTE_SECOND);
    return timeInterval * term;

외출과 조퇴의 경우 부재가 일어난 시작과 끝의 차이를 초로 계산합니다

예를들어

14:30 ~ 18:00에 조퇴를 하였다면

{ 18 * 3600(초) + 0 * 60(초) } - { 14 * 3600(초) + 30 * 60(초) }

가 됩니다

지각의 경우는 계산법이 조금 타이트합니다 .. :)

왜냐하면 9시 00분 11초에 와도 지각이기 때문에 11초만큼의 초를 계산해야 하기 때문입니다

일급 컬렉션 적용

일급 컬렉션을 사용하기 전까지는 잔여일수를 계산하는 공식은 휴가요청(VacationRequest) 클래스와 밀접한 연관이 있는데

휴가요청 자체는 외부에서 List 형식으로 사용되기 때문에 별도의 메서드를 사용해야 했었습니다

예를들어 아래와 같습니다

class 휴가_서비스 {

  // 실제로는 DI 를 통해 주입
   List<VacationRequest> requests = ...;

   // 한 유저에 대한 잔여일수 계산
   getVacationDaysForUser() {

     getRemainVacationDays(user) 호출
   }
  // 잔여 일수 계산
   getRemainVacationDays (User user) {
     // 로직
   }
}

아래는 일급 컬렉션을 적용한 VacationRequests 입니다

public class VacationRequests {

    List<VacationRequest> vacationsRequests = new ArrayList<>();
    Double vacationDays = 20.0;

    public VacationRequests(List<VacationRequest> vacationsRequests) {
        this.vacationsRequests = vacationsRequests;
    }

    public Double getDiffDays() {
        return vacationsRequests.stream()
                .map(VacationRequest::second)
                .reduce(Integer::sum)
                .map(e->e/(double)DateTimeUnit.DAY.getSecond())
                .orElseGet(() -> -1.0);
    }

    public Double getVacationRaminDays() {
        return vacationDays + getDiffDays();
    }
}

VacationRequest 을 컬렉션으로 Wrapping해줄 상위 객체가 있으면 해결이 됩니다

편의상 총 휴가일수(vacationDays)는 20일로 잡았습니다

getDiffDays 메서드를 통해서 지금까지 사용한 총 휴가 증감일을 계산합니다

예를들어 휴가 2일에 반차를 한번 사용하였으면

-2.5일이 휴가증감일이 됩니다

그래서 실제로 휴가를 계산해 보면..?

VacationServiceMain vacationMain = new VacationServiceMain();

VacationRequests vacationRequests = new VacationRequests(vacationMain.initVacationSampleDays());
vacationMain.calcVacationDays(vacationRequests);

위와 같은 main함수에서 실행하는 테스트 코드가 있다고 할 때..

아래와 같은 예시를 준비합니다

private List<VacationRequest> initVacationSampleDays() {

    List<VacationRequest> vacationsRequests = new ArrayList<>();

    // 휴가 8시간
    vacationsRequests.add(
            new VacationRequest(Vacation.V01,
                    LocalDateTime.of(2021, 10, 6, 0, 0)
            ));
    // ...

    return vacationsRequests;
}
  • 휴가 2일
  • 반차 1회
  • 외출 1시간
  • 조퇴 2시간
  • 지각 15분
  • 공가 1회
    • 공가는 잔여일수에 영향을 미치지 않습니다 (+0일)
  • 경조휴가 1회
    • 휴가를 하루 더 부여합니다

계산과정을 표현해보았습니다 (수식) \[-2-\frac{1}{2}-\frac{1}{8[hour]}-\frac{2}{8[hour]}-\frac{15}{(8*60)[minutes]}+0+1\]

수식이 깨지는 것 같아 아래 그림으로 올립니다 !

image

image

위 계산기 결과는 예상 결과입니다

이제 코드로 수행해봅니다

private void calcVacationDays(VacationRequests vacationRequests) {

    double diffDays = vacationRequests.getDiffDays();

    out.println("diffDays = " + diffDays);

    double remainVacationDays = vacationRequests.getVacationRaminDays();

    out.println("remainVacationDays = " + remainVacationDays);
}

결과는.. ?!

image

뚜둥! 계산되었습니다… ㅎㅎ

참고 Latex 문법 https://huni0318.github.io/blog/blog-etc/2020-12-21-markdown-tutorial2/ enum https://wickies.tistory.com/38
enum 다형성 https://velog.io/@ljinsk3/Enum%EC%9C%BC%EB%A1%9C-%EB%8B%A4%ED%98%95%EC%84%B1%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
편의점 예시, 일급 컬렉션 https://velog.io/@tigger/%EC%9D%BC%EA%B8%89-%EC%BB%AC%EB%A0%89%EC%85%98
향로님의 일급 컬렉션 https://jojoldu.tistory.com/412





© 2020.12. by 따라쟁이

Powered by philz