일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 마법의 엘리베이터
- 협업프로젝트
- 카우치코딩
- Spring
- 코딩테스트
- pom.xml
- 유사 칸토어 비트열
- 토이 프로젝트
- 와이어 프레임
- Spring Framework
- 프로젝트 설계
- couchcoding
- 토이프로젝트
- 그리디 알고리즘
- 배포
- LEVEL 2
- 프로그래머스
- Qoddi
- ERD
- Fun편log
- 알고리즘
- DFS
- 빌드 툴
- 백준
- 트랜잭션
- Java
- maven
- GitHub
- 6주포트폴리오
- 테이블 해시 함수
- Today
- Total
소통 하고싶은 개발자
FUN편log 프로젝트 6주차 회고 본문
목차
- 시작하며
- Jpa Custom Repository 적용
- 기타 리팩토링
- 프론트엔드 연동 테스트
- 버그 수정 - 로그아웃 문제
- 1 회차 멘토링
- 버그 수정 - 회원탈퇴 문제
- 2 회차 멘토링
- 마치며
시작하며
벌써 마지막 주차가 되었다..
항상 프로젝트를 하다 보면 느끼는 것이지만 시간이 참 빨리 지나가는 것 같다.
Custom Repository 적용
저번 주에 나는 리팩토링을 하면서 KeywordContentService라는 클래스를 만들었고, 여기서 KeywordContentRepository를 가지고 있으면서 DB에서 가져온 엔티티 객체를 캐싱하도록 만들었다.
KeywordContent테이블은 우리 서비스 주제가 편의점에 대한 리뷰를 남기고, 편의점을 설명할만한 "뷰가 좋아요", "취식공간이 잘 되어있어요"와 같은 문구(키워드콘텐츠)를 저장하는 테이블로 이런 문구가 총 13개 정도 존재한다.
나는 13개의 항목에 대한 엔티티 객체를 획득하기 위해 매번 DB에 쿼리를 생성하는 것이 불합리하다고 생각했다.
때문에 Map<String, KeywordContent>를 만들어서 캐싱하고 있으면 좋겠다는 생각을 했고, KeywordContentService를 만들고 캐싱기능을 추가했던 것이다.
그런데 이렇게 변경하고 저번 멘토링 때 받은 피드백은 "사실 KeywordContentService가 캐싱 기능이 있는 Repository인 것 같습니다." 라는 내용이었고, 자연스럽게 커스텀 리포지토리를 만들어서 리포지토리 확장을 해보자고 이야기가 흘러간 것이다.
어쨌든 관련 자료를 찾아보기 시작했다.
기존에는 위 사진의 구조로 Repository를 사용하도록 하고 있었는데, 구글링을 하다 보니 아래 사진처럼 커스텀 리포지토리 인터페이스를 만들고 이를 구현한 리포지토리를 만들어서 인터페이스를 기존 리포지토리에 상속시키도록 하면 된다고 했다.
또한 CustomRepositoryImpl에서 EntityManager를 직접 사용하도록 작성해야 한다고 했기에 아래처럼 코드를 작성했었다.
package com.example.demo.repository.keywordcontent;
import com.example.demo.entity.KeywordContent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
@Transactional
public class KeywordContentCustomRepositoryImpl implements KeywordContentCustomRepository {
@Autowired
private EntityManager entityManager;
private Map<String, KeywordContent> allKeywordContentMap;
@Override
public KeywordContent getKeywordContent(String keywordContent) {
setAllKeywordContents();
if (!this.allKeywordContentMap.containsKey(keywordContent))
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 키워드입니다!");
return this.allKeywordContentMap.get(keywordContent);
}
@Override
public List<KeywordContent> getKeywordContentsByContent(List<String> contents) {
setAllKeywordContents();
List<KeywordContent> keywordContents = new ArrayList<>();
for (String content : contents) {
KeywordContent keywordContent = allKeywordContentMap.get(content);
if (keywordContent == null)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 키워드입니다!");
keywordContents.add(getKeywordContent(content));
}
return keywordContents;
}
private void setAllKeywordContents() {
if (allKeywordContentMap == null)
allKeywordContentMap = new HashMap<>();
if (allKeywordContentMap.size() == 0)
for (KeywordContent kc : findAll())
allKeywordContentMap.put(kc.getKeywordContent(), kc);
}
private List<KeywordContent> findAll() {
return (List<KeywordContent>) entityManager.createQuery("select kc from KeywordContent kc")
.getResultList();
}
}
나는 새로 작성한 이 코드가 잘 동작하는지 Rest Client로 몇 번 테스트를 했고, 일단 내가 보기에는 이상 없이 잘 동작하는 것으로 생각해서 이 변경 사항으로 Pull Request를 만들었다.
아래 사진은 그런 뒤에 멘토님께서 해주신 피드백이다.
이 피드백을 구조로 나타낸다면 아래 사진처럼 나타낼 수 있다.
만약 CustomRepositoryImpl에서 KeywordContentRepository를 직접 가지고 있었다면 순환 참조가 걸려서 실행할 수 없었을 테지만, 이렇게 하면 커스텀 리포지토리에서도 Jpa 메소드를 사용할 수 있게 된다.
최종적으로는 위 구조처럼 코드 수정을 했고, 커스텀 리포지토리를 만드는 목적을 가진 Pull Request가 Merge 되어 해결되었다!
기타 리팩토링
저번 주에 받았던 피드백 중 리팩토링하지 못했던 항목 중에는 DTO를 세분화하여 작성해보는 작업도 있었다.
원래 내 코드에서는 처음에 만들었던 ReviewDTO를 리뷰 등록 요청, 수정 요청, 조회 요청에 대한 응답의 세 부분에 동일한 DTO를 사용하고 있었다.
그래서 이런 부분에 대해 만약 역할이 달라지면 세분화하는 것이 좋은 구조가 될 것입니다라는 피드백을 받았었고, DTO를 역할에 맞게 아래처럼 나누었다.
- 리뷰 등록 요청에서 받아올 DTO : ReviewCreationDTO
- 리뷰 수정 요청에서 받아올 DTO : ReviewModReqDTO
- 리뷰 조회 요청에 대해 응답하기 위해 사용할 DTO : ReviewRespDTO
이렇게 이름을 짓고 각 요청을 처리하는 부분의 코드에 위 3가지 DTO를 적용시켰다.
프론트엔드 연동 테스트
이쯤에는 API 개발도 완료된 상태였고, 기능적인 문제도 크게 없었기 때문에 프론트엔드 분들 개발 진행 상황을 보다가 백엔드에서 생기는 문제가 있다면 고쳐서 다시 배포하는 등의 작업을 진행했다.
가장 큰 문제라고 한다면 로그인은 되는데 어찌된 일인지 로그아웃, 회원탈퇴가 제대로 동작하지 않는 문제가 있었다.
왜 이런 문제가 발생하는지 확인하기 위해 내 코드에 로그를 심고 다시 배포하는 작업을 반복하다가 로그아웃이 안 되는 문제는 왜 발생하는지 알아낼 수 있었다.
로그아웃 같은 요청은 쿠키에 로그인 정보가 있는지 체크한 뒤에 인증이 완료되어 생성된 User 정보를 컨트롤러에서 받아볼 수 있는데도 불구하고, 핸들러 메소드에서 쿠키를 한 번 더 조사하려고 했다.
몇 번의 로컬 디버깅을 통해 Filter에서는 잘 들어있던 쿠키가 핸들러 메소드에서는 하나도 들어있지가 않은 것을 확인했고, 인증이 완료된 User 객체는 @AuthenticationPrincipal 어노테이션을 사용해서 가져올 수 있다는 것을 구글링을 통해 알아냈다.
그래서 굳이 핸들러 메소드에서 쿠키를 탐색하려고 하지 않고, Filter로부터 받아온 User를 사용해서 로그아웃하도록 만들었더니 해결되었다!
버그 수정 - 로그아웃 관련 문제
위에서 로그아웃이 안되는 문제에 대해 해결했었다.
그리고는 문서 작업과 같은 이런저런 작업을 진행하다가 여전히 회원 탈퇴가 안 되는 문제가 있다는 것을 인지하게 되었다.
이에 대해 로컬에서 디버깅을 하다가 회원 탈퇴가 왜 안되는지 알 수 있었다.
회원 탈퇴를 하려고 할 때 UserService -> UserRepository.deleteUserById()를 통해 유저를 바로 지워버리려고 했는데, 유저 테이블은 구조적으로 리뷰 테이블, 키워드 테이블과 연관 관계가 형성되어 있었기 때문에 해당 유저가 작성한 리뷰나 키워드가 남아있기 때문에 유저를 DB에서 삭제할 수 없었던 것이다.
고민이 됐다. 왜냐면 유저가 회원 탈퇴를 했을 때 해당 유저가 활동했던 것들을 모두 지우도록 만드는 것도 다시 생각해야 하고, 심지어 만든다고 해도 회원 탈퇴 기능에 대한 성능이 매우 안 좋을 것 같았기 때문이다.
일단 유저의 활동에 의해 작성된 리뷰와 키워드를 지우는 것은 쉽게 구현하려면 @OneToMany()에 cascade옵션을 주면 간단히 해결할 수 있다. 일단 유저를 지우면서 내부에 존재하는 것들이 한번에 지워지기 때문이다.
또한 리뷰가 지워지면 cascade되어있는 키워드도 함께 지워지게 된다.
그리고 편의점 요약 정보 테이블인 store_summary에도 반영이 되어야 한다. 왜냐면 리뷰와 키워드가 지워진 상태인데 편의점 정보 테이블엔 남아있다 보니 다른 요청에서 편의점 조회 요청을 수행할 때 오류가 있는 것처럼 보일 것이기 때문이다.
이 부분에 대해서도 StoreSummary클래스 내부에서 리뷰 리스트를 받아와서 편의점 ID를 기준으로 Map<String, List<Review>>객체에 담은 뒤에 이 객체를 StoreSummary.deleteReviews()의 파라미터로 넘겨서 적절히 삭제 및 적용하는 코드를 작성했었다. (지금은 그 소스를 쓸 필요가 없어서 지운 상태라 소스코드가 없다 ㅠㅠ)
어찌되었든 이런 방식으로 유저가 회원 탈퇴를 할 때 유저의 활동 모두를 지우는 로직을 완성한 뒤에 성능 체크를 하기 위해 Rest Client를 써서 몇 초가 걸리는지 체크한 결과는 아래와 같다.
- 리뷰 100개, 키워드 300개에 대한 삭제 : 0.8초
- 리뷰 3000개, 키워드 9000개에 대한 삭제 : 20초
- 리뷰 10000개, 키워드 30000개에 대한 삭제 : 100초
뭔가 활동이 많으면 많을수록 시간이 더 걸리는 결과가 나왔다.
이 결과를 보고 뭔가 이전부터 멘토님이 성능에 관련하여 피드백을 해주실 때는 트래픽이 많아도 견딜 수 있을만한 코드를 작성하도록 피드백을 주셨던 것 같은데 지금 상태는 그렇지 못함을 느꼈다.
그래서 일단 피드백을 받아보고 다시 고쳐보기로 생각하고 넘어갔다.
1 회차 멘토링
멘토링이 시작되기 전부터 프론트엔드 멘토님이 6 주 1 회차 멘토링 시간에 프론트엔드는 배포를 실시할 것이라고 말씀하셨기 때문에 멘토링이 시작되자마자 배포에 대한 이야기를 나누게 되었다.
기존에 백엔드 서버가 배포 중이고 프론트엔드 분들이 로컬에서 테스트할 때 접근하려 할때는 CORS에러가 발생하기 때문에 프론트엔드에서 프록시 기술(자세히는 몰라요)을 사용해서 CORS를 막도록 처리하고 있었다.
그리고 파이어베이스에서는 프론트엔드 분들이 사용하던 프록시 기술을 지원하지 않았기 때문에 급하게 멘토님의 경험을 바탕으로 Netlify(네틀리파이)라는 서비스를 이용해서 프록시만 사용하기로 했다.
그래서 일단 내가 Netlify 아이디를 만들고 배포 준비를 시작하게 되었다. 나는 백엔드이기 때문에 만약 추후에 프론트엔드에 변경점이 생겨서 재배포하는 일이 발생한다면, 프론트엔드 자체적으로 배포를 하지 못하고 내가 배포를 해줘야하는 상황이긴 했지만, 일단 급해서 이렇게 진행했다.
하지만 초반에 프론트엔드 빌드가 잘 안되서
npm run build -> CI=false npm run build
로 빌드 스크립트를 수정했더니 배포가 잘 되었던 것으로 기억한다.
아직 자잘한 고 칠거리가 조금 남았지만 뭔가 배포가 되니 뿌듯했다.
마지막으로 멘토링이 종료되기 전에 나는 준비해온 질문거리들에 대해 질문했다.
- [질문 1] : Java에서 inner static class를 생성하게 되면 static 메모리에 저장되는 건가요? 그렇다면 GC에 의해 메모리가 관리가 될까요?
- 답변 : 일단 관리는 될 것으로 생각하는데, 알아보고 추후에 다시 답변드리겠습니다.
- [질문 2] : 커스텀 리포지토리를 만들었고, 피드백을 받아서 커스텀 리포지토리에서 특수하게 Jpa메소드를 사용하면 편해서 구조를 변경했는데, 일반적으로 이렇게 사용하나요?
- 답변 : 일반적으로는 자주 이렇게 사용하진 않습니다. 이번에는 리포지토리에서 캐싱 기능만 해야하기 때문에 Jpa를 사용할 수 있는 구조를 추천해드린 것이지만, Jpa에서 SqlMapper와 같은 기능을 사용해야할 때 커스텀 리포지토리를 만드는 것이 일반적입니다.
- [질문 3] : 커스텀 리포지토리를 만들고 기존 리포지토리에서 상속하도록 extends를 사용했는데, 원래 extends는 한개만 가능하지 않나요?
- 답변 : 인터페이스끼리 상속은 두 개 이상 가능합니다.
- [질문 4] : 이번에 DTO를 세분화하면서 ReviewCreationDTO, ReviewModReqDTO, ReviewRespDTO를 추가했는데, 네이밍이 괜찮을까요?
- 답변 : 명사 + 명사 형식으로 의미에 맞게 작성하면 되는 부분이라서 괜찮은 것 같습니다.
- [질문 5] : 회원탈퇴 기능을 수행하도록 하려고 했는데, Jpa 리포지토리 메소드인 deleteById() 메소드를 사용하니 연관된 객체들이 있어서 안된다는 Exception이 발생했습니다. 제 생각에는 리뷰나 키워드까진 지울 수 있어도 편의점 평균 정보까지 수정하게 된다면 성능적으로 문제가 있을 것 같은데 어떻게 생각하시는지 피드백 주시면 감사하겠습니다.
- 답변 : 회원 탈퇴라는 기능이 사용될 일이 적기도 하지만, 일반적으로 회원이 탈퇴하더라도 관련 데이터를 모두 지우지는 않는 것 같습니다. 일단 어떻게 결정하는가가 중요하겠지만, User의 상태를 비활성화로 만드는 것이 현재 상황에서 일정에 가장 합리적인 선택인 것 같습니다.
- [질문 6] : 멘티 혹은 주니어 개발자가 PR을 생성했을때, 어떤 것들을 중점적으로 보시고 또 어떤 페이지를 열어서 주로 확인하시는지 알려주시면 감사하겠습니다.
- 답변 : 보통은 소스 전체를 확인하진 않고 file changed를 주로 확인합니다. 일반적으로는 변경 사항이 제대로 돌아간다고 생각하기 때문에 변경 부분만 확인하는 것이고, 때문에 제대로 돌아가는지 테스트 코드를 만드는 등의 방법으로 정상 작동을 하는지 확인을 먼저 하고 Pull Request를 만들어야 합니다.
버그 수정 - 회원탈퇴 문제
1 회차 멘토링이 종료되기 전의 질문답변 시간에 나는 총 여섯 가지의 질문을 했었고, 그중에는 회원탈퇴 문제 관련 조언을 듣기 위한 질문도 있었다.
다음은 내가 회원탈퇴 문제를 해결하기 위해 했던 생각의 흐름 정리본이다.
- 회원 탈퇴 기능을 수행하는 과정에 해당 User를 직접 DB에서 지우는 행위는 에러를 발생시킨다.
- 에러를 발생시킨 원인이 다른 테이블에서 User테이블의 PK를 FK로 사용하고 있었기 때문임을 알았다.
- User테이블과 다른 테이블의 연관관계를 끊는 것은 뭔가 맞는 해결 방법이 아니라고 생각했다.
- cascade 옵션으로 User 삭제 시 연관 테이블에 영향을 주는 것은 어떨지 생각했다.
- 유저 삭제 시 편의점 정보(summary) 테이블에서도 해당 유저의 활동으로 인해 변화된 것들이 삭제되어야 하는데, 편의점 정보 테이블은 리뷰를 등록/수정/삭제할 때 계산해서 적용되도록 만들어져 있기 때문에 삭제되지 않음을 깨달았다.
- 편의점 정보 테이블까지 적용되도록 유저 삭제 로직을 변경해 봤다.
- 성능을 확인해 봤는데, 유저의 활동이 많으면 많을수록 성능이 눈에 보이도록 안 좋아짐을 확인했다.
- 1 회차 멘토링 때 멘토님께 자문했다.
- 멘토님께서는 만약 전부 다 지우려고 한다면 테이블을 더 늘려서 효율적으로 처리하는 방법도 있지만, 유저를 삭제하지 않고 비활성화 상태로 만드는 방법도 있다고 조언해 주셨다.
아무래도 테이블을 더 만들고 로직을 새로 생각하는 것보다는 상대적으로 유저를 비활성화 상태로 변경하는 것이 시간이 적은 당시에 더 끌렸던 것 같다.
수정할 내용은 다음과 같았다.
- DB 명세서 및 ERD Cloud의 유저 테이블에 활성화 상태 컬럼을 추가한다.
- User 클래스에 activeStatus 필드를 추가한다.
- 회원탈퇴 요청을 수행하는 과정에서 유저를 삭제하지 않고 유저의 상태를 비활성화 상태로 바꾸어 저장하도록 설정한다.
- 추가적으로 리뷰 조회 요청을 수행하는 과정에서 비활성화 유저의 리뷰는 '존재하지 않는 유저'로 변경하여 응답하도록 한다.
2 회차 멘토링
2 회차 멘토링은 사전에 예고받기도 했지만, 지금까지 만든 애플리케이션에 대해 발표를 했다.
발표는 우리 팀 3 명 중 아무나 하는 것으로 프론트엔드 분들 중 한 분이 발표를 하셨다.
해당 리뷰들은 임의로 리뷰를 많이 넣어둔 것이다. (실제 편의점의 내용이 아니다)
발표가 끝나고 기획 멘토님께서 몇몇 질문을 하셨다.
- 프로젝트를 기간 안에 계획했던 만큼 완성할 수 있었는데, 소감은 어떤가요?
- 프로젝트를 진행하면서 아쉬운 점이나, 만약 기간이 조금 더 있었다면 어땠을 것 같나요?
- 프로젝트를 마친 지금 가장 만족스러운 점은 무엇인가요?
- 프로젝트를 마친 지금의 바로 다음 목표는 무엇인가요?
- 프로젝트를 막 시작했을 때와 비교해서 좀 더 원하는 개발자의 모습이 된 것 같나요?
마치며
회고 포스팅이기 때문에 마지막 질문들에 대해 내가 어떻게 대답했는지 정확히 기억나지 않아서 답변은 쓰지 않았지만, 개인적으로 Spring Data Jpa, Spring Security를 처음으로 사용하면서 멘토링을 받을 수 있었기도 했고, 혼자 했을 때와 비교해서 멘토링으로 인해 조금 더 단 기간안에 프로젝트를 완수할 수 있었다.
그리고 이전에 업무를 할 때는 UI도 직접 만들고 수정하면서 일을 했었는데, 이렇게 UI를 이쁘게 만들어주신 프론트엔드 분들이 계셔서 확실히 결과물이 더 만족스럽게 만들어질 수 있었던 것 같다.
물론 백엔드 개발자는 나 혼자였지만 그럼에도 불구하고 Git Flow를 사용하면서 깃허브에 대해 더 익숙해질 수 있었던 것 같고, Pull Request를 실제로 만들어보면서 실제로 협업을 할 때 이런 식으로 진행되겠구나 라는걸 알 수 있어서 신청하길 잘했다는 생각이 든다.
아마 당분간은 깃허브 Readme나 시연 영상을 촬영하게 될 것 같다.
감사합니다!!
'카우치코딩 > 주간 학습 및 개발사항' 카테고리의 다른 글
FUN편log 프로젝트 5주차 회고 (0) | 2022.12.19 |
---|---|
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 |