프로젝트 개요
EventLite는 행사와 장소를 등록하고 검색하며 장소별 일정을 확인하는 웹 서비스입니다. 관리자가 행사명과 날짜, 장소를 입력하면 사용자는 목록과 검색 결과에서 행사를 찾고 장소별 일정을 확인할 수 있습니다. 브라우저용 HTML 화면 외에 다른 프로그램이 데이터를 조회할 수 있는 읽기 전용 API도 제공합니다.
API는 행사 정보와 관련 주소를 함께 담는 HAL 형식의 JSON 데이터를 반환합니다. 데이터를 변경하려면 관리자 로그인과 CSRF 검사를 통과해야 합니다. CSRF 검사는 로그인한 사용자의 권한을 다른 사이트가 도용해 변경 요청을 보내는 공격을 막기 위한 절차입니다. 지도와 소셜 서비스 Mastodon의 게시 기능은 계정을 설정하면 사용할 수 있습니다.
맨체스터대학교 COMP23412 Software Engineering 과목의 팀 프로젝트로 개발했습니다. 제 기여는 테스트 작성, 보안과 행사 및 장소 관리 등 주요 기본 기능의 구현입니다. 지도 연결의 원본 구현은 다른 팀원이 담당했습니다. 과목에서 제공한 기본 코드와 팀의 공동 결과를 개인 기여와 구분하며 원래 저작권과 BSD 라이선스를 유지합니다.
화면과 API의 동작을 유지하면서 필요한 데이터만 조회하도록 설계했습니다. 동시 수정과 장소 삭제가 겹칠 때의 일관성, 외부 서비스 장애 대응과 파일 DB 복구를 함께 다루고 서비스 조회 실험과 실제 브라우저 시나리오로 검증했습니다.
핵심 결과
홈 화면에 행사 3개를 보여주기 위해 전체 행사와 장소를 읽던 처리를, 필요한 데이터만 조회하는 방식으로 바꿨습니다. 아래의 SQL 횟수는 데이터베이스에 보낸 조회 명령 수이고, 엔티티 로딩 수는 조회 결과를 Java 객체로 불러온 개수입니다. 반환 내용이 같은 상태에서 이 수치가 줄면 불필요한 조회와 데이터 로딩을 줄였다고 볼 수 있습니다.
- 예정 행사 3개 조회의 SQL
- 101 → 1회
- 행사 10만 개와 장소 100개, 같은 결과 반환
- 같은 조회의 엔티티 로딩
- 100,100 → 6개
- 전체 목록을 읽은 뒤 자르는 처리 제거
- 조회 결과 일치
- 12 / 12
- 데이터 규모 3개 × 조회 유형 4개, 결과 내용의 SHA-256 비교
- 실제 브라우저 시나리오
- 22 / 22
- Windows에서 기존 DB 호환, 생성과 조회 및 수정과 삭제, 동시 수정과 복구
조회 결과는 내용에서 계산한 식별값인 SHA-256 해시로 대조했습니다. 브라우저 시나리오는 사용자가 화면에서 수행하는 동작을 끝까지 확인한 절차이며, 조회 속도와는 별개의 기능 검증 결과입니다.
시간의 중앙값은 측정값을 정렬했을 때 가운데 값이고, p95는 측정한 시간의 95%가 그 값 이하였다는 뜻입니다. 중앙값으로 일반적인 실행 시간을, p95로 느린 쪽의 실행 시간까지 함께 확인합니다. 시간은 밀리초(ms) 단위이며 작을수록 조회가 빠릅니다.
| 조회 유형 (행사 10만 개) | 변경 전 중앙값 | 변경 후 중앙값 | 변경 전 p95 | 변경 후 p95 |
|---|---|---|---|---|
| 예정 행사 3개 | 227.925ms | 0.425ms | 271.034ms | 0.640ms |
| 인기 장소 3개 | 259.951ms | 0.522ms | 297.028ms | 0.932ms |
| 장소별 예정 행사 3개 | 2.072ms | 0.293ms | 2.236ms | 0.418ms |
| 전체 목록 (유지) | 204.145ms | 203.760ms | 231.944ms | 237.591ms |
규모별 12개 조건 전체 보기
| 행사 수 | 조회 유형 | 변경 전 중앙값 | 변경 후 중앙값 | SQL 변경 전 → 후 | 로딩 변경 전 → 후 |
|---|---|---|---|---|---|
| 1,000 | 예정 행사 3개 | 12.742ms | 1.456ms | 101 → 1 | 1,100 → 6 |
| 1,000 | 인기 장소 3개 | 16.878ms | 0.749ms | 101 → 1 | 1,100 → 3 |
| 1,000 | 장소별 예정 행사 3개 | 1.102ms | 1.232ms | 2 → 1 | 11 → 4 |
| 1,000 | 전체 목록 (유지) | 11.212ms | 10.518ms | 101 → 101 | 1,100 → 1,100 |
| 10,000 | 예정 행사 3개 | 34.683ms | 0.518ms | 101 → 1 | 10,100 → 6 |
| 10,000 | 인기 장소 3개 | 23.165ms | 0.445ms | 101 → 1 | 10,100 → 3 |
| 10,000 | 장소별 예정 행사 3개 | 0.488ms | 0.456ms | 2 → 1 | 101 → 4 |
| 10,000 | 전체 목록 (유지) | 20.485ms | 22.511ms | 101 → 101 | 10,100 → 10,100 |
| 100,000 | 예정 행사 3개 | 227.925ms | 0.425ms | 101 → 1 | 100,100 → 6 |
| 100,000 | 인기 장소 3개 | 259.951ms | 0.522ms | 101 → 1 | 100,100 → 3 |
| 100,000 | 장소별 예정 행사 3개 | 2.072ms | 0.293ms | 2 → 1 | 1,001 → 4 |
| 100,000 | 전체 목록 (유지) | 204.145ms | 203.760ms | 101 → 101 | 100,100 → 100,100 |
각 값은 같은 조건의 21회 실행 중앙값입니다. 모든 조건의 반환 결과가 일치했습니다. 전체 목록은 조회 구조를 유지했으며 1만 개 조건처럼 오히려 느려진 결과도 남겼습니다. 이 경로를 최적화했다고 주장하지 않습니다.
측정은 데이터를 메모리에 저장하는 H2 데이터베이스에서 순차 실행한 서비스 조회입니다. 조건마다 준비 실행 5회 후 21회를 측정했으며, 매번 새 트랜잭션과 조회 객체를 관리하는 영속성 컨텍스트를 사용했습니다. 앞선 실행에서 불러온 객체를 그대로 재사용해 조회를 생략하지 않도록 한 조건입니다. 시간에는 조회와 반환값 구성까지 포함되며 HTTP 응답 시간이나 운영 처리량을 뜻하지 않습니다. 변경 전후 비교 대상의 프레임워크도 각각 Spring Boot 3.3.6과 4.0.8로 다릅니다. 조회 코드만의 효과로 해석하지 않도록 SQL과 로딩 수를 함께 제시합니다.
쿼리 인자가 없는 전체 목록은 호환성을 위해 유지했습니다. 이 경로의 SQL 101회와 전체 데이터 로딩은 변경 전후 동일합니다. 데이터가 적은 일부 장소 일정 조회는 시간이 줄지 않았으며 해당 조건을 포함한 원본 결과를 모두 공개합니다.
해결하려던 문제
홈 화면에는 다음 행사와 인기 장소가 각각 3개만 필요했습니다. 전체 데이터를 읽어 정렬하거나 장소마다 행사를 조회하는 구현에서는 결과 수가 작아도 데이터가 늘수록 SQL 호출과 메모리 사용이 증가했습니다.
두 사람이 같은 항목을 열어 수정하면 나중 요청이 앞선 저장을 덮어쓸 수 있었습니다. 장소 삭제와 행사 등록이 겹칠 때는 조회 시점의 상태만으로 관계를 보호하기 어려웠습니다. 외부 타임라인을 매번 기다리는 처리와 파일 DB의 복구 절차도 함께 점검했습니다.
시스템 구조와 구현
Spring Boot로 웹 요청을 처리하고, Java 객체와 데이터베이스의 행을 연결하는 JPA로 데이터를 읽고 저장합니다. 서비스 계층은 행사 등록이나 조회 같은 업무 규칙을 담당합니다. 여러 데이터 변경을 모두 반영하거나 모두 취소하도록 묶는 트랜잭션과, 다른 요청이 먼저 수정했는지 확인하는 버전 검사를 이 계층에 적용했습니다.
- 공개 화면과 API
HTML 목록과 상세 화면, 읽기 전용 HAL API
- 보안과 입력 검증
관리자 역할, CSRF, 폼 검증과 허용 필드
- 서비스 계층
제한 조회와 페이지 처리, 트랜잭션과 버전 충돌 검사
- 저장소와 외부 연동
JPA와 H2, 제한 시간과 캐시를 적용한 외부 어댑터
| 설계 영역 | 적용한 방식 | 유지한 동작 |
|---|---|---|
| 조회 | 상위 3개 제한 조회와 인기 장소 집계, 필요한 장소 동시 조회 | 날짜와 시간 순서, 최대 3개 반환 |
| 목록과 검색 | page와 size를 지정하면 페이지 처리, 특수문자도 일반 검색어로 취급 |
인자가 없으면 원래 전체 목록 제공 |
| 동시 수정 | @Version과 폼 버전 비교, 충돌 시 409와 다시 불러오기 안내 |
같은 ID를 유지하는 수정 |
| 데이터 관계 | 행사 등록과 연결 변경, 장소 삭제에서 같은 장소 행 잠금 | 행사가 연결된 장소 삭제 차단 |
| 외부 타임라인 | 캐시 30초, 마지막 성공 조회 후 최대 300초의 이전 결과, 재시도 대기 10초 | 공개 게시물과 관리자 게시 기능 |
| 실행 환경 | Java 17과 Spring Boot 4.0.8, 체크섬이 고정된 Maven Wrapper | 화면과 API 경로, 인증과 권한 규칙 |
버전 검사는 화면을 열 때의 데이터 버전과 저장 시점의 버전을 비교합니다. 다른 사용자가 먼저 수정했다면 덮어쓰는 대신 HTTP 상태 코드 409로 충돌을 알립니다. 같은 장소의 행사 등록과 삭제가 겹칠 때는 해당 데이터 행에 잠금을 걸어 관계가 어긋나지 않도록 했습니다. 외부 타임라인의 캐시는 한 번 받아 온 게시물을 잠시 보관해 다시 쓰는 방식으로, 반복 호출과 외부 응답 대기를 줄입니다.
날짜 판단은 현재 시간을 제공하는 Clock을 공유하고 시간 미입력 행사의 정렬도 명시했습니다. 테스트에서는 이 시계를 지정해 날짜 경계를 확인할 수 있습니다. 이전 버전의 파일 DB에 새 버전 열이 생기면 비어 있는 값만 0으로 채워 저장된 행사와 장소를 수정할 수 있습니다. 이처럼 저장 데이터를 새 버전에서도 사용할 수 있게 맞추는 과정을 DB 마이그레이션이라고 합니다. JSON 연동은 Spring Boot의 Jackson 2 호환 모듈을 사용하며 Jackson 3 전면 전환까지 완료한 것은 아닙니다.
실험과 검증
| 검증 | 결과 |
|---|---|
| Windows와 Ubuntu의 Java 테스트 | 각각 278개 통과, 실패와 오류 및 건너뜀 0개 |
| 두 환경의 JavaScript 회귀 테스트 | 지도와 스냅샷 15개, 브라우저 실행 도구 2개를 각각 통과 |
| Java 줄 및 분기 커버리지 | 95.31% / 76.32% |
| Maven 커버리지 기준 | 줄 90%, 분기 70% 이상 통과 |
| 두 환경의 실제 Chrome 브라우저 | 새 DB로 시작하는 21개 시나리오를 각각 통과 |
| Windows의 실제 Chrome 브라우저 | 이전 버전 JAR의 DB 마이그레이션을 포함한 22개 시나리오 통과 |
| 외부 계정 API 호출 | 없음, 통제된 가짜 응답 사용 |
줄 커버리지는 전체 실행 대상 코드 줄 중 테스트가 실행한 비율이고, 분기 커버리지는 조건문의 참과 거짓 등 가능한 분기 중 실행한 비율입니다. 테스트가 얼마나 넓게 코드를 지나갔는지 보여주며, 그 비율만으로 모든 오류가 없다는 뜻은 아닙니다. 회귀 테스트는 수정 후에도 기존 기능이 유지되는지 확인하는 검사입니다.
Spring 애플리케이션 컨텍스트는 서비스와 저장소 같은 실행 구성 요소를 생성하고 관리하는 범위입니다. 통합 테스트에서는 컨텍스트마다 UUID라는 고유 식별자를 붙인 이름의 메모리 DB를 사용합니다. 같은 컨텍스트 안에서는 데이터 소스를 공유하며, 실제 컨텍스트 두 개를 열어 다른 컨텍스트의 생성과 종료가 기존 행과 테이블을 지우지 않는지 확인합니다.
조회 실험은 행사 1천 개, 1만 개와 10만 개에 장소 100개를 연결했습니다. 예정 행사, 인기 장소, 장소별 예정 행사와 원래 전체 목록을 비교하고 반환 행의 SHA-256이 12개 조건에서 모두 같은지 확인했습니다. 시간만 고르는 대신 SQL 수, 로딩 수와 개선되지 않은 조건도 기록했습니다.
외부 타임라인 실험에서는 실제 Mastodon 서비스 코드에 150ms 지연의 가짜 응답을 연결했습니다. 연속 조회 21회에서 외부 호출은 21회에서 1회로 줄었습니다. 캐시가 없는 첫 요청은 외부 응답을 기다리므로 빠른 캐시 적중 시간과 구분합니다. 실제 계정의 게시나 지도 API 호출은 이 결과에 포함하지 않습니다.
브라우저 검증은 임시 포트와 파일 DB를 사용합니다. 이전 버전의 실행 파일로 만든 DB를 새 버전에서 열고 레코드 보존과 실제 수정까지 확인했습니다. 정상 재시작, 종료 후 백업, 파일 내용의 변조나 손상을 확인하는 체크섬 검사와 새 DB 복원도 같은 흐름에서 검사합니다. 자동 검사를 실행하는 GitHub Actions의 Windows와 Ubuntu에서 Java 278개와 JavaScript 17개, 새 DB로 시작하는 Chrome 시나리오 21개를 각각 통과했습니다. 이전 버전 DB의 마이그레이션을 포함한 22개 시나리오는 로컬 Windows에서 별도로 확인했으며, 아래 검증 기록과 GitHub Actions에서 각 실행 결과를 확인할 수 있습니다.
오류 분석과 개선 범위
| 재현한 문제 | 수정과 확인 |
|---|---|
| 3개를 보여주려고 전체 데이터 로딩 | 제한 조회와 집계, 10만 개 조건의 SQL과 로딩 수 측정 |
| 두 탭의 수정이 조용히 덮어써짐 | 오래된 버전 저장 차단, 실제 두 탭 검사 |
| 행사 등록과 장소 삭제의 동시 진행 | 같은 장소 행 잠금, 트랜잭션 동시 실행 검사 |
| 페이지 검색어의 이중 인코딩 | 링크 생성 수정, %, 공백과 한글 검색어로 페이지 이동 |
| 이전 버전 DB의 빈 버전 값으로 수정 실패 | 비어 있는 버전만 초기화, 이전 버전 DB의 실제 수정 검사 |
| 관리자 삭제 폼이 403으로 거부됨 | 메서드 변환 순서 수정, 실제 폼과 권한 회귀 테스트 |
unzip 없는 Linux에서 Wrapper 실행 실패 |
체크섬을 유지하며 JDK 압축 해제와 실행 권한 보장 |
| 테스트 컨텍스트가 같은 메모리 DB를 공유해 다른 검사의 행과 테이블을 삭제 | 컨텍스트마다 고유 DB 이름을 사용하고 다른 컨텍스트 생성과 종료 후 데이터 보존 검사 |
| Chrome 시작 실패가 일반적인 시간 초과로만 표시됨 | 조기 종료 감지와 오류 로그 저장, 실행 도구의 실패 경로 검사 |
| Ubuntu에서 다운로드한 Chrome이 샌드박스를 시작하지 못함 | 러너에 기본 설치된 Chrome으로 검증하고 샌드박스 설정 유지 |
| 외부 서비스 지연과 실패 반복 | 캐시와 제한된 이전 결과 제공, 갱신 중복 방지 검사 |
이 결과는 행사 관리 기능과 복구 절차의 검증입니다. 운영 트래픽 부하 시험이나 실제 외부 계정의 응답을 대신하지 않습니다. 파일 DB 백업은 서비스를 정상 종료한 뒤 실행하며 원래 DB 위에 덮어쓰지 않습니다.
실행 화면과 재현 방법

JDK 17을 준비한 뒤 소스를 내려받아 실행합니다. 최초 실행에는 의존성을 받기 위한 인터넷 연결이 필요하며 별도의 Maven이나 Docker는 필요하지 않습니다.
.\mvnw.cmd spring-boot:run '-Dspring-boot.run.profiles=demo'
http://localhost:8080/home에서 확인합니다. 데모 계정은 demo-admin / demo-password이며 demo 프로필의 메모리 DB에서만 사용합니다. 프로필은 실행 목적에 맞는 설정 묶음으로, 여기서는 별도 외부 계정 없이 기능을 확인하는 데모 설정입니다. 지도와 Mastodon은 기본적으로 꺼져 있어 계정 없이 핵심 기능을 확인할 수 있습니다.
.\mvnw.cmd verify
node --test src/test/javascript/event-maps.test.cjs src/test/javascript/database-snapshot.test.mjs
node --test scripts/lib/browser.test.mjs
node scripts/verify-browser.mjs --output target/browser-verification
브라우저 검증에는 Node.js 22.4 이상과 Chrome 또는 Chromium이 필요합니다. Linux와 macOS의 Maven 검증 명령은 sh ./mvnw verify입니다. README에서 브라우저 경로 설정, 조회 실험과 백업 및 복원 명령을 제공합니다.
소스 코드와 검증 자료
- 소스와 검증 자료 다운로드
- GitHub 저장소
- 영어 README, 한국어 README
- 조회 실험 전체 결과, 검증 결과
- 회귀 검증 기록, 브라우저 실행 도구 검증, GitHub Actions 실행 상태
- 구조와 실행 설정, 개인 기여와 설계 과정
비교 대상 코드는 Git 이력에 보존하고 개발 과정의 검증 기록도 함께 남겼습니다. 다운로드 ZIP은 같은 공개 소스와 문서 및 측정 결과를 담으며 Git 메타데이터, DB, 인증정보와 개인 작업 기록은 제외합니다.
소스와 문서 ZIP 다운로드GitHub에서 소스 코드 보기

