평상시에 개발 편의를 위해 아래와 같이 app module에 TypeOrmModule을 imports 할 때,
synchronize : true를 설정한다.
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'admin',
password: 'pw123456',
database: 'project_db',
entities: [__dirname + '/domain/**/*.entity{.ts,.js}'],
synchronize: true, // 개발 중엔 true, 운영에선 false!
...
})
평소대로 개발을 진행하던 도중, 아래와 같은 에러가 발생했다.
[Nest] 3284 - 2025. 06. 28. 오후 4:04:53 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
QueryFailedError: tables can have at most 1600 columns
TypeOrm이 테이블의 column을 한번에 1600개 이상 생성시도하여, 이로인해 에러가 발생한것이다.
우선 synchronize : false 로 설정했을때에는 시스템이 잘 올라오는것을 확인 한 후
대체 무엇이 문제인지 점검에 나섰다.
당장의 의심사항들은 아래와 같았다.
Entity의 구조가 꼬여 상호 참조하는 방식으로 설계됨@Entity 데코레이터를 부착하여, TypeOrm이 Entity로 취급@ManyToOne, @OneToMany 관계를 설정하다가 무한회귀에 빠짐Entity를 수정하지 않음@ApiProperty등의 Swagger 데코레이터가 겹겹이 쌓이며, 설정이 꼬여버림git을 확인해보아도 오늘 작업중 Entity를 직접 수정한 기록이 없었지만,
오늘 수행한 작업만이 직접적인 원인이라는 생각 또한 위험했다.
언제부터 위험이 누적되어왔을지 모를일이니...
어쩌면 그동안 column 생성이 서서히 이루어지다가, 지금에 이르러서야 로그를 통해 확인한것일지도 모를 일이다.
그렇기에 아래 sql로 현재 DB에 있는 테이블들의 column 갯수를 확인했다.
SELECT
table_schema,
table_name,
COUNT(*) AS column_count
FROM
information_schema.columns
GROUP BY
table_schema, table_name
ORDER BY
column_count DESC;
하지만 column 갯수가 제일 많은 테이블은 82개 였으며, 이또한 db의 자체 테이블이었다.
우선 현재의 로그만으론 문제를 분석하기 어려웠으므로,
TypeOrmModule에서 보다 상세한 로그를 찍기 위해 아래 옵션을 추가했다.
logging:['query', 'schema']
다시 Nest를 가동하고 추가로그들을 분석해보았다.
그리고 에러가 발생하기 직전에 column을 drop 하고 create 하는 작업이 발생했음을 확인했다.
columns dropped in review_reports: report_type
query: SELECT "n"."nspname", "t"."typname" FROM "pg_type" "t" INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" WHERE "n"."nspname" = 'public' AND "t"."typname" = 'review_reports_report_type_enum'
query: SELECT "udt_schema", "udt_name" FROM "information_schema"."columns" WHERE "table_schema" = 'public' AND "table_name" = 'review_reports' AND
"column_name"='report_type'
query: ALTER TABLE "review_reports" DROP COLUMN "report_type"
query: DROP TYPE "public"."review_reports_report_type_enum"
columns dropped in review_reports: reportType
query: SELECT "n"."nspname", "t"."typname" FROM "pg_type" "t" INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" WHERE "n"."nspname" = 'public' AND "t"."typname" = 'review_reports_reporttype_enum'
query: SELECT "udt_schema", "udt_name" FROM "information_schema"."columns" WHERE "table_schema" = 'public' AND "table_name" = 'review_reports' AND "column_name"='reportType'
query: ALTER TABLE "review_reports" DROP COLUMN "reportType"
query: DROP TYPE "public"."review_reports_reporttype_enum"
new columns added: reportType
query: SELECT "n"."nspname", "t"."typname" FROM "pg_type" "t" INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" WHERE "n"."nspname" = 'public' AND "t"."typname" = 'review_reports_reporttype_enum'
query: CREATE TYPE "public"."review_reports_reporttype_enum" AS ENUM('INAPPROPRIATE', 'SPOILER')
query: ALTER TABLE "review_reports" ADD "reportType" "public"."review_reports_reporttype_enum" NOT NULL
new columns added: report_type
query: SELECT "n"."nspname", "t"."typname" FROM "pg_type" "t" INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" WHERE "n"."nspname" = 'public' AND "t"."typname" = 'review_reports_report_type_enum'
query: CREATE TYPE "public"."review_reports_report_type_enum" AS ENUM('spoiler', 'inappropriate')
query: ALTER TABLE "review_reports" ADD "report_type" "public"."review_reports_report_type_enum" NOT NULL
query: ROLLBACK
[Nest] 3284 - 2025. 06. 28. 오후 4:04:53 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
reportType과 report_type 이라는, 이름이 거의 비슷한 column을 생성 시도했음을 확인했다.
그리고 DB에서 해당 column들이 모두 다 있음을 확인했다. 여기서 reportType 열은 불청객이다.
우선 ReviewReport 엔티티의 내용은 아래와 같다.
export enum ReportType {
INAPPROPRIATE = 'INAPPROPRIATE',
SPOILER = 'SPOILER',
}
@Entity('review_reports')
@Unique(['review', 'user']) // 동일 유저가 동일 리뷰에 중복 신고 못하도록
export class ReviewReport {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Review, (review) => review.reviewReports, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'review_id' })
review: Review;
@ManyToOne(() => User, (user) => user.reviewReports, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'user_id' })
user: User;
@Column({
type: 'enum',
enum: ReportType,
})
reportType: ReportType;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
영화 평가(Review)에 대한 신고 기록(Report)을 관리하기 위한 테이블이며,
Entity 상에서는 report_type이 없고, reportType만 발견된다.
TypeORM은 reportType이라는 camel case 이름을 자동으로 snake case인 report_type으로 변환해서 DB 칼럼을 만든것으로 추정된다.
@Column 데코레이터에 name 프로퍼티를 넣지 않은 나의 실수였다.
그래서 아래와 같이 프로퍼티를 수정하였다.
@Column({
name: 'report_type',
type: 'enum',
enum: ReportType,
})
reportType: ReportType;
그리고 아래 sql로 문제를 일으킨 컬럼을 둘 다 제거했다.
ALTER TABLE review_reports DROP COLUMN IF EXISTS "reportType";
ALTER TABLE review_reports DROP COLUMN IF EXISTS "report_type";
그리고 Nest를 다시 가동하였고, synchronize 결과 report_type 열만 생성되었음을 확인했다.
@Column 데코레이터에 name 프로퍼티를 넣지 않은것은 명백한 실수지만,
이 문제가 왜 컬럼 무한생성 문제를 일으켰으며, 왜 하필 지금인지는 잘 모르겠다.
왜 TypeORM은 컬럼을 무한히 생성하는 작업을 반복했는가?
어쨋든 report_type과 reportType은 둘 다 있는 상태였는데,
왜 굳이 column을 drop하고 다시 create하는 작업을 반복하였는지...
오늘은 모든 엔티티파일들도 일체 건드린적이 없기 때문에, 다른 엔티티에서 영향을 받았을리가 없다.
TypeORM이 enum type과 관련하여 무언가 잘못된 상태를 인식해서 수백개의 컬럼을 생성하려 시도했다고 조심스레 추측해본다.
특히 PostgreSQL에서 enum 타입과 관련하여 문제가 발생하는 게시글들을 여럿 볼 수 있는데, 자세히 배워봐야겠다.
synchronize 설정을 false로 둔 채 개발을 진행하기엔 생산성이 너무 저하된다.
그리고 이런일이 앞으로도 발생하지 말란 보장은 없다.
특히 앞선 에러 로그처럼 column을 직접 drop 하려는 시도가 있는것으로 보아,
synchronize 설정에 대한 보다 깊이 있는 공부가 필요할것으로 보인다.