일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- couchcoding
- 트랜잭션
- Spring
- ERD
- 프로그래머스
- 알고리즘
- 토이프로젝트
- 코딩테스트
- 백준
- 프로젝트 설계
- LEVEL 2
- pom.xml
- DFS
- 배포
- 그리디 알고리즘
- 카우치코딩
- Java
- 6주포트폴리오
- maven
- GitHub
- 빌드 툴
- Spring Framework
- Fun편log
- 와이어 프레임
- 협업프로젝트
- 유사 칸토어 비트열
- Qoddi
- 테이블 해시 함수
- 마법의 엘리베이터
- 토이 프로젝트
- Today
- Total
소통 하고싶은 개발자
FUN편log 프로젝트 5주차 회고 본문
목차
- 시작하며
- 리뷰 작성 ・ 수정 ・ 삭제 ・ 조회 기능 적용
- 로그인 권한 필터링
- 1 회차 멘토링
- 리펙토링 시작
- 2 회차 멘토링
- 리펙토링 중간 점검
- 마치며
시작하며
벌써 5 주차가 되었다.
무언가에 집중해서 그런지 시간이 정말 빨리 흐르는 것 같다.
프론트엔드와 본격적으로 연동 테스트에 들어간 지 얼마 되지 않았지만, 그래도 점진적으로 개발된 API에 대한 피드백이 들어오면 그때마다 고치고 PR을 생성해서 리뷰를 받는 과정에 익숙해지고 있는 것 같다.
지난주에 편의점 정보(StoreSummary)에 대한 요청을 처리하는 코드를 작성하는 과정에서 Pull Request도 여러 번 생성해보고, 피드백이 담긴 리뷰를 보고 수정해서 다시 Push도 하는 시간을 가지면서 나름 깃허브와 깃플로우에 익숙해졌기 때문이 아닌가 싶다.
일단 이번 포스팅에서는 기능 개발을 얼추 끝내고 리펙토링을 시작했기 때문에 내 코드의 변화를 보여주기 위해 한 가지 예시를 들고, 해당 부분의 코드가 시간이 지나면서 어떻게 변경되었는지 이야기할 생각이다.
리뷰 작성 ・ 수정 ・ 삭제 ・ 조회 기능 적용
원래는 리뷰(Review)라는 엔티티(자원)에 대한 API에 대해 그리 깊게 생각하지 않았었다.
왜냐면 단순히 DB에 저장, 수정, 조회, 삭제만 하면 된다고 생각했기 때문이다.
예를 들어 아래 코드와 같다.
@Transactional
public void createReview(ReviewDTO reviewDTO, String storeId) {
User user = userRepository.findById(reviewDTO.getUserEntryNo())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 유저입니다!"));
Review review = Review.builder()
.reviewContent(reviewDTO.getReviewContent())
.starCount(reviewDTO.getStarCount())
.storeId(storeId)
.user(user)
.keywords(new ArrayList<>())
.build();
reviewDTO.removeSameKeyword();
for (String k : reviewDTO.getKeywords()) {
KeywordContent keywordContent = keywordContentRepository.findByKeywordContent(k)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 키워드입니다!"));
Keyword keyword = Keyword.builder()
.review(review)
.user(user)
.storeId(storeId)
.keywordContent(keywordContent)
.build();
review.getKeywords().add(keyword);
}
reviewRepository.save(review);
}
그런데 이젠 아니게 되었다.
리뷰(Review) 테이블에 변경(저장, 수정, 삭제)이 발생하면
즉각 해당 리뷰와 연관된 편의점 정보를 업데이트하는 코드를 작성해야하기 때문이다.
코드의 순서만 생각해봐도
기존 리뷰 생성 코드
(시큐리티 관련 코드를 제외한 경우이고, 여기서 이야기하는 컨트롤러, 서비스 객체는 직접 만든 객체들이다.)
- 클라이언트로부터 리뷰를 생성하기 위한 파라미터를 전달받는다.
- 리뷰 컨트롤러에서 리뷰 서비스 객체를 호출하여 파라미터를 전달한다.
- 서비스 객체에서 파라미터를 근거로 리뷰 객체를 생성하여 리뷰 리포지토리(JPA)를 사용해서 DB에 저장한다.
바뀐 코드
(시큐리티 코드 제외)
- 클라이언트로부터 리뷰를 생성하기 위한 파라미터를 전달받는다.
- 리뷰 컨트롤러에서 리뷰 서비스 객체를 호출하여 파라미터를 전달한다.
- 서비스 객체에서 파라미터를 근거로 리뷰 객체를 생성하여 리뷰 리포지토리(JPA)를 사용해서 DB에 저장한다.
- 저장한 리뷰 객체를 편의점 서비스(StoreService) 객체로 전달한다.
- 편의점 서비스 객체에서 기존에 저장되어있던 편의점 정보를 편의점 리포지토리(JPA)에서 가져온다.
- 적절한 연산을 통해 DB에서 가져온 편의점 정보 객체를 업데이트한다.
- 수정된 편의점 정보 객체를 다시 DB에 저장한다.
논리적인 단계를 따졌을 때 위 과정만 봐도 4 과정이 추가되는데,
수정, 삭제 시 업데이트까지 늘어나는 것이고,
비슷한 로직일 듯 싶은데 최적화하려면 어찌 되었든 코드가 조금씩은 달라질 것 같은 느낌이 들었다.
이런 생각을 하면서 코드를 작성하고 있는데
문득 그냥 테이블에 수정이 일어난 순간, '해당 편의점에 대한 전체 로우(Law)데이터를 DB에서 가져와서
전체 연산을 통해 업데이트하도록 작성하면 되지 않을까?'라는 생각이 뇌리를 스쳤고, 바로 코드를 작성했다.
public StoreSummary updateStoreSummary(String storeId) {
List<Review> reviews = reviewRepository.findAllByStoreId(storeId);
Long reviewCount = Long.valueOf(reviews.size());
Double starRate = reviews.stream()
.mapToDouble(Review::getStarCount)
.average()
.orElse(0.0);
List<StoreKeyword> updateStoreKeyword = new ArrayList<>();
StoreSummary updateSummary = StoreSummary.builder()
.storeId(storeId)
.reviewCount(reviewCount)
.starRate(starRate)
.storeKeywords(updateStoreKeyword)
.build();
updateStoreKeywords(updateSummary);
return storeSummaryRepository.save(updateSummary);
}
}
이후에는 순조롭게 커밋, 푸시를 진행했고 피드백을 받기 위해 리뷰어 설정을 하고 PR을 생성했다.
피드백 내용이 맞는 말이었다.
의심의 여지도 없었다.
왜냐면 사실 전체를 비교하는게 그리 달가운 일은 아니기도 했고, 테이블을 설계할 때 리뷰의 개수가 많아질 것을 염려하여 리뷰 등록 번호에 대한 컬럼 타입을 bigint로 만들었기 때문에, 만약 DB에서 데이터를 무사히 가져온다고 해도 전체를 비교하는 연산이라는건 말도 안 되기 때문이다.
나는 피드백 받은 부분에 대해 혹시 내가 잘못 이해하고 혼자만의 작업을 만들어버리진 않을까 불안한 마음에 내가 이해한 것이 대략적으로 질문했고, 멘토님은 친절하게 대답해주셨다. (정말 친절하심ㅠㅠ)
내 생각에 확신이 생긴 나는 위에 전체 데이터를 가져와서 비교하던 코드를 아래처럼 수정했다.
@Transactional
public void addReviewInSummary(Review review){
String storeId = review.getStoreId();
StoreSummary summary = storeSummaryRepository.findById(storeId)
.orElse(new StoreSummary(storeId));
Long reviewCount = reviewRepository.countByStoreId(storeId).get();
double starRate = reviewCount != 0 ?
(summary.getStarRate() * (reviewCount-1) + review.getStarCount()) / (reviewCount) : 0;
starRate = Math.round(starRate * 10);
starRate /= 10;
increaseStoreKeywordCounts(summary, review.getKeywords());
StoreSummary updatedSummary = StoreSummary.builder()
.storeId(storeId)
.starRate(starRate)
.reviewCount(reviewCount)
.storeKeywords(summary.getStoreKeywords())
.build();
storeSummaryRepository.save(updatedSummary);
}
물론 리뷰 한 건이 수정되거나 삭제되는 경우에도 수정과 삭제 전용 메소드를 따로 만들었고,
이후에 멘토님께서 Pull Request코드를 develop 브랜치에 머지(merge)해주셨다.
로그인 권한 필터링
만약 유저가 우리 서비스를 이용할 때 리뷰를 작성해야 한다면, 로그인이 되어있는 유저인지 판별하는 작업을 실시한 후에 로그인이 유효했을 경우에만 리뷰를 등록할 수 있도록 만들려고 한다.
이와 같이 우리 팀이 만들고 있는 웹 사이트에는 반드시 로그인이 된 상태여야만 이용할 수 있는 기능이 존재한다.
하지만 이와 별개로 로그인을 하지 않고도 이용할 수 있는 기능들이 분명히 존재한다.
예를 들어, 단순히 현재 위치에서 가까운 편의점들의 정보를 조회한다거나 혹은 해당 편의점에 작성된 리뷰를 조회하는 경우가 바로 그것이다.
이렇게 만들게 된 가장 큰 이유는 서비스 이용의 편의성이다. 내가 생각했을 때에도 이 사이트가 한 번 써보고 싶은 사이트였지만, 단순히 조회하기 위해서 구글 로그인을 하라고 한다? 라면 거기서 안 할 확률이 높다고 생각하기 때문이다.
어떤 연구에는 웹 사이트가 로딩될 때 만약 2초가 넘는다면 대부분의 사람들은 해당 사이트를 굳이 보려고 하지 않는다는 연구 결과가 있다고도 한다. (들은거라 아닐 수도 있어요...)
그렇다면 백엔드단에서도 모든 요청에 대해 파이어베이스 토큰을 검사하기 위해 존재하는 Filter를 항상 작동시켜서는 안 된다.
때문에 저번 주에 feature/access-token-auth 브랜치에서 스프링 시큐리티 관련 코드를 작성해서 develop 브랜치에 병합했었는데, 그때 했던 작업 중에 하나인 WebSecurity.ignoring()에서 요청 URL을 일부 추가 및 삭제했다.
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.antMatchers(HttpMethod.GET, "/users/me")
.antMatchers("/favicon.ico")
.antMatchers("/hello")
.antMatchers(HttpMethod.GET, "/stores/*/reviews")
.antMatchers(HttpMethod.GET, "/stores")
.antMatchers(HttpMethod.GET, "/stores/*");
}
이 작업을 하면서 느낀 건데 그냥 기존에 작업하던 feature/store-summary브랜치에서 바로 작업할 수도 있었지만, 뭔가 처음부터 브랜치에 맞게 작업하는 것에 대한 신경을 쓰지 않는 습관을 들인다면 나중에 협업할 때도 엄청 고생할 것 같은 생각이 들었다.
때문에 이 작업에 대해서 다른 PR이 종료될 때까지 초안만 작성해놓고 기다렸다가 PR이 종료되고 develop에 머지(merge)된 순간에 원격 저장소의 develop을 가져와서 머지(merge)하고 바로 PR을 생성했다!
1 회차 멘토링
멘토링이 시작되고 진행 상황을 보고하는 시간에 나는 리뷰 등록, 수정, 삭제, 조회 기능에 대한 PR이 완료되기 전에 WebSecurity.ignoring()을 수정하는 작업과 더불어 로그인 요청에 대한 응답 포맷을 변경했다고 말씀드렸다.
로그인 요청 응답 포맷은 기존에 로그인이 성공적으로 완료되었을 경우에 유저 이메일 정보만 JSON형태로 리턴하도록 되어있었다.
그러다가 프론트엔드 분들이 로그인 성공적으로 끝나면 해당 유저의 프로필 이미지가 저장된 URL을 추가적으로 백엔드에서부터 전달해달라는 요청을 했고, 내 입장에서는 그리 어려운 작업은 아니었기에 승낙했다.
사실 어렵지 않다고 해서 이런 부탁을 마음대로 승낙하면 안 될 것 같은 느낌이 없었던 것은 아니다.
왜냐면 이전 회사에서도 이런 부탁을 한 두 번 승낙했다가 이후에 수습하기 곤란했던 적이 있었기 때문이다.
또한 이런 부탁을 하는 사람들은 뭔가 내 입장에서는 계속 이런 부탁을 해온다는 생각도 한 몫했다.
그리고 아무리 실제 업무가 아니라도 멘토님이 존재하기 때문에 한 번이라도 물어보기라도 했어야 했다.
솔직히 내가 경솔했다고 생각한다.
그래도 어찌어찌 백엔드/프론트엔드의 현재 진행 상황에 대한 공유가 끝나고 멘토님께서 SOLID 원칙에 대한 설명을 해주셨다.
- SRP (Single Responsibility Principle) : 단일 책임의 원칙
- 하나의 컴포넌트에 너무 많은 기능을 넣지 말 것 / 최대한 다른 모듈로 분리시키기
- OCP (Open Close Principle) : 개방 폐쇄 원칙
- 적절한 props 사용 / 다른 추가 사항이 일어나더라도 기존 구성은 변경하지 않으며 확장에 대한 가능성을 열어줘야 한다, 컴포넌트의 확장성을 높일 수 있음
- LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- ISP (Interface segregation principle) : 인터페이스 분리 원칙
- 필요한 부분만 전달
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다
- DIP (Dependency Inversion Principle) : 의존관계 역전 원칙
- 추상화에 의존해야지, 구체화에 의존하면 안 된다
- 구현체에 의존하지 말자
뭔가 SOLID 원칙에 대해 내가 익숙하지 않은 것 때문인지 이렇게 이름을 보면 대부분 어떤 원칙인지 생각도 나도 이해도 되지만, 이름을 보지 않으면 잘 생각나지 않을 때가 많았기 때문에 이번 설명을 통해 리프레쉬를 할 수 있어서 좋았다.
이번 멘토링에서는 SOLID 원칙에 대한 설명을 듣는 시간이 주를 이뤘고, 마지막으로 질문 답변 시간에 나는 그동안 궁금했던 것들을 질문했다.
- [질문 1] FirebaseTokenFilter에서 SecurityContextHolder에서 getContext().setAuthorization()을 하는 이유는 무엇인가요?
- 답변 : 스프링 시큐리티를 사용할 때 매 요청마다 SecurityContext가 생성됩니다. SecurityContext는 컨트롤러로 전달할 수 있는 객체 중 하나이고, 결론적으로 Authorization객체를 컨트롤러로 전달하기 위해 해당 작업을 실시하는 것입니다.
리펙토링 시작
리뷰 관련 API가 완료되었고 모든 개발 사항이 develop 브랜치에 머지(merge)되었기 때문에 이 시점부터는 기능을 개발하면서 작성한 코드들에 대한 리팩토링을 해야 한다고 생각했다.
그래서 로컬과 원격 브랜치에 feature/refactoring 브랜치를 만들었다.
리팩토링을 진행하면서 크게 변경된 부분은 아래와 같다.
- 각 Controller에서 Service의 메소드를 호출할 때 파라미터 간소화
- Service - Repository 간 상호 의존성 정리
첫 번째부터 보면 기존에는 아래처럼 리뷰 한 건을 등록하기 위해 리뷰 등록 정보가 담긴 객체와 PathVariable, OAuth 인증을 마친 User를 받아와서 각각 서비스 객체의 파라미터로 주입하도록 만들었다.
@PostMapping("")
public ResponseEntity<String> addReview(@RequestBody ReviewDTO reviewDTO,
@PathVariable String storeId,
@AuthenticationPrincipal User user){
reviewService.createReview(reviewDTO, storeId, user.getEmail());
return ResponseEntity.ok().build();
}
글을 삭제하거나 수정할 때도 이런 식으로 분할된 정보들을 서비스 메소드에 각각 주입해주는 모습이 개인적으로 깔끔해 보이지 않았다.
개인적으로는 두 가지 중에 선택해야 할 것 같았다.
- 지금처럼 파라미터가 늘어나긴 하지만 꼭 필요한 정보이니 이대로 두는 것.
- 컨트롤러에서 파라미터들을 받아서 dto에 주입하고, dto 객체 하나만 서비스 메소드로 넣는 것.
일단 고민하다가 내린 결론은 아래 작성된 코드처럼 변경해보고, 일단 PR을 생성해서 피드백을 받아보자는 것이었다.
@PostMapping("")
public ResponseEntity<String> addReview(@RequestBody ReviewCreationReqDTO creationDto,
@PathVariable String storeId,
@AuthenticationPrincipal User user){
creationDto.setStoreId(storeId);
creationDto.setUserEmail(user.getEmail());
reviewService.createReview(creationDto);
return ResponseEntity.ok().build();
}
뭔가 서비스 메소드를 재사용할 일이 크게 있어 보이진 않지만 그래도 파라미터가 적어져서 그런지 마음은 조금 좋아졌다.
두 번째로 Service - Repository 간 의존성 정리에 관해서는 처음부터 이런 생각을 가지고 코드를 수정하려고 하지는 않았다.
오히려 Service와 Repository의 의존성을 줄이는 것은 고민 끝에 내린 결론이었다.
아래의 코드를 보면
public ReviewDTO createReview(ReviewDTO reviewDTO, String storeId) {
Optional<User> optionalUser = userRepository.findById(reviewDTO.getUserEntryNo());
if (!optionalUser.isPresent())
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 유저입니다!");
Review review = Review.builder()
.reviewContent(reviewDTO.getReviewContent())
.keywords(new ArrayList<>())
.build();
for (String k : reviewDTO.getKeywords()) {
Optional<KeywordContent> optionalContent = keywordContentRepository.findByKeywordContent(k);
if (!optionalContent.isPresent())
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 키워드입니다!");
KeywordContent keywordContent = optionalContent.get();
Keyword keyword = Keyword.builder()
.review(review)
.user(optionalUser.get())
.storeId(storeId)
.keywordContent(keywordContent)
.build();
review.getKeywords().add(keyword);
}
reviewRepository.save(review);
keywordRepository.saveAll(review.getKeywords());
return reviewDTO;
}
리뷰를 등록하는 서비스 메소드의 내용이다.
보면 이 메소드에서는 두 종류의 시나리오를 가정하고 Exception처리를 해두었다. 리뷰를 수정하는 경우나 삭제하는 경우에는 DB에 저장된 유저와 리뷰를 수정하도록 요청된 유저가 같은지 비교하고 만약 다르다면 Exception을 처리하는 시나리오를 추가해야 했다.
리팩토링을 하고 있는 시점에서 이런 Exception을 일괄적으로 처리하는 코드를 만들고 싶었다.
처음에는 'Spring Validation을 사용해야 할까?'라는 생각을 가지고 Validation에 대해 잠깐 검색해봤다.
그런데 내가 느끼기에 Spring Validation은 이런 시나리오에 대한 검증이 아닌 각 인스턴스의 형식을 검사하는 느낌을 받았다.
결국은 Spring Validation을 사용한다고 하더라도 모든 Exception을 처리하기는 힘들 것이라는 생각을 하게 되었다.
계속 고민을 이어가다가 멘토님께 질문하기 위해 어떻게 질문하면 좋을지 생각을 정리하고 있는데, 프론트엔드 멘토님이 디스코드에 입장하셔서 본인이 알려줄 수 있는 부분은 같이 고민해주시겠다고 하셨다.
그래서 나의 이런 고민들을 말씀드렸고(사실 생각이 정리되지 않은 상태였기에 중구난방으로 말씀드렸다), 멘토님께서는 이렇게 말씀하셨다.
'Spring과 관련된 전문 기술은 잘 모르겠지만, 레이어드 아키텍처를 구현함에 있어서 Service가 많은 Repository와 의존 관계가 형성되어 있으면 안 될 것 같아요'
솔직히 처음 30분 동안은 프론트엔드 멘토님이 말씀하신 내용이 무슨 말인지 제대로 이해할 수 없었다.
계속 고민을 하는데 문득 같은 아래처럼 역할을 하는 두 코드를 발견할 수 있었다.
User user = userRepository.findById(reviewDTO.getUserEntryNo())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 유저입니다!"));
User user = userService.getUser(dto.getUserEmail());
두 코드 모두 내가 작성한 코드인데 어떤 부분에서는 Service에서 직접 해당 Repository에 접근해서 데이터를 가져오고, 어떤 부분에서는 해당 Repository와 같은 도메인을 가진 Service에서 코드를 가져오고 있었던 것이다!
이걸 발견하고 나는 'Service에서는 같은 도메인을 가진 Repository만 가지게 수정하고 필요한 다른 도메인을 얻기 위해서는 원하는 객체의 도메인과 연관된 Service를 사용하도록 수정하면 코드에서 직접 Exception을 처리하는 일이 적어지겠구나!'라는 생각이 들었고, 즉시 이를 적용했다.
또 추가적으로 줄일 수 있는 부분은 줄여서 아래처럼 코드를 간결하게 만들 수 있었다.
@Transactional
public void createReview(ReviewDTO dto) {
User user = userService.getUser(dto.getUserEmail());
Review review = Review.builder()
.reviewContent(dto.getReviewContent())
.starCount(dto.getStarCount())
.storeId(dto.getStoreId())
.user(user)
.keywords(new ArrayList<>())
.build();
dto.removeSameKeyword();
review.initAllKeywords(keywordContentRepository.getKeywordContentsByContent(dto.getKeywords()));
reviewRepository.save(review);
storeService.addReviewInSummary(review);
}
이 정도로 간결해지고 Exception을 직접 처리하지 않아도 되니 자연스럽게 Spring Validation 같은 추가적인 기능을 쓰지 않아도 된다는 느낌이 들었고, 일단 뭔가를 추가하기 전에 내가 작성한 코드부터 되돌아보자는 마음까지 생겼다.
2 회차 멘토링
2 회차 멘토링이 시작되고 현재 진행되고 있는 상황들을 공유하는 시간이 주어지고, 나는 내가 리팩토링한 부분들이 내심 만족스러워서 리팩토링 진행 상황에 대해 공유했다.
계속해서 프론트엔드 분들의 진행상황을 듣는데 뭔가 아직은 해야 할 일이 많아 보였고, 멘토님께서도 백엔드 진행 상황이 생각보다는 빠르게 진행되서 남은 기간 동안은 리팩토링을 해보면 좋을 것 같다고 말씀하셨다.
그래서 간단한 진행 상황 공유 시간이 종료되고 프론트엔드는 프론트엔드끼리 백엔드는 백엔드끼리 통 화방을 나눠서 진행하기로 했다.
나는 백엔드 멘토님과 함께 현재까지 develop에 작성된 최신 코드를 보면서 멘토님이 말씀해주신 부분들에 대해 최대한 전부 메모하며 설명을 들었는데, 뭔가 생각보다 내 코드에 고칠 점이 엄청나게 많은 느낌을 받았다.
아래는 내가 받아 적은 리팩토링과 관련된 멘토님의 피드백이다.
- 한 패키지에서 같은 종류의 아이템이 3 개 이상이기 때문에 이런 경우에 한 패키지로 만드는 것이 깔끔합니다.
- 코드의 레벨을 맞추는 것이 좋습니다.
- 예를 들어 Service의 getReview() 메소드는 List<ReviewDTO> 타입의 객체를 리턴해야 합니다. 그래서 DB에서 List<Review> 타입을 가져온 것 까지는 좋았으나, 이 메소드에서 직접 for문을 써서 Review -> ReviewDTO로 만들고 리스트에 넣는 작업을 하는 것은 이전까지의 코드와 레벨이 맞지 않습니다.
- Repository에서 엔티티를 가져왔을 때 만약 값이 없는 경우 exception을 발생시키는 부분과 null로 받아서 처리하는 부분이 있는데, 한 가지로 통일하는 게 좋습니다. 저는 exception을 발생시키도록 하는 것을 추천합니다.
- Service 중 KeywordContentService는 사실 Service로써의 역할보다는 Repository로써의 역할을 하는 것 같습니다. 이런 경우 Service로 만드는 것보다는 Repository를 확장하여 커스텀 Repository로 만드는 것이 좋습니다.
- DTO를 만들면서 의미나 역할이 다른 부분을 대처하기 위해 하나의 DTO에 모든 필드를 추가하지 말고, 같은 도메인이라도 역할에 따라 DTO를 세분화해서 만드는 것이 좋습니다.
- 지금은 Service객체에서 Exception을 발생시키면 응답이 종료되도록 작성되어있는데, 사실 이런 Exception도 직접 캐치해서 처리하는 것이 좋습니다.
항목을 크게 나눠서 6개지만, 저 6개가 온통 내 코드에 뒤덮여있었다 ㅋㅋㅋㅋ
물론 전체 코드를 뒤엎어야 할 수도 있기 때문에 현재로서도 전부 고치진 못했다.
그래도 지금 까지 나름대로 1~5번 항목에 대해 조치해서 PR을 생성했고, 현재 피드백에 대해서 수정하고 있는 중이다!
마지막으로 아래는 멘토링 시간 마지막에 질문답변시간에 나눈 대화이다.
- [질문 1] access-token-auth 브랜치에서 PR을 만들기 전에 실수로 적용되지 않은 access-token-auth를 develop에 머지시키고 다른 브랜치들에게도 전부 적용하는 실수를 한적이 있는데, 적용되면 안되는 브랜치에 대해서 git reset hard를 실시하고 git push -f를 해서 원상복구했는데 문제없을까요?
- 답변 : 상관은 없습니다. 일반적으로 그래서 develop 브랜치는 권한을 가진 사람만 접근할 수 있게하기도 합니다.
- [질문 2] Qoddi를 사용하기 위한 설명 블로그를 공유해주신적이 있었는데, 여기서 actions → secrets에 webhook Url을 설정하라고 나와있는데 깃허브에 보니까 actions의 밑에는 secrets가 없었습니다. 어디에 하면 될까요?
- 답변 : 오타입니다. secrets → actions에 설정하시면 됩니다.
- [질문 3] 블로그에 나와있던 deploy.yml를 봤는데 ${secrets.WEBHOOK_URL} 이런 부분이 있던데 이게 secrets에 webhook Url을 설정해놓고 환경변수처럼 가져오는 건가요?
- 답변 : 맞습니다.
- [질문 4] .github/workflows/deploy.yml을 작성하려고 했는데 .github/workflows는 어디에서 찾을 수 있나요?
- 답변 : 어디에 존재하는건 아니고 프로젝트 폴더에 .github/workflows를 만들면 깃허브에서 알아서 인식하게 됩니다.
- [질문 5] access-token-auth 브랜치에서 FirebaseTokenFilterFactory를 만든적이 있었는데, 이렇게 만드는 것에 대해서 어떻게 생각하시나요?
- 답변 : 사실 왜 factory를 만들었는지 이해하지 못했습니다. 이전 기수에서도 이런 식으로 필터를 둘로 나누는 일이 있었는데 AuthService도 둘로 나뉘는 것 때문에 이전 기수에서 Profile을 사용해서 해결했었습니다. 지금 구조에서는 필터에서 AuthService를 가지고 있도록하면 해결될 것으로 보입니다.
마치며
아직 Exception을 처리하는 부분이나 생성된 PR의 피드백을 고치는 중인데, 이게 완료되면 아무래도 프론트엔드와 연동 테스트를 하게 될 것 같다.
연동까지 완료되면 최대한 남은 기간 동안 얻을 수 있는 것들은 얻기 위해 노력할 것이다.
감사합니다!!
'카우치코딩 > 주간 학습 및 개발사항' 카테고리의 다른 글
FUN편log 프로젝트 6주차 회고 (0) | 2022.12.26 |
---|---|
FUN편log 프로젝트 4주차 회고 (0) | 2022.12.12 |
FUN편log 프로젝트 3주차 회고 (0) | 2022.12.04 |
FUN편log 프로젝트 2주차 회고 (0) | 2022.11.28 |
FUN편log 프로젝트 1주차 회고 (0) | 2022.11.22 |