diff --git a/images/clock_mocking.png b/images/clock_mocking.png
new file mode 100644
index 00000000..9d55747a
Binary files /dev/null and b/images/clock_mocking.png differ
diff --git a/images/fixed_clock_class.png b/images/fixed_clock_class.png
new file mode 100644
index 00000000..df3ef81d
Binary files /dev/null and b/images/fixed_clock_class.png differ
diff --git a/images/fixed_clock_method.png b/images/fixed_clock_method.png
new file mode 100644
index 00000000..3ca7aa3c
Binary files /dev/null and b/images/fixed_clock_method.png differ
diff --git a/images/localdatetime.png b/images/localdatetime.png
new file mode 100644
index 00000000..90f8776f
Binary files /dev/null and b/images/localdatetime.png differ
diff --git a/images/localdatetime_clock.png b/images/localdatetime_clock.png
new file mode 100644
index 00000000..fa8e0dcb
Binary files /dev/null and b/images/localdatetime_clock.png differ
diff --git a/images/localdatetime_wrapper.png b/images/localdatetime_wrapper.png
new file mode 100644
index 00000000..ff619e73
Binary files /dev/null and b/images/localdatetime_wrapper.png differ
diff --git a/images/mockStatic.png b/images/mockStatic.png
new file mode 100644
index 00000000..4c7524c8
Binary files /dev/null and b/images/mockStatic.png differ
diff --git a/images/mockStatic_RestAssured.png b/images/mockStatic_RestAssured.png
new file mode 100644
index 00000000..53644d66
Binary files /dev/null and b/images/mockStatic_RestAssured.png differ
diff --git a/images/mockStatic_WebMvc.png b/images/mockStatic_WebMvc.png
new file mode 100644
index 00000000..6325aa84
Binary files /dev/null and b/images/mockStatic_WebMvc.png differ
diff --git a/technical_writing.md b/technical_writing.md
new file mode 100644
index 00000000..e644eb55
--- /dev/null
+++ b/technical_writing.md
@@ -0,0 +1,424 @@
+# 어노테이션 하나로 테스트에서 *LocalDateTime.now()* 제어하기
+
+'테스트 실패해요.'
+
+땅콩 프로젝트는 `LocalDateTime.now()`로 현재 시간을 가져와서 비교하는 비즈니스 로직이 있습니다. 단위 테스트를 작성하고 자신있게 Pull Request를 올렸지만 CI에서 테스트가 실패했습니다. 테스트를 실행할 때마다 현재 시간이 달라져 어느 시점부터 완전히 실패하는 테스트가 되었기 때문이었습니다.
+
+좋은 단위 테스트는 [F.I.R.S.T 원칙](https://howtodoinjava.com/best-practices/first-principles-for-good-tests/)을 따릅니다. 하지만 제가 구현한 테스트는 반복 가능한 테스트, 즉 **Repeatable** 원칙을 만족하지 못하고 있었습니다.
+
+현재 시간과 같은 랜덤 요소를 제어하는 것은 테스트에서 매우 중요합니다. 저는 '랜덤한 시간을 제어해서 반복 가능한 테스트 만들기'를 넘어 두 가지도 함께 고민했습니다.
+
+1. 테스트 가독성 높이기
+2. 다른 팀원들도 테스트에서 쉽게 시간 제어하기
+
+위 고민을 해결하기 위해 어떤 시도를 했는지, 그리고 어노테이션 하나로 시간을 어떻게 제어했는지 소개하려고 합니다.
+
+
+
+## 테스트에서 시간을 어떻게 제어하면 좋을까?
+
+Mock이란 [테스트 더블](https://www.javacodegeeks.com/2019/04/introduction-to-test-doubles.html) 방법 중 하나로, 테스트에서 실제 객체와 동일한 mock 객체를 만들어 특정 동작을 검증하거나 제어할 수 있게 하는 방법입니다. 이와 같은 과정을 모킹(Mocking)이라고 합니다.
+스프링 부트에서는 `spring-boot-starter-test` 의존성에 포함된 [Mockito](https://site.mockito.org/) 프레임워크를 사용해서 객체를 쉽게 모킹할 수 있습니다.
+
+그렇다면 `LocalDateTime.now()`를 모킹해서 원하는 시간을 반환하면 쉽게 해결되지 않을까요? 아쉽게도 `LocalDateTime.now()`는 static 메서드이기 때문에 `Mockito.mock()`과 같은 일반적인 모킹 방법으로는 제어하기 어렵습니다.
+
+### 1. MockedStatic 사용하기
+
+
+
+[Mockito 3.4.0](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48) 버전 이상부터 MockedStatic으로 static 메서드를 모킹할 수 있습니다.
+
+
+
+```java
+@RestController
+public class TimeController {
+
+ @GetMapping("/time")
+ public String time() {
+ LocalDateTime now = LocalDateTime.now();
+ System.out.println("현재 시간: %s".formatted(now));
+ return now.toString();
+ }
+}
+
+```
+
+```java
+@WebMvcTest(TimeController.class)
+class TimeControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void 현재_시간_모킹_테스트() throws Exception {
+ // given
+ LocalDateTime now = LocalDateTime.parse("2024-10-31T12:30:15");
+ MockedStatic localDateTimeMockedStatic = Mockito.mockStatic(LocalDateTime.class);
+ localDateTimeMockedStatic.when(LocalDateTime::now).thenReturn(now);
+
+ // when & then
+ mockMvc.perform(get("/time"))
+ .andExpect(jsonPath("$").value("2024-10-31T12:30:15"));
+
+ localDateTimeMockedStatic.close();
+ }
+}
+```
+간단한 컨트롤러와 테스트를 작성해보겠습니다. MockedStatic으로 LocalDateTime을 모킹한 후 `now()`를 호출했을 때 고정된 시간을 반환하도록 합니다.
+
+
+
+
+
+테스트를 실행하면 고정된 시간을 잘 반환하고 있습니다. 문제를 해결했나 싶었지만 MockedStatic은 스레드 로컬로 동작하기 때문에 문제점이 있습니다.
+
+리소스를 해제하지 않으면 MockedStatic이 스레드에 활성 상태로 남아있게 되고, 같은 스레드를 재사용하는 다른 테스트에 영향을 줄 수 있습니다. 그래서 try-with-resources 구문을 사용하거나 `close()`를 명시적으로 호출해서 **항상 리소스를 해제**해야 합니다.
+
+[@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)](https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html)을 사용하면 HTTP 클라이언트가 테스트와 별도의 스레드에서 실행되기 때문에 스레드 로컬로 처리되는 MockedStatic이 반영되지 않습니다. 땅콩은 컨트롤러 테스트로 RestAssured와 WebEnvironment.RANDOM_PORT를 사용하기 때문에 이 방식으로는 문제를 해결할 수 없습니다.
+
+```java
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+class TimeControllerTest {
+
+ private static final Logger log = LoggerFactory.getLogger(TimeControllerTest.class);
+
+ @LocalServerPort
+ private int port;
+
+ @BeforeEach
+ void setUp() {
+ RestAssured.port = port;
+ }
+
+ @Test
+ void 현재_시간_모킹_테스트() {
+ // given
+ LocalDateTime now = LocalDateTime.parse("2024-10-31T12:30:15");
+ log.info("모킹한 시간: {}", now);
+ MockedStatic localDateTimeMockedStatic = Mockito.mockStatic(LocalDateTime.class);
+ localDateTimeMockedStatic.when(LocalDateTime::now).thenReturn(now);
+
+ // when
+ RestAssured.when()
+ .get("/time");
+
+ localDateTimeMockedStatic.close();
+ }
+}
+
+```
+
+
+
+실제로 테스트를 해보면 서로 다른 스레드에서 실행되어 모킹이 적용되지 않음을 확인할 수 있습니다.
+
+### 2. LocalDateTime을 래핑하는 클래스
+
+```java
+@Component
+public class LocalDateTimeWrapper {
+
+ public LocalDateTime now() {
+ return LocalDateTime.now();
+ }
+}
+```
+
+LocalDateTime을 한 번 감싸는 래핑 클래스를 생성해서 테스트 더블을 사용하는 방법입니다.
+
+```java
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class TimeService {
+
+ private final LocalDateTimeWrapper localDateTimeWrapper;
+
+ public void printCurrentTime() {
+ LocalDateTime now = localDateTimeWrapper.now();
+ log.info("현재 시간: {}", now);
+ }
+}
+```
+
+```java
+@SpringBootTest
+public class TimeServiceTest {
+
+ private static final Logger log = LoggerFactory.getLogger(TimeServiceTest.class);
+
+ @Autowired
+ private TimeService timeService;
+
+ @MockBean
+ private LocalDateTimeWrapper localDateTimeWrapper;
+
+ @Test
+ void 현재_시간_모킹_테스트() {
+ // given
+ LocalDateTime now = LocalDateTime.parse("2024-12-12T00:00:00");
+ log.info("모킹한 시간: {}", now);
+ when(localDateTimeWrapper.now()).thenReturn(now);
+
+ // when
+ timeService.printCurrentTime();
+ }
+}
+```
+
+
+
+가장 간단한 방법이지만 일반적이지 않은 코드라서 팀원들의 인지 비용이 발생할 것이라 생각했습니다.
+
+### 3. Clock 객체를 bean으로 등록 후 모킹
+
+
+
+`LocalDateTime.now()`의 내부를 살펴보면 Clock을 인자로 받는 메서드를 호출하고 있습니다.
+
+
+
+내부적으로만 사용하는 줄 알았는데 접근제어자가 public이네요! JavaDoc을 보니까 테스트를 위해 대체 Clock을 사용할 수 있다고 안내하고 있습니다. 이 메서드를 사용하면 시간을 쉽게 제어할 수 있어 보입니다.
+
+> - Instant
+타임라인에서 한 지점을 나타내는 순간을 나타내며, UTC 기준 `1970-01-01T00:00:00`를 0(epoch)으로 정하고 이로부터 경과된 시간을 양수 또는 음수로 표현합니다.
+> - ZoneId
+UTC, Asia/Seoul 등 특정 지역의 시간대 정보를 나타내는 타임존입니다.
+> - Clock
+Instant와 ZoneId를 사용해 현재 날짜, 시간을 제공하는 추상클래스입니다.
+
+
+
+```java
+@Configuration
+public class ClockConfig {
+
+ @Bean
+ public Clock clock() {
+ return Clock.system(ZoneId.of("Asia/Seoul"));
+ }
+}
+```
+먼저 Clock을 bean으로 등록합니다.
+
+```java
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class TimeService {
+
+ private final Clock clock;
+
+ public void printCurrentTime() {
+ LocalDateTime now = LocalDateTime.now(clock);
+ log.info("현재 시간: {}", now);
+ }
+}
+```
+Clock bean을 의존성 주입 후 `LocalDateTime.now(clock)`로 변경합니다.
+
+```java
+@SpringBootTest
+public class TimeServiceTest {
+
+ private static final Logger log = LoggerFactory.getLogger(TimeServiceTest.class);
+
+ @Autowired
+ private TimeService timeService;
+
+ @MockBean
+ private Clock clock;
+
+ @Test
+ void 현재_시간_모킹_테스트() {
+ Instant now = Instant.parse("2024-12-31T00:00:00Z");
+ log.info("모킹한 시간: {}", now);
+ when(clock.instant()).thenReturn(now);
+ when(clock.getZone()).thenReturn(ZoneOffset.UTC);
+
+ timeService.printCurrentTime();
+ }
+}
+```
+
+테스트에서는 Clock을 MockBean으로 주입하고 현재 시간을 만들어낼 때 사용하는 Instant를 원하는 값으로 반환합니다.
+
+**주의할 점**은 Zone에 따라 Instant에 작성한 시간을 변환하기 때문에 Zone이 UTC가 아니면 `LocalDateTime.now(clock)`에서 예상하지 않은 시간이 반환됩니다.
+
+
+
+테스트를 실행하면 고정된 시간을 반환하고 있습니다. 하지만 Clock을 사용하는 테스트마다 모킹하는 보일러플레이트 코드를 작성해야 하는 점이 매우 번거롭습니다.
+
+`@TestConfiguration`을 사용하면 **고정된 Clock 객체**를 primary bean으로 등록해서 테스트 전역으로 Clock을 제어할 수 있습니다. 가짜 객체가 진짜 객체처럼 행동하는 테스트 더블의 Fake 방법입니다.
+
+```java
+@TestConfiguration
+public class TestConfig {
+
+ @Primary
+ @Bean
+ public Clock testClock() {
+ return Clock.fixed(Instant.parse("2024-12-31T00:00:00Z"), ZoneOffset.UTC);
+ }
+}
+```
+```java
+@SpringBootTest
+@Import(TestConfig.class)
+public class TimeServiceTest {
+
+ @Autowired
+ private TimeService timeService;
+
+ @Test
+ void 현재_시간_모킹_테스트() {
+ timeService.printCurrentTime();
+ }
+}
+```
+`@Import`로 설정을 적용하면 고정된 Clock 객체를 사용합니다. 반복되는 보일러플레이트 코드가 모두 사라졌습니다!
+
+
+
+## 커스텀 어노테이션으로 현재 시간을 제어할 수 없을까?
+
+`@TestConfiguration`을 사용해서 Clock bean을 전역으로 제어했지만 테스트를 작성할 때 여전히 불편함이 있었습니다.
+1. 매번 TestConfiguration에 고정된 시간을 확인하면서 테스트를 작성해야 함 ('시간 언제로 고정되어 있었지?')
+2. 테스트를 유연하게 작성하기 어려움 ('이 테스트에서는 다른 시간으로 고정해야 하는데...')
+3. 테스트에서 데이터를 왜 x시간으로 저장했는지 한 번에 읽히지 않음 ('이 테스트는 왜 x시간으로 저장하지?')
+
+### JUnit 5의 extension 사용
+`@TestConfiguration`의 불편함을 극복하기 위해서 extension 기능을 활용했습니다. JUnit 5부터 도입된 extension은 테스트 라이프사이클의 다양한 단계에 특정 동작을 확장할 수 있는 기능입니다.
+
+extension 중에서 라이프사이클 콜백을 사용하면 테스트 전, 후로 메서드를 실행할 수 있습니다. 실행 순서는 다음과 같습니다.
+```
+1. BeforeAllCallback
+2. @BeforeAll
+3. BeforeEachCallback
+4. @BeforeEach
+5. BeforeTestExecutionCallback
+6. Test 실행
+7. AfterTestExecutionCallback
+8. @AfterEach
+9. AfterEachCallback
+10. @AfterAll
+11. AfterAllCallback
+```
+
+여기서 BeforeEachCallback 인터페이스를 구현해서 Clock bean을 모킹하겠습니다.
+
+```java
+public class FixedClockExtension implements BeforeEachCallback {
+
+ private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
+ private static final Pattern TIME_PATTERN = Pattern.compile("\\d{2}:\\d{2}:\\d{2}");
+
+ @Override
+ public void beforeEach(ExtensionContext context) {
+ Clock clock = SpringExtension.getApplicationContext(context).getBean(Clock.class);
+ FixedClock fixedClockAnnotation = getFixedClockAnnotation(context);
+
+ String date = getDate(fixedClockAnnotation);
+ String time = getTime(fixedClockAnnotation);
+ when(clock.instant()).thenReturn(Instant.parse("%sT%sZ".formatted(date, time)));
+ when(clock.getZone()).thenReturn(ZoneOffset.UTC);
+ }
+
+ private FixedClock getFixedClockAnnotation(ExtensionContext context) {
+ FixedClock fixedClockAnnotation = context.getRequiredTestMethod().getDeclaredAnnotation(FixedClock.class);
+ if (fixedClockAnnotation == null) {
+ fixedClockAnnotation = context.getRequiredTestClass().getDeclaredAnnotation(FixedClock.class);
+ }
+ return fixedClockAnnotation;
+ }
+
+ private String getDate(FixedClock fixedClockAnnotation) {
+ String date = fixedClockAnnotation.date();
+ if (!DATE_PATTERN.matcher(date).matches()) {
+ throw new IllegalArgumentException("yyyy-MM-dd의 date 포맷이어야 합니다. invalid date: %s".formatted(date));
+ }
+ return date;
+ }
+
+ private String getTime(FixedClock fixedClockAnnotation) {
+ String time = fixedClockAnnotation.time();
+ if (!TIME_PATTERN.matcher(time).matches()) {
+ throw new IllegalArgumentException("HH:mm:ss의 time 포맷이어야 합니다. invalid time: %s".formatted(time));
+ }
+ return time;
+ }
+}
+```
+FixedClock은 뒤에 설명할 커스텀 어노테이션입니다. 리플렉션으로 테스트 메서드나 테스트 클래스를 읽어서 `@FixedClock` 어노테이션을 찾습니다. 이때 메서드에 작성된 어노테이션이 클래스에 작성된 어노테이션보다 우선적으로 적용됩니다. Application Context에 존재하는 Clock bean을 찾아서 어노테이션에 작성된 날짜와 시간으로 모킹합니다.
+
+### 커스텀 어노테이션 생성
+```java
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(FixedClockExtension.class)
+public @interface FixedClock {
+
+ String date();
+
+ String time();
+}
+```
+테스트에서 사용할 커스텀 어노테이션입니다.
+extension은 `@ExtendWith` 어노테이션을 작성하면 적용됩니다. 여기서는 `@FixedClock` 어노테이션에 포함했기 때문에 `@FixedClock`을 사용하면 extension이 자동으로 동작하게 됩니다.
+
+### 테스트 적용
+```java
+@SpyBean(Clock.class)
+@FixedClock(date = "2025-01-01", time = "00:00:00")
+@SpringBootTest
+public class TimeServiceTest {
+
+ @Autowired
+ private TimeService timeService;
+
+ @Test
+ void 현재_시간_모킹_테스트1() {
+ timeService.printCurrentTime();
+ }
+
+ @Test
+ @FixedClock(date = "2024-12-25", time = "00:00:00")
+ void 현재_시간_모킹_테스트2() {
+ timeService.printCurrentTime();
+ }
+}
+```
+Clock 객체는 테스트 클래스에서 실제 객체 또는 mock 객체로 모두 사용되기 때문에 SpyBean으로 등록합니다.
+
+
+
+첫 번째 테스트는 클래스 레벨에 있는 `@FixedClock`의 현재 시간을 반환합니다.
+
+
+
+두 번째 테스트는 메서드 레벨에 있는 `@FixedClock`의 현재 시간을 반환합니다.
+
+>`@SpyBean` 어노테이션은 클래스 또는 필드에서만 사용할 수 있습니다. 만약 `@FixedClock`을 클래스에서만 사용할 수 있도록 제한하면 `@SpyBean(Clock.class)`도 `@FixedClock`에 포함할 수 있습니다.
+> 현재 구현은 `@FixedClock`을 메서드에서도 사용할 수 있기 때문에 어노테이션이 메서드 레벨에만 사용됐을 경우 `@SpyBean`이 동작하지 않아 예외가 발생합니다.
+
+이제 `@FixedClock` 어노테이션만 명시하면 어노테이션에 작성한 날짜, 시간으로 현재 시간을 반환할 수 있게 되었습니다!
+
+
+
+## 마치며
+지금까지 테스트에서 현재 시간을 제어하는 여러 가지 방법과 어노테이션을 사용해서 제어하는 방법까지 알아보았습니다. 땅콩은 어노테이션 기반 제어 방법을 적용해서 세 가지의 장점을 얻을 수 있었습니다.
+1. 어노테이션 하나만 사용하면 현재 시간을 쉽게 제어할 수 있다.
+2. 테스트마다 독립적으로 고정된 시간을 사용해서 유연하게 테스트를 작성할 수 있다.
+3. 고정된 시간이 무엇인지 명확히 보여주기 때문에 가독성이 향상된다.
+
+다양한 방법을 비교해 보고 자신 또는 팀에 적합한 방법을 선택하는 것이 중요하다고 생각합니다. 부족한 글이지만, 저와 비슷한 고민을 했던 개발자분들에게 조금이나마 도움이 되었으면 좋겠습니다. 감사합니다.
+
+
+
+## 레퍼런스
+- https://www.baeldung.com/mockito-mock-static-methods
+- https://github.com/mockito/mockito/issues/1013
+- https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html#now-java.time.Clock-
+- https://www.baeldung.com/junit-5-extensions