A Game About Feeding A Black Hole을 레퍼런스로 한 인크리멘탈 게임
서버는 계정·진행 데이터 저장·클라이언트 데이터 검증 등의 백엔드 역할을 맡는다.
6인 팀 개발로 진행한다.
| 구분 | 선택 |
|---|---|
| 언어 / 런타임 | Java 21 (Gradle Toolchain으로 고정) |
| 프레임워크 | Spring Boot 4.1.1 |
| 빌드 | Gradle (Wrapper 사용) |
| DB | MySQL 8.4 |
| 스키마 관리 | Flyway |
| 테스트 | JUnit 5, Testcontainers |
| 컨벤션 | EditorConfig, Checkstyle |
build.gradle의 핵심 의존성dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
implementation 'org.flywaydb:flyway-mysql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
testImplementation 'org.testcontainers:testcontainers-mysql'
// ... (starter-*-test 생략)
}
starter-data-jpa / starter-webmvc: JPA와 REST API(Spring MVC)starter-flyway: Spring Boot가 앱 시작 시 Flyway를 자동 실행해 주는 연동 모듈flyway-mysql: Flyway 10부터 DB별 지원이 별도 모듈로 분리되어, MySQL을 쓰려면 이 모듈이 따로 필요mysql-connector-j: MySQL JDBC 드라이버testcontainers-*: 테스트 때 진짜 MySQL을 Docker 컨테이너로 띄우기 위한 것(후술).env.env# .env.example
DB_PORT=3306
DB_NAME=feedablackhole
DB_USERNAME=blackhole
DB_PASSWORD=change-me
DB_ROOT_PASSWORD=change-me-root
.env는 .gitignore에 포함하고, 값을 비운 .env.example만 공유services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?.env.example을 복사해 .env를 만드세요}
MYSQL_DATABASE: ${DB_NAME:-feedablackhole}
MYSQL_USER: ${DB_USERNAME:-blackhole}
MYSQL_PASSWORD: ${DB_PASSWORD:?.env.example을 복사해 .env를 만드세요}
ports:
- "${DB_PORT:-3306}:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
healthcheck: ...
utf8mb4로 명시${VAR:?메시지} 문법으로 .env가 없으면 안내 메시지와 함께 실패하도록 함# application.properties
spring.config.import=optional:file:.env[.properties]
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:${DB_PORT:3306}/${DB_NAME:feedablackhole}}
spring.datasource.username=${DB_USERNAME:blackhole}
spring.datasource.password=${DB_PASSWORD:}
spring.config.import=optional:file:.env[.properties]: 프로젝트 루트의 .env를 properties 형식으로 읽어옴optional:), 이 경우 실제 환경 변수를 사용.env 없이 환경변수 DB_URL, DB_USERNAME, DB_PASSWORD로 덮어씀spring.jpa.hibernate.ddl-auto=update
ddl-auto 옵션 정리| 값 | 동작 |
|---|---|
none | 아무것도 안 함 |
validate | 엔티티와 스키마가 일치하는지 검사만 함 (다르면 시작 실패) |
update | 엔티티에 맞춰 스키마를 추가 방향으로 변경 시도 |
create | 시작할 때 테이블을 지우고 새로 생성 |
create-drop | create + 종료 시 삭제 |
update의 문제점ALTER 등의 이력이 남지 않음update는 "추가"만 한다NOT NULL 컬럼을 추가하면 실패하거나 의도와 다르게 동작할 수 있다.update는 추가만 하고 있는 걸 바꾸거나 없애지 못한다.create를 쓰면 이 문제는 어느 정도 해결되지만, 이러면 개발 데이터가 매번 날아간다.update 옵션을 사용하는 것은 의도치 않은 스키마 변경의 원인이 되어 위험하다.validate나 none을 쓰는데, 그러면 운영 단계에선 결국 스키마 관리를 개발 단계와 다른 방식으로 해야 한다. 즉, 개발 단계에서 검증하지 않은 방식을 쓰는 셈이다.DB 스키마의 버전 관리 도구
src/main/resources/db/migration/아래에V<번호>__<설명>.sql파일을 둔다.db/migration/ ├── V1__create_account.sql ├── V2__create_progress_tables.sql └── V3__add_xxx_column.sql- 앱이 시작되면 Flyway가 DB에 있는
flyway_schema_history테이블을 확인한다. 이 테이블에는 어떤 버전을, 언제, 어떤 체크섬으로 적용했는지가 기록되어 있다.- 아직 적용되지 않은 버전만 번호 순서대로 실행하고, 결과를
flyway_schema_history에 기록한다.- 이미 적용된 파일은 다시 실행하지 않는다.
V + 번호 + __ + 설명)을 지켜야 인식된다.ddl-auto=update와의 비교ddl-auto=update의 문제 | Flyway의 경우 |
|---|---|
| ① 변경 이력이 없다 | 모든 변경이 SQL 파일로 Git 이력에 남는다. 누가·언제·왜(커밋 메시지) 바꿨는지 추적 가능 |
| ② 추가만 되고 어긋난다 | 컬럼 이름 변경·삭제·타입 변경도 ALTER로 명시적으로 작성한다 |
| ③ 팀원 DB가 제각각 | 빈 DB에서 시작해도 V1부터 순서대로 적용하면 누구든 같은 스키마가 된다 |
| ④ 데이터 이전 불가 | 같은 마이그레이션 파일에 UPDATE/INSERT로 데이터 이전·초기 데이터도 넣을 수 있다 |
| ⑤ 개발/운영 방식이 다르다 | 개발·테스트·운영이 같은 마이그레이션 파일을 같은 순서로 적용한다. 배포 전에 이미 수없이 검증된 경로다 |
| ⑥ 설계가 리뷰에 안 보인다 | CREATE TABLE의 제약조건·인덱스·외래키가 PR diff에 그대로 보인다 |
(예전) 엔티티 수정 → 서버 실행 → 끝 (DB가 알아서 바뀜)
(지금) 1. 새 마이그레이션 파일 추가 (예: V3__add_nickname_to_account.sql)
2. 엔티티 수정
3. 서버 실행 → Flyway가 V3 적용 → Hibernate가 일치 여부 검증
4. SQL 파일과 엔티티를 같은 PR에 포함
ddl-auto는 validate로 둔다.V3__...를 만들면 병합 시 충돌한다. PR 병합 시점에 번호를 확인하고 조정해야 한다.docker compose down -v로 볼륨째 지우고 V1부터 다시 적용하면 된다. 언제든 처음부터 재현 가능하다는 것 자체가 Flyway의 장점 중 하나이다.validate와 테스트로 계속 확인해야 한다. 이제는 자동으로 코드에서 스키마를 만들어주지 않는다.validate가 잡아주는 것과 못 잡는 것엔티티와 스키마의 일치를
validate에 맡긴다고 했지만,validate가 확인하는 범위는 생각보다 좁다.
String↔VARCHAR, long↔BIGINT 등)반대로 다음은 기본 설정에서 검증하지 않는다.
VARCHAR(20)과 VARCHAR(255)를 구분하지 않는다.)NOT NULL 여부CHECK 제약즉 엔티티가 쓰는 테이블과 컬럼이 DB에 있고 타입이 맞는가만 보장한다.
NONE)spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.tooling.schema.unique_key_validation=ALL
spring.jpa.properties.hibernate.tooling.schema.index_validation=ALL
NONE, NAMED, ALL 중 하나이다.NAMED는 UK(대문자)로 시작하는 이름을 검증에서 제외한다. 이름을 붙이지 않아 Hibernate가 자동 생성한 이름을 걸러 내려는 동작이다. 하지만 직접 UK_...로 이름을 붙인 제약도 조용히 빠질 수 있다.ALL을 택했다.@UniqueConstraint(name = "uk_...", columnNames = ...)로 SQL에 있는 제약을 엔티티에 옮겨 적고 있다. 이 옵션을 켜면 이 어노테이션이 SQL과 어긋나는 순간 서버가 시작되지 않는다.엔티티를 일부러 어긋나게 만들고 테스트를 돌려 보았다.
| 어긋나게 한 것 | 결과 |
|---|---|
| 유니크 제약 이름 | 실패 (Missing unique constraint named ...) |
| 유니크 제약 컬럼 순서 | 실패 (Unique-key mismatch) |
| 컬럼 길이 (64 → 10) | 통과 |
nullable = false 추가 (DB는 NULL 허용) | 통과 |
| 외래키 이름 | 통과 |
| 엔티티에 쓰는 요소 | 스키마와 일치 검증 |
|---|---|
테이블 이름 (@Table(name)) | O |
컬럼 이름 (@Column(name)) | O |
| 컬럼 타입 종류 | O |
| 유니크 제약 (이름·컬럼 구성·순서) | O (옵션 활성화 시) |
인덱스 (@Index) | O (옵션 활성화 시) |
컬럼 길이 (length) | X |
NOT NULL (nullable, optional) | X |
| 외래키 | X |
| PK, AUTO_INCREMENT | X |
기본값, CHECK 제약 | X |
@Enumerated의 허용 값 집합 | X (DB는 VARCHAR일 뿐이라 비교 대상이 아니다.) |
NOT NULL인데 DB가 NULL을 허용하면 데이터 품질 문제만 조용히 남는다.@Column, @Table, @UniqueConstraint 등은 스키마를 만들지 않는다. SQL을 코드에서도 읽을 수 있게 옮겨 적은 문서에 가깝다.validate가 잡아 주므로 안전하다.@Column(unique = true)는 자동 생성된 이름이 SQL의 이름과 맞지 않아 실패할 수 있다. @UniqueConstraint(name = ...)로 SQL과 같은 이름을 명시한다.NOT NULL, 외래키는 validate가 잡아주지 않으므로 직접 맞춘다. SQL을 바꾸면 엔티티도 같은 PR에서 같이 고치고, 리뷰에서도 두 파일을 나란히 확인한다.V<번호>__<설명>.sql 파일 추가로만 한다.H2 등 인메모리 DB: 실제 운영 DB(MySQL)와 문법·동작이 달라 테스트 통과가 실제 통과를 보장하지 못한다. Flyway SQL은 MySQL 문법으로 쓰기에 H2로는 검증할 수 없다.
로컬 DB: 팀원마다 상태가 다르고, 이전 테스트 데이터가 남아 영향을 줄 수 있다.
Docker 컨테이너로 실제 DB 같은 의존 서비스를 띄웠다가, 테스트가 끝나면 자동으로 정리해 주는 Java 라이브러리
테스트 시작 → Docker로 mysql:8.4 컨테이너 기동(랜덤 포트)
→ Spring이 그 컨테이너 접속 정보를 DataSource에 자동 연결
→ Flyway가 V1부터 적용 → 테스트 실행
테스트 종료 → 컨테이너 자동 삭제
compose로 띄운 DB(3306)와 충돌하지 않는다.DB 외에 팀 작업을 위해 정한 것들이다.
.gitattributes로 줄바꿈을 LF로 통일 (Windows/Mac 팀원이 섞여 있어도 diff가 지저분해지지 않도록), .gitmessage 커밋 메시지 템플릿 (feat/fix/refactor/docs/test/chore).editorconfig + Checkstyle(maxWarnings = 0). 규칙 위반 시 빌드가 실패한다.main 아래에 dev 브랜치를 디폴트로 설정하고, feat/…, fix/…, chore/… 브랜치를 만들어 PR로 병합