[๊ฐ์]
ํตํฉ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ ๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐ๋์ ์ฃผ์ํ ๊ณ ๋ ค์ฌํญ ์ค ํ๋์ด๋ฉฐ, ํ ์คํธ์ ์์ ์ฑ๊ณผ ์ ๋ขฐ์ฑ์ ๋์ด๊ธฐ ์ํด์๋ ์ค์ ์ด์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๊ฒฝ์์์ ํ ์คํธ๊ฐ ํ์ํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ฌํ ํ๊ฒฝ์ ๊ฐ์ถ๊ธฐ ์ํด์ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์๊ณ , ๊ฐ๊ฐ์ ๋ฐฉ๋ฒ์ ๊ทธ ํน์ฑ๊ณผ ์ฅ๋จ์ ์ด ์์ต๋๋ค. ์ด๋ฒ ๊ฒ์๊ธ์์๋ TestContainer๋ฅผ ์ด์ฉํ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
[DB๋ฅผ ํ ์คํธ ํ๊ฒฝ์ ํตํฉํ๋ ๋ฐฉ๋ฒ]
Local์ ์ค์ DB๋ฅผ ์ฐ๊ฒฐํ๊ธฐ
Local ํ๊ฒฝ์ ์ง์ DB๋ฅผ ์คํํ๋ ๋ฐฉ๋ฒ์ ์ค์ ํ๊ฒฝ๊ณผ ์ ์ฌํ๊ฒ ํ ์คํธ๋ฅผ ํ ์ ์๋ ํฐ ์ฅ์ ์ด ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ฌ๋ฌ ํ ์คํธ๋ฅผ ๋์์ ์งํํ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๋ ์ธก๋ฉด์์ ๋ฉฑ๋ฑ์ฑ(idempotencty)์ ์ ์งํ๋ ๊ฒ์ด ์ด๋ ต๊ณ , ํนํ DDL๊ณผ ๊ด๋ จ๋ ํ ์คํธ๋ฅผ ์งํํ ๋๋ง๋ค ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ์ ๊ฐ์ ์ถ๊ฐ ์์ ์ด ํ์ํฉ๋๋ค.
in-memory DB ํ์ฉํ๊ธฐ
in-momory DB, ์๋ฅผ ๋ค๋ฉด H2๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ํ ์คํธ์ ์๋๊ฐ ๋งค์ฐ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค. ORM ๋๊ตฌ๋ฅผ ํตํด ํน์ DB์ ์์กด์ฑ์ ์ค์ผ ์ ์์ง๋ง, ์ค์ DB์์ ์ฌ์ฉํ๋ ํนํ๋ ๊ธฐ๋ฅ์ด๋ DDL์ ๋ํ ์ ์ฝ์ฌํญ์ด ์กด์ฌํฉ๋๋ค.
Embedded Library ํ์ฉํ๊ธฐ
Embedded Library๋ ์ค์ DB์ ์ฝ๋๋ฅผ ๊ฒฝ๋ํํ ํํ๋ก ์ ๊ณตํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ์ด๋ฅผ ํ์ฉํ๋ฉด in-momory DB์์ ๊ฒช๊ฒ ๋๋ ํน์ DB์ ํนํ๋ ๊ธฐ๋ฅ ํ ์คํธ์ ์ ์ฝ์ ๊ทน๋ณตํ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ ๋ชจ๋ DB๊ฐ Embedded Library๋ฅผ ์ ๊ณตํ์ง ์์ผ๋ฉฐ, ์ง์๋๋ ๋ฒ์ ์ด๋ OS์ ๋ฐ๋ผ ์ ํ์ด ์์ ์ ์์ผ๊ณ , ๊ฐ DB๋ง๋ค ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์ ๋งค๋ฒ ๋ค๋ฅด๊ฒ ๊ตฌํํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค.


TestContainer ํ์ฉํ๊ธฐ
TestContainer๋ Docker ๊ธฐ๋ฐ์ ํ ์คํธ ํ๊ฒฝ์ ์ ๊ณตํ๋ ๋๊ตฌ๋ก, Docker Compose์ ์ ์ฌํ๊ฒ ๋์ํฉ๋๋ค. ๋ํ ์ธ๋ถ ์ค์ ํ์ผ ์์ด๋ Java ์ฝ๋๋ง์ผ๋ก ํ ์คํธ ํ๊ฒฝ์ ๊ฐํธํ๊ฒ ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ ์ค์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ํ ์คํธ๋ฅผ ์ ๊ณตํ๋ ๋์์, ์ปจํ ์ด๋ ๊ฐ์ ํต์ ๋ฌธ์ ๋ ์ด๊ธฐํ ๊ด๋ จ๋ ๋ฌธ์ ๋ฅผ ์ฝ๊ฒ ํด๊ฒฐํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
[TestContainer๋?]
TestContainer๋ Java ํ๋ซํผ์ ํนํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก์, ๋์ปค ์ปจํ ์ด๋๋ฅผ ๊ฐํธํ๊ฒ ๋ค๋ฃจ๋๋ก ์ค๊ณ๋์์ต๋๋ค. ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฃผ๋ ๋ชฉ์ ์ JUnit ํ ์คํธ ์ค ๋์ปค ์ปจํ ์ด๋๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ์ฌ ์ธ๋ถ ์์กด์ฑ์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๋๋ค.
ํตํฉ ํ ์คํธ๋ end-to-end ํ ์คํธ๋ฅผ ์งํํ๋ฉด์ ์ธ๋ถ ์์กด์ฑ ์์๊ฐ ํ์ํ ์ํฉ์ ๋งค์ฐ ํํ๋ฐ, ์๋ฅผ ๋ค๋ฉด (๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ฉ์์ง ์์คํ , ์ธ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ ๋ฑ)์ด ์ด๋ฐ ์์์ ํด๋นํฉ๋๋ค. ์ด๋ฌํ ์๋น์ค๋ค์ ๋งค๋ฒ ํ ์คํธ ํ๊ฒฝ์ ์ค์ ๋ก ์ค์นํ๊ณ ๊ตฌ๋ํ๋ ๊ฒ์ ์๊ฐ์ ์ผ๋ก ๋ง์ด ์๋ชจ๋๋ฉฐ, ํ๊ฒฝ ๋ณต์ก์ฑ์ ๋์ผ ์ ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด TestContainer๋ ๋์ปค ์ปจํ ์ด๋๋ฅผ ํ์ฉํ์ฌ ํ์ํ ์ธ๋ถ ์์กด์ฑ์ ๊ฐ์ํ๋ ํ๊ฒฝ์์ ์ ๊ณตํฉ๋๋ค. ์ฆ, ํ ์คํธ๋ฅผ ์ํด ์ค์ ์๋น์ค๋ฅผ ์คํํ๋ ๋์ ํด๋น ์๋น์ค์ ๋์ปค ์ด๋ฏธ์ง๋ฅผ ๋น ๋ฅด๊ฒ ์คํํ๊ณ ํ ์คํธ๊ฐ ๋๋๋ฉด ์์ฝ๊ฒ ์ข ๋ฃํ ์ ์์ต๋๋ค.
[TestContainer๋ฅผ ์ด์ฉํ ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถ]
build.gradle
// TestContainer ๊ธฐ๋ณธ ์์กด์ฑ testImplementation "org.testcontainers:testcontainers:1.19.0" // MySQL ์์กด์ฑ testImplementation 'org.testcontainers:mysql:1.16.0' // Redis ์์กด์ฑ testImplementation "com.redis.testcontainers:testcontainers-redis-junit:1.6.4" // Kafka ์์กด์ฑ testImplementation("org.testcontainers:kafka:1.17.6")
IntegrationTest.java
@Disabled @SpringBootTest @Transactional @ContextConfiguration(initializers = IntegrationTest.IntegrationTestInitializer.class) public class IntegrationTest { // ๊ฐ ์ปจํ
์ด๋ ์ธ์คํด์ค ์ ์ private static final MySQLContainer<?> MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.26")) .withDatabaseName("container") .withUsername("user") .withPassword("password"); private static final RedisContainer REDIS_CONTAINER = new RedisContainer(RedisContainer.DEFAULT_IMAGE_NAME.withTag("6")); private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0")); @BeforeAll public static void setupContainers() { // MySQL ์ปจํ
์ด๋ ์์ MYSQL_CONTAINER.start(); // Redis ์ปจํ
์ด๋ ์์ REDIS_CONTAINER.start(); // Kafka ์ปจํ
์ด๋ ์์ KAFKA_CONTAINER.start(); } static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(@NotNull ConfigurableApplicationContext applicationContext) { Map<String, String> properties = new HashMap<>(); // ๊ฐ ์๋น์ค์ ์ฐ๊ฒฐ ์ ๋ณด ์ค์ setKafkaProperties(properties); setDatabaseProperties(properties); setRedisProperties(properties); // ์ ํ๋ฆฌ์ผ์ด์
์ปจํ
์คํธ์ ์์ฑ๊ฐ ์ ์ฉ TestPropertyValues.of(properties).applyTo(applicationContext); } // Kafka ์ฐ๊ฒฐ ์ ๋ณด ์ค์ private void setKafkaProperties(Map<String, String> properties) { properties.put("spring.kafka.bootstrap-servers", KAFKA_CONTAINER.getBootstrapServers()); } // ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด ์ค์ private void setDatabaseProperties(Map<String, String> properties) { properties.put("spring.datasource.url", MYSQL_CONTAINER.getJdbcUrl()); properties.put("spring.datasource.username", MYSQL_CONTAINER.getUsername()); properties.put("spring.datasource.password", MYSQL_CONTAINER.getPassword()); } // Redis ์ฐ๊ฒฐ ์ ๋ณด ์ค์ private void setRedisProperties(Map<String, String> properties) { properties.put("spring.redis.host", REDIS_CONTAINER.getHost()); properties.put("spring.redis.port", REDIS_CONTAINER.getFirstMappedPort().toString()); } } }
์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- '@Disabled': IntegrationTest ํด๋์ค๋ ์ง์ ์คํ๋์ง ์๊ณ , ๋ค๋ฅธ ํ ์คํธ ํด๋์ค์์ ์์ํ์ฌ ์ฌ์ฉ๋ฉ๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ๋ฅผ ๋นํ์ฑํํ๋ ์ด๋ ธํ ์ด์ ์ ๋๋ค.
- '@ContextConfiguration': ์คํ๋ง ์ดํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๋ฅผ ์ค์ ํ๋ ์ด๋ ธํ ์ด์ ์ ๋๋ค. IntegrationTestInitializer ํด๋์ค์์ ์ ์ํ ์ด๊ธฐํ ์์ ์ ์ํํฉ๋๋ค.
- 'IntegrationTestInitializer': ApplicationContesxtInitializer ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ๋ด๋ถ ํด๋์ค๋ก, ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
Docker Compose๋ฅผ ์ฌ์ฉํ ํตํฉ ํ ์คํธ ์ค์
๋ง์ฝ Docker Compose๋ฅผ ์ฌ์ฉํ์ฌ DB๋ฅผ ์ธํ ํ๊ณ ์ถ๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ ์ ์์ต๋๋ค.
IntegrationTest.java
@Disabled @SpringBootTest @Transactional @ContextConfiguration(initializers = IntegrationTest.IntegrationTestInitializer.class) public class IntegrationTest { // Docker Compose์ Kafka ์ปจํ
์ด๋ ์ธ์คํด์ค ์ ์ private static final DockerComposeContainer<?> DOCKER_COMPOSE = new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml")) .withExposedService("mysql", 3306, Wait.forLogMessage(".*ready for connections.*", 1)) .withExposedService("redis", 6379, Wait.forLogMessage(".*Ready to accept connections.*", 1)); private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0")); @BeforeAll public static void setupContainers() { DOCKER_COMPOSE.start(); KAFKA_CONTAINER.start(); } static class IntegrationTestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(@NotNull ConfigurableApplicationContext applicationContext) { Map<String, String> properties = new HashMap<>(); setKafkaProperties(properties); setDatabaseProperties(properties); setRedisProperties(properties); TestPropertyValues.of(properties).applyTo(applicationContext); } private void setKafkaProperties(Map<String, String> properties) { properties.put("spring.kafka.bootstrap-servers", KAFKA_CONTAINER.getBootstrapServers()); } private void setDatabaseProperties(Map<String, String> properties) { String rdbmsHost = DOCKER_COMPOSE.getServiceHost("mysql", 3306); int rdbmsPort = DOCKER_COMPOSE.getServicePort("mysql", 3306); properties.put("spring.datasource.url", "jdbc:mysql://" + rdbmsHost + ":" + rdbmsPort + "/container"); properties.put("spring.datasource.username", "root"); properties.put("spring.datasource.password", "password"); } private void setRedisProperties(Map<String, String> properties) { String redisHost = DOCKER_COMPOSE.getServiceHost("redis", 6379); Integer redisPort = DOCKER_COMPOSE.getServicePort("redis", 6379); properties.put("spring.redis.host", redisHost); properties.put("spring.redis.port", redisPort.toString()); } } }
ํ ์คํธ ํ๊ฒฝ์์๋ ์ฌ๋ฌ ์๋น์ค์ ์ปจํ ์ด๋๋ฅผ ๋์์ ์คํํ ๋ Docker Compose๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ํจ์จ์ ์ ๋๋ค. ๊ทธ๋ฌ๋ ์ปจํ ์ด๋ ๊ฐ์ ์ด๊ธฐํ ์๊ฐ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์ปจํ ์ด๋๊ฐ ์ ์์ ์ผ๋ก ์์๋ ๋๊น์ง ๋๊ธฐํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค. ์ด๋ฅผ ์ํด 'Wait'๊ตฌ๋ฌธ์ ํ์ฉํฉ๋๋ค.
private static final DockerComposeContainer<?> DOCKER_COMPOSE = new DockerComposeContainer<>(new File("src/test/resources/docker-compose.yml")) .withExposedService("mysql", 3306, Wait.forLogMessage(".*ready for connections.*", 1)) .withExposedService("redis", 6379, Wait.forLogMessage(".*Ready to accept connections.*", 1));
Docker-compose.yml
version: "3.8" services: mysql: image: mysql:8.0 environment: MYSQL_DATABASE: container MYSQL_ROOT_PASSWORD: password ports: - 3306 # Tip: ํ
์คํธ ํ๊ฒฝ์์๋ ํธ์คํธ ํฌํธ ๋งคํ(์: 3306:3306)์ ์๋ตํ๋ฉด ํฌํธ ์ถฉ๋์ ํผํ ์ ์์ต๋๋ค. redis: image: redis:6 ports: - 6379 # Tip: ํ
์คํธ ํ๊ฒฝ์์๋ ํธ์คํธ ํฌํธ ๋งคํ(์: 6379:6379)์ ์๋ตํ๋ฉด ํฌํธ ์ถฉ๋์ ํผํ ์ ์์ต๋๋ค.
๋ํ ํ
์คํธ์ฉ docker-compose ์ค์ ์์๋ ํธ์คํธ์ ์ปจํ
์ด๋์ ํฌํธ ๋งคํ์ ์๋ตํ์ฌ ํฌํธ ์ถฉ๋์ ์ํ์ ์ต์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
ํ ์คํธ
์๋๋ IntefrationTest ํด๋์ค๋ฅผ ์์๋ฐ์ MySQL, Redis, Kafka ์ฐ๋์ ํ ์คํธ ํ ์ฝ๋์ ๋๋ค.
class DatabaseTest extends IntegrationTest { @Autowired private UserRepository userRepository; @Autowired private EntityManager entityManager; @Test void saveAndRetrieveUserTest() { // given User user = new User(null, "dkswnkk"); userRepository.save(user); // when entityManager.flush(); entityManager.clear(); var retrievedUser = userRepository.findById(1L); // then Assertions.assertEquals("dkswnkk", retrievedUser.get().getName()); } }
public class KafkaConsumerApplicationTests extends IntegrationTest { @Autowired private KafkaProducerService kafkaProducerService; // ์ค์ KafkaConsumerService ๋์ mock ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค. // mock ๊ฐ์ฒด๋ ์ค์ ๋ก์ง์ ์คํํ์ง ์์ผ๋ฉฐ, ํธ์ถ ์ฌ๋ถ๋ ์ธ์ ๊ฐ ๋ฑ์ ํ์ธํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. @MockBean private KafkaConsumerService kafkaConsumerService; // ์นดํ์นด ๋ฉ์์ง ์ ์ก ๋ฐ ์์ ํ
์คํธ @Test public void kafkaSendAndConsumeTest() { String topic = "test-topic"; String expectValue = "expect-value"; kafkaProducerService.send(topic, expectValue); // ArgumentCaptor: mock ๊ฐ์ฒด์ ์ ๋ฌ๋ ์ธ์๋ฅผ ์บก์ฒํด ํ์ ๊ทธ ๊ฐ์ ๊ฒ์ฆํ๊ธฐ ์ํด ์ฌ์ฉํฉ๋๋ค. var stringCaptor = ArgumentCaptor.forClass(String.class); // kafkaConsumerService.process ๋ฉ์๋๊ฐ ํน์ ์๊ฐ ๋ด์ ํ ๋ฒ ํธ์ถ๋์๋์ง ํ์ธํฉ๋๋ค. // ๋ํ, ๊ทธ ๋ ์ด๋ค ์ธ์๋ก ํธ์ถ๋์๋์ง stringCaptor๋ฅผ ํตํด ์บก์ฒํฉ๋๋ค. Mockito.verify(kafkaConsumerService, Mockito.timeout(5000).times(1)) .process(stringCaptor.capture()); // ์บก์ฒ๋ ์ธ์์ ๊ธฐ๋๊ฐ์ด ์ผ์นํ๋์ง ๊ฒ์ฆํฉ๋๋ค. Assertions.assertEquals(expectValue, stringCaptor.getValue()); } }
public class RedisTest extends IntegrationTest { @Autowired private RedisService redisService; @Test @DisplayName("Redis Get / Set ํ
์คํธ") public void redisGetSetTest() { // given String key = "name"; String expectedValue = "dkswnkk"; // when redisService.set(key, expectedValue); // then String actualValue = redisService.get(key); Assertions.assertEquals(expectedValue, actualValue); } }
@Configuration public class KafkaConsumerApplication { @Autowired private KafkaConsumerService kafkaConsumerService; // ์นดํ์นด ํ ํฝ ์ค์ @Bean public NewTopic topic() { return TopicBuilder.name("test-topic").build(); } // ์นดํ์นด ๋ฆฌ์ค๋: ๋ฉ์์ง๋ฅผ ๋ฐ์ผ๋ฉด ์ฒ๋ฆฌ ์๋น์ค๋ฅผ ํธ์ถ @KafkaListener(id = "test-id", topics = "test-topic") public void listen(String message) { kafkaConsumerService.process(message); } }
@Service public class KafkaConsumerService { // ๋ฉ์์ง ์ฒ๋ฆฌ: ํ์ฌ๋ ์ฝ์์ ์ถ๋ ฅ public void process(String message) { System.out.println("processing ... " + message); } }
@Service public class KafkaProducerService { @Autowired private KafkaTemplate<String, String> kafkaTemplate; // ์ฃผ์ด์ง ํ ํฝ์ผ๋ก ๋ฉ์์ง ์ ์ก public void send(String topic, String message) { kafkaTemplate.send(topic, message); } }
@Service public class RedisService { @Autowired private RedisTemplate<String, String> redisTemplate; public String get(String key) { return redisTemplate.opsForValue().get(key); } public void set(String key, String value) { redisTemplate.opsForValue().set(key, value); } }

๊ฒฐ๋ก
ํ ์คํธ ํ๊ฒฝ์์ DB์ ๊ฐ์ ์ธ๋ถ ์์กด์ฑ์ ๊ด๋ฆฌํ๋ ๊ฒ์ ๋ณต์กํ ์ ์์ต๋๋ค. TestContainer๋ฅผ ํ์ฉํ๋ฉด ์ด๋ฌํ ๋ณต์ก์ฑ์ ํฌ๊ฒ ์ค์ด๊ณ , ๊ฒฉ๋ฆฌ๋ ํ๊ฒฝ์์ ํ ์คํธ๋ฅผ ์ํํ ์ ์์ต๋๋ค. ํนํ, CI/CD ํ์ดํ๋ผ์ธ๊ณผ ๊ฐ์ด ์๋ํ๋ ํ๊ฒฝ์์๋ TestContainer๋ฅผ ํ์ฉํ์ฌ ๋์ฑ ์์ ์ ์ธ ํ ์คํธ๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค.
์ฐธ๊ณ
ํ ์คํธ ๊ด๋ จ ๊ฐ์ด ๋ณด๋ฉด ์ข์ ๊ธ
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
@Scheduled ์ฌ์ฉํ ๋ ์ค๋ ๋ ์ค์ (0) | 2023.11.09 |
---|---|
ArchUnit์ผ๋ก ์ํคํ ์ฒ ๊ฒ์ฌํ๊ธฐ (0) | 2023.10.28 |
LocalStack์ ํ์ฉํ AWS S3 ํ ์คํธ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ (2) | 2023.10.22 |
Spring Data Redis์ @Indexed ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2023.08.07 |
WebClient์์ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ํ๋ ๋ฐฉ๋ฒ (0) | 2023.08.03 |
Spring Batch๋? ๊ฐ๋จํ ๊ฐ๋ ๊ณผ ์ฝ๋ ์ดํด๋ณด๊ธฐ (0) | 2023.07.29 |
๋๊ธ