[Spring Boot] 백엔드 설계
이 백엔드는 매일 11시에 최신 구내식당 정보를 크롤링해 캐시로 제공하고, 프런트엔드(React)가 REST API를 통해 손쉽게 받아갈 수 있도록 설계했다. 정적 데이터가 아니라, 매일 갱신되는 외부 페이지를 다루기 때문에 캐싱, 스케줄링, 견고한 파싱이 핵심 포인트다.
1) CORS 설정 – CorsConfig
클라이언트-서버가 다른 도메인에 있을 때를 대비해 CORS를 명시적으로 허용한다.
setAllowedOrigins: 개발 단계에서는http://localhost:3000에서 오는 요청을 허용. (운영 배포 시에는 배포 도메인으로 교체/확장 권장)setAllowedMethods: GET/POST/PUT/DELETE/OPTIONS 등 REST 요청 메서드 허용setAllowedHeaders("*"): 헤더 전체 허용setAllowCredentials(true): 인증 쿠키/헤더를 동반한 요청 허용 (이때allowedOrigins는 ‘*’를 쓰면 안 됨 → 현재 구성 적절)
✅ 운영 팁: 환경별(개발/스테이징/운영)로 허용 오리진을 application-*.yml에 두고 프로파일로 분리하면 안전하다.
2) 최소 보안 설정 – SecurityConfig
데모/내부망 환경을 가정해 csrf를 끄고(csrf.disable()), 모든 요청을 permitAll()로 열어 두었다. 운영 전환 시에는 다음을 고려하자.
- 읽기 API(공개)와 관리 API(비공개)를 분리: 예)
POST /api/cafeterias/crawl은 관리자만 허용 또는 프로파일에 따라 비활성화 - 토큰 기반 인증(JWT) 도입 또는 간단한 Basic Auth라도 적용
3) 컨트롤러 – CafeteriaController
엔드포인트는 단순하고 명확하다.
GET /api/cafeterias: 오늘자 캐시를 반환. 캐시가 비어 있으면 즉시 크롤링 후 채워서 반환.POST /api/cafeterias/crawl: 수동 크롤링 트리거(디버깅/테스트용). 운영에선 인증/권한 체크 또는 프로파일 제한이 안전.
컨트롤러는 오직 서비스 레이어 호출과 응답 변환만 담당해 계층 분리가 깔끔하다.
4) DTO – CafeteriaDto
크롤링 결과를 프런트에 전달하는 데이터 컨테이너.
name: 식당 이름/제목address: 주소 텍스트(추정)price: 식대/영업시간 등 추출 텍스트imageUrl: 대표 이미지 URLpostUrl: 원문 링크
Lombok(@Data, @AllArgsConstructor, @NoArgsConstructor)로 보일러플레이트 제거.
5) 서비스 – CafeteriaService
5-1. 핵심 책임
- 크롤링: 외부 페이지에서 오늘자 구내식당 정보를 파싱
- 캐싱: 같은 날 재호출 시 재크롤링 없이 즉시 반환
- 스케줄링: 매일 11시 자동 갱신
5-2. 시간대 & 날짜 처리
KST(Asia/Seoul) 기준으로 날짜를 계산해서 “M월 d일” 형식 문자열을 만든다. (예: “9월 24일”) 이 값으로 “오늘자 글”을 식별한다.
5-3. 캐시 구조
cached:CopyOnWriteArrayList로 스레드-세이프하게 결과 저장cachedDate: 캐시가 유효한 날짜getTodayMenus(): 같은 날짜 요청이면 캐시 즉시 반환, 아니면 크롤링- 반환 시
Collections.unmodifiableList로 불변 뷰를 제공 → 외부 변경 방지
5-4. 스케줄링
@Scheduled(cron = "0 0 11 * * *", zone = "Asia/Seoul"): 매일 11시 크롤링 후 캐시 갱신- 주의: 프로젝트 어딘가에
@EnableScheduling설정 필요 (예: 메인 애플리케이션 클래스)
5-5. 크롤링 구현 (Selenium + Headless Chrome)
- 드라이버 관리:
WebDriverManager.chromedriver().setup()로 환경에 맞는 드라이버 자동 셋업 - Headless 옵션:
--headless=new,--no-sandbox,--disable-dev-shm-usage로 서버/컨테이너 친화적 실행 - 대상 페이지: 코드 내 상수(
BLOG_LIST_URL)에 정의된 외부 블로그(비공개) 목록 페이지 - 로딩 대기:
WebDriverWait+ CSS 선택자로 목록 로딩을 명시적으로 대기
5-6. “오늘자 글” 판별
목록에서 제목에 오늘 날짜가 포함된 게시글을 찾는다. (실제 문자열 패턴은 내부 구현에 따라 조정) URL과 제목 후보를 수집하고, 원하는 패턴(예: “구내식당 메뉴 + 오늘 날짜”)과 일치하는 글을 선택한다.
5-7. 본문 파싱
- 블록 탐색: 본문 컴포넌트(여러 DIV 블록)를 순회하며 텍스트/이미지를 추출
- 이미지 추출 개선:
data-src,data-lazy-src,data-original→ 마지막엔src순서로 확인. 지도 이미지(map.pstatic.net등)는 제외 - 식당 라인 판별: 텍스트가 특정 키워드(예: “구내식당”, “식대”)를 포함하면 식당 정보 블록으로 간주
- 주소 보정: 다음 블록들 중 주소 패턴(예: “서울특별시”, “경기도”)이 보이면 직전에 생성한 DTO의
address에 보강
5-8. 문자열 파싱 규칙
- 이름: “식대” 키워드 앞부분을 식당명으로 추출
- 가격: 정규식으로 “식대: ####원” 형태를 탐지
- 영업시간: “영업시간: 00:00~00:00” 형태를 별도로 추출, 가격 뒤에 병기
5-9. 예외 처리 & 자원 정리
- 크롤링 실패 시 빈 리스트 반환하고 경고 로그 → API는 캐시(있다면)로 안전하게 응답
- 드라이버는
finally에서quit()로 종료
운영 고려사항 & 개선 아이디어
- 엔드포인트 보안:
POST /crawl은 운영에서 비활성화하거나 관리자 인증(토큰/Basic) 필수로 전환 - 환경 분리: CORS 오리진, 크롤링 대상 URL, 크론 스케줄(11시) 등을
application-*.yml속성화 - 안정성: Selenium은 페이지 구조 변경에 취약 → 가능하면 공식 API/정형 HTML로 전환 고려. 불가 시 선택자를 다중 후보로 두고 장애 감지 로깅 강화
- 성능/비용: Headless Chrome은 무겁다. 주기 크롤링을 1회로 제한하고 API는 캐시만 응답하는 지금 구조는 효율적
- 도커/쿠버: 컨테이너에 Chrome & 폰트를 포함하고,
chromedriver버전을 고정(이미지 태그)하면 예측 가능성이 높아짐 - 테스트: 파서 로직은 샘플 HTML 스냅샷으로 단위 테스트를 만들면 회귀를 빠르게 잡을 수 있음
요약
백엔드는 매일 11시 자동 크롤링 → 메모리 캐시 제공 → React가 단순 GET으로 소비하는 흐름으로 설계했다. CORS/보안 기본 설정과 함께, 크롤링 안정성(대기, 다단계 이미지 추출, 주소 보정, 정규식 파싱)을 신경 썼고, 운영 단계로 갈수록 오리진/보안/크롤링 정책을 환경별로 분리해가는 전략을 취한다.
'2025 현대오토에버 모빌리티 SW 스쿨(클라우드 2기) > 미니프로젝트' 카테고리의 다른 글
| [클라우드] GitOps + ArgoCD를 이용한 EC2 배포 과정 (+HA) (0) | 2025.09.30 |
|---|---|
| [클라우드] CI/CD 자동화 과정 (0) | 2025.09.30 |
| [React] 프론트엔드 설계 (0) | 2025.09.27 |
| [프로젝트 설계] 주제 선정 및 프로젝트 설계 (0) | 2025.09.27 |