diff --git a/backend/build.gradle b/backend/build.gradle index 74eaefe5f..1658c375f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -55,11 +55,15 @@ dependencies { implementation 'org.flywaydb:flyway-mysql' implementation 'org.flywaydb:flyway-core' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.testcontainers:testcontainers-bom:1.20.2' runtimeOnly 'com.mysql:mysql-connector-j:8.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:testcontainers' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'com.h2database:h2' diff --git a/backend/docker-compose-local.yml b/backend/docker-compose-local.yml index c7f0c4d2a..e372a8f27 100644 --- a/backend/docker-compose-local.yml +++ b/backend/docker-compose-local.yml @@ -10,3 +10,17 @@ services: MYSQL_ROOT_PASSWORD: 1234 MYSQL_DATABASE: ody TZ: Asia/seoul + + redis: + image: redis:7.4.1-alpine3.20 + container_name: redis-local + restart: always + ports: + - "6379:6379" + command: > + redis-server + --save "" + --appendonly yes + --auto-aof-rewrite-percentage 0 + environment: + TZ: Asia/Seoul diff --git a/backend/src/main/java/com/ody/common/config/RedisConfig.java b/backend/src/main/java/com/ody/common/config/RedisConfig.java new file mode 100644 index 000000000..4a37ef286 --- /dev/null +++ b/backend/src/main/java/com/ody/common/config/RedisConfig.java @@ -0,0 +1,68 @@ +package com.ody.common.config; + +import java.time.Duration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Profile({"local", "dev", "prod"}) +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(3)) + .build(); + + return new LettuceConnectionFactory(redisStandaloneConfiguration(), clientConfig); + } + + @Bean + public RedisStandaloneConfiguration redisStandaloneConfiguration() { + return new RedisStandaloneConfiguration(); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); + return redisTemplate; + } + + @Bean + public RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer() + ) + ) + .entryTtl(Duration.ofHours(1L)); + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory cf) { + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration()) + .build(); + } +} diff --git a/backend/src/main/java/com/ody/route/domain/RouteClientKey.java b/backend/src/main/java/com/ody/route/domain/RouteClientKey.java new file mode 100644 index 000000000..2f4ced955 --- /dev/null +++ b/backend/src/main/java/com/ody/route/domain/RouteClientKey.java @@ -0,0 +1,22 @@ +package com.ody.route.domain; + +import com.ody.route.service.RouteClient; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum RouteClientKey { + + FAIL_KEY("route:fail:%s"), + BLOCK_KEY("route:block:%s"), + ; + + private final String value; + + public static String getFailKey(RouteClient routeClient) { + return String.format(FAIL_KEY.value, routeClient.getClass().getSimpleName()); + } + + public static String getBlockKey(RouteClient routeClient) { + return String.format(BLOCK_KEY.value, routeClient.getClass().getSimpleName()); + } +} diff --git a/backend/src/main/java/com/ody/route/repository/RouteClientRedisTemplate.java b/backend/src/main/java/com/ody/route/repository/RouteClientRedisTemplate.java new file mode 100644 index 000000000..31fd900cd --- /dev/null +++ b/backend/src/main/java/com/ody/route/repository/RouteClientRedisTemplate.java @@ -0,0 +1,36 @@ +package com.ody.route.repository; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RouteClientRedisTemplate extends RedisTemplate { + + @Autowired + public RouteClientRedisTemplate(RedisConnectionFactory connectionFactory) { + this.setKeySerializer(RedisSerializer.string()); + this.setValueSerializer(RedisSerializer.string()); + this.setHashKeySerializer(RedisSerializer.string()); + this.setHashValueSerializer(RedisSerializer.string()); + this.setConnectionFactory(connectionFactory); + this.afterPropertiesSet(); + } + + public int increment(String key) { + return Optional.ofNullable(opsForValue().increment(key)) + .map(Long::intValue) + .orElse(0); + } + + public int getKeyCount(String key) { + return Optional.ofNullable(opsForValue().get(key)) + .map(Integer::parseInt) + .orElse(0); + } +} diff --git a/backend/src/main/java/com/ody/route/service/RouteClientCircuitBreaker.java b/backend/src/main/java/com/ody/route/service/RouteClientCircuitBreaker.java new file mode 100644 index 000000000..d9387f97a --- /dev/null +++ b/backend/src/main/java/com/ody/route/service/RouteClientCircuitBreaker.java @@ -0,0 +1,56 @@ +package com.ody.route.service; + +import com.ody.route.repository.RouteClientRedisTemplate; +import com.ody.route.domain.RouteClientKey; +import java.time.Duration; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RouteClientCircuitBreaker { + + private static final int MAX_FAIL_COUNT = 3; + private static final String BLOCK = "1"; + public static final Duration FAIL_MINUTES_TTL = Duration.ofMinutes(31); // 지연 시간 고려해 31분으로 설정 + public static final Duration BLOCK_HOUR_TTL = Duration.ofHours(3); + + private final RouteClientRedisTemplate redisTemplate; + + public void recordFailCountInMinutes(RouteClient routeClient) { + String failClientKey = RouteClientKey.getFailKey(routeClient); + int failCount = redisTemplate.increment(failClientKey); + redisTemplate.expire(failClientKey, FAIL_MINUTES_TTL); + log.warn("{} 요청 실패 횟수 : {}", failClientKey, failCount); + } + + public void determineBlock(RouteClient routeClient) { + String failClientKey = RouteClientKey.getFailKey(routeClient); + String blockKey = RouteClientKey.getBlockKey(routeClient); + if (exceedFailCount(failClientKey)) { + block(blockKey); + clearFailCount(failClientKey); + } + } + + private boolean exceedFailCount(String failCountKey) { + return redisTemplate.getKeyCount(failCountKey) >= MAX_FAIL_COUNT; + } + + private void block(String blockKey) { + redisTemplate.opsForValue().set(blockKey, BLOCK); + redisTemplate.expire(blockKey, BLOCK_HOUR_TTL); + log.warn("{}가 차단되었습니다. 해제 예정 시간 : {}", blockKey, LocalDateTime.now().plus(BLOCK_HOUR_TTL)); + } + + private void clearFailCount(String failCountKey) { + redisTemplate.unlink(failCountKey); + } + + public boolean isBlocked(RouteClient routeClient) { + return Boolean.TRUE.equals(redisTemplate.hasKey(RouteClientKey.getBlockKey(routeClient))); + } +} diff --git a/backend/src/main/java/com/ody/route/service/RouteClientManager.java b/backend/src/main/java/com/ody/route/service/RouteClientManager.java new file mode 100644 index 000000000..2a6753eb8 --- /dev/null +++ b/backend/src/main/java/com/ody/route/service/RouteClientManager.java @@ -0,0 +1,32 @@ +package com.ody.route.service; + +import com.ody.common.exception.OdyServerErrorException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RouteClientManager { + + private final List routeClients; + private final RouteClientCircuitBreaker routeClientCircuitBreaker; + + public List getAvailableClients() { + List availableClients = routeClients.stream() + .filter(this::isAvailable) + .toList(); + + if (availableClients.isEmpty()) { + log.error("모든 RouteClient 차단되었습니다."); + throw new OdyServerErrorException("서버에 장애가 발생했습니다."); + } + return availableClients; + } + + private boolean isAvailable(RouteClient routeClient) { + return !routeClientCircuitBreaker.isBlocked(routeClient); + } +} diff --git a/backend/src/main/java/com/ody/route/service/RouteService.java b/backend/src/main/java/com/ody/route/service/RouteService.java index d643cd5df..0e05637e5 100644 --- a/backend/src/main/java/com/ody/route/service/RouteService.java +++ b/backend/src/main/java/com/ody/route/service/RouteService.java @@ -15,38 +15,41 @@ public class RouteService { private static final long CLOSEST_LOCATION_DURATION = 10L; - private final List routeClients; + private final RouteClientManager routeClientManager; private final ApiCallService apiCallService; + private final RouteClientCircuitBreaker routeClientCircuitBreaker; public RouteTime calculateRouteTime(Coordinates origin, Coordinates target) { - for (RouteClient client : routeClients) { - if (isDisabled(client)) { - log.info("{} API 사용이 비활성화되어 건너뜁니다.", client.getClass().getSimpleName()); + List availableClients = routeClientManager.getAvailableClients(); + for (RouteClient routeClient : availableClients) { + if (isDisabled(routeClient)) { + log.info("{} API 사용이 차단되어 건너뜁니다.", routeClient.getClass().getSimpleName()); continue; } - try { - RouteTime routeTime = calculateTime(client, origin, target); - apiCallService.increaseCountByClientType(client.getClientType()); - log.info("{}를 사용한 소요 시간 계산 성공", client.getClass().getSimpleName()); + RouteTime routeTime = calculateTime(routeClient, origin, target); + apiCallService.increaseCountByClientType(routeClient.getClientType()); + log.info("{} API를 사용한 소요 시간 계산 성공", routeClient.getClass().getSimpleName()); return routeTime; - } catch (Exception exception) { - log.warn("Route Client 에러 : {} ", client.getClass().getSimpleName(), exception); + } catch (OdyServerErrorException exception) { + log.warn("{} API 에러 발생 : ", routeClient.getClass().getSimpleName(), exception); + routeClientCircuitBreaker.recordFailCountInMinutes(routeClient); + routeClientCircuitBreaker.determineBlock(routeClient); } } - log.error("모든 소요시간 계산 API 사용 불가"); + log.error("모든 RouteClient API 사용 불가"); throw new OdyServerErrorException("서버에 장애가 발생했습니다."); } - private RouteTime calculateTime(RouteClient client, Coordinates origin, Coordinates target) { - RouteTime calculatedRouteTime = client.calculateRouteTime(origin, target); + private boolean isDisabled(RouteClient routeClient) { + return Boolean.FALSE.equals(apiCallService.getEnabledByClientType(routeClient.getClientType())); + } + + private RouteTime calculateTime(RouteClient routeClient, Coordinates origin, Coordinates target) { + RouteTime calculatedRouteTime = routeClient.calculateRouteTime(origin, target); if (calculatedRouteTime.equals(RouteTime.CLOSEST_EXCEPTION_TIME)) { return new RouteTime(CLOSEST_LOCATION_DURATION); } return calculatedRouteTime; } - - private boolean isDisabled(RouteClient client) { - return Boolean.FALSE.equals(apiCallService.getEnabledByClientType(client.getClientType())); - } } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index e5f927fae..37d7f9038 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -1,4 +1,7 @@ spring: + data: + redis: + host: ody-redis-001.ody-redis.vd1e5e.apn2.cache.amazonaws.com jpa: hibernate: ddl-auto: validate diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index c5e37b35e..aad1128a9 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -1,4 +1,7 @@ spring: + data: + redis: + host: localhost datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:53306/ody diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 0b78f18c1..3c2f84198 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -1,4 +1,7 @@ spring: + data: + redis: + host: ody-redis-001.ody-redis.vd1e5e.apn2.cache.amazonaws.com jpa: hibernate: ddl-auto: validate diff --git a/backend/src/main/resources/common.yml b/backend/src/main/resources/common.yml index ecffabcfb..5282d6bfc 100644 --- a/backend/src/main/resources/common.yml +++ b/backend/src/main/resources/common.yml @@ -2,6 +2,9 @@ server: shutdown: graceful spring: + data: + redis: + port: 6379 jpa: open-in-view: false defer-datasource-initialization: false diff --git a/backend/src/test/java/com/ody/common/BaseRedisTest.java b/backend/src/test/java/com/ody/common/BaseRedisTest.java new file mode 100644 index 000000000..7a61944c1 --- /dev/null +++ b/backend/src/test/java/com/ody/common/BaseRedisTest.java @@ -0,0 +1,33 @@ +package com.ody.common; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.ody.route.repository.RouteClientRedisTemplate; +import com.ody.notification.config.FcmConfig; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +@Import({RedisTestContainersConfig.class, TestRouteConfig.class}) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +public abstract class BaseRedisTest { + + @Autowired + protected RouteClientRedisTemplate redisTemplate; + + @MockBean + private FcmConfig fcmConfig; + + @MockBean + private FirebaseMessaging firebaseMessaging; + + @BeforeEach + void init() { + redisTemplate.getConnectionFactory() + .getConnection() + .serverCommands() + .flushAll(); + } +} diff --git a/backend/src/test/java/com/ody/common/BaseServiceTest.java b/backend/src/test/java/com/ody/common/BaseServiceTest.java index fc7665fce..eac6d53f0 100644 --- a/backend/src/test/java/com/ody/common/BaseServiceTest.java +++ b/backend/src/test/java/com/ody/common/BaseServiceTest.java @@ -3,6 +3,7 @@ import com.google.firebase.messaging.FirebaseMessaging; import com.ody.notification.config.FcmConfig; import com.ody.notification.service.FcmSubscriber; +import com.ody.route.service.RouteClientCircuitBreaker; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -22,6 +23,9 @@ public abstract class BaseServiceTest { @MockBean protected FirebaseMessaging firebaseMessaging; + @MockBean + protected RouteClientCircuitBreaker routeClientCircuitBreaker; + @MockBean protected FcmSubscriber fcmSubscriber; diff --git a/backend/src/test/java/com/ody/common/RedisTestContainersConfig.java b/backend/src/test/java/com/ody/common/RedisTestContainersConfig.java new file mode 100644 index 000000000..b1c4f64fe --- /dev/null +++ b/backend/src/test/java/com/ody/common/RedisTestContainersConfig.java @@ -0,0 +1,21 @@ +package com.ody.common; + +import org.springframework.boot.test.context.TestConfiguration; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@TestConfiguration +public class RedisTestContainersConfig { + + private static final int REDIS_PORT = 6379; + + @Container + private static final GenericContainer redisContainer = new GenericContainer<>("redis:7.4.1-alpine3.20") + .withExposedPorts(REDIS_PORT); + + static { + redisContainer.start(); + } +} diff --git a/backend/src/test/java/com/ody/route/repository/RouteClientRedisTemplateTest.java b/backend/src/test/java/com/ody/route/repository/RouteClientRedisTemplateTest.java new file mode 100644 index 000000000..70f31ff64 --- /dev/null +++ b/backend/src/test/java/com/ody/route/repository/RouteClientRedisTemplateTest.java @@ -0,0 +1,22 @@ +package com.ody.route.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ody.common.BaseRedisTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RouteClientRedisTemplateTest extends BaseRedisTest { + + private static final String TEST_KEY = "test"; + + @DisplayName("key의 counter를 증가시킨다.") + @Test + void increment() { + redisTemplate.increment(TEST_KEY); + + int keyCount = redisTemplate.getKeyCount(TEST_KEY); + + assertThat(keyCount).isEqualTo(1); + } +} diff --git a/backend/src/test/java/com/ody/route/service/RouteClientCircuitBreakerTest.java b/backend/src/test/java/com/ody/route/service/RouteClientCircuitBreakerTest.java new file mode 100644 index 000000000..408e3f95a --- /dev/null +++ b/backend/src/test/java/com/ody/route/service/RouteClientCircuitBreakerTest.java @@ -0,0 +1,68 @@ +package com.ody.route.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.ody.common.BaseRedisTest; +import com.ody.route.domain.RouteClientKey; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.mock.mockito.MockBean; + +class RouteClientCircuitBreakerTest extends BaseRedisTest { + + @Autowired + private RouteClientCircuitBreaker circuitBreaker; + + @MockBean + @Qualifier("odsay") + private RouteClient routeClient; + + @DisplayName("실패 횟수를 기록하고 TTL을 31분으로 설정한다.") + @Test + void recordFailCountInMinutes() { + circuitBreaker.recordFailCountInMinutes(routeClient); + + String failClientKey = RouteClientKey.getFailKey(routeClient); + int failureCount = redisTemplate.getKeyCount(failClientKey); + Long ttlMinutes = redisTemplate.getExpire(failClientKey, TimeUnit.MINUTES); + + // 지연 시간 때문에 TTL을 범위로 테스트 + assertAll( + () -> assertThat(failureCount).isEqualTo(1), + () -> assertThat(ttlMinutes).isGreaterThanOrEqualTo(30).isLessThanOrEqualTo(31) + ); + } + + @DisplayName("실패 횟수가 3회 이상이면 block을 결정하고 실패 횟수를 초기화한다.") + @Test + void determineBlock() { + circuitBreaker.recordFailCountInMinutes(routeClient); + circuitBreaker.recordFailCountInMinutes(routeClient); + circuitBreaker.recordFailCountInMinutes(routeClient); + circuitBreaker.determineBlock(routeClient); + + String failClientKey = RouteClientKey.getFailKey(routeClient); + int failCount = redisTemplate.getKeyCount(failClientKey); + Boolean blocked = redisTemplate.hasKey(RouteClientKey.getBlockKey(routeClient)); + + assertAll( + () -> assertThat(failCount).isZero(), + () -> assertThat(blocked).isTrue() + ); + } + + @DisplayName("RouteClient에 해당하는 block 키가 존재하면 true를 반환한다.") + @Test + void isBlocked() { + String blockKey = RouteClientKey.getBlockKey(routeClient); + redisTemplate.opsForValue().set(blockKey, "1"); + + boolean blocked = circuitBreaker.isBlocked(routeClient); + + assertThat(blocked).isTrue(); + } +} diff --git a/backend/src/test/java/com/ody/route/service/RouteClientManagerTest.java b/backend/src/test/java/com/ody/route/service/RouteClientManagerTest.java new file mode 100644 index 000000000..bcb75a39c --- /dev/null +++ b/backend/src/test/java/com/ody/route/service/RouteClientManagerTest.java @@ -0,0 +1,57 @@ +package com.ody.route.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.ody.common.BaseServiceTest; +import com.ody.common.exception.OdyServerErrorException; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.mock.mockito.MockBean; + +class RouteClientManagerTest extends BaseServiceTest { + + @Autowired + private RouteClientManager routeClientManager; + + @MockBean + @Qualifier("odsay") + private RouteClient odsayRouteClient; + + @MockBean + @Qualifier("google") + private RouteClient googleRouteClient; + + @DisplayName("이용 가능한 RouteClient를 반환한다.") + @Test + void getAvailableClients() { + Mockito.when(routeClientCircuitBreaker.isBlocked(odsayRouteClient)) + .thenReturn(true); + + Mockito.when(routeClientCircuitBreaker.isBlocked(googleRouteClient)) + .thenReturn(false); + + List availableClients = routeClientManager.getAvailableClients(); + + assertThat(availableClients) + .hasSize(1) + .containsExactly(googleRouteClient); + } + + @DisplayName("이용 가능한 RouteClient가 없으면 예외가 발생한다.") + @Test + void FailWhenGetAvailableClientsBy() { + Mockito.when(routeClientCircuitBreaker.isBlocked(odsayRouteClient)) + .thenReturn(true); + + Mockito.when(routeClientCircuitBreaker.isBlocked(googleRouteClient)) + .thenReturn(true); + + assertThatThrownBy(() -> routeClientManager.getAvailableClients()) + .isInstanceOf(OdyServerErrorException.class); + } +} diff --git a/backend/src/test/java/com/ody/route/service/RouteServiceTest.java b/backend/src/test/java/com/ody/route/service/RouteServiceTest.java index ff4204f19..5d7b7fb30 100644 --- a/backend/src/test/java/com/ody/route/service/RouteServiceTest.java +++ b/backend/src/test/java/com/ody/route/service/RouteServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import com.ody.common.BaseServiceTest; +import com.ody.common.exception.OdyServerErrorException; import com.ody.meeting.domain.Coordinates; import com.ody.route.domain.ClientType; import com.ody.route.domain.RouteTime; @@ -98,7 +99,7 @@ void calculateRouteTimeByGoogleRouteClient() { target = new Coordinates("37.515253", "127.102895"); when(odsayRouteClient.calculateRouteTime(origin, target)) - .thenThrow(new RuntimeException("Odsay API 에러 발생")); + .thenThrow(new OdyServerErrorException("Odsay API 에러 발생")); when(googleRouteClient.calculateRouteTime(origin, target)).thenReturn(new RouteTime(18)); long result = routeService.calculateRouteTime(origin, target).getMinutes();