ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 확장성을 고려하여 객체지향적으로 영업시간 구현하기
    프로젝트/BLUE DELIVERY 2021. 7. 29. 23:35

    blue-delivery 프로젝트를 진행하면서 주로 '확장 가능한 코드'를 작성하기 위한 고민들을 하였습니다. 하나는 영업시간을 구현하면서 했던 고민이고, 하나는 전체적인 구조에 대한 고민입니다.

    이번 글에서는 확장성있게 영업시간을 구현하기 위한 고민을 정리해보았습니다.

    입력받은 영업시간 정책을 어떻게 구분하고, 각 요일로 변환할까?

    (실제론 UI가 없지만) 대략 아래와 같이 입력받는다고 가정합니다.

    주말 포함이라면 한가지 케이스의 오픈/마감 시간을 입력받아야 하고, 주말 별도라면 평일, 토요일, 일요일 총 3가지 케이스를 입력받아야 합니다.

    그리고 어떤 정책(주말 포함or별도)이냐에 따라 적절한 요일로 변환해서 데이터베이스에 저장해야 합니다.

    (예를 들어, 위의 그림처럼 평일, 토요일, 일요일을 입력받으면.. → 월-금, 토, 일에 해당하는 객체 총 7개로 변환 만약 주말포함이면 1가지 케이스만 입력받고 → 모두 동일한 시간의 월-일 에 해당하는 객체 총 7개로 변환)

     

    정책이라는 것은 때에 따라 추가될 수도 있고, 사라질 수도 있습니다. 가령 지금은 두 가지 정책만 존재하지만, 나중엔 모든 요일에 대해 입력받는 방식이 추가될 수도 있습니다.

    저는 어떻게 추후에 확장성있게, 구조적인 변경없이 새로운 정책을 추가할 수 있을까? 를 고민하게 되었습니다.

     

    if문 분기

    아래 예제에서는 편의상 getter 를 사용하지 않습니다.

    정책을 구분하기 위해 가장 쉽게 생각할 수 있는 방법은 if문 으로 분기하는 방법일 것입니다.

    // service
    if (dto.businessHourPolicy == EVERY_SAME_TIME) {
        ... // 월화수목금토일을 같은 시간으로
    } else if (dto.businessHourPolicy == WEEKDAY_WEEKEND) {
      ... // 월화수목금, 토, 일로 구분
    }

    새로운 정책이 추가되면 else if 분기를 추가해주면 됩니다.

    그러나 정책을 구분하는 기준이 추가된다면 어떨까요?

     

    지금은 dto.businessHourPolicy 만을 확인하고 있지만, 클라이언트로부터 잘못된 데이터를 받을 수도 있습니다. (EVERY_SAME_TIME 인데, 시간을 여러 케이스 받는 등..)

     

    그래서 검증하는 과정으로 입력받은 시간이 몇개의 케이스인지 확인하는 로직을 추가해보겠습니다.

    // service
    if (dto.businessHourPolicy == EVERY_SAME_TIME) {
      if (dto.hours.size() != 1) {throw new IllegalArgumentException();} 
        ... // 월화수목금토일을 같은 시간으로
    } else if (dto.businessHourPolicy == WEEKDAY_WEEKEND) {
        if (dto.hours.size() != 3) {throw new IllegalArgumentException();} 
      ... // 월화수목금, 토, 일로 구분
    } 

    고작 2가지 정책에 각각 검증 로직 1줄을 추가했을 뿐인데, 코드가 지저분해지기 시작했습니다. 다음과 같은 고민을 해봅니다.

    • 만약 정책이 2개가 아니라, 10개 쯤 된다면?
    • 검증 로직이 1줄이 아니라, 10줄 이상이 된다면?

     

    // 정책과 로직이 늘어난다면....
    if (dto.businessHourPolicy == EVERY_SAME_TIME) {
      if (dto.hours.size() != 1) {throw new IllegalArgumentException();} 
        // 또다른 검증 로직 추가....
        // 또 또다른 검증 로직 추가....
    
        ... // 월화수목금토일을 같은 시간으로
    } else if (dto.businessHourPolicy == WEEKDAY_WEEKEND) {
        if (dto.hours.size() != 3) {throw new IllegalArgumentException();} 
        // 또다른 검증 로직 추가....
        // 또 또다른 검증 로직 추가....  
    
        ... // 월화수목금, 토, 일로 구분
    } else if (dto.businessHourPolicy == 어떤_정책_3) {
        if (dto.hours.size() != 4) {throw new IllegalArgumentException();} 
        // 또다른 검증 로직 추가....
        // 또 또다른 검증 로직 추가....
    
        ... // 정책에 맞는요일별 영업일 추가
    } else if (dto.businessHourPolicy == 어떤_정책_4) {
        // 검증로직1
        // 또다른 검증 로직 추가....
        // 또 또다른 검증 로직 추가....
    
        ... // 정책에 맞는 요일별 영업일 추가
    } ....

    위와 같은 경우 하나의 메소드 안에 정책을 구분하는 로직이 너무 커집니다.

    • 각 정책을 구분하기 어려워지면서 원하는 코드가 어디있는지 알기 어려워집니다. 각 정책에 대한 유지보수성이 떨어집니다.
    • 정책이 사라지거나 추가되면 기존의 코드를 변경해야합니다. SOLID의 OCP를 위반하면서 확장성이 떨어집니다.

     

    각 정책을 객체로 만들자

    if문 분기와 같은 단점을 만들지 않기 위해서 각 정책을 객체로 만들었습니다.

    우선 각 정책을 구분하고 요일별로 구분해줄 DayOfWeekMapper 를 만들었습니다.

    public class DayOfWeekMapper {
            // 각 정책을 가진 list
        private static final List<BusinessHourCondition> conditions = new ArrayList<>();
    
            // 각 정책을 Mapper에 등록해준다.
        static {
            conditions.add(new EverydayBusinessHourCondition());
            conditions.add(new WeekdayWeekendBusinessHourCondition());
        }
    
            // 등록된 Mapper를 순회하면서 조건에 맞는 정책을 찾아 각 요일을 구분해준다.
            // ex. EVERY_SAME_TIME 09시~18시인 dto (1개 케이스) -> 월~일 각 요일의 09시~18시 (총 7개 케이스)
         public static List<BusinessHour> map(BusinessHoursTarget target) {
            BusinessHourType type = target.getBusinessHourType();
            Map<BusinessHourDay, BusinessHourParam> hours = target.getBusinessHours();
            
            for (BusinessHourCondition condition : conditions) {
                if (condition.isSatisfied(type, hours)) {
                    List<BusinessHour> list = condition.mapToDayOfWeek(hours);
                    Collections.sort(list);
                    return list;
                }
            }
            throw new IllegalArgumentException("wrong values for business hour");
        }
    }

     

    // 정책
    public interface BusinessHourCondition {
            // 파라미터가 정책 조건에 부합하는지 검사
        boolean isSatisfied(BusinessHourType type, Map<BusinessHourDay, BusinessHourParam> params);
          // 파라미터를 요일-시간 map 으로 반환
        List<BusinessHour> mapToDayOfWeek(Map<BusinessHourDay, BusinessHourParam> params);
    }
    • 각 정책을 객체로 만들어서 Mapper에 등록했습니다. (static { conditions.add(...); } ) 만약 새로운 정책이 추가된다면, 정책 객체를 따로 만들고 'add' 만 해주면 됩니다.
    • public static List<BusinessHour> map(BusinessHoursTarget target) 메소드에서 Mapper가 가진 정책을 순회하면서 파라미터가 정책에 부합하는지 확인합니다. (if (condition.isSatisfied(type, hours)) {...})

     

    if 문을 사용했을 때와 비교해보겠습니다.

    • if 문에서는 새로운 정책이 생기면 if문, 그 안에 검증 로직, 운영시간 매핑 코드등 기존의 코드에 많은 변경이 발생했습니다. 지금은 정책을 등록만 해주면 손 댈 부분이 없습니다. (등록도 spring 기능을 사용해서 등록하면 따로 분리가 가능해질 것이고, 그렇게되면 이 Mapper에서는 손 댈 부분이 없어집니다)
    • if 문에서는 기존 정책을 변경하려면, 복잡한 if문 속에서 정책을 찾고, 어떤 부분이 검증 코드인지 등등 기능에 해당하는 코드를 일일이 찾아야 했습니다. 지금은 각 정책에 해당하는 클래스로 이동하여 isSatisfied()mapToDayOfWeek() 등 독립된 메소드를 확인하면 됩니다.

    최종적인 구조는 아래와 같습니다.

     

    결론

    이로서 어느정도 제가 생각한 확장성 있는 코드를 작성하게 된 것 같습니다.

    그러나 여전히 의구심이 드는 점이 있습니다. 과연 운영시간 정책이 그렇게 확장될 가능성이 있나? 하는 점입니다.

    실제로 이 고민을 하느라 상당히 많은 시간을 보냈는데, 확장 또는 변경될 가능성이 없다면 if문으로 작성해도 문제가 없었을 것이고, 그렇다면 꽤 많은 시간을 절약했을지도 모릅니다.

     주어진 기간동안 개발해서 사용자에게 서비스를 제공해야 하는 상황을 생각하면 때로는 좀 별로인 것 처럼 보이는 방법을 선택할 줄도 알아야 할 것 같다는 생각이 들기도 했습니다.

    댓글

Designed by Tistory.