코드스테이츠 프리프로젝트가 끝난 이후,
제가 AWS를 사용하면서 겪은 예상치 못한 에러 하나와,
기간내에 해결하지 못한 에러 두개를 작성해보려 합니다.
1. AWS EC2 서버에서 스프링 프로젝트 빌드 무한로딩 에러
프로젝트를 완성 후, 기존에 사용하는 ngrok 이라는 프로그램으로 통신을 하면 get 메서드가 제대로 전달이 안되는 오류가 있어서, 개발중인 상태인 서버를 AWS EC2에 올려서 테스트를 해야하는 일이 있었습니다.EC2 인스턴스에 환경을 세팅하고, git으로 서버도 클론해오고, 빌드를 하려는 순간,
빌드 중 76%에서 시간만 증가하고 빌드가 이루어지지 않는 오류가 발생했습니다.여러가지를 찾아본 끝에 발견한 문제점은 메모리 문제였습니다.
제가 쓰는 인스턴스는 AWS freetier 에서 제공되는 무료로 쓸수있는 우분투 환경의 t2.micro 라는스턴스였고,
메모리가 1G밖에 안되는 상태에서 로컬 개발환경에서 쓰기위해 설정한 H2 메모리DB 환경으로 실행하려니까
메모리가 부족해 무한로딩이 걸리는 현상이였습니다.
해당 문제는 swap 메모리라는 것을 할당하는 것으로 해결했고, 이후 정상 빌드되는것을 확인할 수 있었습니다.
2. 쿠키 전략
이전 프리프로젝트 1, 2주차 에서 다룬 쿠키가 포스트맨으로 정상적으로 노출되지만,
실제 환경 (프론트서버와 도메인이 다른 환경)에서는 쿠키가 전달되지만 유지가 되지 않는 문제가 생겼습니다.
해당 문제는 쿠키의 SameSite 디폴트 설정값이 Lax 여서 생기는 문제였으며,
해당 설정에서는 Get 요청에만 한해서 다른 도메인에서도 쿠키가 전달이 될수있도록 설정이 되어있다고 합니다.
저희는 로그인 절차에서 email 과 password 를 보내야 해서 post 메서드를 써야했기에,
실제 쿠키가 헤더로 전달이 되도 유지가 안되는 것이였습니다.
또한, SameSite 옵션을 Lax 가 아닌 None 으로 설정하면 Secure 옵션이 true 여야지만 블록킹이 안된다고 하고,
(postman 환경에서는 samesite=none, secure=false 여도 쿠키가 정상적으로 오긴 했었습니다.)
Sucure 옵션이 true이기 위해서는 https 환경이여야 하고, 이를 위해서는 또 인증서를 발급받아야 하고,
mkcert를 통해서 로컬 환경에서만 유효한(?) 인증서를 발급받아서 postman으로 쿠키가 넘어오는것까지 확인했습니다만,
프론트쪽으로는 해당 인증서로는 막혀서, let's encrypt 를 통해서 인증서를 발급받으려 했지만 또 무언가 오류가 생겨서 막혀서....
해당 시점에서 프리프로젝트 마감까지 시간이 얼마 남지 않았던 상태라,
그냥 쿠키방식이 아닌 헤더로 넘겨서 프론트서버에서 핸들링하게끔 빠르게 결정을 내리고 쿠키는 완성하지 못했습니다.
아주 짧은 경험을 가진 제 생각이지만, 올바른 SSL인증서만 있었으면 정상작동하지 않았을까 합니다.
어쨌든 이번 경험으로 쿠키가 얼마나 개복치(...) 스러운지 알게되었고, 다음번에는 해낼 수 있을거라는 생각이 듭니다.
실패하긴 했지만, 아까우므로(...) 실패한 코드대로 공개하겠습니다.
// cookie
ResponseCookie cookie = ResponseCookie
.from("Authorization", "Bearer"+accessToken)
.path("/")
.sameSite("None")
.httpOnly(false)
.secure(true)
.maxAge(jwtTokenizer.getAccessTokenExpirationMinutes())
.build();
ResponseCookie refresh = ResponseCookie
.from("Refresh", refreshToken)
.path("/")
.sameSite("None")
.httpOnly(false)
.secure(true)
.maxAge(jwtTokenizer.getRefreshTokenExpirationMinutes())
.build();
3. QueryDsl 커스텀 쿼리의 group by 에서 데이터가 뻥튀기 되는 에러
이것 또한 제가 이전에 짰던 코드에서 시작합니다.
@Override
public List<QuestionResponseSimple> getQuestionsByQuestionPage(QuestionPage questionPage) {
QQuestion question = QQuestion.question;
QUser user = QUser.user;
QAnswer answer = QAnswer.answer;
List<QuestionResponseSimple> result = jpaQueryFactory
.select(Projections.fields(QuestionResponseSimple.class,
question.questionId,
question.title,
question.content,
user.displayName.as("user"),
question.createdAt,
question.votes,
question.view,
answer.count().as("answers")))
.from(question)
.join(question.user, user)
.on(question.user.userId.eq(user.userId))
.leftJoin(question.answers, answer)
.on(question.questionId.eq(answer.question.questionId))
.groupBy(question.questionId)
.limit(30)
.offset((long) (questionPage.getPage() - 1) * 30)
.orderBy(question.questionId.desc())
.fetch();
return result;
}
위에는 이전에 작성된 쿼리입니다.
사실 이 부분까지만 해도 문제가 없었습니다.
문제는, 해당 기능에서 Question 도메인에 또 한가지의 기능을 추가하면서 생겼습니다.
@Override
public List<QuestionResponseSimple> getQuestionsByQuestionPage(QuestionSearch questionSearch) {
QQuestion question = QQuestion.question;
QUser user = QUser.user;
QAnswer answer = QAnswer.answer;
QQuestionVote vote = QQuestionVote.questionVote;
List<QuestionResponseSimple> result = jpaQueryFactory
.select(Projections.fields(QuestionResponseSimple.class,
question.questionId,
question.title,
question.content,
user.displayName.as("user"),
question.createdAt,
vote.voteQ.sum().as("votes"),
question.view,
answer.count().as("answers"),
question.tags))
.from(question)
.join(question.user, user)
.on(question.user.userId.eq(user.userId))
.leftJoin(question.answers, answer)
.on(question.questionId.eq(answer.question.questionId))
.leftJoin(question.votes, vote)
.on(question.questionId.eq(vote.question.questionId))
.where(tagEq(questionSearch, question)) // 태그검색
.where(questionEq(questionSearch, question)) // 단어검색
.groupBy(question.questionId)
.limit(questionSearch.getSize())
.offset((long) (questionSearch.getPage() - 1) * questionSearch.getSize())
.orderBy(question.questionId.desc())
.fetch();
return result;
}
private static BooleanExpression tagEq(QuestionSearch questionSearch, QQuestion question) {
if (questionSearch.getTag() == null) {
return null;
}
return question.tags.contains(questionSearch.getTag());
}
private static BooleanExpression questionEq(QuestionSearch questionSearch, QQuestion question) {
if (questionSearch.getQ() == null) {
return null;
}
return question.content.contains(questionSearch.getQ())
.or(question.title.contains(questionSearch.getQ()));
}
현재 프리프로젝트에 적용되어있는 코드입니다.
여기서 잘 보시면 Question 도메인에 Vote라는 투표 기능이 추가된 것을 알수있고,
해당 기능도 Answer 와 마찬가지로 Question 입장에서 1:N 연관관계가 걸려있습니다.
문제는 여기서, 컬랙션 타입의 값 (Answer 와 Vote) 이 추가될 때마다 서로가 서로에게 영향을 받아,
group by 로 묶이는 데이터들이 뻥튀기 되어서 sum 값이랑 count 값에 원하는 값이 출력되지 않고 있습니다.
문제는 해당 버그를 마감 날짜에 전체적인 테스트를 하는 상황에서 뒤늦게 발견되었던 것입니다.
원인을 알았고, 고칠 자신도 있었지만, 시간이 없었기에 해당버그는 놔둔채로 마감짓게 되었습니다.
다음번에는 컬랙션 타입을 2개이상 join 할때는 좀더 주의깊게 설계를 해야할 것 같고,
너무 욕심부리지 말고 쿼리문을 하나정도 더 날리는 식으로 해서 문제를 해결할 수 있을것 같습니다.
// 사족
쿠키 기능은 사실 CodeStates 교육과정에서 다루지 않았던 내용이라 저에게는 하나의 새로운 도전이였고,
그만큼 배우고 구현하는데 많은 시간을 썻지만 결국 완성시키지 못해서 아쉽게 되었습니다.
또한, 페이징 쿼리도 저 나름 쿼리문 한번으로 어떻게든 최적화 해보겠다고 열심히 짠거였지만,
예상치 못한 버그가 발생했기에 뭔가 필살기가 막힌듯한 느낌(?)을 받아서 이것또한 정말 아쉽습니다.
그래도 시간만 조금더 주어졌더라면 해결 가능했을거란 자신감은 생겨서 여러가지로 성공적인 프로젝트가 된 것 같습니다.
쿠키 부분은 시간이 될 때 SSL인증서를 발급받아서 한번 더 테스트해보면 될 것 같고,
(이랬는데 안되면 자신감이 좀 떨어질 것 같네요ㅜㅜ)
쿼리 부분은, 아직 다 보지못한 JPA 강의를 보거나, 아니면 앞서 말했던대로 쿼리문을 두번날리는 식으로 해서 다음번에는 유연하게 대처할 수 있을거라 생각합니다.
'프로젝트' 카테고리의 다른 글
Project algo - websocket 기술검토 (0) | 2023.06.25 |
---|---|
CodeStates 메인프로젝트 - 웹소켓을 이용한 실시간 채팅 구현 / 코드 파해치기 (0) | 2023.03.19 |
CodeStates / StackOverflow 프리프로젝트 1, 2주차 (0) | 2023.02.27 |
토이프로젝트 / NodeToSpring - 페이징 기능 개선 (SqlResultSetMapping ) (0) | 2022.12.25 |
토이프로젝트 - NodeToSpring 4. 리팩토링 약간... (0) | 2022.12.13 |