이번 글은 날짜와 달력에 관해서 어떤 방식으로 접근할지 생각해보기 전에 살펴보면 좋은 글이라 여겨 가지고 왔다.
다른 타입(숫자형, 문자형 등)들에 비해 다양한 객체로 사용할 수 있어 추후 날짜 및 달력 또는 시간까지 고민할 경우 이 글을 보고 참고하면 좋을 듯하다.
1. 개요
날짜와 시간을 처리하는 것은 많은 Jav 애플리케이션의 기부족인 부분입니다. 수년에 걸쳐 Java는 날짜를 처리하는 데 있어 발전하여 개발자를 위해 일을 단순화하는 더 나은 솔루션을 도입했습니다.
여기서는 먼저 Java의 날짜 역사를 살펴보겠습니다. 오래된 클래스부터 시작하여 최신 모범 사례로 넘어가면서 날짜를 자신 있게 다룰 수 있도록 하겠습니다.
2. 레거시 접근 방식
java.time 패키지가 나오기 전에는 Date와 Calendar클래스가 주로 날짜 관리를 담당했습니다. 이 클래스들은 작동했지만, 그들만의 독특한 특징이 있었습니다.
2.1. java.util.Date 클래스
java.util.Date 클래스는 날짜를 처리하기 위한 Java의 원래 솔루션이었지만 몇 가지 단점이 있었습니다.
- 변경이 가능하므로 스레드 안전 문제가 발생할 수 있습니다.
- 시간대는 지원되지 않습니다.
- 이 함수는 혼란스러운 메서드 이름과 반환 값을 사용하는데, 예를 들어 1900년 이후의 연도를 반환하는 getYear() 함수가 있습니다.
- 현재 많은 메서드가 더 이상 사용되지 않습니다.
인수 없는 생성자를 사용하여 Date 객체를 만드는 것은 현재 날짜와 시간(객체가 생성된 순간)을 나타냅니다. Date 객체를 인스턴스화하고 값을 출력해 보겠습니다.
Date now = new Date();
logger.info("Current date and time: {}", now);
이렇게 하면 Thu Jan 02 2025 13:32:37 PDT 2025와 같이 현재 날짜와 시간이 출렵됩니다. 이 생성자는 여전히 작동하지만 언급된 이유로 새 프로젝트에는 더 이상 권장되지 않습니다.
2.2. java.util.Calendar 클래스
Date의 한계에 부딪힌 후 Java는 개선 사항을 제공하는 Calendar 클래스를 도입했습니다.
- 다양한 캘린더 시스템 지원
- 시간대 관리
- 날짜를 조장하는 더 직관적인 방법
Calendar를 사용하여 날짜를 조작할 수도 있습니다.
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 5);
Date fiveDaysLater = cal.getTime();
이 예제에서는 현재 날짜로부터 5일 후의 날짜를 계산하여 Date 객체에 저장합니다.
하지만 Calendar에도 단점이 있습니다.
- Date와 마찬가지로 변경 가능하며 스레드로부터 안전하지 않습니다.
- 해당 API는 몇 달 동안의 0부터 시작하는 인덱싱처럼 혼란스럽고 복잡할 수 있습니다.
3. 현대적 접근 방식: java.time 패키지
Java 8에서는 java.time 패키지가 도착하여 날짜오 ㅏ시간을 처리하기 위한 현대적이고 강력한 API를 제공했습니다. 이는 이전 Date 및 Calendar 클래스의 많은 문제를 해결하도록 설계되어 날짜 및 시간 조작을 보다 직관적이고 사용자 친화적으로 만들었습니다.
인기 있는 Joda-Time 라이브러리에서 영감을 받은 java.time은 이제 날짜와 시간을 다루는 핵심 Java 솔루션입니다.
3.1. java.time의 주요 클래스
java.time 패키지는 실제 애플리케이션에서 자주 사용되는 여러 가지 중요한 클래스를 제공합니다. 이러한 클래스는 세 가지 주요 범주로 그룹화할 수 있습니다.
::: 시간 컨테이너(Time Containers) :::
- LocalDate : 날짜만 나타냄(시간이나 시간대 없음)
- LocalTime : 날짜나 시간대를 포함하지 않고 시간을 나타냄
- LocalDateTime : 날짜와 시간을 결합하지만 시간대는 포함하지 않음
- ZonedDateTime : 날짜와 시간, 그리고 시간대를 모두 포함함
- Instant : 타임스탬프와 유사하게 타임라인의 특정 지점을 나타냄
::: 시간 조작자(Time Manipulators) :::
- Duration : 시간 기반의 시간을 나타냄(예 : "5시간" 또는 "30초")
- Period : 날짜 기반의 시간을 나타냄(예 :"2년 3개월")
- TemporalAdjusters : 날짜를 조정하는 방법을 제공(예 : 다음 월요일 찾기)
- Clock : 표준 시간대를 사용하여 현재 날짜-시간을 제공하고 시간 제어를 허용함
::: Formatter/Printers :::
- DateTimeFormatter : 날짜-시간 객체의 포맷팅 및 구문 분석에 사용됨
3.2. java.time의 장점
java.time 패키지는 기존의 날짜 및 시간 클래스에 비해 여러 가지 개선 사항을 제공합니다.
- Immutability(불변성) : 모든 클래스는 불변하므로 스레드 안정성이 보장됩니다.
- Clear API(명확한 AP) : 메서드가 일관되어 있어 API를 이해하기 더 쉽습니다.
- Focused Classes : 각 클래스는 날짜를 저장하거나, 조작하거나, 서식을 지정하는 등 특정 역할을 갖습니다.
- Formatting and Parsing : 내장된 메서드를 사용하면 날짜를 쉽게 서식 지정하고 구문을 분석할 수 있습니다.
4. java.time의 사용 예제
더 고급 기능에 뛰어들기 전에 java.time 패키지를 사용하여 날자 및 시간 표현을 만드는 기본 사항부터 시작해 보겠습니다. 탄탄한 기초가 생기면 날짜를 조정하는 방법과 날짜를 포멧하고 구문 분석하는 방법을 살펴보겠습니다.
4.1. 날짜 표현생성
java.time 패키지는 날짜와 시간의 다양한 측면을 나타내는 여러 클래스를 제공합니다. LocalDate, LocalTime, LocalDateTime을 사용하여 기본 날짜를 만들어 보겠습니다.
@Test
void givenCurrentDateTime_whenUsingLocalDateTime_thenCorrect() {
LocalDate currentDate = LocalDate.now(); // Current date
LocalTime currentTime = LocalTime.now(); // Current time
LocalDateTime currentDateTime = LocalDateTime.now(); // Current date and time
assertThat(currentDate).isBeforeOrEqualTo(LocalDate.now());
assertThat(currentTime).isBeforeOrEqualTo(LocalTime.now());
assertThat(currentDateTime).isBeforeOrEqualTo(LocalDateTime.now());
}
필수 매개변수를 전달하여 특정 날짜와 시간을 생성할 수도 있습니다.
@Test
void givenSpecificDateTime_whenUsingLocalDateTime_thenCorrect() {
LocalDate date = LocalDate.of(2024, Month.SEPTEMBER, 18);
LocalTime time = LocalTime.of(10, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);
assertEquals("2024-09-18", date.toString());
assertEquals("10:30", time.toString());
assertEquals("2024-09-18T10:30", dateTime.toString());
}
4.2. TemporalAdjusters를 사용한 날짜 표현 조정
날짜 표현이 있으면 TemporalAdjusters를 사용하여 조정할 수 있습니다. TemporalAdjusters 클래스는 날짜를 조작하기 위한 미리 정의된 메서드 집합을 제공합니다.
@Test
void givenTodaysDate_whenUsingVariousTemporalAdjusters_thenReturnCorrectAdjustedDates() {
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
assertThat(nextMonday.getDayOfWeek())
.as("Next Monday should be correctly identified")
.isEqualTo(DayOfWeek.MONDAY);
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
assertThat(firstDayOfMonth.getDayOfMonth())
.as("First day of the month should be 1")
.isEqualTo(1);
}
사전 정의된 조정자 외에도 특정 요구 사항에 맞게 사용자 정의 조정자를 생성할 수 있습니다.
@Test
void givenCustomTemporalAdjuster_whenAddingTenDays_thenCorrect() {
LocalDate specificDate = LocalDate.of(2024, Month.SEPTEMBER, 18);
TemporalAdjuster addTenDays = temporal -> temporal.plus(10, ChronoUnit.DAYS);
LocalDate adjustedDate = specificDate.with(addTenDays);
assertEquals(
today.plusDays(10),
adjustedDate,
"The adjusted date should be 10 days later than September 18, 2024"
);
}
4.3. 날짜 형식 지정
java.time.format 패키지의 DateTimeFormatter 클래스를 사용하면 스레드 안전한 방식으로 날짜-시간 객체를 포맷하고 구문 분석할 수 있습니다.
@Test
void givenDateTimeFormat_whenFormatting_thenVerifyResults() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
LocalDateTime specificDateTime = LocalDateTime.of(2024, 9, 18, 10, 30);
String formattedDate = specificDateTime.format(formatter);
LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter);
assertThat(formattedDate).isNotEmpty().isEqualTo("18-09-2024 10:30");
}
우리의 필요에 따라 미리 정의된 형식이나 사용자 정의 패턴을 사용할 수 있습니다.
4.4. 날짜 분석
마찬가지로 DateTimeformatter는 문자열 표현을 날자 또는 시간 객체로 다시 구문 분석할 수 있습니다.
@Test
void givenDateTimeFormat_whenParsing_thenVerifyResults() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter);
assertThat(parsedDateTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getYear()).isEqualTo(2024);
assertThat(time.getMonth()).isEqualTo(Month.SEPTEMBER);
assertThat(time.getDayOfMonth()).isEqualTo(18);
assertThat(time.getHour()).isEqualTo(10);
assertThat(time.getMinute()).isEqualTo(30);
});
}
4.5. OffsetDateTime 및 OffsetTime을 통한 시간대 작업
다양한 시간대로 작업할 경우 OffsetDateTime 및 OffsetTime 클래스는 UTC로부터의 날짜 및 시간 값 또는 오프셋을 처리하는데 유용합니다.
@Test
void givenVariousTimeZones_whenCreatingOffsetDateTime_thenVerifyOffsets() {
ZoneId parisZone = ZoneId.of("Europe/Paris");
ZoneId nyZone = ZoneId.of("America/New_York");
OffsetDateTime parisTime = OffsetDateTime.now(parisZone);
OffsetDateTime nyTime = OffsetDateTime.now(nyZone);
assertThat(parisTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getOffset().getTotalSeconds())
.isEqualTo(parisZone.getRules().getOffset(Instant.now()).getTotalSeconds());
});
// Verify time differences between zones
assertThat(ChronoUnit.HOURS.between(nyTime, parisTime) % 24)
.isGreaterThanOrEqualTo(5) // NY is typically 5-6 hours behind Paris
.isLessThanOrEqualTo(7);
}
여기서는 서로 다른 시간대에 대한 OffsetDateTime 인스턴스를 생성하고 오프셋을 확인하는 방법을 보여줍니다. ZoneId를 사용하여 뉴욕의 시간대를 정의하는 것으로 시작합니다. 그런 다음 OffsetDateTime.now()를 사용하여 두 시간대의 현재 시간을 캡쳐합니다.
테스트는 파리 시간대 오픗셋이 파리 시간대의 예상 오프셋과 일치하는지 확인합니다. 마지막으로, 뉴욕과 파리의 시간 차이를 확인하여 표준 시간대 차이를 반영하여 일반적인 5~7시간 범위 내에 있는지 확인합니다.
4.6. 고급 사용 사례 : Clock
java.time 패키지의 Clock 클래스는 특정 시간대를 고려하여 현재 날짜와 시간에 액세스하는 유연한 방법을 제공합니다. 시간을 더 많이 제어해야 하는 시나리오나 시간 기반 논리를 테스트할 때 유용합니다.
시스템의 현재 시간을 가져오는 LocalDateTime.now()를 사용하는 것과 달리 Clock을 사용하면 특정 시간대에 대한 시간을 얻거나 테스트 목적으로 시간을 시뮬레이션할 수도 있습니다. Clok.system() 메서드에 ZoneId를 전달하면 모든 지역의 현재 시간을 가져올 수 있습니다. 예를 들어, 아래 테스트 사례에서 Clock 클래스를 사용하여 "America/New_York" 시간대의 현재 시간을 검색합니다.
@Test
void givenSystemClock_whenComparingDifferentTimeZones_thenVerifyRelationships() {
Clock nyClock = Clock.system(ZoneId.of("America/New_York"));
LocalDateTime nyTime = LocalDateTime.now(nyClock);
assertThat(nyTime)
.isNotNull()
.satisfies(time -> {
assertThat(time.getHour()).isBetween(0, 23);
assertThat(time.getMinute()).isBetween(0, 59);
// Verify it's within last minute (recent)
assertThat(time).isCloseTo(
LocalDateTime.now(),
within(1, ChronoUnit.MINUTES)
);
});
}
또한 이러한 특징으로 인해 Clock은 여러 시간대를 관리하거나 시간의 흐름을 일관되게 제어해야 하는 애플리케이션에 매유 유용합니다.
5. 레거시 클래스에서 현대 클래스로의 마이그레이션
Date 또는 Calendar를 사용하는 레거시 코드나 라이브러리를 여전히 처리해야 할 수 도 있습니다. 다행히도 오래된 날짜-시간 클래스에서 새로운 날짜-시간 클래스로 마이그레이션 할 수 있습니다.
5.1. Date를 Instant로 변환
레거시 Date 클래스는 toInstant() 메서드를 사용하여 Instant로 쉽게 변환할 수 있습니다. java.time 패키지의 클래스로 마이그레이션할 때 유용합니다. Instant는 타임라인(epoch)의 한 지점을 나타냅니다.
@Test
void givenSameEpochMillis_whenConvertingDateAndInstant_thenCorrect() {
long epochMillis = System.currentTimeMillis();
Date legacyDate = new Date(epochMillis);
Instant instant = Instant.ofEpochMilli(epochMillis);
assertEquals(
legacyDate.toInstant(),
instant,
"Date and Instant should represent the same moment in time"
);
}
기존 날짜를 인스턴스로 변환하고, 둘 다 동일한 epoch 밀리초에서 생성하여 동일한 순간을 나타내도록 할 수 있습니다.
5.2. Calendar을 ZonedDateTime으로 마이그레이션
Calendar를 사용할 때 날짜와 시간, 그리고 시간대 정보를 모두 처리하는 보다 현대적인 ZonedDateTime으로 마이그레이션 할 수 있습니다.
@Test
void givenCalendar_whenConvertingToZonedDateTime_thenCorrect() {
Calendar calendar = Calendar.getInstance();
calendar.set(2024, Calendar.SEPTEMBER, 18, 10, 30);
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
assertEquals(LocalDate.of(2024, 9, 18), zonedDateTime.toLocalDate());
assertEquals(LocalTime.of(10, 30), zonedDateTime.toLocalTime());
}
여기에서는 Calendar 인스턴스를 ZonedDateTime으로 변환 하고 두 이스턴스가 동일한 날짜-시간을 나타내는지 확인합니다.
6. 모범 사례
이제 java.time 클래스를 사용하기 위한 몇가지 모범 사례를 살펴보겠습니다.
- 새로운 프로젝트에서는 java.time 클래스를 사용해야 합니다.
- 시간대가 필요하지 않으면 LocalDate, LocalTime 또는 LocalDateTime을 사용할 수 있습니다.
- 시간대나 타임스탬프를 사용하는 경우 대신 ZonedDateTime 또는 Instant를 사용하세요.
- 날짜를 구문 분석하고 서식을 지정하려면 DateTime Formatter를 사용해야 합니다.
- 혼란을 피하기 위해 항상 시간대를 명확하기 밝혀야 합니다.
이러한 모범 사례는 Java에서 날짜와 시간을 다루기 위한 튼튼한 기반을 만련해 주며, 이를 통해 애플리케이션에서 날짜와 시간을 효율적이고 정확하게 처리할 수 있습니다.
7. 결론
Java8에 도입된 java.time 패키지는 날짜와 시간을 처리하는 방식을 극적으로 개선했습니다. 게다가 이 API를 채택하면 더 깨끗하고 유지 관리하기 쉬운 코드가 보장됩니다.
Date 또는 Calendar와 같은 오래된 클래스를 접할 수 있지만, 새로운 개발에 java.time API를 도입하는 것은 좋은 생각입니다. 마지막으로, 설명된 모범 사례는 더 깔끔하고, 더 효율적이며, 더 유지 관리하기 쉬운 코드를 작성하는 데 도움이 될 것입니다.
이렇게 날짜 및 시간에 관련되 객체들을 살펴보았는데요.
개발하는데 있어서 "반드시 이게 옳다"라는 것은 없지만 최근에 나온 java.time 패키지에 객체를 사용하게 될 경우 비교적 안정적인 서비스를 제공할 수 있는것은 부정할 수 없을거 같습니다.
개인적으로는 신규로 시작하거나 리팩토링 하는 프로젝트가 있다면 한번쯤 고심해서 도입해보는 것이 좋아보입니다.
'Development > Java' 카테고리의 다른 글
[JAVA] 중첩된 목록을 기준으로 목록을 필터링하는 방법 (0) | 2025.01.06 |
---|---|
[JAVA] 문자열에서 IP 주소 추출 (0) | 2025.01.02 |
[JAVA] "Could not create the Java Virtual Machine" 에러 수정 방법 (2) | 2024.09.02 |
[Java] 정규식을 사용하여 문자열 바꾸기(back referece와 lookaround 비교) - spring boot gradle (0) | 2024.08.19 |
[JAVA] 배열의 정렬된 인덱스 가져오기(배열 보존) (0) | 2024.08.13 |