공부/Java

자바 날짜와 시간

Stair 2024. 9. 12. 14:35
반응형

날짜와 시간을 계산하는 것은 단순하게 생각하면 아주 쉬워보이지만, 실제로는 매우 어렵고 복잡하다.

왜 그런지 이유를 알아보자

 

1. 날짜와 시간 차이 계산

특정 날짜에서 다른 날짜까지의 정확한 일수를 계산하는 것은 생각보다 복잡하다. 유년ㄴ, 각 달의 일수 등을 모두 고려해야하며, 간단한 뺄셈 연산으로는 결과를 정확하게 얻기 어렵다.

 

2. 윤년 계산

지구가 태양을 한 바퀴 도는 데 걸리는 평균 시간은 대략 365.2425일, 즉 365일 5시간 48분 45초정도 이다. 우리가 사용하는 그레고리력은 1년이 365일로 설정되어 있기 때문에 둘의 시간이 정확히 맞지 않아 윤년(4년마다 하루를 추가하는 달력)이 도입되었다.

윤년 계산은 간단해보이지만 실제로는 매우 복잡하다. 윤년은 보통 4년마다 한번씩 발생하지만, 100년 단위일 때는 윤년이 아니며, 400년 단위일 때는 다시 윤년이다.

 

3. 일광 절약 시간(Daylight Saving Time, DST)변환

보통 3월에서 10월은 태양이 일찍 뜨고, 나머지는 태양이 상대적으로 늦게 뜬다. 시간도 여기에 맞추어 1시간 앞당기거나 늦추는 제도를 일광 절약 시간제 또는 썸머타임이라 한다. 일광 절약 시간은 국가나 지역에 따라 시작 및 종료 날짜가 다르다. 이로 인해 날짜와 시간 계산 시 1시간의 오차가 발생할 수 있으며, 이를 정확히 계산하는 것은 복잡하다.

 

4. 타임존 계산

세계는 다양한 타임존으로 나뉘어 있으며, 각 타임존은 UTC(협정 세계시)로부터의 시간 차이로 정의된다. 타임존 간의 날짜와 시간 변환을 정확히 계산하는 것은 복잡하다.

 

이러한 복잡성 때문에 대부분의 현대 개발 환경에서는 날짜와 시간을 처리하기 위해 잘 설계된 라이브러리를 사용해야 한다. 이러한 라이브러리는 위에서 언급한 복잡한 계산을 추상화하여 제공하므로, 개발자는 보다 안정적이고 정확하며 효율적인 코드를 작성할 수 있다.

 

자바는 날짜와 시간 라이브러리를 지속해서 업데이트 했다

 

자바가 표준으로 제공했던 Date, Calendar는 사용성이 너무 떨어지고, 문제가 많은 라이브러리였다. 이런 문제를 해결하기 위해 결국 Joda-Time이라는 오픈소스 라이브러리가 등장하였다.

 

 

자바 날짜와 시간 라이브러리는 자바 공식 문서가 제공하는 다음 표로 정리할 수 있다.

 

LocalDate : 날짜만 표현할 때 사용한다. 예)2013-11-21

 

LocalTime : 시간만을 표현할 때 사용한다. 예)08:20:30.213

 

LocalDateTime : LocalDate와 LocalTime을 합한 개념이다 예)2023-11-21T08:20:30.213

 

ZonedDateTime : 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이 포함된다. 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]

Asia/Seoul은 타임존이라고 한다. 이 타임존을 알면 오프셋과 일광 절약 시간제에 대한 정보를 알 수 있다.

 

offsetDateTime : 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로부터의 시간대 차이닌 고정된 오프셋만 포함한다.

예) 2013-11-21T08:20:30.213+9:00

 

Year, Month, YearMonth, MonthDay 등도 있지만 자주 사용하지 않는다.

 

Instant : UTC를 기준으로 하는 시간의 한 지점을 나타낸다. 1970년 1월 1일 0시 0분 0초를 기준을 경과한 시간으로 계산된다. 초 데이터만 들어있다.

 

Period, Duration

"시간의 개념은 크게 2가지로 표현할 수 있다.

1. 특정 시점의 시간(시각)

2. 시간의 간격(기간)

Period,Duration은 시간의 간격(기간)을 표현하는데 사용된다.

시간의 간격은 영어로 amount of time으로 불린다.

 

Period : 두 날짜 사이의 간격을 년, 월, 일 단위로 나타낸다.

Duration : 두 시간 사이의 간격을 시, 분, 초(나노초) 단위로 나타낸다.

 

 

 

가장 기본이 되는 날짜와 시간 클래스는 LocalDate, LocalTime, LocalDateTime이다.

LocalDate : 날짜만 표현할 때 사용 예)2013-11-21

LocalTime : 시간만을 표현할 때 사용 예)08:20:30.213

 

LocalDateTime : 위 두개를 합한 개념이다 예)2023-11-21T08:20:30.213

 

 

LocalDate

타임존이 적용되지 않아 특정 지역의 날짜와 시간만 고려할 때 사용한다. 애플리케이션 개발 시 국내 서비스만 고려할 때 거의 이걸 사용한다

public static void main(String[] args) {
    LocalDate nowDate = LocalDate.now();
    LocalDate ofDate = LocalDate.of(2024, 11, 23);

    System.out.println("오늘 날짜=" + nowDate);
    System.out.println("지정 날짜=" + ofDate);

    ofDate = ofDate.plusDays(10);
    System.out.println("지정 날짜=" + ofDate);

}

 

now() : 현재 시간을 기준으로 생성한다.

of() : 특정 날짜를 기준으로 생성한다.

plusDays() : 특정 일을 더한다. 다양한 plusXxx() 메서드가 존재한다.

 

 

LocalTime

public static void main(String[] args) {
    LocalTime nowTime = LocalTime.now();
    LocalTime ofTime = LocalTime.of(9, 10, 30);

    System.out.println("현재 시간=" + nowTime);
    System.out.println("지정 시간=" + ofTime);

    //계산(불변)
    LocalTime ofTimePlus = ofTime.plusSeconds(30);
    System.out.println("지정 시간 +30초 =" + ofTimePlus);
}

위는 LocalTime이다. LocalTime은 현재 시간, 지정 시간 및 시간에 + 시 분 초를 더할 수 있는 메서드들을 제공하고 있다.

 

 

 

 

LocalDateTime

LocalDateTime은 위 두개를 합한 개념이다 예)2023-11-21T08:20:30.213

public static void main(String[] args) {
    LocalDateTime nowDt = LocalDateTime.now();
    LocalDateTime ofDt = LocalDateTime.of(2016, 8, 16, 8, 10, 1);

    System.out.println("현재 날짜시간= " + nowDt);
    System.out.println("지정 날짜시간= " + ofDt);

    //날짜 시간 분리
    LocalDate localDate = ofDt.toLocalDate();
    LocalTime localTime = ofDt.toLocalTime();
    System.out.println("localDate = " + localDate);
    System.out.println("localTime = " + localTime);

    //날짜 시간 합체
    LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
    System.out.println("localDateTime = " + localDateTime);


    //계산(불변)
    LocalDateTime ofDtPlus = ofDt.plusDays(1000);
    System.out.println("ofDtPlus = " + ofDtPlus);
    LocalDateTime localDateTime1 = ofDt.plusYears(1);
    System.out.println("localDateTime1 = " + localDateTime1);

    //비교
    System.out.println("현재 날짜시간이 지정 날짜시간보다 이전인가? " + nowDt.isBefore(ofDt));
    System.out.println("현재 날짜시간이 지정 날짜시간보다 이후인가? " + nowDt.isAfter(ofDt));
    System.out.println("현재 날짜시간이 지정 날짜시간이 일치하나? " + nowDt.isEqual(ofDt));

}

 

 

위의 시간 계산을 하는 객체들은 모두 불변객체이다.

 

 

 

ZonedDateTime

시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이 포함된다

Asia/Seoul같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC로부터의 시간 차이 등이 들어있다.

public static void main(String[] args) {
    for(String availableZoneId : ZoneId.getAvailableZoneIds()){
        System.out.println("availableZoneId = " + availableZoneId);
        ZoneId zoneId = ZoneId.of(availableZoneId);
        System.out.println(zoneId + " | " + zoneId.getRules());

    }

    ZoneId zoneId = ZoneId.systemDefault();
    System.out.println("zoneId.systemDefault = " + zoneId);

    ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
    System.out.println("seoulZoneId = " + seoulZoneId);
}

 

 

 

참고

ZonedDateTime이나 OffsetDateTime 은 글로벌 서비스를 하지 않으면 잘 사용하지 않는다. 따라서 깊이있게 파기 보다 대략 이런 것이 있구나 정도로만 학습하도록 하자.

 

 

 

Instant

Instant는 UTC를 기준으로 하는 시간의 한 지점을 나타낸다. 나노초 정밀도로 표편하며 1970년 1월 일을 기준으로 경과한 시간으로 계산한다.

쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다.(나노초 포함)

참고 - Epoch시간은 컴퓨터 시스템에서 시간을 나타내는 방법 중 하나이다. 이는 1970년 1월 1일부터 현재까지 경과된 시간을 초 단위로 표현한 것이다. 즉, Unix 시간은 1970년 1월 1일 이후로 경과한 전체 초의 수로, 시간대에 영향을 받지 않는 절대적인 시간 표현 방식이다.

 

장점

시간대 독립성 : Instant는 UTC를 기준으로 하므로, 시간대에 영향을 받지 않는다. 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용한다.

고정된 기준점 : 모든 Instant는 1970년 1월 1일 UTC를 기준으로 하기 때문에, 시간 계산 및 비교가 명확하고 일관된다.

 

단점

사용자 친화적이지 않음 : 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다.

시간대 정보 부재 : Instant에는 시간대 정보가 포함되어 있지 않아, 특정 지역의 날짜와 시간으로 변환하려면 작업이 필요하다.

 

사용 예는 다음과 같다

1. 전 세계적인 시간 기준 필요 시

2. 시간대 변환 없이 시간 계산 필요 시

3. 데이터 저장 및 교환

 

Instant는 날짜를 계산하기 어렵기 때문에 앞서 사용 예와 같은 특별한 경우에 한정해서 사용하면 된다.

public static void main(String[] args) {
    Instant now = Instant.now();
    System.out.println("now = " + now);

    Instant epochStart = Instant.ofEpochSecond(0);
    System.out.println("epochStart = " + epochStart);

    Instant instant = epochStart.plusSeconds(3600);
    System.out.println("instant = " + instant);
    
}

 

 

 

 

 

Duration, Period

시간의 개념은 크게 2가지로 표현할 수 있다.

1. 특정 시점의 시간(시각)

ex) 이 프로젝트는 2013년 9월 10일까지 완료해야해

2. 시간의 간격(기간)

ex) 이 프로젝트는 3개월 남았어

 

Period, Duration은 시간의 간격(기간)을 표현하는데 사용된다.

시간의 간격은 영어로 amount of time(시간의 양)으로 불린다.

 

Period : 두 날짜 사이의 간격을 년, 월 일 단위로 나타낸다

Duration : 두 시간 사이의 간격을 시, 분, 초 단위로 나타낸다

public static void main(String[] args) {
    Period period = Period.ofDays(10);
    System.out.println("period = " + period);

    LocalDate currentDate = LocalDate.of(2030, 1, 1);
    LocalDate plusDate = currentDate.plus(period);
    System.out.println("currentDate = " + currentDate);
    System.out.println("plusDate = " + plusDate);

    LocalDate startDate = LocalDate.of(2023, 1, 1);
    LocalDate endDate = LocalDate.of(2024, 4, 2);
    Period between = Period.between(startDate, endDate);
    System.out.println("between = " + between);
}
public static void main(String[] args) {
    Duration duration = Duration.ofMinutes(30);
    System.out.println("duration = " + duration);

    LocalTime lt = LocalTime.of(1, 0);
    System.out.println("lt = " + lt);

    LocalTime plusTime = lt.plus(duration);
    System.out.println("plusTime = " + plusTime);

    LocalTime start = LocalTime.of(9, 0);
    LocalTime end = LocalTime.of(10, 0);
    Duration between = Duration.between(start, end);
    System.out.println("between = " + between);

}

 

 

 

 

날짜와 시간의 핵심 인터페이스는 TemporalAccessor와 TemporalAmount이다.

 

 

 

TemporalAccessor인터페이스

날짜와 시간을 읽기 위한 기본 인터페이스이다.

이 인터페이스는 특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공한다.

 

 

Temporal 인터페이스

TemporalAccessor의 하위 인터페이스로, 날짜와 시간을 조작하기 위한 기능을 제공한다. 이를 통해 날짜와 시간을 변경하거나 조작할 수 있다.

 

TemporalAccess는 읽기 전용 접근을, Temporal은 읽기 쓰기 모두 지원한다.

 

 

TemporalAmount 인터페이스

시간의 간격(시간의 양, 기간)을 나타내며, 날짜와 시간 객체에 적용하여 그 객체를 조정할 수 있다.

 

 

다음으로 날짜와 시간의 핵심 인터페이스는 시간의 단위를 뜻하는 TemporalUnit(ChronoUnit)과 시간의 각 필드를 뜻하는 TemporalField(ChronoField)이다.

public static void main(String[] args) {
    ChronoUnit[] values = ChronoUnit.values();
    for (ChronoUnit value : values) {
        System.out.println("value = " + value);
    }
    System.out.println("HOURS = " + ChronoUnit.HOURS);
    System.out.println("HOURS.getDuration = " + ChronoUnit.HOURS.getDuration().getSeconds());
    System.out.println("ChronoUnit.DAYS = " + ChronoUnit.DAYS);

    //차이 구하기
    LocalTime lt1 = LocalTime.of(1, 10, 0);
    LocalTime lt2 = LocalTime.of(1, 20, 0);
    long secondsBetween = ChronoUnit.SECONDS.between(lt1, lt2);
    System.out.println("secondsBetween = " + secondsBetween);

    
}

 

 

다음은 날짜와 시간 조회해보자

날짜와 시간을 조회하려면 날짜와 시간 항목 중에 어떤 필드를 조회해야할지 선택해야 한다. 이 때 날짜와 시간의 필드를 뜻하는 ChronoField가 사용된다.

public static void main(String[] args) {
    LocalDateTime dt = LocalDateTime.of(2023, 1, 2, 3, 4, 5);
    System.out.println("Year="+dt.get(ChronoField.YEAR));
    System.out.println("MONTH_OF_YEAR="+dt.get(ChronoField.MONTH_OF_YEAR));
    System.out.println("DAY_OF_MONTH = " + dt.get(ChronoField.HOUR_OF_DAY));
    System.out.println("MINUTE_OF_HOUR = " + dt.get(ChronoField.MINUTE_OF_HOUR));
    System.out.println("SECOND_OF_MINUTE = " + dt.get(ChronoField.SECOND_OF_MINUTE));

    System.out.println("편의 메서드 제공");
    System.out.println("Year="+dt.getYear());
    System.out.println("MONTH_OF_YEAR="+dt.getMonthValue());
    System.out.println("DAY_OF_MONTH = " + dt.getDayOfMonth());
    System.out.println("MINUTE_OF_HOUR = " + dt.getMinute());
    System.out.println("SECOND_OF_MINUTE = " + dt.getSecond());

    
}
public static void main(String[] args) {
    LocalDateTime dt = LocalDateTime.of(2023, 1, 2, 3, 4, 5);
    System.out.println("dt = " + dt);

    //with == 특정 부분만 바꾸고 싶을때 사용
    //불변객체이기에 인스턴스를 하나 더 만들어야한다
    LocalDateTime changedDt1 = dt.with(ChronoField.YEAR, 2020);
    System.out.println("changedDt1 = " + changedDt1);

    LocalDateTime changedDt2 = dt.withYear(2020);
    System.out.println("changedDt2 = " + changedDt2);

    //TemporalAdjuster 사용
    //다음주 금요일
    LocalDateTime with1 = dt.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
    System.out.println("기준 날짜: " + dt);
    System.out.println("다음 금요일: " + with1);

    LocalDateTime with2 = dt.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
    System.out.println("같은 달의 마지막 일요일"+with2);


}

 

 

 

이런 날짜들을 파싱하고 포맷팅할 수 도 있다. 자주 쓰이니 참고하자.

포맷팅은 날짜와 시간 데이터를 원하는 포맷의 "문자열"로 변경하는 것이고,

파싱은 문자열을 "날짜와 시간 데이터"로 변경하는 것이다.

public static void main(String[] args) {
    //포매팅: 날짜를 문자로
    LocalDate date = LocalDate.of(2024, 12, 31);
    System.out.println("date = " + date);
    System.out.println(date.getYear()+"년 "+ date.getMonthValue()+"월");

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
    String formattedDate = date.format(formatter);
    System.out.println("날짜와 시간 포맷팅 = "  + formattedDate);

    //파싱: 문자를 날짜로
    String input = "2030년 01월 01일";
    LocalDate parsed = LocalDate.parse(input, formatter);
    System.out.println("문자열 파싱 날짜와 시간: " + parsed);
}

 

LocalDate와 같은 날짜 객체를 원하는 형태의 문자로 변경하려면 DateTimeFormatter를 사용하면 된다. 여기에 ofPattern()으로 원하는 포맷을 지정하면 된다. 여기서는 yyyy년 MM월 dd일 포맷을 지정했다.

* MM은 대문자이다. mm 하면 에러뜬다 주의하자 -- m은 '분'이다 

 

 

 

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 지역 클래스  (0) 2024.09.14
자바 중첩 클래스, 내부 클래스  (1) 2024.09.13
자바 열거형-ENUM  (3) 2024.09.11
자바 래퍼 클래스(wrapper class)  (0) 2024.09.10
자바 String 클래스  (1) 2024.09.10