diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index bff7a3b8..9d197bac 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -162,4 +162,4 @@ media:
coupang:
partners:
# 제휴 링크에 lptag로 노출되는 공개 식별자다. 소재 등록 시 남의 링크가 아닌지 대조하는 데 쓴다.
- id: ${COUPANG_PARTNERS_ID}
+ id: ${COUPANG_PARTNERS_ID:}
diff --git a/src/test/java/com/swyp/picke/global/config/AdPropertyDefaultTest.java b/src/test/java/com/swyp/picke/global/config/AdPropertyDefaultTest.java
new file mode 100644
index 00000000..6d43f335
--- /dev/null
+++ b/src/test/java/com/swyp/picke/global/config/AdPropertyDefaultTest.java
@@ -0,0 +1,46 @@
+package com.swyp.picke.global.config;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 광고 환경변수에 기본값이 없으면 값이 빠진 서버에서 앱 전체가 뜨지 않는다.
+ * 광고와 무관한 API까지 같이 죽는다. 실제로 COUPANG_PARTNERS_ID 때문에 dev가 502로 내려간 적이 있다.
+ *
+ *
테스트 설정(application-test.yml)이 값을 박아 두기 때문에 컨텍스트를 띄우는 것만으로는
+ * 이 경우를 재현할 수 없다. 그래서 설정 파일의 플레이스홀더 자체를 검사한다.
+ */
+class AdPropertyDefaultTest {
+
+ private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{(COUPANG_[A-Z0-9_]+|ADPICK_[A-Z0-9_]+)(:[^}]*)?}");
+
+ @Test
+ @DisplayName("광고 환경변수는 모두 기본값을 갖는다. 값이 없어도 앱은 떠야 한다")
+ void adPlaceholders_haveDefaults() throws IOException {
+ String yml = new String(new ClassPathResource("application.yml").getInputStream().readAllBytes(),
+ StandardCharsets.UTF_8);
+
+ Matcher matcher = PLACEHOLDER.matcher(yml);
+ List missingDefaults = new ArrayList<>();
+ int found = 0;
+ while (matcher.find()) {
+ found++;
+ if (matcher.group(2) == null) {
+ missingDefaults.add(matcher.group(1));
+ }
+ }
+
+ assertThat(found).isPositive();
+ assertThat(missingDefaults).isEmpty();
+ }
+}
diff --git a/src/test/java/com/swyp/picke/global/config/SwaggerAdGroupTest.java b/src/test/java/com/swyp/picke/global/config/SwaggerAdGroupTest.java
new file mode 100644
index 00000000..4767b884
--- /dev/null
+++ b/src/test/java/com/swyp/picke/global/config/SwaggerAdGroupTest.java
@@ -0,0 +1,48 @@
+package com.swyp.picke.global.config;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * 사용자 그룹은 FE_USED_OPERATIONS 화이트리스트로, 관리자 그룹은 pathsToExclude 로 광고를 걷어낸다.
+ * 둘 중 하나만 손대도 광고 탭이 조용히 비어 버리므로 그룹 구성 자체를 고정해 둔다.
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class SwaggerAdGroupTest {
+
+ private static final String AD_GROUP = "3. 광고 API";
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ @DisplayName("광고 API는 스웨거 그룹 선택 목록에 별도 탭으로 나온다")
+ void swaggerConfig_exposesAdGroup() throws Exception {
+ mockMvc.perform(get("/v3/api-docs/swagger-config"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.urls[?(@.name == '" + AD_GROUP + "')]").exists());
+ }
+
+ @Test
+ @DisplayName("광고 탭에는 앱용 조회·집계와 관리자용 소재 관리가 함께 묶인다")
+ void adGroup_containsAppAndAdminOperations() throws Exception {
+ mockMvc.perform(get("/v3/api-docs/" + AD_GROUP))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.paths['/api/v1/ads'].get").exists())
+ .andExpect(jsonPath("$.paths['/api/v1/ads/impressions'].post").exists())
+ .andExpect(jsonPath("$.paths['/api/v1/admin/ads'].get").exists())
+ .andExpect(jsonPath("$.paths['/api/v1/admin/ads/stats'].get").exists())
+ .andExpect(jsonPath("$.paths['/api/v1/admin/ads/clicks'].get").exists());
+ }
+}