항해66
지난 3주간 스프링을 익히며
배운 것들을 사용할 수 있는 기회로
생각하며 도전해 보았다.
프로젝트 소개
66일이 남은 시점..
우리도 이제 자신 있게
할 말 못 할 말 모두
말할 수 있다.
국내 1위 직장인 커뮤니티
블라인드를 밴치마킹
모두에게 즐거움을 주는 놀이터
당당히 말할 수 없던
속 마음을 터놓고 이야기해 보자
유저 아이디의 첫 글자, 주특기, 기수만 공개된다.
ex) u***** (Spring) 14기
개발 기간
2023.05.04 ~ 2023.05.11
S.A
와이어 프레임
ERD
API 명세서
깃허브 및 BE팀원
이름 | 역할 |
이현규 @OliveLover |
- 댓글 API - 게시글, 댓글 좋아요 API - 서버 배포 - 검색 기능 - DB 연동 |
정성윤 @kanteluv |
- RefreshToken, AccessToken - 유저 API - entity 연관관계 |
김종범 @JayB202 |
- 게시글 API - 검색 기능 |
한승희 @seunghee58 |
- 댓글 API - 게시글, 댓글 좋아요 API - 검색 기능 |
FE 팀 깃허브 : https://github.com/HaeJinS2/MiniProject_Hanghae66_FE
BE 팀 깃허브 : https://github.com/OliveLover/miniproject_hanhae66
기술 스택
기능
- 게시글 작성 API
- 카테코리를 선택 후 글을 작성 - 게시글 수정 API
- 내가 작성한 글에 한하여 수정 가능 - 게시글 삭제 API
- 내가 작성한 글에 한하여 삭제 가능 - 게시글 목록 조회 API
- 게시글 목록을 내림차순으로 조회
- 카테고리 별 게시글 조회 - 게시글 상세 조회 API
- 게시글의 내용과 댓글 리스트 - 게시글 좋아요 API
- 게시글에 좋아요 버튼을 눌러 좋아요 표시
- 버튼을 한 번 더 누르면 취소 - 게시글 검색 API
- 키워드를 입력하여 제목+내용, 제목, 내용으로 게시글 조회 - 댓글 작성 API
- 모든 게시글에 댓글 작성 가능 - 댓글 좋아요 API
- 댓글에 좋아요 버튼을 눌러 좋아요 표시 가능
- 버튼을 한 번 더 누르면 취소
둘러보기
트러블 슈팅
Unsuppored class file major version 64 빌드오류 발생
이슈 내용
깃에서 프로젝트를 가져와서 실행 시 빌드를 하지 못하고
“Unsupported class file major version 64” 메시지를 보여주는 오류
해결을 위하여 시도해 본 것
https://velog.io/@murphytklee/Unsupported-class-file-major-version-64-에러
위 링크에서 해결방법 검색
해결 방법
“File” → “Project Structre”를 가서 아래의 표시한 부분 확인
“File” → “Settings” → “Build, Execution, Deployment” → “Gradle” 에서 아래의 표시한
부분을 확인하여 설정 변경 한 뒤 SET JDK를 한다.
Users table 생성 불가
이슈 내용
“Users table” 생성 불가
이슈 원인
user 엔티티에서
String 형태의 PK를 @GeneratedValue(strategy = GenerationType.IDENTITY)
설정을 해주어서 발생하였다.
해결 방법
없애 주었다.
UnsatisfiedDependencyException 오류 발생
이슈 내용
UnsatisfiedDependencyException 오류 발생
org.springframework.beans.factory**.**UnsatisfiedDependencyException: Error creating bean with name 'commentController' defined in file [C:\Users\tikto\OneDrive\바탕 화면\miniproject_hanhae66\build\classes\java\main\com\sparta\hanghae66\controller\CommentController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'commentService' defined in file [C:\Users\tikto\OneDrive\바탕 화면\miniproject_hanhae66\build\classes\java\main\com\sparta\hanghae66\service\CommentService.class]: Unsatisfied dependency expressed through constructor parameter 2: Error creating bean with name 'commentLikesRepository' defined in com.sparta.hanghae66.repository.CommentLikesRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Could not create query for public abstract java.util.Optional com.sparta.hanghae66.repository.CommentLikesRepository.findByUsernameAndCommentId(java.lang.String,java.lang.Long); Reason: Failed to create query for method public abstract java.util.Optional com.sparta.hanghae66.repository.CommentLikesRepository.findByUsernameAndCommentId(java.lang.String,java.lang.Long); No property 'username' found for type 'CommentLikes' at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) ~[spring-beans-6.0.8.jar:6.0.8] at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:245) ~[spring-beans-6.0.8.jar:6.0.8]
스프링 빈 생성 중 발생한 오류
해결 방법
리팩토링 과정에서 달라진 변수에 의한 Repository 쿼리가 맞지 않아 발생한 오류
likeservice에 관한 JPQL쿼리문을 달라진 변수로 바꾸거나 테스트를 위해 해당 부분 모두 주석
ClassCastException 오류 발생
이슈 내용
ClassCastException 오류 발생
g.ClassCastException: class java.lang.String cannot be cast to class com.sparta.hanghae66.entity.User (java.lang.String is in module java.base of loader 'bootstrap'; com.sparta.hanghae66.entity.User is in unnamed module of loader 'app') at com.sparta.hanghae66.service.UserService.login(UserService.java:70) ~[main/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
이슈 원인
String 타입인 Id를 User 객체로서 사용하려는 과정에서 발생한 오류
해결을 위하여 시도해 본 것
try 문 안에
Optional<User> found = userRepository.findByUserId(userId);
if (found.isPresent()) {
return new ResponseDto("아이디 중복", HttpStatus.*BAD_REQUEST*);
}
이곳은 해결되어도 found는 user의 id값만 가지고 있어서
비밀번호 인증에 사용할 수 없었음
해결 방법
@Repositorypublic interface UserRepository extends JpaRepository<User, String> {
`@Query("SELECT u FROM TB_USER u WHERE u.id = :userId")`
Optional<User> findByUserId(@Param("userId") String userId);
}
UserRepository에서 쿼리문을
@Query(”SELECT u. id ….. “) → @Query(”SELECT u ….”) 로변경하여
uere의 string id 값만이 아닌 user전체를 가지고 오도록 변경하여 로그인 성공
게시글 선택 조회 시 코멘트 리스트를 불러오지 못하는 오류
이슈 내용
게시글 선택 조회 시 코멘트 리스트를 불러오지 못하는 오류가 발생하였다.
• 오류 코드
해결을 위하여 시도해 본 것
• 팀원들과 문제점에 대하여 이야기하여 로직 체크 → 전체 코멘트 리스트를 불러오면서 매칭시킬 게시글의 시퀀스를 가져오지 못하는 로직이 됐음을 확인
해결 방법
• 전체 코멘트 리스트를 가지고 오는 것이 아니라 게시글 시퀀스를 매개변수로 받아 코멘트 리스트를 불러오도록 변경
게시글 조회 시 조회수가 올라가지 않는 오류
이슈 내용
게시글 조회를 하였음에도 조회수가 올라가지 않는 오류가 발생함
• 오류 코드
해결을 위하여 시도해본 것
• 테스트 시 visitCnt가 정상적으로 올라가는 것은 확인, 하지만 실제 데이터베이스에 변경되지 않는 것을 체크 후 로직을 다시 살펴보았다.
해결 방법
• 오류 코드에서는 @Transactional(readOnly = true)를 사용하여 조회 작업만 수행,
데이터베이스에서 조회한 post 객체는 영속성 컨텍스트에서 관리되고 있지만 변경이 되지 않았다.
(readOnly = true)를 삭제함으로 간단하게 해결되었다..!
• 해결 코드
게시글 좋아요 카운트 문제 수정
이슈 내용
게시글에 좋아요를 누르면 테이블을 조회 시 카운트가 올라가는 것을 확인했으나 로컬 페이지에서는 좋아요 개수가 1개로만 출력되고 있음
해결을 위하여 시도해 본 것
로직수정 - 게시글 조회 시 getpost 메서드에서 postdto으로 응답하나 해당 dto에는 좋아요 카운트가 집계되지 않도록 되어 있었음
해결 방법
getpost 메서드에서 좋아요 카운트를 postdot에 세팅되도록 로직 변경
mySQL 연결 오류
이슈 내용
H2 → mySQL연결 시 데이터베이스를 쿼리를 날려 테이블생성은 되지만
오류가 뜨며 저장이 되지 않음
해결 방법
webSecurityConfig에서 H2권한 허용하는 부분 주석처리
데이터베이스 불필요한 연결 증가
이슈 내용
계속하여 불필요한 연결이 증가하는 현상
해결을 위하여 시도해 본 것
해결 방법
토큰 로직 변경으로 리프레쉬 키값이 없다면 로그인창으로 팅구면서 세션 닫히게끔 유도
useQuery 데이터 리패칭
이슈 내용
useQuery 사용 시 자동 데이터 리패칭 기능으로 인한 axios 요청 이슈
해결을 위하여 시도해 본 것
useEffect 의존성 배열에서 data 지우기
해결 방법
useQuery에 refetchOnWindowFocus: false 옵션 추가
인증 정보를 포함한 요청 시 CORS 오류 발생
이슈 내용
Cookie 혹은 Authorization 헤더에 인증 정보를 함께 보내야 하는 요청 시(Credentialed Request) 시에 CORS 오류 발생
해결을 위하여 시도해 본 것
https://it-eldorado.tistory.com/163
위 자료 4-3 참고하여 별도 설정
해결 방법
CORS 정책에 관한 것들을 다 설정해 주었음에도 불구하고 에러가 떴다.
자료를 참고해 보니 인증 정보를 포함한 요청 시에는 클라이언트 단에서도 별도의 설정이 필요함을 확인하고
jQuery의 ajax, 또는 axios를 사용하여 withCredentials 옵션을 true로 설정을 해주었고,
Access-Control-Allow-Credentials 헤더는 true로 설정해주었다.
게시글 조회 카운트가 렌더링 될 때마다 올라가는 이슈
이슈 내용
게시글 조회수를 올리기 위해 조회 시 카운트를 1씩 더하도록 로직을 구성함
하지만 좋아요나 댓글 작성, 게시글 수정 등을 하면 바뀐 정보값을 반영하기 위해 렌더링 됨
렌더링을 하면 카운트를 더하는 로직이 작동하므로 조회수가 계속 카운트되는 현상 발생
해결을 위하여 시도해 본 것
- 쿠키에 조회한 게시글 정보값을 남겨 쿠키값이 있을 시 그 값을 확인해서 카운트 더하는 로직을 타지 않도록 구성하였음 → 실패 → 포스트맨으로 테스트 완료하였으나, 실제 배포 환경에서는 쿠키가 생성되지 않음 → 여러 가지 정책 설정값과, 도메인이 일치하지 않으면 쿠키가 잘 생성되지 않는다고 하여 보류
Cookie oldCookie = null;
Cookie[] cookies = request.getCookies();
Long visitCnt = 0L;
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("postView")) {
oldCookie = cookie;
}
}
}
if (oldCookie != null) {
if (!oldCookie.getValue().contains("[" + postId.toString() + "]")) {
visitCnt = post.getPostVisitCnt();
visitCnt++;
post.setPostVisitCnt(visitCnt);
oldCookie.setValue(oldCookie.getValue() + "_[" + postId + "]");
oldCookie.setPath("/");
oldCookie.setDomain(".hanghae66.s3-website.ap-northeast-2.amazonaws.com");
oldCookie.setMaxAge(60 * 60 * 12);
oldCookie.setSecure(true);
oldCookie.setHttpOnly(false);
response.addCookie(oldCookie);
}
} else {
visitCnt = post.getPostVisitCnt();
visitCnt++;
post.setPostVisitCnt(visitCnt);
Cookie newCookie = new Cookie("postView","[" + postId + "]");
newCookie.setPath("/");
newCookie.setDomain(".hanghae66.s3-website.ap-northeast-2.amazonaws.com");
newCookie.setMaxAge(60 * 60 * 12);
newCookie.setSecure(true);
newCookie.setHttpOnly(false);
response.addCookie(newCookie);
}
해결 방법
게시글 방문 기록을 데이터 테이블로 남겨 로그화를 진행 → 성공 사용자가 방문한 게시글 기록을 테이블에 저장시켜서, 조회수를 카운트하도록 기능을 추가 →
1. 테이블에 방문한 유저 id, 게시글 번호, 방문일, 방문시간을 저장
2. 게시글 번호는 1, 2, 3,.. 순으로 , 으로 스플릿 할 수 있도록 저장시킴
3. 조회 시 유저 id를 확인하여 테이블에 데이터가 있는지 확인하고, 없으면 생성
4. 저장된 데이터의 방문일을 비교하여 방문일 날짜가 현재 날짜보다 작으면 날짜일을 최신으로, 그리고 지금 방문한 게시글 번호를 데이터로 업데이트 처리 그리고 조회수 카운트 증가
5. 현재 날짜값과 같거나 크면(클 경우는 없음) 게시글 번호를 리스트로 받아 현재 보고 있는 게시글 번호가 있는지 확인
6. 있으면 조회수 카운트는 증가하지 않고, 없으면 게시글 번호를 업데이트해주면서 조회수 증가
@Transactional
public void postViewCheck(String userId, Post post, PostDto postDto) {
Long postId =post.getPostId();
String visitDat = "";
List<String> visitDataList = new ArrayList<>();
int crDat = 0;
int crTime = 0;
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmm");
LocalDate nowDate = LocalDate.now();
LocalTime nowTime = LocalTime.now();
VisitInfo visitInfo = null;
crDat = Integer.parseInt(nowDate.toString().replaceAll("-", ""));
crTime = Integer.parseInt(nowTime.format(timeFormatter));
Optional<VisitInfo> chkVisitInfo = visitInfoRepository.findVisitInfoByVisitUserId_Opt(userId);
if(chkVisitInfo.isPresent())
{
visitInfo = visitInfoRepository.findVisitInfoByVisitUserId_ent(userId);
//현재날짜가 visitInfo의 데이터 날짜보다 크거나 같을때 1, // 20230509
if(crDat > visitInfo.getVisitCrdAt()) {
visitDat = postId + ", ";
visitInfo.setVisitCrdAt(crDat);
visitInfo.setVisitCrTime(crTime);
visitInfo.setVisitData(visitDat);
visitInfoRepository.save(visitInfo);
visitCnt(post, postDto);
}
else { //crDat이 작을때 -> visitInfo의 데이터 날짜가 더 클때
visitDat = visitInfo.getVisitData(); // 1, 2,
visitDataList = Arrays.asList(visitDat.split(", "));
if(!visitDataList.contains(postId.toString())) {
visitDat += postId + ", "; //1, 2, 3,
visitCnt(post, postDto);
}
visitInfo.setVisitData(visitDat);
visitInfoRepository.save(visitInfo);
}
}
else {
visitDat = postId + ", "; //1, 2, 3, 4, .....
visitInfo = new VisitInfo(userId, visitDat, crDat, crTime);
visitInfoRepository.save(visitInfo);
visitCnt(post, postDto);
}
}
//조회수 증가 메서드
public void visitCnt(Post post, PostDto postDto) {
Long visitCnt;
visitCnt = post.getPostVisitCnt();
visitCnt++;
postDto.setPostVisitCnt(visitCnt);
post.setPostVisitCnt(visitCnt);
}
다음 프로젝트에 반영할 점
N+1 문제에 대하여
@jsonignore같은 어노테이션 방법보다
fetch join 을 사용할것
아쉽더라도 프로젝트 완성시 불필요한 주석은 제거할것
주석문 없이도 이해되는 코드가 좋은코드이고
오히려 유지보수에 어려움을 준다고 한다.
리프레시 토큰은 액세스를 발급하면
반드시 리프레시토큰을 초기화 시켜야한다고 한다.
레포지토리에서는 쿼리어노테이션보다는 jpql로 사용하라고한다.
Qeury문은 오타 또는 컴파일 시점을 잡아줄수 없어
String 형태로 짜는것은 정말 필요할 때가 아니면 피하라고 한다.
querydsl이나 JPA로 관리하는 것이 실수를 막으며
관련 플로그인으로 JPA Buddy가 있으니 사용해 보면 좋다고한다.
MySql 연결, h2설정시 프로퍼티스(yml) 프로파일 설정을 통해서
local일때는 h2로, 운영일 때는 MySql로 할 수 있다고 하므로 알아봐야겠다.
userId만 필요한 상황이면 JPAaudititng을 사용하는게 좋은데
이런 유저의 필드를 3개이상 정도 Post가 가져가야되는 상황이면,
연관관계를 맺는게 일반적으로 더 좋다고 한다.
회고
이번 프로젝트에서 첫 BE의 팀장을 맡으며
발표도 직접 해보았다.
이번 첫 FE와의 첫 협업을 하며 많은 것을 느꼈다.
API 명세의 중요함을 정말 많이 느꼈던 것 같다.
다음 프로젝트 때에는 swagger로 변수명에 오타 없이
최대한 빠르게 명세를 작성해 주는 것을 노력해 볼 것이다.
이번 프로젝트는 이전 주특기를 갈고닦는 주간에 배운 것을
FE와 한 번 제대로 만들어보자는 의미가 있는 시간이었다.
'프로젝트 회고' 카테고리의 다른 글
실전 프로젝트) Apoorpoor - 작성했던 테스트 코드의 방향성 (0) | 2023.07.02 |
---|---|
실전 프로젝트) Apoorpoor - 챌린지 생성 (2) | 2023.07.02 |
실전 프로젝트) Apoorpoor - 아이템 필터 (0) | 2023.07.02 |
실전 프로젝트) Apoorpoor (0) | 2023.07.01 |
토이 프로젝트) 너의 기름은? (0) | 2023.04.01 |