diff --git a/meeting/section_id.md b/meeting/section_id.md new file mode 100644 index 0000000..e5c0c2e --- /dev/null +++ b/meeting/section_id.md @@ -0,0 +1,203 @@ +# Meeting 서비스 section_id 이슈 해결 기록 + +## 문제 상황 + +### 발생한 에러 +``` +Caused by: org.postgresql.util.PSQLException: ERROR: column "section_id" of relation "minutes_sections" contains null values +``` + +### 에러 원인 +- JPA Entity인 `MinutesSectionEntity`는 `section_id`를 Primary Key로 정의 +- 실제 데이터베이스 테이블에는 `section_id` 컬럼이 존재하지 않음 +- JPA가 `ddl-auto: update` 모드로 NOT NULL 제약조건을 추가하려고 시도했으나 실패 + +## 해결 과정 + +### 1단계: 문제 분석 +- 마이그레이션 파일 확인: V1이 없고 V2, V3만 존재 +- JPA 설정 확인: `ddl-auto: update` 모드 사용 중 +- Entity 분석: `MinutesSectionEntity.section_id`는 Primary Key로 정의됨 + +### 2단계: 해결 방안 선택 +사용자 선택: +- **방안 1**: 기존 데이터 정리 후 스키마 재구성 +- **옵션 B**: Foreign Key 제약조건 제거하여 기존 데이터 보존 + +### 3단계: Flyway 마이그레이션 설정 + +#### build.gradle 수정 +```gradle +// Flyway 의존성 추가 +implementation 'org.flywaydb:flyway-core' +runtimeOnly 'org.flywaydb:flyway-database-postgresql' +``` + +#### application.yml 수정 +```yaml +jpa: + hibernate: + ddl-auto: validate # update → validate로 변경 + +# Flyway 설정 추가 +flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + validate-on-migrate: true +``` + +### 4단계: 마이그레이션 파일 생성 + +#### V1__create_initial_schema.sql +- 모든 초기 테이블 생성 +- Foreign Key 제약조건 주석 처리 (옵션 B 선택) + - `fk_todos_minutes`: todos 테이블의 minutes_id 외래키 + - `fk_sessions_meetings`: sessions 테이블의 meeting_id 외래키 + +```sql +-- 외래키 제약조건 비활성화 예시 +-- ALTER TABLE todos DROP CONSTRAINT IF EXISTS fk_todos_minutes; +-- ALTER TABLE todos ADD CONSTRAINT fk_todos_minutes +-- FOREIGN KEY (minutes_id) REFERENCES minutes(minutes_id); +``` + +#### V2__create_meeting_participants_table.sql +- V1에 통합되어 no-op 처리 +```sql +SELECT 1; -- No-op statement +``` + +#### V4__fix_missing_columns.sql +- `minutes_sections` 테이블에 `section_id` 컬럼 추가 +- 시퀀스 생성 후 기존 데이터에 자동 ID 할당 + +```sql +-- 1. 시퀀스 생성 +CREATE SEQUENCE IF NOT EXISTS minutes_sections_temp_seq; + +-- 2. section_id 컬럼 추가 +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'minutes_sections' AND column_name = 'section_id') THEN + -- 임시 컬럼 추가 + ALTER TABLE minutes_sections ADD COLUMN temp_section_id VARCHAR(50); + + -- 기존 데이터에 ID 생성 + UPDATE minutes_sections + SET temp_section_id = 'section-' || nextval('minutes_sections_temp_seq'::regclass) + WHERE temp_section_id IS NULL; + + -- 기존 Primary Key 제거 + ALTER TABLE minutes_sections DROP CONSTRAINT IF EXISTS minutes_sections_pkey; + + -- 컬럼명 변경 + ALTER TABLE minutes_sections RENAME COLUMN temp_section_id TO section_id; + ALTER TABLE minutes_sections ALTER COLUMN section_id SET NOT NULL; + + -- 새로운 Primary Key 설정 + ALTER TABLE minutes_sections ADD PRIMARY KEY (section_id); + END IF; +END $$; +``` + +### 5단계: 실행 프로파일 수정 + +#### .run/meeting-service.run.xml +```xml + +``` + +## 발생한 에러들과 해결 + +### 에러 1: PostgreSQL 인증 실패 +- 증상: psql 직접 연결 실패 +- 해결: Flyway 마이그레이션 사용으로 전환 + +### 에러 2: 컬럼 존재하지 않음 (42703) +- 증상: V1 마이그레이션의 DELETE 문에서 존재하지 않는 컬럼 참조 +- 해결: DELETE 문 제거, CREATE TABLE IF NOT EXISTS 활용 + +### 에러 3: Foreign Key 제약조건 위반 (23503) +- 증상: `ERROR: insert or update on table "todos" violates foreign key constraint "fk_todos_minutes"` +- 해결: 사용자 선택(옵션 B)에 따라 Foreign Key 제약조건 주석 처리 + +### 에러 4: 인덱스 중복 (42P07) +- 증상: V2 마이그레이션이 이미 존재하는 테이블/인덱스 생성 시도 +- 해결: V2를 no-op으로 변경 (SELECT 1) + +### 에러 5: section_id 컬럼 누락 +- 증상: `Schema-validation: missing column [section_id] in table [minutes_sections]` +- 해결: V4 마이그레이션 생성하여 컬럼 추가 + +### 에러 6: 시퀀스 존재하지 않음 (42P01) +- 증상: V4에서 시퀀스를 생성 전에 사용 시도 +- 해결: 시퀀스 생성을 DO 블록 앞으로 이동 + +### 에러 7: 포트 사용 중 +- 증상: 8082 포트가 이미 사용 중 +- 해결: `lsof -ti:8082 | xargs -r kill -9`로 프로세스 종료 + +## 최종 결과 + +### 서비스 정상 실행 확인 +```bash +curl http://localhost:8082/actuator/health +``` + +응답: +```json +{ + "status": "UP", + "components": { + "db": { + "status": "UP", + "details": { + "database": "PostgreSQL", + "validationQuery": "isValid()" + } + }, + "diskSpace": {"status": "UP"}, + "ping": {"status": "UP"}, + "redis": {"status": "UP"} + } +} +``` + +### 마이그레이션 적용 결과 +- V1: 초기 스키마 생성 (FK 제약조건 제외) +- V2: no-op (V1에 통합) +- V3: 기존 유지 +- V4: section_id 컬럼 추가 + +## 주요 변경 파일 + +1. **meeting/src/main/resources/db/migration/V1__create_initial_schema.sql** (생성) +2. **meeting/src/main/resources/db/migration/V2__create_meeting_participants_table.sql** (수정) +3. **meeting/src/main/resources/db/migration/V4__fix_missing_columns.sql** (생성) +4. **meeting/src/main/resources/application.yml** (수정) +5. **meeting/.run/meeting-service.run.xml** (수정) +6. **build.gradle** (수정) + +## 교훈 + +1. **Flyway를 초기부터 사용**: JPA의 `ddl-auto: update`는 프로덕션 환경에서 위험 +2. **Baseline 마이그레이션 전략**: 기존 스키마를 버전 0으로 인정하고 점진적 수정 +3. **데이터 보존 vs 무결성**: 비즈니스 요구사항에 따라 Foreign Key 제약조건 선택적 적용 +4. **마이그레이션 테스트**: 각 마이그레이션은 독립적으로 실행 가능해야 함 +5. **환경 변수 관리**: `ddl-auto` 설정은 환경별로 다르게 관리 필요 + +## 향후 개선 사항 + +1. Foreign Key 제약조건 재검토 + - 데이터 정리 후 제약조건 활성화 검토 + - 참조 무결성 확보 방안 논의 + +2. 마이그레이션 전략 수립 + - 개발/스테이징/프로덕션 환경별 마이그레이션 절차 + - 롤백 계획 수립 + +3. 모니터링 강화 + - 데이터베이스 스키마 변경 추적 + - 마이그레이션 실패 알림 설정 diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/event/publisher/NoOpEventPublisher.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/event/publisher/NoOpEventPublisher.java index 7a6c211..f3079ec 100644 --- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/event/publisher/NoOpEventPublisher.java +++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/event/publisher/NoOpEventPublisher.java @@ -1,5 +1,6 @@ package com.unicorn.hgzero.meeting.infra.event.publisher; +import com.azure.messaging.eventhubs.EventHubProducerClient; import com.unicorn.hgzero.meeting.infra.event.dto.MeetingStartedEvent; import com.unicorn.hgzero.meeting.infra.event.dto.MeetingEndedEvent; import com.unicorn.hgzero.meeting.infra.event.dto.TodoAssignedEvent; @@ -8,7 +9,6 @@ import com.unicorn.hgzero.meeting.infra.event.dto.MinutesAnalysisRequestEvent; import com.unicorn.hgzero.meeting.infra.event.dto.MinutesFinalizedEvent; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import java.time.LocalDate; @@ -20,8 +20,7 @@ import java.util.List; * EventHub가 설정되지 않은 경우 사용되는 더미 구현체 */ @Component -@Primary -@ConditionalOnMissingBean(name = "eventProducer") +@ConditionalOnMissingBean(EventHubProducerClient.class) @Slf4j public class NoOpEventPublisher implements EventPublisher {