개발을 계속하면서 여러 문제를 마주하게 되었다. 그 중 몇 가지 기억에 남는 문제 중 개발하면서 마주친 데이터베이스에 대한 문제 이야기해보려고 한다.
모든 코드는 예시코드입니다.
솔직히 말하자면, 우리 Strapi에는 버그 아닌 버그가 있었다.
바로 타임존(Timezone) 문제... 물론 내가 만든 건 아니지만 ㅎㅎ…
사실 예전에 잠깐 백엔드 인수인계를 받았을 때 전 개발자분께 여쭤봤던 적이 있다. "서버시간 한국시간으로 안 맞는 것 같은데요?" → "Strapi 설정 때문인지 맞지 않아서 로직적으로 타임존 처리를 했습니다." → ?
그때는 그냥 그렇구나 했지만, 내 백엔드 짧은 개발 구력(사실 프론트엔드였지만 백엔드도 좋아했다.)으로 봤을 때, 타임존 문제는 어딘가 설정이 잘못된 거였다. 경험상 서버나 RDS 설정이 UTC로 되어있으면 당연히 9시간 차이가 나게 되어 있지. 그래서 한국 시간대로 제대로 저장이 안 되는 거겠지라고 생각했다.
다행히도 NestJS에는 TypeORM이 있어서 @CreateDateColumn()과 @UpdateDateColumn() 같은 데코레이터가 자동으로 생성 시간(created_at)과 업데이트 시간(updated_at)을 넣어준다.
TypeScript@Entity('Entity', { schema: 'public' }) export class Entity { @PrimaryGeneratedColumn({ type: 'integer', name: 'id' }) id: number; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; }
"오, 그러면 뭐 금방 고치겠네?" 라고 생각했다. 하지만, 이게 웬걸? 아무리 RDS와 백엔드 서버를 시작할 때 데이터베이스 쿼리로 Asia/Seoul로 설정해도, bootstrap()의 서버타임존을 아무리 초기화해도, @CreateDateColumn()과 @UpdateDateColumn()에 Date 형식의 데이터가 제대로 안 들어갔다.
TypeScriptthis.createdAt = new Date();
이렇게 로직적으로 해결할 수는 있었다. 그러나 이 문제를 언젠가는 제대로 고쳐야 한다는 걸 알았고, 내 자존심이 허락하지 않았다.
이미 많은 데이터가 DB에 쌓여 있어서 typorm과의 동기화가 안되어있나를 의심하게 되었지만 아무리 로컬환경이여도, synchronize: true 같은 위험한 옵션을 켤 수는 없었다. (절대 켜면 안 된다...) 그래서 완벽한 마이그레이션 기능을 구현하기 전까지는 다른 해결책을 찾아야 했다.
ts-node를 이용한 쉬운 필드 변경 설정이다. 언젠가는 마이그레이션 마지막 단계(과연 올까…)에 사용할려고, 정의해놓은 아래 옵션을 사용하며 아예 새로운 필드명으로 정의해볼까 생각도 했다.JSON"db:migrate": "npm run typeorm migration:run -- -d src/data-source.ts"
TypeScriptconst dataSource = new DataSource( // 데이터베이스 연결 설정 객체 ); export default dataSource;
이 문제의 근본은 PostgreSQL의 시간 처리 방식과 Strapi의 설정에서 오는 미묘한 차이였다.
PostgreSQL은 시간을 관리할 때 두 가지 방식이 있다:
TIMESTAMP WITHOUT TIME ZONE: 시간대 정보 없이 시간만 기록.TIMESTAMP WITH TIME ZONE: 시간대 정보를 포함한 시간 기록.문제는 Strapi가 기본적으로 TIMESTAMP WITHOUT TIME ZONE을 사용한다는 것. 서버가 서울 시간대든 어디든 간에, 결국 UTC 기준으로 시간이 기록되었다.
결국 수동으로 해결해야 했다.
Strapi가 생성한 테이블에서 TIMESTAMP WITHOUT TIME ZONE을 TIMESTAMP WITH TIME ZONE으로 바꾸고, 서버 시간대를 반영하게 해야 했다.
TIMESTAMP WITHOUT TIME ZONE → TIMESTAMP WITH TIME ZONE먼저 테이블의 시간대 설정을 바꿔주었다. 그래야만 UTC가 아닌 서울 시간대(KST)로 기록되도록 할 수 있었다.
SQLALTER TABLE public.some ALTER COLUMN created_at TYPE timestamp with time zone, ALTER COLUMN updated_at TYPE timestamp with time zone;
이렇게 바꾸면 Strapi가 사용하는 테이블의 시간이 UTC가 아닌 서버의 시간대로 저장된다.
CURRENT_TIMESTAMP그 다음은 테이블에 기본 값을 설정하는 것이다. CURRENT_TIMESTAMP를 설정해줘야만 @CreateDateColumn()과 @UpdateDateColumn() 데코레이터가 제대로 동작하게 된다.
SQLALTER TABLE public.some ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP, ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP;
이렇게 하면 새로운 레코드가 추가되거나 업데이트될 때, UTC가 아닌 서울 시간대로 자동으로 시간이 기록된다.
결국 Strapi와 TypeORM 간의 타임존 문제는 PostgreSQL 테이블 설정에서 발생한 것이었다.
이걸 해결하려면 수동으로 쿼리를 작성해 테이블을 수정하고, CURRENT_TIMESTAMP 같은 기본 값 설정도 직접 해줘야 했다.
하지만 이 작업을 끝낸 후로는 더 이상 타임존 문제로 골머리를 앓을 필요가 없었다.
이제 내가 만든 기능 부분은 Strapi와 NestJS가 서로 잘 맞춰서 서울 시간대(KST)로 정확히 동작하게 되었다.
또한 알게된 사실은 이렇게 수동설정하고 Strapi에서 필드를 조작하게 되어도 성질이 변하지 않는다.
created_at와 updated_at 설정을 고정하는 방법을 고안하지 않아도 되었다.→ 나중에 로직적으로 집어넣는 부분을 전부 해결해하면, 자동화 쿼리를 짜서 바꾸겠지만 그전까지는 필요한 필드만 수동으로 변경할 것 같다.
데이터베이스 이야기가 나온 김에, 내가 만든 서버의 보안은 정말 안전한가에 대한 생각이 들어 NestJS의 보안 부를 재구성한 이야기를 해볼까한다.
원래는 로걸에서는 기본 yml파일을 통해 production 환경과, development만 분기하고, .env 파일을 사용한 다음과 같은 설정을 적용했었다.
TypeScriptimport { join } from 'path'; export const typeormConfig = new DataSource({ type: 'postgres', host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: [], migrations: [`${__dirname}/migrations/*.ts`], synchronize: false, logging: env, });
그런데 배포 사양으로 맞추고, 배포를 진행하기 앞서, NestJS의 ConfigService를 사용해 .env 파일의 타입과 유효성 검사를 추가했다. 이때 Joi를 사용하여 데이터베이스 연결 설정에 대한 유효성 검사를 적용했다.
예전에 프론트엔드 공부하면서 Joi로 인풋창의 유효성 검사를 했던 기억이 났다. 그런데 이번에는 데이터베이스 연결 설정에서 유효성 검사를 적용할 수 있다는 사실을 알게 되었고, 이를 바로 적용했다.
TypeScriptimports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', loadYamlConfig()], validationSchema: Joi.object({ NODE_ENV: Joi.string().valid('development', 'production').required(), RDS_TYPE: Joi.string().required(), RDS_HOST: Joi.string().required(), RDS_PORT: Joi.number().required(), RDS_USERNAME: Joi.string().required(), RDS_PASSWORD: Joi.string().required(), RDS_DATABASE: Joi.string().required(), }), }), // TypeORM 비동기 연결 설정 TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => typeormConfig(configService), inject: [ConfigService], }), // 다른 설정들 임포트 ],
위 코드에서 useFactory를 사용하여 ConfigService를 의존성 주입하고, TypeOrmModule.forRootAsync로 TypeORM의 설정을 관리하게 했다.
만약 일전처럼 TypeOrmModule.forRoot 메서드를 사용하게 된다면 의존성 주입을 시킬 수가 없었다.
TypeScriptimport { ConfigService } from '@nestjs/config'; import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { join } from 'path'; export const typeormConfig = (configService: ConfigService): TypeOrmModuleOptions => ({ type: configService.get<string>('RDS_TYPE') as 'postgres', host: configService.get<string>('RDS_HOST'), port: configService.get<number>('RDS_PORT'), username: configService.get<string>('RDS_USERNAME'), password: configService.get<string>('RDS_PASSWORD'), database: configService.get<string>('RDS_DATABASE'), entities: [join(__dirname, '**', '*.entity.{ts,js}')], synchronize: false, logging: configService.get('NODE_ENV') === 'development' ? ['query', 'error'] : false, // 개발 환경에서만 SQL 로깅 활성화 extra: { timezone: 'Asia/Seoul', }, });
위의 설정을 보면, Joi로 유효성 검사를 추가함으로써 데이터베이스 설정이 제대로 입력되지 않으면 오류가 발생한다. 예를 들어, RDS_TYPE 필드가 postgres라는 문자열이 아닌 즉, 제대로 설정되지 않으면 문제가 생기고, NODE_ENV설정이 development이나 production의 문자열이 아니면 유효성 검사, 즉 에러를 발생시킨다.
이렇게 설정을 마무리한 후에는, Client-side 뿐만 아니라 Server-side에서도 보안을 강화할 방법을 생각하게 되었다.
특히 AWS의 Key Management Service(KMS)를 사용하여 데이터를 암호화하고 키를 관리하는 방법을 고려 중이다. KMS와의 조합을 통해 더 안전한 보안 체계를 마련할 수 있을 것 같다.
향후 계획으로는 KMS를 활용해 서버 보안을 더욱 강화할 예정이다.
외래키는 실무에서 지양 vs 지향하는게 좋다라는 의견들이 있고, 사실 어느 정도 결과는 에상이 가지만, 다른 분들의 의견도 궁금합니다.
외래키를 사용하면 데이터의 무결성을 보장하고, 관계를 명확하게 정의할 수 있다는 큰 장점이 있듯이,
데이터베이스 상에서 명시적으로 관계를 보여주니까, 데이터를 유지보수할 때도 관리하기가 쉽고 실수가 적어진다고 생각합니다.
하지만, 반대로 중간 테이블을 선언해서 외래키 대신 사용하면 null 처리에도 유리하고, 데이터 구조를 유연하게 만들 수 있다는 얘기도 많이보입니다. 특히 외래키로 강하게 묶여 있는 경우보다 더 복잡한 관계를 다룰 때 유연성이 생기고, 일부 데이터가 없어도 동작에 문제가 없으니까 확장성 면에서 좋다 느끼고, 그러다 보니, 특히 null 처리 같은 부분에서 중간 테이블이 더 안전하게 느껴지기도 합니다.
그래서 이번 개모임 마지막에, 실무에서 다들 외래키에 대해 어떻게 생각하시는지, 그리고 외래키 vs 중간 테이블 중에서 어떤 선택을 더 선호하시는지 여쭤보고 싶습니다. 물론 상황에 따라 달라질 수 있겠지만, 여러분의 경험을 들어보고 싶습니다.