본문 바로가기
대외활동

2024 관광데이터 활용 공모전 우수상 후기

by davidlee_ 2025. 6. 24.
반응형

시작하며

지난 2024년 5월부터 10월까지 약 5개월 간 진행했던 관광데이터 활용 공모전에 대한 회고를 남겨보고자 한다.

백엔드 개발이 나의 주된 역할이었지만 팀원이 나를 포함하여 3명이었기 때문에 기획 단계부터 최종 출시까지 팀원 모두가 참여하여 마무리했던 공모전이었다. 팀원 모두가 컴퓨터 관련 학과였기에 기획 문서 정리부터 디자인까지 어려움이 많았지만 모두 배우면서 성장할 수 있는 계기가 된 공모전이었던 것 같다. 이 글에서는 다양한 성장과 경험을 얻었던 관광데이터 공모전을 회고하고자 한다.

2024 관광데이터 활용 공모전

공모전 공고문

한국관광공사와 카카오가 주관한 공모전으로 TourAPI, 카카오 OpenAPI 및 각종 공공데이터API를 활용하여 신규/융복합 관광서비스(웹, 앱 등)를 개발하는 공모전이다. 개발 후 출시까지 완료하는 것이 조건이기 때문에 구글과 애플의 어플 심사기간까지 고려하여 개발일정을 계획해야 한다.

공모전 상세일정

개발기간 자체는 나름 여유가 있는 편이다. 예비심사로 제안서의 타당성을 검증하고 제출한 제안서를 바탕으로 개발한 서비스를 평가하여 1차 심사 합격자를 발표한다. 1차 심사에 합격했다면 수상자 명단에 올랐다는 의미이다. 이후 1차 심사 합격자 중 대상과 최우수상 수상자를 결정하기 위해 2차 심사를 진행한 후 합격자들의 한해 발표를 진행한 후 시상식을 진행한다.

아이디에이션

먼저 우리는 TourAPI에서 사용할 수 있는 관광데이터 목록을 살폈다. 관광지 사진, 오디오 가이드, 주변 시설 정보 등 많은 정보를 제공하고 있어서 어떤 아이템이든 녹여낼 수 있다고 생각했다.

우리 팀은 브레인스토밍으로 여러 아이템을 던지면서 회의를 진행했고, 그중 겹치는 지점이 많은 키워드들만 모아서 개발할 아이템으로 발전시켰다. 총 7개의 아이템이 나왔는데, 우리는 확실한 타켓층이 있는 서비스를 만드는 것이 경쟁력이 있을 것이라고 생각했고 최종적으로 드론 취미자를 위한 여행 서비스를 만들자는 결론에 도달하게 되었다.

본격적인 기획

우리는 아이디에이션 한 내용을 바탕으로 Dravel (드론 + 트래블)이라는 서비스를 기획했다. Dravel은 드론 촬영을 즐기는 사용자들을 위한 종합 플랫폼이다. 단순히 드론 비행 정보만 제공하는 것이 아니라, 해당 지역의 공역 및 날씨 정보를 한눈에 확인할 수 있도록 했다. 특히 우리가 주목한 점은 지역 경제와의 연계 가능성이었다. 사용자들이 드론스팟에 방문하며 자연스럽게 주변 맛집을 방문하거나 관광지를 둘러보며 그 지역의 경제 활성화에 기여할 수 있는 기회를 제공하고자 했다.

서비스 구현을 위해 한국관광공사 국문 관광정보 서비스, 국토교통부 항공정보도, 기상청 단기예보 조회서비스 등 세 가지 주요 API를 연동하여 드론스팟 정보 제공, 코스 추천 기능, 드론스팟 정보 공유 기능을 구현하기로 했다.

구체화한 내용을 바탕으로 제안서를 작성하여 제출하였고, 다행히 예비심사에 합격하여 개발을 진행할 수 있게 되었다.

예비심사 합격 메시지

팀 구성과 기술 스택 선택

팀 역할 분담

3명으로 구성된 우리 팀의 주요 역할 분담은 다음과 같았다:

  • 나 (팀장): 백엔드 개발
  • 팀원 1: 백엔드 개발
  • 팀원 2: 프론트엔드 개발

백엔드 개발자가 2명이었기 때문에 API 개발과 데이터 처리 업무를 효율적으로 분담할 수 있었다. 특히 팀원 중 한 분이 디자인까지 담당해 주어서 개발자들만으로 구성된 팀의 약점을 보완할 수 있었다.

백엔드 개발 프레임워크는 FastAPI를 선택하였다. 왜 FastAPI를 선택했는가...라고 묻는다면 이전 교내 해커톤에서 사용해 본 경험이 있었기 때문이다. 당시에 정말 하루 만에 프로토타입을 만들었어야 했는데 백엔드 개발을 해본 적이 없었는데도 쉽고 빠르게 프레임워크를 익히며 진행했던 경험이 있어서 이번에도 써보기로 결정한 것이다.

하지만 그때는 백엔드 개발을 처음 해봤어서 기본적인 원칙도 지키지 못했었는데, 이번에는 restful한 API를 개발하고자 노력했다. 특히 빠르게 개발해야 하는 공모전 프로젝트 특성상 빠른 프로토타이핑과 자동 API 문서 생성 기능은 큰 도움이 되었다.

개발 과정

본격적인 개발에 앞서 개발에 필요한 문서들을 작성하였다. 전에는 주먹구구식으로 개발 도중에 필요하면 작성하거나 생략해버리기도 했는데 그때마다 문서화 작업의 중요성을 느낀 터라 이번엔 제대로 설계하고 시작하고자 하였다.

ERD와 API 명세서 목차

협업 프로세스 적용

이번 프로젝트에서는 협업을 좀 더 체계적으로 진행해보고 싶다는 생각이 들었다. 팀원 중 한 분이 Confluence와 Jira를 통한 협업 경험이 있다고 하여 해당 협업 툴로 프로젝트를 체계화하여 진행했다.

아이디에이션 과정부터 API 명세서, GitHub 컨벤션 등 개발에 필요한 정보들을 Confluence에 문서화하고, 애자일 개발방법론을 적용했다. 매일 밤 10시에 비대면으로 데일리 스크럼을 진행하고, 매주 일요일 오전 10시 회의를 통해 프로덕트 백로그를 작성하고 스프린트 리뷰를 했다. 전까지는 협업을 할 때 매주 하루 날을 잡아서 회의를 하고 메모장에 할 일을 나누는 형태였는데, 데일리 스크럼과 백로그 작성을 통해 협업을 하니 서로의 상황을 빠르게 알 수 있고 문제 상황이 생겼을 때 바로바로 피드백할 수 있어서 편리하고 효율적이라는 생각이 들었다.

진행했던 데일리 스크럼과 스프린트

백엔드 개발

내가 백엔드 개발에 기여한 부분은 크게 4가지 기능이다.

1. 로그인 및 로그아웃 기능

2. 회원가입 및 유저 관리 기능

3. 리뷰 관련 기능

4. 주변 관광 장소 수집 및 드론 스팟 관련 기능

 

 

1.1 로그인 및 로그아웃 기능

인증 시스템은 JWT 기반의 토큰 방식으로 설계했다. 사용자가 로그인을 시도하면, 서버는 데이터베이스에서 해당 ID로 사용자를 조회하고, 비밀번호는 해시값과 비교하여 검증한다.

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

이때 비밀번호 해싱에는 bcrypt 기반의 Pydantic PasswordContext를 활용하고, 추가적인 보안을 위해 salt 값을 더해 관리했다.

로그인에 성공하면 서버는 사용자 정보를 담은 access token과 refresh token을 각각 발급한다.

from jose import jwt

def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

access token의 만료 시점이 지나면 refresh token을 이용해 토큰을 재발급받을 수 있도록 했고, refresh token은 DB에 저장·관리되도록 하였다.

만약 기존의 refresh token이 존재한다면 삭제 후 새로 발급하는 방식으로, 중복 로그인이나 토큰 탈취 상황에도 대응할 수 있게 했다.

로그아웃 기능의 경우, 클라이언트가 로그아웃 요청을 보내면 DB에서 해당 refresh token을 삭제하고, 클라이언트에 저장된 access/refresh 토큰 쿠키도 즉시 만료되도록 하였다.

# 로그아웃 시 refresh token 삭제
db.query(RefreshTokenModel).filter(
    RefreshTokenModel.user_id == user.id
).delete()
db.commit()

 

JWT 토큰 방식에 대해 처음 접해보는 것이라서 각 토큰에 대한 개념, 구현 방법 등 스스로 학습하며 구현하였다. 특히 access token과 refresh token이 왜 각각 존재하는지, 어느 시점에 어떤 방식으로 사용되고 관리되어야 하는지 개념을 정확히 잡는 데 시간이 걸렸다.

 

 

2.1 회원가입 및 유저 관리 기능

회원가입 시 비밀번호는 바로 저장하지 않고, 해싱 과정을 거쳐 안전하게 DB에 저장하도록 하였다.

가입이 완료되면 유저 table을 바로 refresh 해서, 프론트엔드와 API 간 연동이 끊어지지 않게 관리하였다.

프로필 수정 시에는 인증된 사용자만 접근할 수 있도록 접근 권한 관련 로직을 추가했고, 본인이 아닌 경우 접근을 제한하여 개인정보 보호에 유의하였다.

 

 

3.1 리뷰 관련 기능

드론 스팟별로 리뷰를 작성·수정·삭제·조회할 수 있는 리뷰 시스템을 구현했다. 리뷰 작성 시에는 글뿐 아니라 이미지 파일도 첨부할 수 있도록 하였다.
이미지 파일 첨부는 API에서 multipart/form-data 방식으로 구현했으며, 텍스트 데이터(Form)와 이미지 파일(File)을 함께 전송받아 처리하도록 설계하였다.

@router.post("/review/{drone_spot_id}")
async def create_review(
    drone_spot_id: int,
    comment: str = Form(...),
    drone_type: str = Form(...),
    file: Optional[UploadFile] = File(None),
    ...
):
    if file:
        contents = await file.read()
        # 파일 저장 처리

각 리뷰는 작성자, 드론 종류, 비행/카메라 허가여부, 비행 날짜, 코멘트 등 다양한 정보를 담고 있으며, 리뷰 작성 및 수정 시에는 해당 필드만 선택적으로 업데이트할 수 있도록 하였다.

리뷰에는 좋아요(Like) 기능과 신고(Report) 기능도 구현했다.
좋아요 기능은 리뷰별로 누가 눌렀는지 기록하여 중복 방지 및 통계 집계가 가능하도록 했고,
신고 기능은 동일 사용자가 같은 리뷰를 여러 번 신고하지 못하도록 하였다.

# 좋아요 중복 방지
like_exists = db.query(UserReviewLikeModel).filter(
    UserReviewLikeModel.user_uid == user_db.uid,
    UserReviewLikeModel.review_id == review_id
).first()
if like_exists:
    raise HTTPException(status_code=400, detail="이미 해당 리뷰에 좋아요를 눌렀습니다.")

# 신고 중복 방지
review_report = db.query(ReviewReportModel).filter(
    ReviewReportModel.review_id == review_id,
    ReviewReportModel.user_uid == uid
).first()
if review_report:
    raise HTTPException(status_code=400, detail="이미 신고한 리뷰입니다.")

 

이외에도 랜덤/최신/좋아요순 리뷰 리스트, 유저별/스팟별 리뷰 리스트 등 다양한 조회 기능을 제공하여, 사용자들이 드론 스팟을 효율적으로 찾을 수 있도록 제공했다.

# 예시: 좋아요순, 최신순 정렬
if order == 1:
    db_review = db_review.outerjoin(UserReviewLikeModel).group_by(ReviewModel.id).order_by(func.count(UserReviewLikeModel.review_id).desc())
else:
    db_review = db_review.order_by(ReviewModel.flight_date.desc())

 

 

4.1 주변 장소 및 드론 스팟 관련 기능

사용자가 새로운 드론스팟을 등록하면, 단순히 장소 정보만 저장하는 것이 아니라, 해당 위치 주변의 식당·숙소 등 장소 데이터를 관광정보 API와 연동해 자동으로 수집·저장하는 기능을 개발했다.
비동기 처리(async/await)를 활용해 외부 API 연동 시 발생할 수 있는 병목을 줄일 수 있었다.

async def save_place(dronespotID):
    # 외부 API에서 장소 데이터 비동기 수집
    image_accom = await getplace_img(content_id)
    # DB 저장
    place_data = PlaceModel(
        name=result_accom[index]['title'],
        lat=result_accom[index]['mapy'],
        ...
    )
    db.add(place_data)
    db.commit()

 

관광 데이터는 contentTypeId와 좌표 정보를 기반으로 호출하며, 반환 데이터가 복잡한 딕셔너리/리스트 구조여서, 데이터 파싱 및 DB mapping에 어려움을 겪었다. 또한 DB에 주변 관광 장소 데이터를 저장할 때 중복 데이터가 발생하는 문제가 생겨 해결과정에 어려움이 있었다. 자세한 내용은 밑에 기술하도록 하겠다.  

결론적으로 4번 기능 덕분에, 사용자는 단순히 드론스팟만 등록해도 자동으로 주변 편의시설 정보까지 한 번에 확인할 수 있어, 실제 서비스 사용성을 크게 높일 수 있었다.

 

트러블 슈팅

개발 과정에서 겪은 어려움 중 가장 기억에 남는 것은 사용자 위치 기반 관광지 정보 저장 시 발생한 중복 데이터 문제였다.

한국관광공사 API를 통해 사용자 위치 반경 20km 내 관광 정보를 수집해 저장했는데, 반경이 겹치는 범위에서 같은 장소가 반복 저장되면서 DB 용량이 불필요하게 커지고 데이터 접근 시 느려짐 현상이 발생하는 것 같았다. 반복 저장되는 데이터가 많다는 것을 파악하였고 중복 저장되는 데이터를 필터링해야 했다. 나는 이 문제를 해결하기 위해 데이터베이스 정규화를 적용했다. place 테이블과 drone_place 중간 테이블을 분리 설계하여 문제를 해결하고자 하였다.

def is_place_exists(db: Session, content_id: str) -> bool:
    return db.query(PlaceModel).filter(PlaceModel.type == content_id).first() is not None

한국관광공사 API의 contentid를 Place.type 컬럼에 저장하고, 이 값이 이미 DB에 존재하는지 확인하는 방식으로 중복을 체크했다. 기존에 해당 장소가 있다면 drone_place 테이블에서만 연결 관계를 추가하고, place 테이블에는 새로 저장하지 않도록 로직을 구현했다.

if is_place_exists(db, content_id):
    db_place = db.query(PlaceModel).filter(PlaceModel.type == content_id).first()
    # DronePlace 연결만 확인하고 추가
    db_droneplace = db.query(DronePlaceModel).filter(
        DronePlaceModel.dronespot_id == spot_id,
        DronePlaceModel.place_id == db_place.id
    ).first()
    if not db_droneplace:
        drone_place_data = DronePlaceModel(
            dronespot_id=spot_id,
            place_id=db_place.id
        )
        db.add(drone_place_data)
        db.commit()
    continue

DronePlace 테이블의 역할은 드론 촬영지(Dronespot)와 장소(Place) 사이의 N:M(다대다) 관계를 연결해 주는 중간 테이블이었다. 하나의 드론 촬영지 주변에는 여러 관광명소가 있을 수 있고, 반대로 하나의 장소도 여러 드론 촬영지에서 접근 가능하기 때문에 이런 구조가 필요했다.

결과적으로 저장량을 약 30% 절감할 수 있었다. 이 경험을 통해 단순히 CRUD API를 작성하는 것이 백엔드 개발의 전부가 아니라, 데이터 처리와 DB 설계 등 시스템에 대한 전반적인 이해가 필요하다는 것을 깨달았다.

결과물

서비스 흐름도

 

Flutter로 앱개발을 진행하여 ios와 안드로이드 모두 빌드를 할 수 있었고, 최종적으론 원스토어와 플레이스토어에 출시를 하였다.(앱스토어는 여러 가지 규정으로 인해 출시하지 못하였다.) 기능설명서를 포함한 최종 제출 문서를 작성하고 팀원들과 결과를 기다렸다.

 

우수상 선정 메시지, 상장

최종적으로 우수상을 수상할 수 있었다! 목표는 크게 잡아 대상이었지만 막상 결과 발표가 다가오니 장려상이라도 받길 바랬는데 우수상에 선정되어서 기뻤다. 5개월 간의 개발 과정이 결실을 맺은 것 같아 뿌듯하기도 했다.

공모전을 마무리하며..

배운 점

MariaDB를 사용하면서 데이터를 실시간으로 처리하는 시스템을 구축해봤다. ERD 설계부터 데이터 중복 문제 해결까지, 시스템 전반에 대한 이해도를 크게 높일 수 있었다. 무엇보다 체계적인 협업 프로세스를 경험할 수 있었던 것이 가장 큰 수확이었다. Confluence, Jira를 활용한 문서화와 이슈 관리, 애자일 방법론을 통한 스프린트 진행은 향후 개발 프로젝트를 진행할 때 큰 도움이 될 것이라 생각한다.

아쉬운 점

원래는 코스 추천 기능도 넣으려 했는데 시간 관계상 구현하지 못했다. 기회가 된다면 추천 알고리즘도 적용하여 코스 추천 기능을 추가해 보고 싶다. 그리고 RESTful API에 맞게 API를 설계했는지에 대한 확신이 부족했다. 다음에 백엔드 개발을 진행할 때는 좀 더 설계를 체계적으로 해서 여러 변화에 유연하게 대응할 수 있게 해보고 싶다.

느낀 점

5개월간의 관광데이터 활용 공모전을 통해 기술적 성장뿐만 아니라 협업, 기획, 그리고 실제 서비스 출시까지의 전 과정을 경험할 수 있었다. 특히 명확한 타겟층을 설정하고 그들의 니즈를 해결하는 서비스를 기획하고 구현한 경험은 개발자로서 사용자 중심의 사고를 기를 수 있는 소중한 기회였다.

또한 데이터베이스 정규화를 통한 중복 데이터 문제 해결 경험은 시스템 최적화에 대해 고민하는 계기가 되었다. 개발자는 단순히 동작하는 코드를 작성하는 것이 아니라, 효율적이고 확장 가능한 시스템을 설계하는 것이 중요하다는 것을 깨달았다.

앞으로도 이런 경험을 바탕으로 더 나은 서비스를 만들어가고 싶다. 특히 시스템 설계와 아키텍처에 대한 이해를 더욱 깊이 있게 공부하여, 확장 가능하고 유지보수하기 좋은 서비스를 만드는 개발자가 되고 싶다.

반응형

'대외활동' 카테고리의 다른 글

오픈소스 컨트리뷰션 결과 보고서  (0) 2022.04.22
오픈소스 컨트리뷰션 시작  (0) 2021.08.08

댓글