1. 개요

상대적인 시간과 두 시점 사이의 기간을 계산하는 것은 소프트웨어 시스템에서 일반적인 사용 사례입니다. 예를 들어 소셜 미디어 플랫폼에 새 사진을 게시하는 것과 같은 이벤트 이후 얼마나 많은 시간이 흘렀는지 사용자에게 보여주고 싶을 수 있습니다. 이러한 "시간 전" 텍스트의 예로는 "5분 전", "1년 전" 등이 있습니다.

단어의 의미와 선택은 완전히 상황에 따라 다르지만 전체적인 아이디어는 동일합니다.

이 사용방법(예제)에서는 Java에서 시간 전을 계산하기 위한 몇 가지 솔루션을 살펴봅니다. Java 8에 새로운 날짜 및 시간 API가 도입 되었기 때문에 버전 7과 버전 8에 대한 솔루션을 별도로 논의할 것입니다.

2. 자바 버전 7

Java 7에는 시간과 관련된 여러 클래스가 있습니다. 또한 Java 7 Date API의 단점으로 인해 여러 타사 시간 및 날짜 라이브러리도 사용할 수 있습니다.

먼저 순수한 Java 7을 사용하여 "시간 전"을 계산해 보겠습니다.

2.1. 순수한 자바 7

서로 다른 시간 세분성을 보유하고 이를 밀리초로 변환 하는 Enum 을 정의합니다 .

public enum TimeGranularity {
    SECONDS {
        public long toMillis() {
            return TimeUnit.SECONDS.toMillis(1);
        }
    }, MINUTES {
        public long toMillis() {
            return TimeUnit.MINUTES.toMillis(1);
        }
    }, HOURS {
        public long toMillis() {
            return TimeUnit.HOURS.toMillis(1);
        }
    }, DAYS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(1);
        }
    }, WEEKS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(7);
        }
    }, MONTHS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(30);
        }
    }, YEARS {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(365);
        }
    }, DECADES {
        public long toMillis() {
            return TimeUnit.DAYS.toMillis(365 * 10);
        }
    };

    public abstract long toMillis();
}

시간 변환을 위한 강력한 도구인 java.util.concurrent.TimeUnit enum 을 사용 했습니다. TimeUnit Enum을 사용 하여 TimeGranularity Enum 의 각 값에 대해 toMillis() 추상 메서드를 재정의하여 각 값에 해당하는 밀리초 수를 반환하도록 합니다. 예를 들어 "decade"의 경우 3650일에 대한 밀리초 수를 반환합니다.

TimeGranularity Enum 을 정의한 결과 두 가지 메서드를 정의할 수 있습니다. 첫 번째는 java.util.Date 객체와 TimeGranularity 의 인스턴스를 가져 오고 "time ago" 문자열을 반환합니다.

static String calculateTimeAgoByTimeGranularity(Date pastTime, TimeGranularity granularity) {
    long timeDifferenceInMillis = getCurrentTime() - pastTime.getTime();
    return timeDifferenceInMillis / granularity.toMillis() + " " + 
      granularity.name().toLowerCase() + " ago";
}

이 메서드는 현재 시간과 주어진 시간의 차이를 밀리초 단위의 TimeGranularity 값으로 나눕니다. 결과적으로 지정된 시간 단위로 주어진 시간 이후 경과된 시간을 대략적으로 계산할 수 있습니다.

현재 시간을 얻기 위해 getCurrentTime() 메서드를 사용 했습니다. 테스트를 위해 고정된 시점을 반환하고 로컬 시스템에서 시간을 읽는 것을 피합니다. 실제로 이 메서드는 System.currentTimeMillis() 또는 LocalDateTime.now()를 사용하여 현재 시간의 실제 값을 반환합니다.

방법을 테스트해 보겠습니다.

Assert.assertEquals("5 hours ago", 
  TimeAgoCalculator.calculateTimeAgoByTimeGranularity(
    new Date(getCurrentTime() - (5 * 60 * 60 * 1000)), TimeGranularity.HOURS));

또한 가장 적합한 시간 세분성을 자동으로 감지하고 보다 인간 친화적인 출력을 반환하는 메서드를 작성할 수도 있습니다.

static String calculateHumanFriendlyTimeAgo(Date pastTime) {
    long timeDifferenceInMillis = getCurrentTime() - pastTime.getTime();
    if (timeDifferenceInMillis / TimeGranularity.DECADES.toMillis() > 0) {
        return "several decades ago";
    } else if (timeDifferenceInMillis / TimeGranularity.YEARS.toMillis() > 0) {
        return "several years ago";
    } else if (timeDifferenceInMillis / TimeGranularity.MONTHS.toMillis() > 0) {
        return "several months ago";
    } else if (timeDifferenceInMillis / TimeGranularity.WEEKS.toMillis() > 0) {
        return "several weeks ago";
    } else if (timeDifferenceInMillis / TimeGranularity.DAYS.toMillis() > 0) {
        return "several days ago";
    } else if (timeDifferenceInMillis / TimeGranularity.HOURS.toMillis() > 0) {
        return "several hours ago";
    } else if (timeDifferenceInMillis / TimeGranularity.MINUTES.toMillis() > 0) {
        return "several minutes ago";
    } else {
        return "moments ago";
    }
}

이제 테스트를 통해 사용 예를 살펴보겠습니다.

Assert.assertEquals("several hours ago", 
  TimeAgoCalculator.calculateHumanFriendlyTimeAgo(new Date(getCurrentTime() - (5 * 60 * 60 * 1000))));

문맥에 따라 "적은", "여러", "많은" 또는 정확한 값과 같은 다른 단어를 사용할 수 있습니다.

2.2. 조다 타임 라이브러리

Java 8이 출시되기 전에 Joda-Time 은 Java의 다양한 시간 및 날짜 관련 작업에 대한 사실상의 표준이었습니다. Joda-Time 라이브러리의 세 가지 클래스를 사용하여 "시간 전"을 계산할 수 있습니다.

  • org.joda.time.Period 는 org.joda.time.DateTime 의 두 개체를 가져와 이 두 시점 간의 차이를 계산합니다.
  • org.joda.time.format.PeriodFormatter 는 기간 객체 를 인쇄하기 위한 형식을 정의합니다.
  • 사용자 정의 PeriodFormatter 를 생성하기 위한 빌더 클래스인 org.joda.time.format.PeriodFormatuilder

이 세 가지 클래스를 사용하여 현재와 과거 시간 사이의 정확한 시간을 쉽게 얻을 수 있습니다.

static String calculateExactTimeAgoWithJodaTime(Date pastTime) {
    Period period = new Period(new DateTime(pastTime.getTime()), new DateTime(getCurrentTime()));
    PeriodFormatter formatter = new PeriodFormatterBuilder().appendYears()
      .appendSuffix(" year ", " years ")
      .appendSeparator("and ")
      .appendMonths()
      .appendSuffix(" month ", " months ")
      .appendSeparator("and ")
      .appendWeeks()
      .appendSuffix(" week ", " weeks ")
      .appendSeparator("and ")
      .appendDays()
      .appendSuffix(" day ", " days ")
      .appendSeparator("and ")
      .appendHours()
      .appendSuffix(" hour ", " hours ")
      .appendSeparator("and ")
      .appendMinutes()
      .appendSuffix(" minute ", " minutes ")
      .appendSeparator("and ")
      .appendSeconds()
      .appendSuffix(" second", " seconds")
      .toFormatter();
    return formatter.print(period);
}

샘플 사용법을 살펴보겠습니다.

Assert.assertEquals("5 hours and 1 minute and 1 second", 
  TimeAgoCalculator.calculateExactTimeAgoWithJodaTime(new Date(getCurrentTime() - (5 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000))));

보다 인간 친화적인 출력을 생성하는 것도 가능합니다.

static String calculateHumanFriendlyTimeAgoWithJodaTime(Date pastTime) {
    Period period = new Period(new DateTime(pastTime.getTime()), new DateTime(getCurrentTime()));
    if (period.getYears() != 0) {
        return "several years ago";
    } else if (period.getMonths() != 0) {
        return "several months ago";
    } else if (period.getWeeks() != 0) {
        return "several weeks ago";
    } else if (period.getDays() != 0) {
        return "several days ago";
    } else if (period.getHours() != 0) {
        return "several hours ago";
    } else if (period.getMinutes() != 0) {
        return "several minutes ago";
    } else {
        return "moments ago";
    }
}

테스트를 실행하여 이 메서드가 보다 인간에게 친숙한 "time ago" 문자열을 반환하는지 확인할 수 있습니다.

Assert.assertEquals("several hours ago", 
  TimeAgoCalculator.calculateHumanFriendlyTimeAgoWithJodaTime(new Date(getCurrentTime() - (5 * 60 * 60 * 1000))));

다시 말하지만 사용 사례에 따라 "하나", "몇 개" 또는 "여러"와 같은 다른 용어를 사용할 수 있습니다.

2.3. Joda-Time TimeZone

Joda-Time 라이브러리를 사용하여 "시간 전" 계산에 시간대를 추가하는 것은 매우 간단합니다.

String calculateZonedTimeAgoWithJodaTime(Date pastTime, TimeZone zone) {
    DateTimeZone dateTimeZone = DateTimeZone.forID(zone.getID());
    Period period = new Period(new DateTime(pastTime.getTime(), dateTimeZone), new DateTime(getCurrentTimeByTimeZone(zone)));
    return PeriodFormat.getDefault().print(period);
}

getCurrentTimeByTimeZone() 메서드 는 지정된 시간대의 현재 시간 값을 반환합니다. 테스트를 위해 이 메서드는 고정된 시점을 반환하지만 실제로는 Calendar.getInstance(zone).getTimeInMillis() 또는 LocalDateTime.now(zone)를 사용하여 현재 시간의 실제 값을 반환해야 합니다.

3. 자바 8

Java 8 은 Joda-Time 라이브러리의 많은 아이디어를 채택한 새롭게 개선된 날짜 및 시간 API 를 도입했습니다. 기본 java.time.Durationjava.time.Period 클래스를 사용하여 "시간 전"을 계산할 수 있습니다.

static String calculateTimeAgoWithPeriodAndDuration(LocalDateTime pastTime, ZoneId zone) {
    Period period = Period.between(pastTime.toLocalDate(), getCurrentTimeByTimeZone(zone).toLocalDate());
    Duration duration = Duration.between(pastTime, getCurrentTimeByTimeZone(zone));
    if (period.getYears() != 0) {
        return "several years ago";
    } else if (period.getMonths() != 0) {
        return "several months ago";
    } else if (period.getDays() != 0) {
        return "several days ago";
    } else if (duration.toHours() != 0) {
        return "several hours ago";
    } else if (duration.toMinutes() != 0) {
        return "several minutes ago";
    } else if (duration.getSeconds() != 0) {
        return "several seconds ago";
    } else {
        return "moments ago";
    }
}

위의 코드 스니펫은 시간대를 지원하고 기본 Java 8 API만 사용합니다.

4. PrettyTime 라이브러리

PrettyTime은 특히 i18n 지원과 함께 "시간 전" 기능을 제공하는 강력한 라이브러리입니다 . 또한 사용자 정의가 가능하고 사용하기 쉬우며 Java 버전 7 및 8 모두에서 사용할 수 있습니다.

먼저 pom.xml 에 의존성 을 추가해 보겠습니다 .

<dependency>
    <groupId>org.ocpsoft.prettytime</groupId>
    <artifactId>prettytime</artifactId>
    <version>3.2.7.Final</version>
</dependency>

이제 인간 친화적인 형식으로 "시간 전"을 얻는 것은 매우 쉽습니다.

String calculateTimeAgoWithPrettyTime(Date pastTime) {
    PrettyTime prettyTime = new PrettyTime();
    return prettyTime.format(pastTime);
}

5. Time4J 라이브러리

마지막으로 Time4J는 Java에서 시간 및 날짜 데이터를 조작하기 위한 또 다른 훌륭한 라이브러리입니다. 시간 전을 계산하는 데 사용할 수 있는 PrettyTime 클래스가 있습니다.

의존성 을 추가해 보겠습니다 .

<dependency>
    <groupId>net.time4j</groupId>
    <artifactId>time4j-base</artifactId>
    <version>5.9</version>
</dependency>
<dependency>
    <groupId>net.time4j</groupId>
    <artifactId>time4j-sqlxml</artifactId>
    <version>5.8</version>
</dependency>

이 의존성을 추가한 후 이전 시간을 계산하는 것은 매우 간단합니다.

String calculateTimeAgoWithTime4J(Date pastTime, ZoneId zone, Locale locale) {
    return PrettyTime.of(locale).printRelative(pastTime.toInstant(), zone);
}

PrettyTime 라이브러리와 동일하게 Time4J는 기본적으로 i18n도 지원합니다.

6. 결론

이 기사에서는 Java에서 이전 시간을 계산하는 다양한 방법에 대해 설명했습니다.

순수 Java 및 타사 라이브러리 모두에 대한 솔루션이 있습니다. 새로운 날짜 및 시간 API가 Java 8에 도입되었으므로 순수한 Java 솔루션은 8 이전 버전과 이후 버전에서 다릅니다.

항상 그렇듯이 예제의 소스 코드는 GitHub에서 사용할 수 있습니다 .

Generic footer banner