처음부터 차근차근

[Java] 날짜와 시간 본문

Language/Java

[Java] 날짜와 시간

HangJu_95 2024. 8. 27. 21:42
728x90

날짜와 시간 라이브러리가 필요한 이유

우리가 프로그래밍을 하면서, 로그를 남기거나, 혹은 회원 생성 시간 및 게시글 생성 시간 등 날짜와 시간에 대한 기록이 필요하다.
단순히 날짜와 시간만 기록하면 되는게 아닌가? 싶지만, 날짜와 시간을 계산하는 것은 의외로 복잡하다.

  1. 날짜와 시간 차이 계산
  2. 윤년 계산
  3. 일광 절약 시간 변환
  4. 타임존 계산

이를 위해서 Java에서는 라이브러리를 제공하고 있다.

Java 날짜와 시간 라이브러리

[원문] https://docs.oracle.com/javase/tutorial/datetime/iso/overview.html

  • *: 초는 나노초 단위의 정밀도로 캡처된다. (밀리초, 나노초 가능)
  • **: 이 클래스는 이 정보를 저장하지는 않지만 이러한 단위로 시간을 제공하는 메서드가 있다.
  • ***: ZonedDateTimePeriod 를 추가하면 서머타임 또는 기타 현지 시간 차이를 준수한다.

LocalDate, LocalTime, LocalDateTime

  • LocalDate: 날짜만 표현할 때 사용한다. 년, 월, 일을 다룬다. 예) 2013-11-21
  • LocalTime: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213
    • 초는 밀리초, 나노초 단위도 포함할 수 있다.
  • LocalDateTime: LocalDateLocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213

**Method

  • now() : 현재 시간을 기준으로 생성한다.
  • of(...) : 특정 날짜를 기준으로 생성한다. 년, 월, 일을 입력할 수 있다.
  • plusDays() : 특정 일을 더한다. 다양한 plusXxx() 메서드가 존재한다.
  • isBefore(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이전이라면 true 를 반환한다.
  • isAfter(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이후라면 true 를 반환한다.
  • isEqual(): 다른 날짜시간과 시간적으로 동일한지 비교한다. 시간이 같으면 true 를 반환한다.

isEqual() vs equals()

  • isEqual() 는 단순히 비교 대상이 시간적으로 같으면 true 를 반환한다. 객체가 다르고, 타임존이 달라도 시간적으로 같으면 true 를 반환한다. 쉽게 이야기해서 시간을 계산해서 시간으로만 둘을 비교한다.
    • 예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 true 를 반환한다.
  • equals() 객체의 타입, 타임존 등등 내부 데이터의 모든 구성요소가 같아야 true 를 반환한다.
    • 예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 타임존의 데이터가 다르기 때문에 false 를 반환한다.

모든 날짜 클래스는 불변이다. 따라서 변경이 발생하는 경우 새로운 객체를 생성해서 반환하므로 반환값을 꼭 받아야 한다.

ZonedDateTime, OffsetDateTime

  • ZonedDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이 포함된다.
    • 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]
    • +9:00 은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라 한다. 한국은 UTC보다 +9:00 시간이다.
    • Asia/Seoul 은 타임존이라 한다. 이 타임존을 알면 오프셋과 일광 절약 시간제에 대한 정보를 알 수 있다.
    • 일광 절약 시간제가 적용된다.
  • OffsetDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터의 시간대 차이인 고정된 오프셋만 포함한다.
    • 예) 2013-11-21T08:20:30.213+9:00
    • 일광 절약 시간제가 적용되지 않는다.

Year, Month, YearMonth, MonthDay

년, 월, 년월, 달일을 각각 다룰 때 사용한다. 자주 사용하지는 않는다.
DayOfWeek 와 같이 월, 화, 수, 목, 금, 토, 일을 나타내는 클래스도 있다.

Instant

Instant 는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타낸다. Instant 는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC)를 기준으로 경과한 시간으로 계산된다.
쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다. (나노초 포함)

Period, Duration

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

package time;

import java.time.LocalDate;
import java.time.Period;

public class PeriodMain {
    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);
        System.out.println("더한 날짜: " + plusDate);

        //기간 차이
        LocalDate startDate = LocalDate.of(2023, 1, 1);
        LocalDate endDate = LocalDate.of(2023, 4, 2);
        Period between = Period.between(startDate, endDate);
        System.out.println("기간: " + between.getMonths() + "개월 " + between.getDays() + "일");
    }
}
period = P10D
현재 날짜: 2030-01-01
더한 날짜: 2030-01-11
기간: 3개월 1일

of() : 특정 기간을 지정해서 Period 를 생성한다.

  • of(년, 월, 일)
  • ofDays()
  • ofMonths()
  • ofYears()

Period.between(startDate, endDate) 와 같이 특정 날짜의 차이를 구하면 Period 가 반환된다.

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

package time;

import java.time.Duration;
import java.time.LocalTime;

public class DurationMain {
    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);

        //계산에 사용
        LocalTime plusTime = lt.plus(duration);
        System.out.println("더한 시간 = " + plusTime);

        //시간 차이
        LocalTime start = LocalTime.of(9, 0);
        LocalTime end = LocalTime.of(10, 0);
        Duration between = Duration.between(start, end);
        System.out.println("차이: " + between.getSeconds() + "초");
        System.out.println("근무 시간: " + between.toHours() + "시간 " + between.toMinutesPart() + "분");
    }
}
duration = PT30M
기준 시간 = 01:00
더한 시간 = 01:30
차이: 3600초
근무 시간: 1시간 0분

of() : 특정 시간을 지정해서 Duration 를 생성한다.

  • of(지정)
  • ofSeconds()
  • ofMinutes()
  • ofHours()

Duration.between(start, end) 와 같이 특정 시간의 차이를 구하면 Duration 이 반환된다.

날짜와 시간의 핵심 인터페이스

날짜와 시간은 특정 시점의 시간(시각)과 시간의 간격(기간)으로 나눌 수 있다.

  • TemporalAccessor 인터페이스
    날짜와 시간을 읽기 위한 기본 인터페이스로, 특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공
  • Temporal 인터페이스
    TemporalAccessor의 하위 인터페이스로, 날짜와 시간을 조작하기 위한 기능을 제공한다. 이를 통해 날짜와 시간을 변경하거나 조정할 수 있다.
  • TemporalAmount 인터페이스
    시간의 간격을 나타내며, 날짜와 시간 객체에 적용하여 그 객체를 조정할 수 있다. 예를 들어 특정 날짜에 일정 기간을 더하거나 빼는 데 사용된다.

시간의 단위와 시간 필드

시간의 단위 - TemporalUnit, ChronoUnit

  • TemporalUnit 인터페이스는 날짜와 시간을 측정하는 단위를 나타내며, 주로 사용되는 구현체는 java.time.temporal.ChronoUnit 열거형으로 구현되어 있다.
  • ChronoUnit 은 다양한 시간 단위를 제공한다.
  • 여기서 Unit 이라는 뜻을 번역하면 단위이다. 따라서 시간의 단위 하나하나를 나타낸다.
  • package time;

import java.time.LocalTime;
import java.time.temporal.ChronoUnit;

public class ChronoUnitMain {
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.duration = " + ChronoUnit.HOURS.getDuration().getSeconds());
System.out.println("DAYS = " + ChronoUnit.DAYS);
System.out.println("DAYS.duration() = " + ChronoUnit.DAYS.getDuration().getSeconds());

    // 차이 구하기
    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);
    long minutesBetween = ChronoUnit.MINUTES.between(lt1, lt2);
    System.out.println("minutesBetween = " + minutesBetween);

}

}

value = Nanos
value = Micros
value = Millis
value = Seconds
value = Minutes
value = Hours
value = HalfDays
value = Days
value = Weeks
value = Months
value = Years
value = Decades
value = Centuries
value = Millennia
value = Eras
value = Forever
HOURS = Hours
HOURS.duration = 3600
DAYS = Days
DAYS.duration = 86400
secondsBetween = 600
minutesBetween = 10

`ChronoUnit` 을 사용하면 두 날짜 또는 시간 사이의 차이를 해당 단위로 쉽게 계산할 수 있다.
예제 코드에서는 두 `LocalTime` 객체 사이의 차이를 초, 분 단위로 구한다.

**시간 필드 - ChronoField**
`ChronoField` 는 날짜 및 시간을 나타내는 데 사용되는 열거형이다. 이 열거형은 다양한 필드를 통해 **날짜와 시간의 특정 부분을 나타낸다.** 여기에는 연도, 월, 일, 시간, 분 등이 포함된다.
- `TemporalField` 인터페이스는 날짜와 시간을 나타내는데 사용된다. 주로 사용되는 구현체는
- `java.time.temporal.ChronoField` 열거형으로 구현되어 있다.
- `ChronoField` 는 다양한 필드를 통해 날짜와 시간의 특정 부분을 나타낸다. 여기에는 연도, 월, 일, 시간, 분 등이 포함된다.
- 여기서 필드(Field)라는 뜻이 날짜와 시간 중에 있는 특정 필드들을 뜻한다. 각각의 필드 항목은 다음을 참고하자.
예를 들어 2024년 8월 16일이라고 하면 각각의 필드는 다음과 같다.
`YEAR` : 2024
`MONTH_OF_YEAR` : 8
`DAY_OF_MONTH` : 16
- 단순히 시간의 단위 하나하나를 뜻하는 `ChronoUnit` 과는 다른 것을 알 수 있다. `ChronoField` 를 사용해야 날짜와 시간의 각 필드 중에 원하는 데이터를 조회할 수 있다.
```Java
package time;

import java.time.temporal.ChronoField;

public class ChronoFieldMain {
    public static void main(String[] args) {
        ChronoField[] values = ChronoField.values();
        for (ChronoField value : values) {
            System.out.println(value + ", range = " + value.range());
        }

        System.out.println("MONTH_OF_YEAR.range() = " + ChronoField.MONTH_OF_YEAR.range());
        System.out.println("DAY_OF_MONTH.range() = " + ChronoField.DAY_OF_MONTH.range());
    }
}
NanoOfSecond, range = 0 - 999999999
NanoOfDay, range = 0 - 86399999999999
MicroOfSecond, range = 0 - 999999
MicroOfDay, range = 0 - 86399999999
MilliOfSecond, range = 0 - 999
MilliOfDay, range = 0 - 86399999
SecondOfMinute, range = 0 - 59
SecondOfDay, range = 0 - 86399
MinuteOfHour, range = 0 - 59
MinuteOfDay, range = 0 - 1439
HourOfAmPm, range = 0 - 11
ClockHourOfAmPm, range = 1 - 12
HourOfDay, range = 0 - 23
ClockHourOfDay, range = 1 - 24
AmPmOfDay, range = 0 - 1
DayOfWeek, range = 1 - 7
AlignedDayOfWeekInMonth, range = 1 - 7
AlignedDayOfWeekInYear, range = 1 - 7
DayOfMonth, range = 1 - 28/31
DayOfYear, range = 1 - 365/366
EpochDay, range = -365243219162 - 365241780471
AlignedWeekOfMonth, range = 1 - 4/5
AlignedWeekOfYear, range = 1 - 53
MonthOfYear, range = 1 - 12
ProlepticMonth, range = -11999999988 - 11999999999
YearOfEra, range = 1 - 999999999/1000000000
Year, range = -999999999 - 999999999
Era, range = 0 - 1
InstantSeconds, range = -9223372036854775808 - 9223372036854775807
OffsetSeconds, range = -64800 - 64800
MONTH_OF_YEAR.range() = 1 - 12
DAY_OF_MONTH.range() = 1 - 28/31

날짜와 시간 조회하기

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

package time;

import java.time.LocalDateTime;
import java.time.temporal.ChronoField;

public class GetTimeMain {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2030, 1, 1, 13, 30, 59);
        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.DAY_OF_MONTH));
        System.out.println("HOUR_OF_DAY = " + 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("HOUR_OF_DAY = " + dt.getHour());
        System.out.println("MINUTE_OF_HOUR = " + dt.getMinute());
        System.out.println("SECOND_OF_MINUTE = " + dt.getSecond());

        System.out.println("편의 메서드에 없음");
        System.out.println("MINUTE_OF_DAY = " + dt.get(ChronoField.MINUTE_OF_DAY));
        System.out.println("SECOND_OF_DAY = " + dt.get(ChronoField.SECOND_OF_DAY));
    }
}

TemporalAccessor.get(TemporalField field)
LocalDateTime 을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 TemporalAccessor 인터페이스를 구현한다.
TemporalAccessor 는 특정 시점의 시간을 조회하는 기능을 제공한다.
get(TemporalField field) 을 호출할 때 어떤 날짜와 시간 필드를 조회할 지 TemporalField 의 구현인 ChronoField 를 인수로 전달하면 된다.
편의 메서드 사용
get(TemporalField field) 을 사용하면 코드가 길어지고 번거롭기 때문에 자주 사용하는 조회 필드는 간단한 편의 메서드를 제공한다.
dt.get(ChronoField.DAY_OF_MONTH)) dt.getDayOfMonth()
편의 메서드에 없음
자주 사용하지 않는 특별한 기능은 편의 메서드를 제공하지 않는다.
편의 메서드를 사용하는 것이 가독성이 좋기 때문에 일반적으로는 편의 메서드를 사용하고, 편의 메서드가 없는경우 get(TemporalField field) 을 사용하면 된다.

날짜와 시간 조작하기

날짜와 시간을 조작하려면 어떤 시간 단위(Unit)를 변경할 지 선택해야 한다. 이때 날짜와 시간의 단위를 뜻하는 ChronoUnit 이 사용된다.

import java.time.*;
import java.time.temporal.ChronoUnit;

public class ChangeTimePlusMain {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2018, 1, 1, 13, 30, 59);
        System.out.println("dt = " + dt);

        // plus는 숫자를 넣고, 단위를 넣을 수 있다.
        LocalDateTime plusDt1 = dt.plus(10, ChronoUnit.YEARS);
        System.out.println("plusDt1 = " + plusDt1);

        System.out.println("편의 메서드 사용");
        LocalDateTime plusDt2 = dt.plusYears(10);
        System.out.println("plusDt2 = " + plusDt2);

        System.out.println("Period 사용");
        Period period = Period.ofYears(10);
        LocalDateTime plusDt3 = dt.plus(period);
        System.out.println("plusDt3 = " + plusDt3);
    }
}

Temporal plus(long amountToAdd, TemporalUnit unit)**
LocalDateTime 을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 Temporal 인터페이스를 구현한다.
Temporal 은 특정 시점의 시간을 조작하는 기능을 제공한다.
plus(long amountToAdd, TemporalUnit unit) 를 호출할 때 더하기 할 숫자와 시간의 단위(Unit)를 전달하면 된다. 이때 TemporalUnit 의 구현인 ChronoUnit 을 인수로 전달하면 된다.
불변이므로 반환 값을 받아야 한다.
참고로 minus() 도 존재한다.

편의 메서드 사용
자주 사용하는 메서드는 편의 메서드가 제공된다.
dt.plus(10, ChronoUnit.YEARS) dt.plusYears(10)

Period를 사용한 조작
PeriodDuration 은 기간(시간의 간격)을 뜻한다. 특정 시점의 시간에 기간을 더할 수 있다.

현재 타입에 필요한 정보가 있는지 확인하는 메서드

package time;

import java.time.LocalDate;
import java.time.temporal.ChronoField;

public class IsSupportedMain2 {
    public static void main(String[] args) {
        LocalDate now = LocalDate.now(); // LocalDate는 년, 월, 일만 다룬다.
        boolean supported = now.isSupported(ChronoField.SECOND_OF_MINUTE);
        System.out.println("supported = " + supported);
        if (supported) {
            int minute = now.get(ChronoField.SECOND_OF_MINUTE);
            System.out.println("minute = " + minute);
        }
    }
}

with() 메서드를 통한 조작

package time;

import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAdjusters;

public class ChangeTimeWithMain {
    public static void main(String[] args) {
        LocalDateTime dt = LocalDateTime.of(2018, 1, 1, 13, 30, 59);
        System.out.println("dt = " + dt);

        LocalDateTime changeDt1 = dt.with(ChronoField.YEAR, 2020);
        System.out.println("changeDt1 = " + changeDt1);

        // 편의 메서드 제공
        LocalDateTime changeDt2 = dt.withYear(2020);
        System.out.println("changeDt2 = " + changeDt2);

        // 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);
    }
}
dt = 2018-01-01T13:30:59
changedDt1 = 2020-01-01T13:30:59
changedDt2 = 2020-01-01T13:30:59
기준 날짜: 2018-01-01T13:30:59
다음 금요일: 2018-01-05T13:30:59
같은 달의 마지막 일요일 = 2018-01-28T13:30:59

Temporal with(TemporalField field, long newValue)
Temporal.with() 를 사용하면 날짜와 시간의 특정 필드의 값만 변경할 수 있다.
불변이므로 반환 값을 받아야 한다.

편의 메서드
자주 사용하는 메서드는 편의 메서드가 제공된다.
dt.with(ChronoField.YEAR, 2020) dt.withYear(2020)

TemporalAdjuster 사용
with() 는 아주 단순한 날짜만 변경할 수 있다. 다음주 금요일, 이번 달의 마지막 일요일 같은 복잡한 날짜를 계산하고 싶다면 TemporalAdjuster 를 사용하면 된다.

원래대로 하면 이 인터페이스를 직접 구현해야겠지만, 자바는 이미 필요한 구현체들을 TemporalAdjusters 에 다 만들어 두었다. 우리는 단순히 구현체들을 모아둔 TemporalAdjusters 를 사용하면 된다.
TemporalAdjusters.next(DayOfWeek.FRIDAY) : 다음주 금요일을 구한다.
TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY) : 이번 달의 마지막 일요일을 구한다.

날짜와 시간 문자열 파싱과 포멧팅

  • 포맷팅: 날짜와 시간 데이터를 원하는 포맷의 문자열로 변경하는 것, Date String
  • 파싱: 문자열을 날짜와 시간 데이터로 변경하는 것, String Date
package time;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class FormattingMain1 {
    public static void main(String[] args) {
        // 포멧팅 : 날짜를 문자로
        LocalDate date = LocalDate.of(2024, 12, 31);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");

        String formattedDate = date.format(formatter);
        System.out.println("날짜와 시간 포멧팅: " + formattedDate);

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

    }
}

 

포멧팅과 파싱을 하기 위해서는 Formatter 패턴을 알아두어야 한다.

 

DateTimeFormatter (Java Platform SE 8 )

Parses the text using this formatter, without resolving the result, intended for advanced use cases. Parsing is implemented as a two-phase operation. First, the text is parsed using the layout defined by the formatter, producing a Map of field to value, a

docs.oracle.com

package time;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class FormattingMain2 {
    public static void main(String[] args) {
        // 포맷팅: 날짜와 시간을 문자로
        LocalDateTime now = LocalDateTime.of(2024, 12, 31, 13, 30, 59);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);
        System.out.println("날짜와 시간 포맷팅: " + formattedDateTime);

        // 파싱: 문자를 날짜와 시간으로
        String dateTimeString = "2030-01-01 11:30:00";
        LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, formatter);
        System.out.println("문자열 파싱 날짜와 시간: " + parsedDateTime);
    }
}

 

참고

 

김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을

www.inflearn.com

 

'Language > Java' 카테고리의 다른 글

[Java] Enum  (0) 2024.08.09
[Java] Wrapper Class  (0) 2024.07.31
[Java] String 클래스  (0) 2024.07.31
[Java] 불변 객체  (0) 2024.07.26
[Java] Object 클래스  (0) 2024.07.24