본문 바로가기
Back-end/nest.js

Nest.js : Session 로그인을 위한 Redis 연동, 그리고 Ports and Adapters Architecture에 적용하기

by hsloth 2023. 2. 16.

 

처음으로 nest.js에 Redis라는 것을 사용해보기 위해 이리저리 구글링을 해보았다. 캐시용으로 말고, 세션용으로!

진짜 굉장히 삽질 많이했다.

그냥 쓰라고하면 쓰겠는데, Ports and Adapters 아키텍쳐에 맞게 무언가 만들려고 하니까... 머리가 더욱 복잡했다.

결국 최적의 방법은 찾지 못한듯 하다 ㅠㅠ

 

나는 CacheModule의 인터셉터와 데코레이터를 사용하지 않고, cacheManager 만을 이용해서 redis를 이용한 세션 로그인을 구현하고 싶었다. (그리고 저걸로 해도 아마 안될 것이다. 딥하게 건드리지 않는 이상)

 

그래서 생각한 첫 번째 방법이 RedisModule과 RedisService를 만들어서 필요한 모듈마다 import하는 것이다.

// redis.module.ts
import { CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-ioredis';

@Module({
  imports: [
    CacheModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        isGlobal: true,
        store: redisStore,
        host: configService.get('REDIS_HOST'),
        port: configService.get('REDIS_PORT'),
        ttl: 5000,
      }),
    }),
  ],
  controllers: [],
  providers: [RedisService],
  exports: [RedisService],
})
export class RedisModule {}

// redis.service.ts
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class RedisService {
	constructor(
    	@Inject(CACHE_MANAGER)
        private readonly cacheManager: Cache,
    ) {}
    
      async get(key: string): Promise<any> {
        return await this.cacheManager.get(key);
      }

      async set(key: string, value: any, ttl?: number): Promise<void> {
        await this.cacheManager.set(key, value, ttl);
      }

      async del(key: string): Promise<void> {
        await this.cacheManager.del(key);
      }

      async reset(): Promise<void> {
        await this.cacheManager.reset();
      }
}
  • 여기서 주의할 점은 Cache는 항상 cache-manager 에서 import 되어야 한다는 것을 잊지말자는 것이다. 그냥 자동완성으로 Cache를 쓸 시, 따로 import 절이 생성되지 않는다.
  • 위처럼 코드를 작성했으면 아래와 같이 사용하면 된다.
// cat.module.ts
@Module({
	imports: [RedisModule],
	...
})
export class CatModule {}
  • 자신이 Redis를 사용하고 싶은 모듈에 import를 하고
// cat.service.ts
@Injectable()
export class CatService {
	constructor(
    	private readonly redisService: RedisService,
    ) {}
    
    async setCat(name: string) {
    	await this.redisService.set(name, new Date().getTime());
    }
}
  • 이런 식으로 RedisService를 주입하여 사용할 수 있다.
  • 여기서 주의할 점은 CatModule에 RedisModule 이외에 RedisService를 주입하면 안된다. (의존성 문제가 터집니다...)

 

 

그런데 Ports and Adapters Architecture에 적용하기에는 좋지 않았다.

ports and adapters 구조에 사용하기에는 Redis가 Service로부터 분리되지 않아 적합하지 않았다.

그래서 아래의 방법을 생각해냈다. 하지만, 이 방법은 전혀 nest스럽지 않은 방법 같은 느낌이 든다...

 

먼저, Redis Module을 변수로 export한다.

그리고 RedisModule을 Class가 아닌 변수의 형태로 사용할 Module에 주입하는 방식이다.

// redis.module.ts
import { CacheModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as redisStore from 'cache-manager-ioredis';

export const RedisModule = CacheModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    isGlobal: true,
    store: redisStore,
    host: configService.get('REDIS_HOST'),
    port: configService.get('REDIS_PORT'),
    ttl: 5000,
  }),
});

// cat.module.ts

@Module({
	imports: [RedisModule],
    ...
})
export CatModule {}

 

Redis Service를 만들지 않고 Redis Repository 파일을 만든다.

// redis-repository.outbound-port.ts
export const REDIS_REPOSITORY_OUTBOUND_PORT =
  'REDIS_REPOSITORY_OUTBOUND_PORT' as const;

export type RedisGetOutbountInputDto = {
  key: string;
};
export type RedisGetOutboundOutputDto = Promise<any>;

export type RedisSetOutboundInputDto = {
  key: string;
  value: any;
  ttl?: number;
};
export type RedisSetOutboundOutputDto = Promise<void>;

export type RedisDelOutboundInputDto = {
  key: string;
};
export type RedisDelOutboundOutputDto = Promise<void>;

export type RedisResetOutboundInputDto = void;
export type RedisResetOutboundOutputDto = Promise<void>;

export interface RedisRepositoryOutboundPort {
  get(params: RedisGetOutbountInputDto): RedisGetOutboundOutputDto;

  set(params: RedisSetOutboundInputDto): RedisSetOutboundOutputDto;

  del(params: RedisDelOutboundInputDto): RedisDelOutboundOutputDto;

  reset(params: RedisResetOutboundInputDto): RedisResetOutboundOutputDto;
}


// redis.repository.ts
import { CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import {
  RedisDelOutboundInputDto,
  RedisDelOutboundOutputDto,
  RedisGetOutboundOutputDto,
  RedisGetOutbountInputDto,
  RedisRepositoryOutboundPort,
  RedisResetOutboundInputDto,
  RedisResetOutboundOutputDto,
  RedisSetOutboundInputDto,
  RedisSetOutboundOutputDto,
} from './redis-repository.outbound-port';

export class RedisRepository implements RedisRepositoryOutboundPort {
  constructor(
    @Inject(CACHE_MANAGER)
    private readonly cacheManager: Cache,
  ) {}
  async get(params: RedisGetOutbountInputDto): RedisGetOutboundOutputDto {
    return await this.cacheManager.get(params.key);
  }

  async set(params: RedisSetOutboundInputDto): RedisSetOutboundOutputDto {
    await this.cacheManager.set(params.key, params.value, params.ttl);
  }

  async del(params: RedisDelOutboundInputDto): RedisDelOutboundOutputDto {
    await this.cacheManager.del(params.key);
  }

  async reset(params: RedisResetOutboundInputDto): RedisResetOutboundOutputDto {
    await this.cacheManager.reset();
  }
}

 

이렇게 하면, Redis Repository를 outbound Port의 토큰을 이용하여 주입해서 사용할 수 있다.

@Module({
	imports: [RedisModule],
    providers: [
    	{
        	provider: REDIS_REPOSITORY_OUTBOUND_PORT,
            useClass: RedisRepository,
        }
    ],

})
export class CatModule {}



// cat.service.ts
@Injectable()
export class CatService {
	constructor(
    	@Inject(REDIS_REPOSITORY_OUTBOUND_PORT)
        private readonly redisRepositoryOutboundPort: RedisRepositoryOutboundPort,
    ) {}
	
    
      async serializeUser(
        params: SerializeUserInboundInputDto,
      ): Promise<SerializeUserInboundOutputDto> {
        await this.redisRepositoryOutboundPort.set({
          key: params.user,
          value: params.date,
          ttl: params.ttl,
        });
      }

}