검색
검색
공개 노트 검색
회원가입로그인

2024.04.26 중간발표 세미나

be 개발 타임라인

날짜내용커밋
2024.03.12프로젝트 생성@1c473ff
2024.03.14jwt & kakao o-auth 구현@f195d92
2024.03.15entity 설계 & docker 개발환경 설정@77a183c
2024.03.16user & post 기초 CRUD api 제작@1baeefd
2024.03.21복잡한 db 접근 로직 QueryBuilder 패턴으로 변경@9d3f224
2024.03.25aws s3 연동 & 이미지 업로드 api 제작@9a57572
2024.04.04repository layer 생성하여 db access 로직 분리@15b7b8d
2024.04.11imgly 라이브러리로 누끼 기능 추가@456d9ba
2024.04.15Guard & Middleware 기능 세분화@1ac1ef8
2024.04.20cursor based pagination & 무한스크롤 구현@0b52925

러닝 포인트

1. custom guard로 로그인 분기처리


i) 문제 제기

게시글 공개범위 중 이웃에게만 공개 설정을 했을 경우 api를 호출한 유저의 정보를 받아야하는데, 조회 API를 비회원이 호출하는 경우도 존재하여 guard를 사용할 수 없다. nest.js의 PassportStrategy를 확장한 JwtGuard의 경우 토큰이 유효하지 않을 경우 자동으로 UnauthorizedException을 발생시켜서 막아내는 역할을 하기 때문에 토큰이 없는 경우 호출 자체가 막혀버리는 이슈가 있다. 따라서 jwt token이 있을 경우 토큰 값을 넘겨주는 미들웨어와, 토큰이 존재할 경우에만 api를 호출 할 수 있게 하는 guard를 로직을 분리시킬 필요가 있다.

ii) 구현

//auth-token.middleware.ts
@Injectable()
export class AuthTokenMiddleware implements NestMiddleware {
	constructor(
		private readonly jwtService: JwtService,
		private readonly configService: ConfigService,
	) {}
	public async use(req: Request, res: Response, next: () => void) {
		const userId = this.verifyUser(req);
		req.user = { userId };
		return next();
	}
	private verifyUser(req: Request): Promise<number> {
	let user = null;
	try {
		const accessToken = req.cookies.accessToken;
		const decoded = this.jwtService.verify(accessToken, {
		secret: this.configService.get('JWT_SECRET'),
	});
	user = decoded.userId;
	} catch (e) {}
	return user;
	}
}

jwt-token이 있을경우 해독해서 payload를 request에 담아서 controller layer에 전달하는 로직을 수행하는 미들웨어

//auth.guard.ts
@Injectable()
export class AuthGuardV2 implements CanActivate {
	constructor(private readonly reflector: Reflector) {}
	public canActivate(context: ExecutionContext): boolean {
	const req = context.switchToHttp().getRequest();
	if (!req.user.userId)
		throw new UnauthorizedException('권한이 없습니다');
	return true;
	}
}

미들웨어에서 jwt token이 존재하지 않는 경우 api 호출을 막아주는 guard

2. custom repository 패턴 적용


i) 문제 제기

QueryBuilder 적용 시 service layer에서 db 접근 로직이 노출되는 문제 발생. service layer가 비즈니스 로직 뿐만 아니라 db access까지 처리하게 되어 단일 책임 원칙을 위배한 설계가 된다. 코드의 유지보수성을 위해 querybuilder를 사용하는 부분을 repository layer로 은닉하여 설계할 필요가 생겼다.

ii) 구현

//posts.repository.ts
export class PostsRepository extends Repository<Posts> {
	constructor(private dataSource: DataSource) {
		super(Posts, dataSource.createEntityManager());
	}
}

typeOrm에서 기본제공하는 메서드들을 사용하기 위해서 Repository를 상속받아 사용한다.

iii) 주의할 점

const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try{
	const repo = queryRunner.manager.withRepository(this. postsRepository) // error 발생
}

커스텀 제작한 레포지토리를 transaction 내부에서 사용 할 경우 createEntityManager() 에서 충돌이 발생. transaction 사용 시 repository 대신 datasource를 통해 직접 접근해야 한다.

3. cursor-based-pagination으로 무한 스크롤 구현


i) 문제 제기

기존의 offset-based-pagination으로는 무한 스크롤 구현 시 데이터를 중복으로 fetching 할 수 있는 문제가 생긴다. 그러나 cursor를 제작하려면 정렬 및 식별 가능한 칼럼을 커서로 삼아야 한다. unique key가 아닌 좋아요나 댓글 수 정렬 기능을 제작하기 위해서는 칼럼을 그대로 커서로 삼는 게 아닌 특정 unique 칼럼과의 조합을 통해 custom cursor를 생성해야 한다.

ii) 구현

//posts.service.ts
async createCustomCursor({ cursorIndex, order }): Promise<string> {
	const posts = await this.postsRepository.find();
	const customCursor = posts.map((posts) => {
	const id = posts.id;
	const _order = posts[order];
	const customCursor: string =
	String(_order).padStart(7, '0') + String(id).padStart(7, '0');
		return customCursor;
	});
}
//posts.repository.ts
    queryBuilder
      .take(cursorOption.take)
      .where(queryByPriceSort, {
        customCursor: cursorOption.customCursor,
      })
      .innerJoin('p.user', 'user')
      .innerJoinAndSelect('p.postBackground', 'postBackground')
      .innerJoinAndSelect('p.postCategory', 'postCategory')
      .addSelect([
        'user.kakaoId',
        'user.description',
        'user.profile_image',
        'user.username',
      ])
      .where('p.isPublished = true')
      .andWhere(`p.userKakaoId = any(${subQuery})`)
      .andWhere('p.scope IN (:...scopes)', {
        scopes: [OpenScope.PUBLIC, OpenScope.PROTECTED],
      }) //sql injection 방지를 위해 만드시 enum 거칠 것
      .orderBy(`p.${ORDER}`, cursorOption.sort as any)
      .addOrderBy('p.id', cursorOption.sort as any);

iii) 문제 될 사항

  • auto_increment 칼럼에 대한 의존성 => db 샤딩 등의 과정에서 문제가 생길 수 있으나, 현재의 단계에서 고려하기에는 오버 엔지니어링이라 판단.

  • count 과정에서 db full scan => 성능 문제 => 부하테스트 해보고 성능 이슈가 클 경우 전체 데이터 카운트 제공을 하지 않고, 다음 데이터 존재 유무만 확인 하는 식으로 로직 수정

조회수 : 73
heart
공유하기
카카오로 공유하기
페이스북 공유하기
트위터로 공유하기
url 복사하기
T
페이지 기반 대답
AI Chat