ํด๋น ๊ธ์ ์๋ฅ์๋ฅ ๊ธฐ์ ๋ธ๋ก๊ทธ์ ์์ฑ๋ ๊ธ๊ณผ ๋์ผํฉ๋๋ค.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class AuthCode {
private static final long VALID_MINUTE = 5L;
@CreatedDate
private LocalDateTime createdAt;
//...
public void verifyTime() {
LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE);
if (LocalDateTime.now().isAfter(expireTime)) {
throw new InvalidAuthCodeException();
}
}
}
์ธ์ฆ์ฝ๋๊ฐ ์์ฑ์์ ์์ 5๋ถ์ด ์ง๋๋ฉด ๋ง๋ฃ
๋๋ ๋ก์ง์ ์ง๋ ๋์ค์ด์๋ค.
๊ตฌํ ํ ํ ์คํธ์ฝ๋๋ฅผ ์ง๋ ค๊ณ ๋ณด๋ ๋ฌธ์ ๊ฐ ์๊ฒผ๋ค.
LocalDateTime.now()
๋ ์์คํ
์ ์ง๊ธ ์๊ฐ ์ ๋ฆฌํดํ๋๋ฐ
์ด๋ป๊ฒ ํน์ ์๊ฐ์ผ๋ก๋ถํฐ ๋ง๋ฃ๋์๋์ง
๊ฒ์ฆํ๋ ํ
์คํธ ์ฝ๋๋ฅผ ์งค ์ ์์ง? (์ด๋ป๊ฒ ํ์๋จธ์ ์ ํ์ง??)
์ฆ, ์๊ฐ ๋ณ์๋ฅผ ํ
์คํธ์์ ์์๋ก ์ง์ ํด ์ธ ์ ์์๋ค.
๊ทธ๋์ ํ์ฌ์๊ฐ์ ์์์ง์ ํ ๋ชจํน
ํด ํ
์คํธํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํด๋ดค๋ค.
์ฌ๋ฌ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์๋ค.
1. Clock ์ ๋น ๋ฑ๋กํด ๋ชจํนํ๊ธฐ
2. LocalDateTime ์ static mock์ผ๋ก ๋ชจํนํ๊ธฐ
3. LocalDateTime.now() ๋ฅผ ๋ฆฌํดํ๋ ์ ํ์๋จธ์ ํด๋์ค ์์ฑํด ๋ชจํนํ๊ธฐ
4. AuthCode ์์ฑ์์์ LocalDateTime ์ธํฐํ์ด์ค๋ฅผ ๋ฐ๊ณ , ๊ทธ ์์์ LocalDateTime/mock์ ๋ฃ๊ธฐ(์ ๋ตํจํด)
์ฌ๋ฌ ๋ฐฉ๋ฒ๋ค ์ค ๋๋ 1๋ฒ,
Clock
์ ๋น ๋ฑ๋กํด ๋ชจํนํ๊ธฐ ๋ฅผ ํํ๋ค.
์๋ํ๋ฉด
2๋ฒ static mock์ ์ํฐํจํด์ด๊ณ , ํ ์คํธ์ฝ๋ ์์ฑ ๋ฐฉ์์ด ๋ณต์กํ๊ณ , try๋ฌธ์ผ๋ก ๊ฐ์ธ์ผ ํ๋ค. ์ผ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํธํ๋์ง ์๋๋ค.
3๋ฒ์ ์๋ก์ด ํด๋์ค๋ฅผ ๋ง๋๋ ๊ณผ์ ์ด ๋ฒ๊ฑฐ๋กญ๊ณ , ๊ธฐ์กด LocalDateTime์ ๊ทธ๋๋ก ์ฌ์ฉํ๋ ๋ฐฉ์์ด ์๋์ด์ ํด๋์ค ์ดํด๊ฐ ํ์ํ๋ค.
4๋ฒ์ ์ด๋๊น์ง ํด๋น ๊ฐ์ฒด๋ฅผ ๋ฃ์ด์ค์ง ๋ฒ์๋ฅผ ๋์ด์ฃผ๊ธฐ ์ด๋ ค์ ๋ค.(Controller โserviceโdomainโฆ)
Clock
์ธ๊ฐ?LocalDateTime
์ ๋ชจํนํ ์ ์๋คpublic final class LocalDateTime
implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {
public static LocalDateTime now() {
return now(Clock.systemDefaultZone());
}
public static LocalDateTime now(ZoneId zone) {
return now(Clock.system(zone));
}
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);
}
//... of() ๋ฑ ์ด์ธ ๋ฉ์๋
}
LocalDateTime.now()
๋ ๋ค์ ๋ฐฉ์์ผ๋ก ์ง์ฌ์๋ค.
๋ณด๋ค์ํผ now()๋ static ๋ฉ์๋์ด๊ธฐ ๋๋ฌธ์
LocalDateTime์ ๊ฐ์ฒด๋ก ๋ฑ๋กํด ์ฌ์ฉํ๋๋ผ๋ ๋ฉ์๋๋ฅผ ๊บผ๋ด์ธ ์ ์๋คโฆใ ใ
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ์ฃผ๋ชฉํ ์ ์ด ์๋ค.
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);
}
ํ์์๋ LocalDateTime.now()
๋ก ํ์ฌ ์๊ฐ์ ๊ฐ์ ธ์ค์ง๋ง,
์ฌ์ค LocalDateTime.now(clock)
ํํ๋ก, Clock
๊ฐ์ฒด๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ์๊ฐ์ ๊ฐ์ ธ์ค๋ ํํ์๋ค!
Clock
์ด ๋ญ์ง?๊ทธ๋ ๋ค๋ฉด ์ด Clock
์ ๋ญ๊น?
์ ํํ ๋งํ๋ฉด
final Instant now = clock.instant(); // called once
์์ ์ฌ์ฉ๋๋ clock.instant()
๋ ๋ญ๊น?
public abstract class Clock {
//-----------------------------------------------------------------------
/**
* Gets the current instant of the clock.
*<p>
* This returns an instant representing the current instant as defined by the clock.
*
*@returnthe current instant from this clock, not null
*@throwsDateTimeExceptionif the instant cannot be obtained, not thrown by most implementations
*/
public abstract Instant instant();
//...
}
Clock ๊ฐ์ฒด์ ํ์ฌ instant๋ฅผ ๋ฆฌํดํด์ฃผ๋ ์ถ์ ๋ฉ์๋์ด๋ค.
Clock
์ ๋น์ผ๋ก ๋ฑ๋กํ์์ฆ,
LocalDateTime
์ ๋ฉ์๋๋ค์static
๋ฉ์๋์ง๋ง,
clock
๊ฐ์ฒด๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ ์ ์์ผ๋
clock
์ ๋น์ผ๋ก ๋ฑ๋กํด ์ฌ์ฉํ๋ฉด ๋๋ค!
now
)์ ํ๋ผ๋ฏธํฐ๋ก ์ถ์ถpublic class AuthCode {
public void verifyTime(LocalDateTime now) { // now๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ด๋
LocalDateTime expireTime = this.createdAt.plusMinutes(VALID_MINUTE);
if (now.isAfter(expireTime)) {
throw new InvalidAuthCodeException();
}
}
}
authcode.verifyTime()
๋ฉ์๋์์
ํ์ฌ ์๊ฐ(now
)์ ํ๋ผ๋ฏธํฐ๋ก ์ถ์ถํ๋ค.
clock
์ Bean
๋ฑ๋กimport java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TimeConfig {
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
}
๋ค์, clock
์ @Configuration
์ผ๋ก ๋น ๋ฑ๋กํ๋ค.
public abstract class Clock {
public static Clock systemDefaultZone() {
return new SystemClock(ZoneId.systemDefault());
}
}
์ด๋ Clock.systemDefaultZone()
์ ํ์ฉํ๋ค.
clock
ํด๋์ค์ ํ์ฌ์๊ฐ clock
๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ค.
AuthService
์์ Bean
์ฃผ์
๊ทธ๋ฆฌ๊ณ AuthService
์์ clock
์ ๋น์ผ๋ก ์ฃผ์
๋ฐ์ ํ
AuthCode
๊ฐ์ฒด์ clock
์ ์ฃผ์
ํ๋ค.
@Service
public class AuthService {
private final AuthCodeRepository authCodeRepository;
// ...
private final Clock clock;
public AuthService(//...,
Clock clock) {
this.authCodeRepository = authCodeRepository;
this.clock = clock;
}
public void verifyAuthCode(VerificationRequest verificationRequest) {
AuthCode authCode = authCodeRepository.findBySerialNumber(serialNumber)
.orElseThrow(SerialNumberNotFoundException::new);
//...
LocalDateTime now = LocalDateTime.now(clock);
authCode.verifyTime(now);
}
// ...
}
AuthCode
(๋๋ฉ์ธ ๊ณ์ธต)AuthCode
์ ๊ฒฝ์ฐ ๋ฑ๋ก๋ clock
๋น์ ์ฌ์ฉํ์ง ์๊ณ ๋ ๋ค์ ๋ฐฉ์์ผ๋ก ํ
์คํธํ ์ ์๋ค.
@SpringBootTest
class AuthCodeTest {
@DisplayName("์ธ์ฆ์ฝ๋ ์์ฑ์๊ฐ์ผ๋ก๋ถํฐ 5๋ถ์ด ์ง๋๋ฉด ์ธ์ฆ์ด ๋ถ๊ฐ๋ฅํ๋ค")
@Test
void verifyTime_Exception_Time() {
AuthCode authCode = AuthCode.builder()
.code("ABCDEF")
.serialNumber("21f46568bf6002c23843d198af30bb2bc8123695bd3d12ce86e0fc35bc5d3279")
.createdAt(LocalDateTime.parse("2007-12-03T10:15:30"))
.build();
assertThatThrownBy(() -> authCode.verifyTime(LocalDateTime.parse("2007-12-03T10:20:31")))
.isInstanceOf(InvalidAuthCodeException.class);
}
}
AuthService
(์๋น์ค ๊ณ์ธต)AuthService
์ ๊ฒฝ์ฐ clock
๋น์ ์ด์ฉํด ๋ค์ ๋ฐฉ์์ผ๋ก ํ
์คํธํ ์ ์๋ค.
class AuthServiceTest extends IntegrationTest {
private static final Clock FUTURE_CLOCK = Clock.fixed(Instant.parse("3333-08-22T10:00:00Z"), ZoneOffset.UTC);
@Autowired
private AuthService authService;
@Autowired
private AuthCodeRepository authCodeRepository;
@SpyBean
private Clock clock;
@DisplayName("์ธ์ฆ๋ฒํธ ๋ง๋ฃ ์ ์์ธ ๋ฐ์")
@Test
void verifyAuthCode_Exception_Expired() {
AuthCode authCode = //~
authCodeRepository.save(authCode);
// clock.instant() ๋ฆฌํด๊ฐ์ ์์๋ก ์ง์ ํด ๋ฏธ๋๋ก ํ์ฌ ์๊ฐ์ ๋ฐ๊พผ๋ค!
doReturn(Instant.now(FUTURE_CLOCK))
.when(clock)
.instant();
VerificationRequest verificationRequest = new VerificationRequest("test@gmail.com", "ABCDEF");
assertThatThrownBy(() -> authService.verifyAuthCode(verificationRequest))
.isInstanceOf(InvalidAuthCodeException.class);
}
}
(Java) ํ์๋จธ์ ์ ํ๊ณ ์๊ฐ ์ฌํ ๋ ๋๊ธฐ
ํ
์คํธ ์ฝ๋์์ LocalDate.now()๋ฅผ ์ฐ์ง๋ง์.
How can I mock java.time.LocalDate.now()
I think this is the first time I am looking at these codings and the great work of by an individual. In case if you need help in promoting your work consider hire social media manager london. As they are very professional in doing digital marketing
์ ํ๋ฅญํ๋ค์!!