트러블슈팅
자주 발생하는 문제와 해결 방법.
연결 오류
"Database not connected"
OrmError [ORM_NOT_CONNECTED]: Database connection has not been established.원인: em.register()가 끝나기 전에 쿼리 메서드를 호출했습니다.
// 잘못된 사용 -- register는 비동기
const em = new EntityManager();
em.register({ ... }); // await을 잊었습니다
const users = await em.find(User); // 예외 발생
// 올바른 사용
await em.register({ ... });
const users = await em.find(User);"Connection refused" 또는 "ECONNREFUSED"
데이터베이스 서버가 실행 중이 아니거나 호스트/포트가 잘못되었습니다.
# 데이터베이스가 실행 중인지 확인
# PostgreSQL
pg_isready -h localhost -p 5432
# MySQL
mysqladmin ping -h localhost -P 3306
# SQLite -- 서버가 필요 없으니 파일 경로 확인
ls ./mydb.sqlite잘못된 설정 오류
v0.9.x부터 register()는 연결 전에 옵션을 검증합니다. ORM_INVALID_CONFIG가 보인다면:
OrmError [ORM_INVALID_CONFIG]: Invalid database configuration:
- 'port' must be an integer between 1 and 65535, got "3306".port가 숫자인지(.env에서 가져온 문자열이 아닌지) 확인하고 모든 필수 필드가 있는지 확인하세요.
// 잘못된 사용
{ port: process.env.DB_PORT } // 문자열 "3306"
// 올바른 사용
{ port: parseInt(process.env.DB_PORT || "5432", 10) }엔티티 & 데코레이터 오류
"Entity metadata not found"
OrmError [ORM_ENTITY_METADATA_NOT_FOUND]: Entity metadata for "User" was not found.원인:
- 클래스에
@Entity()데코레이터가 없음 entities배열에 엔티티 클래스가 등록되지 않음- 진입점 파일 상단에
import "reflect-metadata"가 빠짐
// 1. @Entity() 추가
@Entity()
class User {
@PrimaryGeneratedColumn()
id!: number;
}
// 2. entities 배열에 포함
await em.register({
entities: [User], // <-- 잊지 마세요
...
});
// 3. reflect-metadata 임포트 (앱 최상단에서 한 번)
import "reflect-metadata";"Primary key not found"
OrmError [ORM_PRIMARY_KEY_NOT_FOUND]: Primary key for entity "User" was not found.모든 엔티티는 최소 하나의 기본 키 컬럼이 필요합니다:
@Entity()
class User {
@PrimaryGeneratedColumn() // 자동 증가
id!: number;
// 또는 UUID
@PrimaryGeneratedColumn("uuid")
id!: string;
// 또는 수동 PK
@PrimaryColumn()
code!: string;
}컬럼이 데이터베이스에 저장되지 않음
클래스에 속성이 있는데 저장되지 않는다면 @Column()을 잊었을 가능성이 큽니다:
@Entity()
class User {
@PrimaryGeneratedColumn()
id!: number;
@Column() // <-- 영속화되는 모든 필드에 필요
name!: string;
bio: string; // 저장되지 않음 -- @Column() 없음
}쿼리 오류
N+1 쿼리 문제
다음과 같은 반복 쿼리가 보인다면:
SELECT * FROM "post" WHERE "author_id" = 1
SELECT * FROM "post" WHERE "author_id" = 2
SELECT * FROM "post" WHERE "author_id" = 3N+1 감지를 켜고 즉시 로딩을 사용하세요:
// 감지 활성화
await em.register({
logging: { nPlusOne: true },
...
});
// 해결: 관계를 미리 로드
const users = await em.find(User, {
relations: ["posts"],
});"Unknown column in criteria"
OrmError [ORM_INVALID_QUERY]: Unknown column "userName" in criteria for entity "User".where 절의 키가 어떤 컬럼과도 일치하지 않습니다. 속성명을 확인하세요(DB 컬럼명이 아닙니다):
// 엔티티가 다음과 같다면:
@Column({ name: "user_name" })
name!: string;
// DB 컬럼명이 아닌 속성명을 사용하세요
await em.find(User, { where: { name: "Alice" } }); // 정상
await em.find(User, { where: { user_name: "Alice" } }); // 잘못됨falsy 값을 가진 WHERE 절
0, false, ""는 유효한 값입니다. where 절에서 정상적으로 동작합니다:
await em.find(User, { where: { age: 0 } }); // age = 0인 사용자 조회
await em.find(User, { where: { active: false } }); // 비활성 사용자 조회관계 오류
관계 데이터가 로드되지 않음
관계는 기본적으로 lazy 입니다. 명시적으로 요청해야 합니다:
// 이렇게 하면 posts가 로드되지 않습니다
const user = await em.findOne(User, { where: { id: 1 } });
console.log(user.posts); // undefined
// 이렇게 하면 posts가 로드됩니다
const user = await em.findOne(User, {
where: { id: 1 },
relations: ["posts"],
});
console.log(user.posts); // Post[]또는 관계를 eager로 표시할 수 있습니다:
@OneToMany(() => Post, (post) => post.author, { eager: true })
posts!: Post[];순환 관계 오류
두 엔티티가 서로를 참조할 때는 지연 함수 참조를 사용하세요:
// 순환 임포트 문제를 피하려면 () => Entity 사용
@ManyToOne(() => Author)
author!: Author;
@OneToMany(() => Post, (post) => post.author)
posts!: Post[];SQLite 관련 이슈
SQLite에서 지원되지 않는 기능
SQLite는 다음을 지원하지 않습니다:
ALTER COLUMN(타입 변경, 이름 변경)DROP COLUMN(SQLite 3.35.0 이전)ENUM타입 (대신varchar사용)- 스키마 네임스페이스
- 동시 쓰기 트랜잭션 다중 실행
파괴적 작업을 피하려면 synchronize: "safe"를 사용하세요:
await em.register({
type: "sqlite",
database: "./mydb.sqlite",
synchronize: "safe", // 테이블 생성과 컬럼 추가만 수행
entities: [User],
});마이그레이션 오류
"Migration table already exists"
정상입니다. ORM이 적용된 마이그레이션을 추적하기 위해 __migrations 테이블을 만듭니다. 이 테이블이 이미 존재한다는 오류가 보인다면, 여러 프로세스에서 동시에 실행되어 발생한 경합 조건일 가능성이 큽니다. 마이그레이션은 단일 프로세스에서 실행하세요.
생성된 마이그레이션에 TODO가 있음
migrate:generate가 TODO 주석이 들어간 불완전한 SQL을 만들었다면, 스키마 diff가 정확한 DDL을 결정할 수 없었다는 의미입니다. 실행하기 전에 생성된 파일을 직접 수정하세요.
# 생성
npx stingerloom migrate:generate -n AddUserEmail
# 생성된 파일을 검토 후 수정한 다음 실행
npx stingerloom migrate:runSynchronize 모드
| 모드 | 테이블 생성 | 컬럼 추가 | 컬럼 변경 | 컬럼 삭제 |
|---|---|---|---|---|
true | Yes | Yes | Yes | Yes |
"safe" | Yes | Yes | No | No |
"dry-run" | 로그만 | 로그만 | 로그만 | 로그만 |
false | No | No | No | No |
권장: synchronize: true는 개발에서만 사용하세요. 프로덕션에서는 마이그레이션을 사용하세요.
디버깅 팁
SQL 로깅 활성화
await em.register({
logging: {
queries: true, // 모든 SQL 쿼리 로그
slowQueryMs: 1000, // 1초 이상 쿼리 경고
nPlusOne: true, // N+1 패턴 감지
},
...
});EXPLAIN으로 쿼리 분석
const plan = await em.explain(User, {
where: { status: "active" },
relations: ["posts"],
});
console.log(plan);엔티티 메타데이터 확인
import { ENTITY_TOKEN } from "@stingerloom/orm";
const meta = Reflect.getMetadata(ENTITY_TOKEN, User);
console.log(meta); // { name, columns, relations, ... }