Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

자동 배포 구현 #419

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
0dc0e39
refactor: 에디터 unsaved 뱃지 제거
pkh0106 Jan 8, 2025
7aa8272
refactor: websocket 디렉토리 내부 setField 모두 setFields로 변경
mssak Jan 8, 2025
72d1bb7
refactor: redis 트랜젝션 방식 변경
mssak Jan 8, 2025
1bd2153
refactor: useCanvas 미사용용 반환값 제거
pkh0106 Jan 8, 2025
3fe4308
refactor: 빈 문자열 사용 제거거
pkh0106 Jan 8, 2025
d5e2e07
refactor: 내부에서만 사용하는 모듈 export 제거거
pkh0106 Jan 8, 2025
860d156
refactor: page 관련 미사용 API 제거거
pkh0106 Jan 8, 2025
af7ca2a
refactor: Separator default 변경
pkh0106 Jan 8, 2025
10b4644
refactor: useEffect 중복 로직 제거
pkh0106 Jan 8, 2025
eff8680
refactor: TODO 주석 삭제
baegyeong Jan 8, 2025
bc6a568
refactor: 필요없는 주석 삭제
baegyeong Jan 8, 2025
c9a832a
fix: 스케쥴러 돌때 redis 데이터가 제대로 삭제되지 않는 오류 수정정
mssak Jan 9, 2025
ea4f324
chore: 주석 제거
mssak Jan 9, 2025
f475ef8
chore: 임시로 edge delete 기능 비활성화
mssak Jan 9, 2025
40d14f0
Merge pull request #10 from boostcampwm-2024/refactor-be-#4
ezcolin2 Jan 9, 2025
5feffd9
refactor: EditorView의 스켈레톤 UI 추가
baegyeong Jan 9, 2025
6b8db17
refactor: 도커 최적화 - 중복되는 볼륨 처리
mssak Jan 9, 2025
1b3a74e
refactor: editorView에 lazy loading 적용
baegyeong Jan 9, 2025
e9b183b
fix: 스켈레톤 UI에서 prop으로 스타일을 받도록 수정
baegyeong Jan 9, 2025
5fa6292
refactor: 스켈레톤 UI불러올 때 크기를 지정하도록 설정
baegyeong Jan 9, 2025
47cbcb7
fix: 스켈레톤 컴포넌트 내의 padding 제거
pkh0106 Jan 9, 2025
a611c67
Merge pull request #11 from boostcampwm-2024/refactor-be-#4
ezcolin2 Jan 9, 2025
972f836
refactor: 워크스페이스 패널 분리 및 lazy loading 적용
pkh0106 Jan 9, 2025
fd6eb9a
Merge pull request #6 from boostcampwm-2024/refactor-fe-#2
pkh0106 Jan 9, 2025
77ba38f
refactor: 페이지 목록 패널 분리 및 lazy loading 적용
pkh0106 Jan 9, 2025
0d9a0b3
refactor: 사용자 정보 관리 패널 lazy loading 적용
pkh0106 Jan 9, 2025
374837f
fix: cn을 이용하여 Skeleton 컴포넌트 내 div 통합
pkh0106 Jan 9, 2025
3fa0411
refactor: 노드 색상 관리 패널 lazy loading 적용
pkh0106 Jan 9, 2025
5446971
Merge pull request #12 from boostcampwm-2024/refactor-fe-#9
pkh0106 Jan 9, 2025
94414c0
refactor: red lock 제거 후 redis 낙관적 락 적용
ezcolin2 Jan 9, 2025
8ca857c
Merge pull request #14 from boostcampwm-2024/refactor-be-#13
ezcolin2 Jan 9, 2025
39a8bea
feat: migrate 함수와 redis 값 변경 연산에 redis 분산 락 적용
ezcolin2 Jan 11, 2025
7b8fc80
Merge pull request #16 from boostcampwm-2024/refactor-be-#15
ezcolin2 Jan 12, 2025
8b8f683
fix: redis service의 setFields가 존재하지 않아서 추가
ezcolin2 Jan 13, 2025
4ba4a74
refactor: LCP 개선을 위한 Suspense 적용
baegyeong Jan 13, 2025
29308c9
refactor: 접근성 개선을 위한 aria label 지정
baegyeong Jan 13, 2025
471a038
refactor: 이미지 요소에 alt 속성 적용
baegyeong Jan 13, 2025
d49af48
refactor: 백그라운드와 포그라운드 대비 개선
baegyeong Jan 13, 2025
2ece725
Merge pull request #20 from boostcampwm-2024/refactor-fe-#17
baegyeong Jan 13, 2025
902535f
Merge pull request #19 from boostcampwm-2024/refactor-fe-#18
baegyeong Jan 13, 2025
c00d02d
refactor: 사용하지 않는 커스텀훅 삭제
baegyeong Jan 13, 2025
53f4f4e
feat: production에서 에러 console를 지우는 라이브러리 추가
baegyeong Jan 13, 2025
9d343ab
chore: index.html에 meta description 설명 추가
baegyeong Jan 13, 2025
930a59b
chore: SEO 개선을 위한robots.txt 파일 생성
baegyeong Jan 13, 2025
c204c21
Merge pull request #24 from boostcampwm-2024/refactor-fe-#21
pkh0106 Jan 13, 2025
2e01985
Merge pull request #23 from boostcampwm-2024/refactor-fe-#22
baegyeong Jan 13, 2025
4d7f3cf
refactor: docker 환경 개선
ezcolin2 Jan 13, 2025
48fecde
fix: redis lock을 획득하지 못 하는 이슈 해결
ezcolin2 Jan 13, 2025
90c9373
Merge branch 'refactor-be-#25' of https://github.com/boostcampwm-2024…
ezcolin2 Jan 13, 2025
2f5a7fa
feat: production 환경 용 nginx conf 파일 추가
ezcolin2 Jan 13, 2025
e9c5baa
refactor: docker 상용 환경 변경
ezcolin2 Jan 13, 2025
8c738b7
fix: certbot 인증서 이메일 및 도메인 변경
ezcolin2 Jan 13, 2025
24cdf62
refactor: 상용 환경 설정 파일 개선
ezcolin2 Jan 14, 2025
d0af367
refactor: connected 상태 색상 변경
baegyeong Jan 14, 2025
8da472d
chore: remove console을 위한 babel 설정 파일 삭제
baegyeong Jan 14, 2025
3c3c0b2
chore: vite-plugin-remove-console 설치
baegyeong Jan 14, 2025
4331ae9
Merge pull request #28 from boostcampwm-2024/refactor-fe-#27
pkh0106 Jan 14, 2025
f11821e
refactor: nginx 이미지 빌드할 때 리액트 정적 파일 삽입 구현
ezcolin2 Jan 14, 2025
3f0384d
refactor: websocket, backend 이미지 멀티 스테이지 빌드 제거
ezcolin2 Jan 14, 2025
063c02c
fix: react 정적 파일을 복사하는 nginx 디렉토리 이름 수정
ezcolin2 Jan 14, 2025
a3c2097
Merge pull request #29 from boostcampwm-2024/refactor-be-#25
summersummerwhy Jan 15, 2025
d0cea19
fix: Suspense를 사용하기 때문에 isLoading 상태 제거
baegyeong Jan 15, 2025
0c8604b
chore: 사용하지 않는 파일 삭제
baegyeong Jan 15, 2025
afa8b7a
Merge pull request #31 from boostcampwm-2024/refactor-fe-#27
github-actions[bot] Jan 15, 2025
8ebaa1f
hotfix: lint 오류 수정
ezcolin2 Jan 15, 2025
51f6677
fix: root package.json에 build 스크립트 추가
ezcolin2 Jan 15, 2025
08b8ca5
Merge pull request #32 from boostcampwm-2024/hotfix-be-#30
github-actions[bot] Jan 15, 2025
e431423
refactor: compose.prod.yml build를 override 파일로 분리
ezcolin2 Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Dockerfile.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# node_modules를 가지고 있는 이미지
# 이 이미지를 기반으로 각 workspace 별 이미지를 만들면
# yarn install 레이어를 공유하게 된다.
FROM node:20-alpine

WORKDIR /app

# 호이스팅을 위해
COPY package.json yarn.lock ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY apps/websocket/package.json ./apps/websocket/

# 의존성 설치
RUN yarn install
2 changes: 0 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { WorkspaceModule } from './workspace/workspace.module';
import { RoleModule } from './role/role.module';
import { TasksModule } from './tasks/tasks.module';
import { ScheduleModule } from '@nestjs/schedule';
import { RedLockModule } from './red-lock/red-lock.module';

@Module({
imports: [
Expand Down Expand Up @@ -54,7 +53,6 @@ import { RedLockModule } from './red-lock/red-lock.module';
WorkspaceModule,
RoleModule,
TasksModule,
RedLockModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
2 changes: 0 additions & 2 deletions apps/backend/src/page/page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ import { Page } from './page.entity';
import { PageRepository } from './page.repository';
import { NodeModule } from '../node/node.module';
import { WorkspaceModule } from '../workspace/workspace.module';
import { RedLockModule } from '../red-lock/red-lock.module';

@Module({
imports: [
TypeOrmModule.forFeature([Page]),
forwardRef(() => NodeModule),
WorkspaceModule,
RedLockModule,
],
controllers: [PageController],
providers: [PageService, PageRepository],
Expand Down
57 changes: 17 additions & 40 deletions apps/backend/src/page/page.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Inject } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { NodeRepository } from '../node/node.repository';
import { WorkspaceRepository } from '../workspace/workspace.repository';
import { PageRepository } from './page.repository';
Expand All @@ -8,30 +8,20 @@ import { UpdatePageDto } from './dtos/updatePage.dto';
import { UpdatePartialPageDto } from './dtos/updatePartialPage.dto';
import { PageNotFoundException } from '../exception/page.exception';
import { WorkspaceNotFoundException } from '../exception/workspace.exception';
import Redlock from 'redlock';

const RED_LOCK_TOKEN = 'RED_LOCK';

@Injectable()
export class PageService {
constructor(
private readonly pageRepository: PageRepository,
private readonly nodeRepository: NodeRepository,
private readonly workspaceRepository: WorkspaceRepository,
@Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock,
) {}
/**
* redis에 저장된 페이지 정보를 다음 과정을 통해 주기적으로 데이터베이스에 반영한다.
*
* 1. redis에서 해당 페이지의 title과 content를 가져온다.
* 2. 데이터베이스에 해당 페이지의 title과 content를 갱신한다.
* 3. redis에서 해당 페이지 정보를 삭제한다.
*
* 만약 1번 과정을 진행한 상태에서 page가 삭제된다면 오류가 발생한다.
* 위 과정을 진행하는 동안 page 정보 수정을 막기 위해 lock을 사용한다.
*
* 동기화를 위해 기존 페이지에 접근하여 수정하는 로직은 RedLock 알고리즘을 통해 락을 획득할 수 있을 때만 수행한다.
* 기존 페이지에 접근하여 연산하는 로직의 경우 RedLock 알고리즘을 사용하여 동시 접근을 방지한다.
*/
async createPage(dto: CreatePageDto): Promise<Page> {
const { title, content, workspaceId, x, y, emoji } = dto;
Expand Down Expand Up @@ -62,42 +52,29 @@ export class PageService {
}

async deletePage(id: number): Promise<void> {
// 락을 획득할 때까지 기다린다.
const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000);
try {
// 페이지를 삭제한다.
const deleteResult = await this.pageRepository.delete(id);
// 페이지를 삭제한다.
const deleteResult = await this.pageRepository.delete(id);

// 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것
if (!deleteResult.affected) {
throw new PageNotFoundException();
}
} finally {
// 락을 해제한다.
await lock.release();
// 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것
if (!deleteResult.affected) {
throw new PageNotFoundException();
}
}

async updatePage(id: number, dto: UpdatePageDto): Promise<Page> {
// 락을 획득할 때까지 기다린다.
const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000);
try {
// 갱신할 페이지를 조회한다.
// 페이지를 조회한다.
const page = await this.pageRepository.findOneBy({ id });

// 페이지가 없으면 NotFound 에러
if (!page) {
throw new PageNotFoundException();
}
// 페이지 정보를 갱신한다.
const newPage = Object.assign({}, page, dto);
// 갱신할 페이지를 조회한다.
// 페이지를 조회한다.
const page = await this.pageRepository.findOneBy({ id });

// 변경된 페이지를 저장
return await this.pageRepository.save(newPage);
} finally {
await lock.release();
// 페이지가 없으면 NotFound 에러
if (!page) {
throw new PageNotFoundException();
}
// 페이지 정보를 갱신한다.
const newPage = Object.assign({}, page, dto);

// 변경된 페이지를 저장
return await this.pageRepository.save(newPage);
}

async updateBulkPage(pages: UpdatePartialPageDto[]) {
Expand Down
27 changes: 0 additions & 27 deletions apps/backend/src/red-lock/red-lock.module.ts

This file was deleted.

5 changes: 2 additions & 3 deletions apps/backend/src/redis/redis.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Module, forwardRef } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisService } from './redis.service';
import Redis from 'ioredis';
import { RedLockModule } from '../red-lock/red-lock.module';

// 의존성 주입할 때 redis client를 식별할 토큰
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';

@Module({
imports: [ConfigModule, forwardRef(() => RedLockModule)], // ConfigModule 추가
imports: [ConfigModule], // ConfigModule 추가
providers: [
RedisService,
{
Expand Down
121 changes: 92 additions & 29 deletions apps/backend/src/redis/redis.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import Redis from 'ioredis';
import Redlock from 'redlock';
const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT';
const RED_LOCK_TOKEN = 'RED_LOCK';

export type RedisPage = {
title?: string;
Expand All @@ -18,17 +16,71 @@ export type RedisNode = {
};

export type RedisEdge = {
fromNode: number;
toNode: number;
type: 'add' | 'delete';
fromNode?: number;
toNode?: number;
type?: 'add' | 'delete';
};

const releaseScript = `
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
`;
@Injectable()
export class RedisService {
constructor(
@Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis,
@Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock,
) {}
async acquireLock(
redisClient: Redis,
key: string,
retryCount = 10,
retryDelay = 100,
) {
key = 'lock:' + key;
// retryCount만큼 시도
for (let i = 0; i < retryCount; i++) {
const value = Date.now().toString();
Logger.log(`redis lock info ${key} : ${value}`);
const acquireResult = await redisClient.set(key, value, 'EX', 10, 'NX');
Logger.log(acquireResult);

// 락 획득 성공
if (acquireResult == 'OK') {
Logger.log(`시도 횟수 : ${i}`);
const release = async () => {
Logger.log(`release 하려는 key : ${key}`);
Logger.log(`release 하려는 value : ${value}`);
const releaseResult = await redisClient.eval(
releaseScript,
1,
key,
value,
);
// 락 해제 성공
Logger.log(`락 해제 결과 : ${releaseResult}`);
if (releaseResult === 1) {
Logger.log('락 해제 성공');
return true;
}
// 락 해제 실패
Logger.log('락 해제 실패');
return false;
};
return release;
}

// 락 획득 실패하면 retryDelay이후 다시 획득 시도
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, retryDelay);
});
}
throw new Error('락 획득 실패');
}

async getAllKeys(pattern) {
return await this.redisClient.keys(pattern);
Expand All @@ -46,32 +98,43 @@ export class RedisService {
}

async set(key: string, value: object) {
// 락을 획득할 때까지 기다린다.
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
try {
await this.redisClient.hset(key, Object.entries(value));
} finally {
lock.release();
}
}
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(this.redisClient, key);
await this.redisClient.hset(key, Object.entries(value));

// 락 해제
await release();
}
async setFields(key: string, map: Record<string, string>) {
// 락 획득할 수 있을 때만 set
const release = await this.acquireLock(this.redisClient, key);
// fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환
const flattenedFields = Object.entries(map).flatMap(([field, value]) => [
field,
value,
]);
// 락 해제
await release();
// hset을 통해 한 번에 여러 필드를 설정
return await this.redisClient.hset(key, ...flattenedFields);
}
async setField(key: string, field: string, value: string) {
// 락을 획득할 때까지 기다린다.
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
try {
return await this.redisClient.hset(key, field, value);
} finally {
lock.release();
}
// 획득할 수 있을 때만 set
const release = await this.acquireLock(this.redisClient, key);
const result = await this.redisClient.hset(key, field, value);

// 락 해제
await release();
return result;
}

async delete(key: string) {
// 락을 획득할 때까지 기다린다.
const lock = await this.redisLock.acquire([`user:${key}`], 1000);
try {
return await this.redisClient.del(key);
} finally {
lock.release();
}
// 획득할 수 있을 때만 set
const release = await this.acquireLock(this.redisClient, key);
const result = await this.redisClient.del(key);

// 락 해제
await release();
return result;
}
}
Loading
Loading