[๊ฐ์]
ํตํฉ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ ๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐ๋์ ์ฃผ์ํ ๊ณ ๋ ค์ฌํญ ์ค ํ๋์ด๋ฉฐ, ํ ์คํธ์ ์์ ์ฑ๊ณผ ์ ๋ขฐ์ฑ์ ๋์ด๊ธฐ ์ํด์๋ ์ค์ ์ด์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๊ฒฝ์์์ ํ ์คํธ๊ฐ ํ์ํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ฌํ ํ๊ฒฝ์ ๊ฐ์ถ๊ธฐ ์ํด์ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์๊ณ , ๊ฐ๊ฐ์ ๋ฐฉ๋ฒ์ ๊ทธ ํน์ฑ๊ณผ ์ฅ๋จ์ ์ด ์์ต๋๋ค. ์ด๋ฒ ๊ฒ์๊ธ์์๋ 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 |
๋ถ์ฐ๋ฝ์ผ๋ก ์ ์ฐฉ์ ์ด๋ฒคํธ ๊ตฌํํ๊ธฐ (3) | 2023.08.31 |
Spring Data Redis์ @Indexed ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2023.08.07 |
WebClient์์ ์๋ฌ ์ฒ๋ฆฌ์ ์ฌ์๋ํ๋ ๋ฐฉ๋ฒ (0) | 2023.08.03 |
๋๊ธ