EDN+ 서버 재개발기 — Spring MVC로 다시 짓고, 당근마켓에 광고를 서빙하기까지
Gmarket의 외부 광고 네트워크 EDN+ 서버를 Node.js 레거시에서 Spring MVC(서블릿 컨테이너) 기반으로 재개발하고, 당근마켓·다나와 광고 지면에 상품+광고 데이터를 통합 서빙한 과정을 공유합니다.
참고: 이 글의 코드는 실제 업무에서 경험한 내용을 바탕으로 개념적으로 재작성한 것입니다. 실제 회사 코드와는 무관합니다.
EDN+란 무엇인가
EDN+(Ebay Display Network AD)는 Gmarket의 외부 디스플레이 광고 네트워크입니다. Google GDN, Criteo처럼 Gmarket CPC 광고를 당근마켓·다나와 같은 외부 매체에 노출시키는 상품입니다.
EDN+ API는 외부 매체가 광고 요청을 보내오면, 광고 데이터와 상품 데이터를 조합해 응답을 반환합니다. 당근마켓 앱에 보이는 배너 광고가 이 API를 통해 서빙됩니다.
왜 재개발했나
기존 EDN+ 서버는 Node.js로 구성되어 있었습니다. AdTech팀이 Spring 기반으로 기술 스택을 통일하는 흐름에서 EDN+도 같이 이관하게 되었습니다.
재개발 방향은 심플했습니다. 기존 Node.js 서버가 하던 동작을 Spring MVC(서블릿 컨테이너)로 그대로 래핑하되, 외부 매체 서빙 API의 성능을 개선하는 것이었습니다.
시스템 구조
광고 서빙 로직: CPC 고정, Redis에서 조회
EDN+가 서빙하는 광고는 CPC 광고로 고정되어 있었습니다. 복잡한 타겟팅 선택 엔진이 필요하지 않았고, 이미 선정된 광고 데이터를 Redis에서 조회해 반환하는 구조였습니다.
@RestController
@RequestMapping("/api/v1/edn")
public class EdnAdController {
@GetMapping("/ads")
public AdResponse getAd(
@RequestParam String placementCode,
@RequestParam(required = false) String userId
) {
// Redis에서 CPC 광고 데이터 조회
AdData ad = adRedisRepository.findByPlacement(placementCode);
if (ad == null) {
return AdResponse.empty();
}
// 상품 + 미니샵 데이터 조합
ProductData product = productService.getProductData(ad.getProductId());
return AdResponse.of(ad, product);
}
}
당근마켓 연동에서 만난 문제: 상품 API 지연
당근마켓은 광고 요청에 대해 최대 200ms 내 응답을 요구했습니다. 광고 데이터는 Redis에서 빠르게 나오는데, 문제는 내부 상품 API였습니다.
당근마켓은 광고 데이터 외에 미니샵(판매자 정보) 데이터도 함께 요구했습니다. 내부 상품 API가 이 데이터를 포함해 응답하는데, p99 기준으로 100ms 이상 걸리는 경우가 빈번했습니다. 200ms 안에 응답하려면 EDN+ 자체 처리 시간을 포함해 상품 API는 최대 60ms 안에 끝나야 했습니다.
해결 1: 상품 API 타임아웃 제한
상품 API 호출에 60ms 타임아웃을 설정했습니다.
@Service
public class ProductService {
private static final Duration PRODUCT_API_TIMEOUT = Duration.ofMillis(60);
public ProductData getProductData(String productId) {
try {
return productApiClient.get()
.uri("/products/{id}", productId)
.retrieve()
.bodyToMono(ProductData.class)
.timeout(PRODUCT_API_TIMEOUT)
.block();
} catch (TimeoutException e) {
log.warn("Product API timeout for productId: {}", productId);
return ProductData.empty(); // 타임아웃 시 빈 데이터 반환
}
}
}
해결 2: 로컬 캐시로 정적 데이터 성능 개선
미니샵 정보나 기본 메타 정보는 자주 변경되지 않습니다. 이런 데이터를 매 요청마다 내부 API로 조회하는 것은 낭비입니다.
Caffeine 기반 로컬 인메모리 캐시를 적용했습니다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES) // 5분 TTL
.maximumSize(1000)
);
return manager;
}
}
@Service
public class MiniShopService {
@Cacheable(value = "miniShop", key = "#shopId")
public MiniShopData getMiniShopInfo(String shopId) {
return miniShopApiClient.get(shopId); // 캐시 미스 시에만 API 호출
}
}
결과
- Node.js 레거시 → Spring MVC 신규 서버 전환 완료
- 당근마켓·다나와 광고 지면 통합 서빙 API 안정적 운영
- 로컬 캐시 적용으로 내부 API 의존 감소, 응답 시간 안정화
마치며
이 프로젝트에서 배운 핵심은 외부 파트너 SLA를 역산해 내부 의존성의 타임아웃을 설계하는 것이었습니다. 당근마켓이 요구하는 200ms에서 시작해서, 내 서버 처리 시간을 빼면 상품 API에 허용 가능한 시간이 자연스럽게 정해집니다. 그 시간 안에 내부 시스템이 응답하지 못한다면, 로컬 캐시나 타임아웃 처리로 보완하는 것이 현실적인 접근입니다.