안녕하세요 예를 들어 MockUserRepository를 만들고 그 안에 mydb라는 데이터가 있습니다. 만약 기존 User 테이블이 Post 테이블과 관계가 맺어져 있다면 Post까지 mydb에 담아야 하나요? mydb = [{ id= '', name:='', password= '' }] 이렇게 해야 하는 건지 아니면 mydb = [{ id: '', name:'', password: '', post: { id: '', title: ''}] 이렇게 작성해야 하는 건지 궁금합니다!! 그리고 userService에서 postService를 사용하는 로직이 있는데 userService 테스트시 MockPost 테이블까지 같이 구현해야 하는 건지..아니면 다른 방법이 있는 건지ㅜㅜ 궁금하네요 하나만 더 여쭤보자면.. ㅜㅜ 제가 이해가 잘 안가서 그러는데 controller.ts를 테스트할 때는 service를 mock하고, service.ts를 테스트할 때는 데이터베이스를 mock해서 사용하는 건가요??
안녕하십니까 영한님. 영한님 덕분에 JAVA Spring 기반 백엔드 개발의 시작을 할 수 있었고, 이제는 5개월 차 스타트업 백엔드 개발자로써 커리어를 시작하게 되었습니다. 현업에 와서는 오히려 취준생 때 보다 더 많은 고민을 하게 되느데요, 질문 1. 그중 최근에 CRUD의 각 상황별 API 응답을 어떻게 보내는게 적절할지, 질문 2. 그리고 ResponseDto를 어떻게 구성하는것이 적절할지에 대한 고민을 팀장님과 함께 하고있어서, 영한님의 생각을 여쭙고자 질문글을 올리게 되었습니다. 답변 해주시면 정말 감사할 거 같습니다! Q1. 생성, 조회 , 수정, 삭제 API의 응답을 각각 어떻게 보내시는지 여쭤보고 싶습니다. 가장 먼저 조회의 경우는 말 그대로 path의 Entity 및 관련된 Entity 정보를 조합하여 응답 DTO로 변환하여 보내고 있습니다. 그런데 나머지 Write Operation에 대한 응답을 어디까지 보내야 하냐가 이슈 입니다. 예를들면 엔티티의 생성의 경우 엔티티의 응답 DTO를 보내면 - 프론트에서 별도의 조회 API 호출 없이 바로 프론트가 화면에 뿌려줄 수 있으니깐 저는 생성의 경우에도 조회와 마찬가지로 Entity의 정보를 조합하여 응답 DTO로 변환하여 보내고 있었습니다. 그런데 이러한 부분이 Command Query Sperate 원칙에 어긋나는것 같아, 영한님께서는 혹시 생성한 Entity의 Key만 보내시는지, 아니면 Entity의 정보를 DTO로 변환하여 보내시는지 궁금합니다. 만약 Entity의 Id만 보내신다면 이후에 별도로 조회API를 호출해야 하고 그 또한 비용일텐데 이러한 부분은 어떻게 하시는지 여쭤보고 싶습니다. 이제 수정 API인데요, 제가 다룬 비즈니스 로직의 경우 수정 비즈니스 로직이 다양하고 , 각 비즈니스 로직의 경우 다뤄지는 Entity의 종류가 다른 경우였습니다. (중심 Entity는 동일하지만, 연관된 Entity를 누구까지 건드리냐의 차이) 그래서 하나의 통일된 응답으로 보내기 모호한 점이 첫 번째 이유이고, 애초에 수정 후에 프론트 화면에서 그 엔티티의 정보를 보여줄 필요가 없어서 라는 두번째 이유에 의해서 에초에 엔티티의 Id값도 보내지 않고 있었는데요, 이 수정 API의 응답을 영한님은 어떻게 진행하지는지 그 이유가 궁금합니다. 마지막으로 삭제의 경우는 정말, 프론트에게 보낼 응답이 없어도 되는 경우 라고 생각했는데요, 팀장님의 의견은 만약에 나중에 삭제한 Entity를 복구하는 요구사항이 추가되는것을 고려하여 Id 정도는 넘기자는 의견을 내어주셨습니다. 마찬가지로 삭제의 경우도 어떤식으로 수행하시는지 그 이유가 궁금합니다. Q3. 마지막으로 Entity의 ResponseDto의 필드를 어떤식으로 구성하시는지 궁금합니다 예를들면 저의 경우는 API는 프론트와 서버 간의 스펙이라고 생각하고, Entity의 단건조회의 경우는 단건 조회용 ResponseDto를, 전체조회의 경우는 전체 조회용 SummaryResponseDto를 별도로 만들어서 사용하고 있었습니다. 저희 팀장님 께서는 프론트쪽도 일을 해오시다가 , 백엔드쪽 분야로 전향하신 케이스 인데요, 그렇다 보니 어떻게 해야 프론트의 생산성이 올라가는지를 고려하시는 분이셨고, 팀장님의 생각은 서버에서 넘겨주는 응답에 일관성이 있어야 그 응답을 사용하는 프론트 측도 학습이 되고 놓치는 부분 없이 생산성이 올라간다 는 의견이셨습니다. 그래서 Entity별로 당장 사용하지 않더라도 가능한 모든 필드를 담은 ResponseDto를 하나만 만들고, 해당 ResponseEntity의 조합으로 각 API별 응답 Dto를 만들어서 사용하면 , 프론트 측 에서는 일관성 있는 응답값을 사용할 수 있다는 의견이셨습니다. 물론 이 방법이 네트워크 패킷의 양을 쓸데없이 증가시킨다는 것을 알고 계시면서도, 생산성에 큰 영향을 미치는 부분이라고 생각하셨습니다. 예를들어 다음과 같이 각 Entity의 응답 Dto의 조합별로 API의 ResponseDto를 만들 수 있습니다. ResponseDto{ UserDto{ id : 1, name : “aaa” … // User엔티티의 거의 모든 필드 } ItemDto{ id : 2, name : “bbb”, … // Item엔티티의 거의 모든 필드 } } 저는 이러한 부분에 대해 생각해 본 적이 없이, 그냥 제가 “해당 API를 호출하는 화면에서 필요한 정보들만을 담아 (혹은 여러 화면에서 쓰인다면 여러개를 고려) ResponseDto를 각각 만들어서” 넘겼는데요 영한님께서는 이러한 ResponseDto를 구성하는 부분에 있어서 상황별로 필드를 재구성 하여 ResponseDto를 정의하여 사용하시는 편 인지 (SummaryResponseDto 등의 별도 Dto에 사용될 Entity의 필드들을 풀어서 정의하시는지) 아니면 생산성을 고려하여 각 Entity별 Dto를 만들고, 이들을 조합하여 ReponseDto를 정의하시는 편 이신지 , 혹은 다른 규칙이 있으신지 궁금합니다. 물론 그렇다고 , 팀장님의 의견에서 전체조회시 사용하는 DTO와 단건조회시 사용하는 DTO가 동일하더라도, 전체조회 후 단건조회를 할때 단건조회 API를 호출하지 말고 기존 Front가 가지고 있는 값을 쓰지는 말자는 의견 이십니다 (단건조회API는 별도로 호출해야 한다) 그저 핵심은 프론트가 다루는 ResponseDto의 일관성을 위해서 입니다 (결론은 생산성을 위해) 긴글 읽어주셔서 감사합니다. 항상 건강하셨으면 좋겠고, 다음 강의들도 손꼽아 기다리고 있습니다!
직접 생성한 모듈 파일을 불러올 때 절대 경로를 사용한다면 node_modules에서 찾기 때문에 유효하지 않다고 했는데, 이게 제스트로 실행할 때만 문제가 되고, 노드로 그냥 실행시킬 때에는 문제가 되지 않는 이유는 뭔가요? 노드로 그냥 실행시킬 때에도 node_modules에서 해당 모듈 파일을 찾아서 에러가 발생해야 하는 것이 아닌가요?
깃허브 코드를 보면 @ApiOperation({ summary: '회원가입' }) @UseGuards(NotLoggedInGuard) @Post() async join(@Body() data: JoinRequestDto) { const user = this.usersService.findByEmail(data.email); if (!user) { throw new NotFoundException(); } const result = await this.usersService.join( data.email, data.nickname, data.password, ); if (result) { return 'ok'; } else { throw new ForbiddenException(); } } 이런데 여기서 user가 없는데 왜 NotFoundException()을 날리는 건가요? user가 없으면 그대로 join하여 사용자 등록을 해야 하는 것이 아닌가요?
테이블 삭제도 시도해보고 싶어서 터미널에 $ npm run schema:drop 을 입력하였더니 아래와 같은 내용이 나왔습니다. Drops all tables in the database on your default dataSource. To drop table of a concrete connection's database use -c option. 옵션: -h, --help 도움말 표시 [불리언] -d, --dataSource Path to the file where your DataSource instance is defined. [필수] -v, --version 버전 표시 [불리언] 필수 인수가 주어지지 않았습니다: dataSource dataSource가 주어지지 않았다고 해서 살펴보다 package.json/scripts/schema에 다른 명령어들과는 다르게 경로가 지정되어 있지 않는것 같아 아래와 같이 수정했고, { "scripts": { "seed": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d ./dataSource.ts", "schema:drop": "ts-node ./node_modules/typeorm/cli.js schema:drop -d ./dataSource.ts", "schema:sync": "ts-node ./node_modules/typeorm/cli.js schema:sync", } } 테이블 삭제에 성공했습니다. $ npm run schema:drop > a-nest@0.0.1 schema:drop > ts-node ./node_modules/typeorm/cli.js schema:drop -d ./dataSource.ts query: SELECT VERSION() AS `version` query: SELECT * FROM `INFORMATION_SCHEMA`.`SCHEMATA` WHERE `SCHEMA_NAME` = '00_nestjs_typeorm' query: START TRANSACTION query: SELECT concat('DROP VIEW IF EXISTS `', table_schema, '`.`', table_name, '`') AS `query` FROM `INFORMATION_SCHEMA`.`VIEWS` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' query: SELECT concat('DROP TABLE IF EXISTS `', table_schema, '`.`', table_name, '`') AS `query` FROM ` `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA` = '00_nestjs_typeorm' query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channelchats` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channelmembers` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`channels` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`dms` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`mentions` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`migrations` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`users` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`workspacemembers` query: DROP TABLE IF EXISTS `00_nestjs_typeorm`.`workspaces` query: SET FOREIGN_KEY_CHECKS = 1; query: COMMIT Database schema has been successfully dropped. 혹시 schema:drop, schema:sync 명령어에 경로를 지정하지 않으신 이유가 있으신지, 경로를 빼놓은 상태로 유지하다가, 정말 필요할 때에만 경로 지정하고 명령어를 사용하는게 좋은지가 궁금합니다. 실수로 입력해서 데이터 손실이 발생하는 것을 막기 위해서일까요?
[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core
access token이 만료되었을 떄 refresh 토큰을 통해 access token을 새로 발급 받는 것은 이해가 되는데.. refresh 토큰이 만료되어서 새로운 refresh 토큰을 발급받기 위한 시점에서 만료된 refresh 토큰을 사용할 수 있는 것인가요..? 조금 헷갈리내요..
[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core
비동기 로직을 호출만 하고 끝나는게 아니라 결과값을 활용해야할 때 async, await를 활용해야 한다고 이해하고 있는데요. 한가지 이해가 잘 가지 않는 부분이 Auth Service에서 async registerWithEmail( user: Pick<UsersModel, 'nickname' | 'email' | 'password'>, ) { const hash = await bcrypt.hash(user.password, HASH_ROUNDS); const newUser = await this.usersService.createUser({ ...user, password: hash, }); return this.loginUser(newUser); } async 함수는 결국 Promise를 반환하니까 Auth Controller에서 @Post('/register/email') registerByEmail( @Body('nickname') nickname: string, @Body('email') email: string, @Body('password') password: string, ) { return this.authService.registerWithEmail({ nickname, email, password, }); } async, await를 사용하지 않으면 제대로 동작하지 않을 줄 알았는데 비동기 호출의 결과인 token이 잘 반환되더라구요. 혹시 이유를 알 수 있을까요? async, await를 판단하는 강사님 만의 기준이 혹시 있으신지, 그리고 굳이 async, await가 필요 없는 곳에도 사용하게되면 성능의 차이가 생기는지도 궁금합니다.
[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core
안녕하세요. 강의 덕분에 많은 것을 배우고 좀 더 nestjs에 익숙해질 수 있었습니다. 그런데 Follow Count Incremet & Decrement 작업하기 - 2 강의에서 다룬 내용이 comment count 작업과 관련한 내용이었습니다. Follow Count Incremet & Decrement 작업하기 - 1 마지막에는 다음 시간에 followee count 증가 감소 내용을 다룰 것이라 하셨었거 든요. 강의가 누락된 것 같습니다. 참고해 주세요! ㅎ 좋은 강의 감사합니다. 이어지는 강의도 많이 기대됩니다. 힘내세요!
[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core
안녕하세요 강사님! 다름이 아니라, UpdatePostDto 코드 작성하는 부분에서 의문이 생겨 질문 드립니다. UpdatePostDto의 코드를 이렇게 작성을 해주셨는데, 하단에 이 부분이 왜 필요한지가 이해가 되지 않습니다. PartialType(CreatePostDto)만으로도 충분히 CreatePostDto 안에 있는 프로퍼티들을 Optional 프로퍼티들로 바꾸는거 아닌가요?? 더불어 @IsString validator같은 경우에는 엔티티에서 이미 적용을 해주었기에 더욱 필요없지 않나 싶습니다! 따라서 이 두 코드는 동일한 기능을 하는 코드로 생각이 되는데, 이렇게 명시적으로 작성을 하신 이유가 있으신가 해서 여쭤봅니다. 좋은 강의 감사드립니다!
프론트에서 formData넣기 직전에 파일 객체를 봤을때 한글이 제대로 잘 들어가 있었는데, 이걸 보내고 multer 설정에서 받자마자 바로 destination 메서드와 filename메서드에서 file.originalname을 콘솔 로그로 확인을 해보니 이미 한글이 깨져있더라고요. 그래서 프로젝트 로컬에서 터미널로 chcp 65001 명령어로 인코딩을 utf-8로 바꿔줬는데 그래도 여전히 이미지 파일명의 한글이 깨지던데 시도해볼 다른 방법이 더 있나요?
[코드팩토리] [초급] NestJS REST API 백엔드 완전 정복 마스터 클래스 - NestJS Core
// posts.service.ts async getAllPost() { return this.postsRepository.find(); } 안녕하세요, 궁금한 사항이 있어 질문 남깁니다. 위 코드에서 find() 메서드는 Promise를 리턴하니까 service에 존재하는 getAllPost 함수는 Promise를 리턴하게 되지 않나요? 해당 코드를 실제로 돌려보니 return await this.postsRepository.find() 처럼 돌아가는게 이해가 잘 안됩니다. 강의에서는 "컨트롤러에서 바로 반환을 해주기 때문에 async, await을 안 붙혀도 상관이 없다" 라고 말씀해주셨는데, 음... 바로 반환을 하면 Promise가 return되어야 하지 않나란 생각이 듭니다. 이 부분에 대한 부연 설명이 가능할까요? 감사합니다.
안녕하세요 얼마전에 Access Token 관련 질문을 드렸습니다. 저는 Backend 강의만 듣고 있어서 Frontend 강의가 어떻게 진행되었는지 몰라서 여쭤봅니다! 현재 전역 스테이트 변수인 recoil을 사용해서 AccessToken을 저장 중 입니다. 하지만 그냥 recoil만 사용할 때는 새로 고침 시 데이터가 삭제되어서 문제가 되고 있습니다. persistAtom 이라는 속성을 사용해서 상태가 유지되도록 할 수 있다는 것을 알게 되었는데 이렇게 사용할 경우 localstorage에 토큰 값이 그대로 들어가더군요. 그러면 토큰 값이 그대로 노출되어서 보안상 문제가 될 것 같은데 혹시 이게 문제가 안되는지, 그대로 사용해도 되는지 아니면 새로고침시 계속 token이 사라지니까 모든 컴포넌트에 useEffect를 사용해서 restoreAccessToken을 호출하도록 만들면 어떨까요? 다른 방법이 있다면 방향을 알려주세요! 감사합니다.
[질문 템플릿] 1. 강의 내용과 관련된 질문인가요? (예) 2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예) 3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예) [질문 내용] 검증오류가 날 시 BindingResult에 의해 들어가는 게 아니라, 저렇게 인자로 받는 건 이미 모델에 들어가 주는거죠?