사전 지식
LocalDateTIme에 대해
자바에서는 시간을 다루는 여러 방법이 있습니다. 이번에는 자바 8로 넘어오면서 사용하는 LocalDateTime에 대해 알아보겠습니다.
Date
기존 제공하던 클래스로 java.util.Date
에 존재합니다. 문제는 기준이 1900년으로 값을 넣어줄 때, 1900년을 기준으로 현재 날짜를 넣어줘야 한다는 단점이 있습니다.
따라서
@Test
void utilDateTest() {
// given
// 기준 시간이 1900년대
Date date = new Date(122,8,21);
// when
System.out.println(date);
// then
// Wed Sep 21 00:00:00 KST 2022
}
1900을 기준으로 하며, 각 값이 불변이 아닌 가변상태라는 단점으로 대부분의 메서드가 현재는 Deprecated
된 상태입니다.
여러 문제점들을 가지고 있던 기존 Date대신 많은 개발자들이 다양한 서드파티를 사용했는데 그중 Joda-time에 대한 라이브러리를 Java.time에 추가하게 되었습니다.
이번에는 가장 많이 사용하고 있는 LocalDateTime에 대해 알아보겠습니다.
Java.time
java.time
에서는 많은 기능을 제공해 주는데 시간과 날짜를 제공해 주는 LocalDateTime
부터 간단한 LocalDate
에 대한 클래스도 제공해 줍니다.
이러한 클래스들은 하나의 특징을 가지고 있는데, 바로 불변
이라는 점입니다.
LocalDate
LocalDate
는 {년, 월, 일}에 대한 값만 나타내는 클래스입니다. 따라서 세부적인 시간과 관련해서는 알 수가 없습니다.
하지만 {년, 월, 일}에 대해서 자세하게 다루는 만큼 기능이 많습니다.
월을 가져올 때도 숫자로만 가져오는 것이 아닌 해당 월에 대한 영문도 가져올 수 있습니다.
@Test
void localDate_test() {
// given
LocalDate date = LocalDate.of(2017,9,21);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
int d = date.getDayOfYear();
DayOfWeek dayOfWeek = date.getDayOfWeek();
// when
System.out.println("date : "+date);
System.out.println("year : "+year);
System.out.println("month : "+month);
System.out.println("month : "+date.getMonthValue());
System.out.println("day : "+day);
System.out.println("All days : "+d);
System.out.println("dayOfWeek : "+dayOfWeek);
// then
// date : 2017-09-21
// year : 2017
// month : SEPTEMBER
// month : 9
// day : 21
// All days : 264
// dayOfWeek : THURSDAY
}

위와 같이 내부적으로 접근이 가능한 모든 필드 변수는 불변인 것을 볼 수 있으며, 다른 메서드들도 util클래스처럼 static
을 붙여서 제공하는 것을 알 수 있습니다. 따라서 내부 값을 변경하려면 새로운 인스턴스를 제공하게 됩니다.
LocalTime
LocalDate
가 날짜에 대해서 자세하게 다뤘다면, LocalTime
은 시간에 대해 자세하게 다룰 수 있는 클래스입니다.
@Test
void localTime_test() {
// Given
LocalTime time = LocalTime.of(12,34,56);
int hour = time.getHour();
int min = time.getMinute();
int sec = time.getSecond();
// When
System.out.println(hour);
System.out.println(min);
System.out.println(sec);
// Then
// 12
// 34
// 56
}
위 클래스 또한 내부 변수는 final
을 통해 불변으로 만들었고 값을 변경할 때는 새로운 클래스를 반환하는 방식으로 구현했습니다. 따라서 static
메서드로 되어 있습니다.
LocalDateTIme
LocalDateTime
은 LocalDate
와 LocalTime
에서 사용할 수 있는 기능을 모두 사용가능하다 생각하면 될 것 같습니다.
@Test
void localDateTime_test() {
LocalDateTime now = LocalDateTime.now();
LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
LocalDateTime ldt = LocalDateTime.of(ld, lt);
LocalDateTime ldt2 = ld.atTime(lt);
LocalDateTime ldt3 = lt.atDate(ld);
LocalDate getLd = ldt.toLocalDate();
LocalTime getLt = ldt.toLocalTime();
System.out.println(now); // 2023-08-04T22:28:40.460970800
System.out.println(ldt); // 2023-08-04T22:28:40.460970800
System.out.println(ldt2); // 2023-08-04T22:28:40.460970800
System.out.println(ldt3); // 2023-08-04T22:28:40.460970800
System.out.println(getLd);// 2023-08-04
System.out.println(getLt);// 22:28:40.460970800
}
위와 같이 LocalDateTime의 현재 시간을 가져올 수도 있으며, LocalDate
나 LocalTime
에서의 현재 시간을 통해 LocalDateTime을 만들 수도 있습니다.
또한

와 같이 월을 직접 넣어서 사용할 수도 있습니다. 그렇다고 값이 크게 AUGUST로 들어가는 것은 아닙니다 숫자 그대로 8이 들어가게 됩니다.
여기서 LocalDateTime이나 LocalDate, LocalTime의 경우 두 시간이나 날짜를 비교할 수는 있지만, 차이를 계산할 수는 없는데요.
이걸 해결하기 위해서는 Duration
과 Period
을 사용해야 합니다.
Duration & period
Duration
의 경우 시간을 단위로 두 객체 간 시간차이를 구하지만, Period
는 LocalDate
객체 간 날짜의 차이를 구하게 됩니다.
@Test
void duration_test() {
LocalDate ld = LocalDate.now();
LocalDate ld2 = ld.plusMonths(1);
System.out.println(Period.between(ld2,ld)); // P-1M
System.out.println(Period.between(ld,ld2)); // P1M
LocalTime lt = LocalTime.now();
LocalTime lt2 = lt.plusHours(1);
LocalTime lt3 = LocalTime.of(12,34,56);
// Duration duration = Duration.between(ld2, ld); // 실패
Duration duration = Duration.between(lt3, lt);
System.out.println(Duration.between(lt2,lt)); // PT23H
System.out.println(duration); // PT10H42M22.9972026S
}
각 값의 차이를 알 수 있는데, 파라미터는 순서에 따라 값이 다르게 나오는 것을 첫 출력으로 확인할 수 있습니다.
Formatting
사실, 실제로 사용하게 된다면 우리가 원하는 format을 지정해서 사용하게 되는데 이럴 땐 어떻게 해야 할까요?
바로 DateTimeFormatter
를 사용해야 합니다.
@Test
void formatting() {
LocalDateTime now = LocalDateTime.now();
String isoDate = now.format(DateTimeFormatter.ISO_DATE);
String basicIsoDate = now.format(DateTimeFormatter.BASIC_ISO_DATE);
String localDateTime = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(isoDate); // 2023-08-05
System.out.println(basicIsoDate); // 20230805
System.out.println(localDateTime); // 2023-08-05T01:15:03.0225432
LocalDate idtIsoDate = LocalDate.parse(isoDate, DateTimeFormatter.ISO_DATE);
System.out.println(idtIsoDate); // 2023-08-05
LocalDate idtBasic = LocalDate.parse(basicIsoDate, DateTimeFormatter.BASIC_ISO_DATE);
// LocalDateTime idtB = LocalDateTime.parse(basicIsoDate, DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(idtBasic); // 2023-08-05
LocalDateTime ldt = LocalDateTime.parse(localDateTime, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(ldt); // 2023-08-05T01:15:03.022543200
}
위와 같이 Formatter의 경우 여러 기본 Formatting을 제공해 주고 해당 format을 사용할 수 있습니다.
하지만 위에 코드는 이미 제공하는 formatting이고, 직접 한다면
@Test
void custom_test() {
LocalDateTime now = LocalDateTime.now();
String customNow = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
System.out.println(customNow); // 2023/08/05
LocalDate localDate1 = LocalDate.parse(customNow, DateTimeFormatter.ofPattern("yyyy/MM/dd"));
LocalDateTime localDateTime1 = localDate1.atTime(now.toLocalTime());
System.out.println(localDateTime1); // 2023-08-05T01:34:39.886807300
LocalDateTime nextDay = now.plusDays(10);
DateTimeFormatter customFormat = DateTimeFormatter.ofPattern("yy MM dd");
String nextCustom = nextDay.format(customFormat);
System.out.println(nextCustom); // 23 08 15
LocalDate localDate= LocalDate.parse(nextCustom, customFormat);
LocalDateTime localDateTime = localDate.atTime(now.toLocalTime());
System.out.println(localDateTime); // 2023-08-15T01:34:39.886807300
}
아래와 같이 값을 지정하여 변경할 수 있습니다.
다만 생각하셔야 할 것은, {년, 월}과 같이 특정 데이터만 가지고 있게 하기 위해서는 String으로 변환해서 사용해야 합니다.
UTC & GMT / Time Zone & Offset
GMT
그리니치 천문대를 기준으로 태양이 가장 높이 뜨는 시점을 측정하여 만든 표준시
영어권 나라에서는 UTC+00:00으로도 사용된다.
하지만 태양의 위치를 기준으로 하다 보니 UTC와 최대 0.9초가 다를 수 있기 때문에 정밀도가 필요한 목적으로 사용되면 안 됩니다.
UTC
UTC는 협정 세계시로 전 세계에서 시계와 시간을 규제하는 기본 시간 표준
입니다. 원자시계를 기준으로 하여 정하지만, 자전운동을 반영하지 않기 때문에 오차 보정을 위해 6월 30일이나 12월 31일에 더하거나 뺍니다.
표준시에 대한 내용
UTC는 고정이지만, 나머지 UTC를 기반으로 한 표준시의 경우 시간을 바뀔 수 있다.
참고로 KST는 한국 표준시로 UTC+09:00시이다.
예시.
UTC 01:00은 KST(한국시간) 10:00 즉 10시인 것이고,
UTC 22:00는 KST(한국시간) 다음날 07:00입니다.
Time Zone
법적, 상업적 및 사회적 목적을 위해 사용하는 균일한 표준 시간, 경도를 기준으로 지리적으로 구성이 되어 있다.
예를 들어, 중국 같은 경우에는 여러 시간대로 나누는 것이 아닌 하나의 시간을 사용한다.
따라서 나라별로 정하는 표준시간 대가 다르다
DST (Daylight saving time)
일광 절약 시간제로써 서머타임이라고 하며, 표준시를 일정 기간 동안 시간을 조정하여 주간에 더 많은 절약을 하기 위해 사용됩니다.
예를 들어 서머타임은 봄과 가을에 실행이 되는데, 시계를 한 시간 앞으로 조정하여 봄에는 더 빠르게 시작할 수 있으며, 서머타임이 종료되면 1시간 더 늘게 시작할 수 있습니다.
따라서
서머타임기간 : 표준시 12시 -> 1시로 변경
서머타임종료: 표준시 12시 -> 11시로 변경
UTC Offset
특정 위치에서 UTC와의 차이를 나타낸다
정리
따라서 정리를 해보자면
그리니치에서 태양이 가장 높이 뜬 평균을 구하여 표준으로 삼은 것이 GMT
이고, 오차가 있기 때문에
원자시계를 기준으로 한 UTC
를 사용한다. 이 또한 기준은 그리니치와 동일하고 경도를 기준으로 시간이 달라지게 된다. 경도에 따라 시간을 분류한 기준이 Time Zone
이라 할 수 있습니다.
하지만 Time Zone
의 경우 기준이 절대적인 것이 아닌, 법, 상업, 사회적 목적을 통해 변경될 수 있어서 절대적인 것은 아니며, 대표적으로 중국은 하나의 시간대를 사용한다. 또한 특정 나라에서는 15도가 아닌 30, 45도 사용하기 때문에 일정하지는 않다.
또한 이렇게 자신의 Time Zone을 가지게 되었을 때 이 위치를 기점으로 UTC와의 차이를 나타낸 것이 UTC Offset
이다.
대표적으로 우리나라는 UTC+09:00이다. 즉 우리나라의 어디에서든 UTC+09:00을 사용하고, 중국은 중국 어디에서나 UTC+08:00을 사용한다고 생각하면 된다.
Time Zone의 경우 다른 방식으로도 변경이 되는데 DST, 즉 서머타임이다. 이러한 서머타임을 적용하는 나라에서는 서머타임이 적용되는 시점에 따라 다른 시간이 적용된다.
참고 링크
List of UTC offsets - Wikipedia
Instant & ZoneDateTime
Instant
기계의 관점에서 UTC의 특정 시점을 나타낸 것
@Test
void test() {
Instant instant = Instant.now();
System.out.println(instant); // 2023-08-05T05:02:15.416546900Z
System.out.println(LocalDateTime.now()); // 2023-08-05T14:02:15.424609800
System.out.println(Instant.ofEpochSecond(3)); // 1970-01-01T00:00:03Z
System.out.println(Instant.ofEpochSecond(3,0)); // 1970-01-01T00:00:03Z
System.out.println(Instant.ofEpochSecond(3, 1_000_000_000)); // 1970-01-01T00:00:04Z
System.out.println(Instant.ofEpochSecond(3, 999_999_001)); // 1970-01-01T00:00:03.999999001Z
}
와 같이 현재 시점에 대해 알 수도 있으며, 특정 시점에 대한 값도 알 수 있습니다.
다만 LocalDateTime와 Instant의 경우서로 다른 점이 존재하는데 Z
라고 볼 수 있습니다. 이는 UTC표준을 사용하고 있다고 생각하면 되기 때문에
첫 번째 출력에서는 UTC에 대한 값을 가져오는 반면, LocalDateTime의 경우 특정한 지역에 사용되는 것은 아닙니다.
만약 여기서 offset이나 현재 지역을 가져오고 싶다면
@Test
void offsetTest() {
OffsetDateTime offsetDateTime = OffsetDateTime.now();
System.out.println(Instant.now()); // 2023-08-05T05:20:04.630521600Z
System.out.println(LocalDateTime.now()); // 2023-08-05T14:20:57.754290600
System.out.println(offsetDateTime); // 2023-08-05T14:20:57.748291900+09:00
ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZoneOffset zoneOffset = zoneId.getRules().getOffset(Instant.now());
System.out.println(zoneId); // Asia/Seoul
System.out.println(zoneOffset); // +09:00
}
와 같이 사용하면 됩니다.
ZoneDateTime
zone에 대한 rules들이 함께 들어가서 쉽게 생각하면 Instant에 Offset과 Zone이 같이 섞여 있는 것으로 생각하면 됩니다.
@Test
void zone_test() {
ZonedDateTime myHome = ZonedDateTime.now();
System.out.println("my home : " +myHome); // my home : 2023-08-05T14:34:13.394698+09:00[Asia/Seoul]
System.out.println(myHome.getOffset()); // +09:00
ZoneId zone = ZoneId.of("Asia/Tokyo");
ZonedDateTime zonedDateTime = ZonedDateTime.now(zone);
System.out.println("other home : " + zonedDateTime); // other home : 2023-08-05T14:34:13.397697100+09:00[Asia/Tokyo]
System.out.println(zonedDateTime.getOffset()); // +09:00
}
따라서 서울과 도쿄는 같은 zone을 가지고 있는 것을 볼 수 있습니다.
zone에 대한 rule을 설정하기 위해서는 {지역/도시}와 같이 정해줘야 하며, KST와 같은 축약어는 사용하면 안 됩니다.
만약 withZoneSameInstant
을 사용하면 다른 시간대로 변경이 가능하게 됩니다.
ZonedDateTime newHome = myHome.withZoneSameInstant(ZoneId.of("America/New_York"));
Instant vs LocalDateTime의 Clock 비교
LocalDateTime
LocalDateTime
의 경우 System clock
을 사용한다. 따라서 내부적으로 값을 생성해 줄 때 Clock.systemDefaultZone()
이나 Clock.system(zone)
즉 ZoneId
를 사용하여 가져옵니다.
public static LocalDateTime now() {
return now(Clock.systemDefaultZone());
}
----
public static Clock systemDefaultZone() {
return new SystemClock(ZoneId.systemDefault());
}
----
public static ZoneId systemDefault() {
return TimeZone.getDefault().toZoneId();
}
위에서부터 순서대로 값을 가져오게 되는데

local System의 timezone을 통해 값을 가져오는 것을 알 수 있습니다.
그럼 어떤 기준으로 가져오냐 생각을 해보면

Clock을 가져올 때 공통적으로 best available system clock
에 기반하여 가져오고 System.currentTimeMillis()
나 더 좋은 clock를 사용한다는 것입니다.

결국 the best available system clock
을 사용해서 값을 가져오는데 가져오는 방식이 System.currentTiemMillis()
나 그 이상의 클록 메커니즘을 통해 가져오며, 이 값을 system property의 기본 time-zone을 가져오고 가져온 값을 사용 CLock객체를 만들게 됩니다.
이렇게 Clock을 만들면

Clock에서 instant를 통해 LocalDate
와 LocalTime
을 만들어 생성하게 된다 생각하면 됩니다.
Instant
instance 또한 LocalDateTime처럼 system clock을 사용하지만 기본 값인 system UTC clock
을 통해 instant를 만드는 것을 볼 수 있습니다.

결국 Instant는 systemUTC()를 통해 인스턴스를 만들게 되지만, LocalDateTime의 경우 자신의 Time-zone에 따라 Instant를 만드는데, 둘 다 utc를 사용하지만 결국 이 값을 다시 변경하여 이 LocalDateTime을 만드는 차이가 있다.
Instant와 LocalDateTime 언제 사용할까?: 아직은 직접 경험해 보지는 않았기 때문에 추론만
- LocalDateTime에 다른 Clock을 넣어줄 수 있습니다.
- 표준 시간을 변경은 가능하지만 굳이?라는 생각이 듭니다. LocalDateTime자체가 Local이 붙었기 때문에 의미하는 것이 자신의 지역을 사용하는 것이라는 느낌이 강해서 이렇게는 안 할 것 같습니다.
LocalDateTime
:- 내부적으로만 유의미할 때: 생일 관리 어플이나 알람기능이 필요하다 하면 외부적으로 동기화가 필요 없다고 생각합니다.
- Time zone이 필요 없을 때 : Local time-zone에만 의존하여 시간만 가져오게 된다. 따라서 지역에 크게 상관없이 시간과 날짜에 집중하고 싶을 때라고 생각한다.
-
Instant
: 여러 서버에서 동시에 작업해야 할 때
- LoL과 같은 대회나, 콘서트의 티켓팅을 할 때, 한, 중, 일에서 동시에 서버를 열어야 한다고 생각하면, 시간대를 맞춰야 하기 때문에 사용할 것 같습니다.
- DB에 저장할 때에도, DB의 Timestamp 역시 UTC를 사용하기 때문에 Instant에서 utc를 제공할 수 있다.
LocalDateTime 대신 ZoneDateTime
LocalDateTime에서는 Time zone 자체를 처음에만 설정한 후 date, time만 다룰 수 있지만, ZoneDateTime의 경우 LocalDateTIme의 기능에 더해 time-zone을 다룰 수 있다는 점에서 더 넓은 기능을 제공해 준다. 또한 Time-zone에 대한 기능을 runtime환경에서 제공해 준다는 것이 또 다른 이점이 아닐까 생각한다.
LocalDateTime testing
localDateTime의 경우 불변 객체로써 내부적으로 static 메서드를 통해 불변을 유지하며 값을 변경합니다.
이러한 LocalDateTime은 테스팅을 할 때 까다로운데 이것에 대해 알아보겠습니다.
public class Board {
private static final int VALID_DATE = 7;
private LocalDateTime createdAt;
public Board() {
this.createdAt = LocalDateTime.now();
}
public boolean canRemove() {
LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_DATE);
return !(LocalDateTime.now().isAfter(expireTime));
}
}
위 예제는 생성일로 부터 일주일 후에 제거를 할 수 있다는 것을 확인하기 위한 메서드입니다.
이러한 경우 테스트를 작성하게 되면
@Test
void board_valid_test() {
Board board = new Board();
assertThat(board.canRemove()).isTrue();
}
이렇게 테스트를 할 수 있을 것이다.
이러한 상황에서 어떻게 테스트하면 좋을까 생각을 해보면,
- LocalDateTime.now()를 파라미터로 변경하자
- static mocking
- Mocking과 유사한 LocalDateTime.now()를 인터페이스화
- LocalDateTime 내부 Clock모킹
예제의 경우 하나의 방법만으로도 가능하지만, 추후 유사한 문제를 맞이했을 때 다양한 솔루션을 위해 여러 방법을 확인해 보겠습니다.
1. LocalDateTime을 파라미터화
가장 쉬운 방법으로, 메서드 내의 LocalDateTime.now()를 파라미터로 변경하여 외부에서 값을 제공해 주는 방식이다.
public boolean canRemove(LocalDateTime now) {
LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_DATE);
return !(now.isAfter(expireTime));
}
-----------
Board board = new Board();
LocalDateTime now = LocalDateTime.of(2023,1,1,12,13,45);
assertThat(board.canRemove(now)).isTrue();
2. Static Mocking
가장 하면 안 되는 방법 중 하나라고 생각한다. static의 경우 overriding을 하지 못한다 따라서 객체지향의 다형성을 위반한다 볼 수 있다. 하지만 static으로 제공하는 이점이 있어서 사용한 상태에서 Mocking을 해야 하는 경우가 발생한다면 해당 방법을 공유한다.
추가적으로, mockito에서 static Mocking을 제공하지 않는 이유는 내부적으로 정적 메서드는 동적 바인딩이 되기 전 컴파일 때 이루어지는데, class에 종속적이게 되면 클래스의 인스턴스가 필요한 mockito는 사용하지 못한다.
@Test
void board_valid_test() {
Board board = new Board();
LocalDateTime newDate = LocalDateTime.of(2023,1,1,0,0,0);
try (MockedStatic mocked = Mockito.mockStatic(LocalDateTime.class)) {
mocked.when(LocalDateTime::now).thenReturn(newDate);
assertThat(board.canRemove()).isTrue();
}
}
위와 같이 Mockito.mockStatic을 통해서 구현할 수 있는데, mockStatic
의 경우 mocking할 때의 값을 계속 가지고 있다 보니 close
를 해줘야 한다. 따라서 위에서는 try resource
로 구현했습니다.
사실 Static Mocking은 안티패턴이라는 말이 있는 만큼 사용하기를 권장하지는 않습니다.
Enable mocking static methods in Mockito · Issue #1013 · mockito/mockito (github.com)
위 issue에서도 리팩토링하라는 의견이 있었다.
번외. private Mocking
private과 public으로 분리했다는 것은 외부에 공개하는 것을 줄여서 결합도를 낮추는 목표가 크다고 생각합니다.
우리가 인터페이스를 통해 구현 정보를 감추듯이, public으로 접근을 가능하게 하고, 세부적인 값들은 private으로 감추는 것이고, 그만큼 private은 public보다 변경될 확률이 높다고 생각합니다.
따라서 private Mocking은 설계 자체가 잘못된 것이 아닌가에 대한 생각을 해볼 필요가 있다고 생각합니다.
(https://shoulditestprivatemethods.com/)
우연히 발견한 것인데 재밌어서 같이 공유합니다.
3. LocalDateTime 인터페이스화
localDateTime에서 now()
는 static메서드로 mocking이 불가능한데, 사실 LocalDateTime말고도 다른 서드파티 라이브러리 등 여러 경우 필요한 기능이 static이어서 테스트를 하기가 힘들어질 수도 있습니다. 이번 인터페이스화는 이럴 때 사용하기 좋다고 생각합니다.
먼저 static메서드를 mocking하지 못하므로 해당 값을 반환해 주는 메서드를 만들어 줘야 합니다. 이러한 메서드 들을 바로 mocking하여 사용해도 되지만, LocalDateTime
이외 에도 Instant
나 ZoneDateTime
으로 변경을 하더라도 동일한 동작을 위해 인터페이스로 추상화하여 적용할 수 있다.
public interface TimeService {
LocalDateTime now();
}
---------
public class TimeServiceImpl implements TimeService {
@Override
public LocalDateTime now() {
return LocalDateTime.now();
}
}
따라서 위와 같이 해당 service를 추상화하여
public class BoardService {
private TimeService localDateTimeService;
public BoardService(TimeService localDateTimeService) {
this.localDateTimeService = localDateTimeService;
}
public void createBoard() {
Board board = new Board();
if (!board.canRemove(localDateTimeService.now())) {
throw new IllegalArgumentException();
}
}
}
BoardService
에서는 해당 인터페이스를 주입받아 사용할 수 있도록 하는 것이다.
@Test
void interface_test() {
TimeService mock = mock(TimeService.class);
Board board = new Board();
given(mock.now()).willReturn(LocalDateTime.of(2023,1,1,0,0,0));
assertThat(board.canRemove(mock.now())).isTrue();
}
이렇게 테스트를 하게 되면 정상 동작이 되는 것을 볼 수 있다.
4. LocalDateTime내부 Clock을 Mocking
localDateTime.now()
를 실행하면 내부적으로 now(Clock.systemDefaultZone());
가 동작한다.
최종적으로 해당 메서드가 실행되면서 LocalDateTiem의 인스턴스를 제공해 주는데, 여기서 봐야 할 것은 clock.instant()
이다.
public static LocalDateTime now(Clock clock) {
Objects.requireNonNull(clock, "clock");
final Instant now = clock.instant(); // called once
ZoneOffset offset = clock.getZone().getRules().getOffset(now);
return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
}

해당 instant()
는 public 메서드로 mocking이 가능하다.
먼저 Clock을 넣어주고, 공통된 값을 넣어주기 위해 Bean으로 등록, 이후 Test에서도 사용
@Component
public class Clock {
@Bean
public java.time.Clock clock() {
return java.time.Clock.systemDefaultZone();
}
}
@Service
public class BoardService {
private Clock clock;
public BoardService(Clock clock) {
this.clock = clock;
}
public void createBoard() {
Board board = new Board();
if (!board.canRemove(clock)) {
throw new IllegalArgumentException();
}
}
}
@Component
public class Board {
private LocalDateTime createdAt;
public Board() {
this.createdAt = LocalDateTime.now();
}
public boolean canRemove(Clock clock) {
LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_DATE);
return !(LocalDateTime.now(clock).isAfter(expireTime));
}
}
LocalDateTime에 Clock을 전달하기 위해 파라미터로 받아오고, Clock은 Component
로 등록하였으니 값을 주입받으면 끝이 난다.
@InjectMocks
private Board board;
@Mock
Clock clock;
@Test
void clock_test() {
given(clock.instant()).willReturn(Instant.parse("2023-08-05T05:20:04.630521600Z"));
given(clock.getZone()).willReturn(ZoneId.of("Asia/Seoul"));
assertThat(board.canRemove(clock)).isTrue();
}
와 같이 값을 넣어주면 테스트가 성공하는 것을 볼 수 있다.
여러 가지 테스팅을 알아봤습니다.
2번의 경우, private과 static으로 해결하려고 할 때는 다시 설계를 해봐야 하지 않을까 라는 생각이 있으며, 3번 인터페이스화같은 경우에는 LocalDateTime뿐 아니라 다른 곳에서도 유용하게 사용할 것 같습니다.
참고 링크
Java의 날짜, 시간에 대한 기본적인 정책 (gmarket.com)
[Mockito] LocalDateTime.now() 테스트하기 (tistory.com)
'JAVA > test' 카테고리의 다른 글
Mockito 간단 정리 (0) | 2023.08.04 |
---|