Skip to content

Commit

Permalink
[NDD-235]: Redis를 이용한 비디오 URL 해싱 (6h / 5h) (#90)
Browse files Browse the repository at this point in the history
* chore: redis 사용을 위한 ioredis 의존성 추가

* feat: 싱글톤으로 Redis Client를 생성하는 로직 구현

* feat: 비디오 url 단방향 해시 로직 추가

* feat: redis client를 사용하여 저장하고 삭제하는 메서드 구현 완료

* feat: 비디오 상태 토글 시 redis에 해시값이 알맞게 저장/삭제 되고, 클라이언트에게도 알맞게 해시값/null이 반환되도록 구현

* feat: 비디오 hash로 조회 시 redis에서 조회 후 원본 URL 반환하도록 구현

* refactor: 사용하지 않는 메서드 및 로직 정리

* feat: md5 해싱 시 에러가 발생할 경우를 핸들링하기 위한 try catch 추가

* test: 로직의 변경된 사항들에 맞추어 video controller 단위 테스트 코드 변경

* refactor: redisInstance를 불러오는 로직에서 if문 이중중첩을 방지하기 위해 코드 변경
  • Loading branch information
quiet-honey authored Nov 23, 2023
1 parent e384e08 commit c30e748
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 47 deletions.
71 changes: 71 additions & 0 deletions BE/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions BE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"ioredis": "^5.3.2",
"mysql2": "^3.6.1",
"mysql3": "^0.6.0",
"nest-winston": "^1.9.4",
Expand Down
49 changes: 49 additions & 0 deletions BE/src/util/redis.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Redis from 'ioredis';
import 'dotenv/config';
import {
RedisDeleteException,
RedisRetrieveException,
RedisSaveException,
} from 'src/video/exception/video.exception';

let redisInstance: Redis;

function getRedisInstance() {
if (redisInstance) return redisInstance;

const redisUrl: string = process.env.REDIS_URL as string;
if (redisUrl) {
redisInstance = new Redis(redisUrl);
return redisInstance;
}

throw new Error('REDIS_URL 환경 변수가 설정되지 않았습니다.');
}

export const saveToRedis = async (key: string, value: string) => {
try {
const redis = getRedisInstance();
await redis.set(key, value);
} catch (error) {
throw new RedisSaveException();
}
};

export const deleteFromRedis = async (key: string) => {
try {
const redis = getRedisInstance();
await redis.del(key);
} catch (error) {
throw new RedisDeleteException();
}
};

export const getValueFromRedis = async (key: string) => {
try {
const redis = getRedisInstance();
const value = await redis.get(key);
return value;
} catch (error) {
throw new RedisRetrieveException();
}
};
60 changes: 44 additions & 16 deletions BE/src/video/controller/video.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { Request, Response } from 'express';
import { CreatePreSignedUrlRequest } from '../dto/createPreSignedUrlRequest';
import { PreSignedUrlResponse } from '../dto/preSignedUrlResponse';
import {
DecryptionException,
EncryptionException,
IDriveException,
Md5HashException,
RedisDeleteException,
RedisRetrieveException,
RedisSaveException,
VideoAccessForbiddenException,
VideoNotFoundException,
VideoOfWithdrawnMemberException,
Expand Down Expand Up @@ -250,17 +252,17 @@ describe('VideoController 단위 테스트', () => {
);
});

it('해시로 비디오 조회 시 복호화에 실패하면 DecryptionException을 반환한다.', async () => {
it('해시로 비디오 조회 시 Redis에서 비디오의 URL을 얻어내는 중 에러가 발생하면 RedisRetrieveException을 반환한다.', async () => {
// given

// when
mockVideoService.getVideoDetailByHash.mockRejectedValue(
new DecryptionException(),
new RedisRetrieveException(),
);

// then
expect(controller.getVideoDetailByHash(hash)).rejects.toThrow(
DecryptionException,
RedisRetrieveException,
);
});
});
Expand Down Expand Up @@ -355,17 +357,15 @@ describe('VideoController 단위 테스트', () => {
);
});

it('비디오 상세 정보 조회 시 암호화에 실패하면 EncryptionException을 반환한다.', async () => {
it('비디오 상세 정보 조회 시 해시 생성에 실패한다면 Md5HashException를 반환한다.', async () => {
// given

// when
mockVideoService.getVideoDetail.mockRejectedValue(
new EncryptionException(),
);
mockVideoService.getVideoDetail.mockRejectedValue(new Md5HashException());

// then
expect(controller.getVideoDetail(1, mockReq)).rejects.toThrow(
EncryptionException,
Md5HashException,
);
});
});
Expand Down Expand Up @@ -407,7 +407,7 @@ describe('VideoController 단위 테스트', () => {
expect(result.hash).toBeNull();
});

it('비디오 상세 정보 조회 시 회원 객체가 없으면 ManipulatedTokenNotFilteredException을 반환한다.', async () => {
it('비디오 상태 토글 시 회원 객체가 없으면 ManipulatedTokenNotFilteredException을 반환한다.', async () => {
// given
const nullMember = { user: null } as unknown as Request;

Expand All @@ -422,7 +422,7 @@ describe('VideoController 단위 테스트', () => {
);
});

it('비디오 상세 정보 조회 시 해당 비디오가 삭제되었다면 VideoNotFoundException를 반환한다.', async () => {
it('비디오 상태 토글 시 해당 비디오가 삭제되었다면 VideoNotFoundException를 반환한다.', async () => {
// given

// when
Expand All @@ -436,7 +436,7 @@ describe('VideoController 단위 테스트', () => {
);
});

it('비디오 상세 정보 조회 시 다른 회원의 비디오를 조회하려 한다면 VideoAccessForbiddenException를 반환한다.', async () => {
it('비디오 상태 토글 시 다른 회원의 비디오를 토글하려 한다면 VideoAccessForbiddenException를 반환한다.', async () => {
// given

// when
Expand All @@ -450,17 +450,45 @@ describe('VideoController 단위 테스트', () => {
);
});

it('비디오 상세 정보 조회 시 암호화에 실패하면 EncryptionException을 반환한다.', async () => {
it('비디오 상태 토글 시 url 해싱에 실패한다면 Md5HashException를 반환한다.', async () => {
// given

// when
mockVideoService.toggleVideoStatus.mockRejectedValue(
new Md5HashException(),
);

// then
expect(controller.toggleVideoStatus(1, member)).rejects.toThrow(
Md5HashException,
);
});

it('비디오 상태 토글 시 Redis에 데이터 저장 중 에러가 발생한다면 RedisSaveException을 반환한다.', async () => {
// given

// when
mockVideoService.toggleVideoStatus.mockRejectedValue(
new RedisSaveException(),
);

// then
expect(controller.toggleVideoStatus(1, member)).rejects.toThrow(
RedisSaveException,
);
});

it('비디오 상태 토글 시 Redis에서 데이터 삭제 도중 에러가 발생한다면 RedisDeleteException을 반환한다.', async () => {
// given

// when
mockVideoService.toggleVideoStatus.mockRejectedValue(
new EncryptionException(),
new RedisDeleteException(),
);

// then
expect(controller.toggleVideoStatus(1, member)).rejects.toThrow(
EncryptionException,
RedisDeleteException,
);
});
});
Expand Down
20 changes: 16 additions & 4 deletions BE/src/video/exception/video.exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,27 @@ export class VideoNotFoundException extends HttpException {
}
}

export class EncryptionException extends HttpException {
export class RedisSaveException extends HttpException {
constructor() {
super('암호화 중 에러가 발생했습니다.', 500);
super('Redis에 저장 중 오류가 발생하였습니다.', 500);
}
}

export class DecryptionException extends HttpException {
export class RedisDeleteException extends HttpException {
constructor() {
super('복호화 중 에러가 발생했습니다.', 500);
super('Redis에서 삭제 중 오류가 발생하였습니다.', 500);
}
}

export class RedisRetrieveException extends HttpException {
constructor() {
super('Redis에서 정보를 가져오는 중 오류가 발생하였습니다.', 500);
}
}

export class Md5HashException extends HttpException {
constructor() {
super('MD5 해시 생성 중 오류가 발생했습니다.', 500);
}
}

Expand Down
Loading

0 comments on commit c30e748

Please sign in to comment.