😍

백엔드

2.1 피어 세션 피드백
  • HttpSession을 사용하여 로그인을 구현할 때, 흐름이 웹 브라우저 → 프론트(웹 서버) → 백엔드(WAS) 이렇게 되는데, 배포할 때 쿠키 처리와 세션에 문제가 있을까?
  • 특장점
    • 차 사진 등록할 때 차 종류, 모델명에 옵션도 추가하면 좋겠다
    • 완성도를 높이자
 
  • 협업 방식(역할 분담)
    • 이슈 별로? API 별로?
 
  • 배포 환경
    • 깃허브 액션
 
  • ec2에 mysql 설치해서 사용 (RDS x)
 
2.10 피어 세션 메모
쿠키 - 세션 allowCredentials

 
 
❗️유저로부터 이름이 같은 다른 이미지파일이 들어올 수도 있어서 이미지 파일 이름을 어떻게 저장할지 고민해봐야할듯. eg) postId/이미지이름.jpg 로 저장해도 괜찮을듯
 
좋아요 동시성 처리
문제점: 글을 불러올 때마다 좋아요 테이블 전체조회 > 성능 하락
notion image
notion image
999,353 테이블 조회 시간: 92ms(0.092s)
notion image
해결방안
  • 반정규화
    • 게시글 테이블에 좋아요 개수 칼럼 추가
    • 문제점
    • 데이터 정합성 문제
    • 데이터 정합성을 위해 스케줄러 사용 → post가 많을 경우, 모든 post마다 너무 자주하면 오히려 성능 저하 가능성
    • 성능 저하 생각? 기본적으로 디스크의 read보다 write가 오래걸림
    • + index → 쓰기 성능 저하..? 상관없을듯
    • 고려할 점: 유저가 게시글을 클릭하는 경우마다 join vs 매 n초마다 모든 post 데이터의 like 수 update.
      • 좋아요 수가 많으면 join 때문에 조회 성능 저하 VS 스케줄러 사용 시 전체 post 수가 많으면, 모든 post의 좋아요 수를 한 번에 update하므로 속도 저하
    • 내가 생각하는 타협점: 유저의 팔로워수든, 게시글의 인기수든 판단을 해서 인기많은 것들은 스케줄러, 일반적인 글은 join 사용 (글 조회 횟수가 적으니 join도 적음)
 
좋아요 버튼 광클
좋아요 여부 판단 > 좋아요 true > 좋아요 취소 진행
좋아요 여부 판단 > 좋아요 true > 좋아요 취소 진행 > commit
1번 요청
2번 요청
좋아요
좋아요 여부 판단
true
좋아요 여부 판단
true
좋아요 true
true
좋아요 true
true
좋아요 취소 진행 > isDeleted = 1
false
좋아요 취소 진행 > isDeleted = 1
false
false
false
UnLike Success
UnLike Success
1번 요청
2번 요청
좋아요
좋아요 여부 판단
false
좋아요 여부 판단
false
좋아요 false
false
좋아요 false
false
좋아요 진행 새로 만들거나 isDeleted = 0
true
좋아요 진행 새로 만들거나 isDeleted = 0으로 변경 인데 이미 있다고 판단 > 같은 값으로 덮어씀
true
true
true
Like Success
Like Success
 
hard delete인 경우
1번 요청
2번 요청
좋아요
좋아요 여부 판단
false
좋아요 여부 판단
false
좋아요 false
false
좋아요 false
false
좋아요 진행 새로 만들기
true
좋아요 진행 새로 만들기
true
true
true
Like Success
Like Success
➡️ 좋아요가 2개 삽입됨.
soft delete를 통해 동시성 이슈 해결
 
문제: 좋아요 스케쥴링을 모든 post마다 너무 자주해주면 오히려 성능이 저하될 수 있다…
결과적 일관성: NoSQL에서는 응답시간, 일관성, 지속성의 균형을 위해 결과적 일관성을 구현한다.
 
무한 스크롤
문제점: 게시글 추가할 경우 게시글 중복, 게시글 삭제할 경우 게시글을 보지 못함.
게시글 추가 시
100번 게시글 추가 시 104번 게시글이 중복되어 사용자에게 출력됨.
101
102
103
104
105
106
107
108
100
101
102
103
104
105
106
107
게시글 삭제 시
101번 게시글 삭제 시 105번 게시글이 사용자에게 출력되지 않음.
101
102
103
104
105
106
107
108
102
103
104
105
106
107
108
109
 
해결방안
 
 
인덱스를 게시글 수로 잡지 않고 게시글 id로 잡으면 해결!
게시글 추가 시
104번을 요청으로 보내주면 105번부터 출력해주니 해결
단, 새로운 게시물은 새로고침을 통해 확인
101
102
103
104
105
106
107
108
109
100
101
102
103
104
105
106
107
108
게시글 삭제 시
104번을 요청으로 보내주면 105번부터 출력해주니 해결
단, 사용자가 101번 게시글로 접근 했을 때 예외처리 필요
101
102
103
104
105
106
107
108
102
103
104
105
106
107
108
109
 
비로그인
기존 쿼리
SELECT img.post_id, img.image_url FROM POST AS p INNER JOIN IMAGE AS img ON p.id = img.post_id WHERE p.is_deleted = false ORDER BY p.create_date DESC LIMIT ?, ?"
 
변경 방안
SELECT img.post_id, img.image_url FROM POST AS p INNER JOIN IMAGE AS img ON p.id = img.post_id WHERE p.is_deleted = false and p.id < ?(postId) ORDER BY p.create_date DESC LIMIT ? (size)
 
로그인
기존 쿼리
FROM POST AS p, IMAGE AS img, FOLLOW AS f where f.is_deleted = false and p.is_deleted = false and f.follower_id = ? and f.following_id = p.user_id and p.id = img.post_id ORDER BY p.create_date DESC LIMIT ?, ?
 
변경 방안
FROM POST AS p, IMAGE AS img, FOLLOW AS f where f.is_deleted = false and p.is_deleted = false and p.id < ?(postId) and f.follower_id = ? and f.following_id = p.user_id and p.id = img.post_id ORDER BY p.create_date DESC LIMIT ? (size)
 
검색
PostServicesearchByTags()에서 호출하는 findImagesOfPostsStartsWithIndex() 메서드 로직 수정
 
🎉 2/8(수) 백엔드 첫 배포
백엔드 3명이서 깃헙 액션 설정하고 쉘 스크립트 짜고 자바 깔고 난관 하나씩 해결

너무 어려웠던 issues

aws Permission denied 오류 >> 재훈 해결중,, >> 해결완료
macbook16@macbook16ui-noteubug ~ % chmod 400 Carbook-key.pem macbook16@macbook16ui-noteubug ~ % ssh -i CarBook-key.pem ubuntu@ec2-3-37-102-17.ap-northeast-2.compute.amazonaws.comubuntu@ec2-3-37-102-17.ap-northeast-2.compute.amazonaws.com: Permission denied (publickey).
우분투 노드 버전 문제!!!
프론트 - 백엔드 요청 경로 문제 해결!!
dto 필드가 1개면 json 파싱이 안돼요,,,,
CSV 저장했는데 글씨가 깨짐…..
스크린샷
notion image
users.to_csv('users.csv', index = False, encoding='cp949')로 해결!
sample data python code
mysql 시간 문제
Repository test in-memory testdb(h2)
@JdbcTest 어노테이션 추가하고 진행 > datasource 찾지 못하는 오류
@AutoConfigureTestDatabase(replace = Replace.NONE) 추가 > mysql로 데이터소스 가져옴
h2 데이터베이스 build.gradle에 추가, application-test.properties로 h2 설정
테스트 클래스에서 datasource를 @Autowired로 받아와서 해결!
h2와 mysql 문법이 달라서 계속 오류 > datasource url에 mode = mysql;추가하면 해결된다
>해결안됨 > create_table.sql에 SET MODE MYSQL 추가해서 해결!
 
mysql, h2 keyholder
mysql: biginteger
h2: int
생성된 id 값의 이름도 다름
mysql: GENERATED_KEY
h2: ID
 
 

개발 메모

주형 개발 메모
2/6 월 TODO LIST
@ExceptionHandler 공부 및 적용
Controller에서 ResponseEntity 생성
isLoginSuccess > login 함수명 변경
@methodargumentnotvalidexception >> @Valid 예외 처리
클래스명 SignupNicknameDuplicateException보다 NicknameDuplicateException
email, nickname 중복 메세지 api 문서대로
Test 코드 작성
DB 연결
methodargumentnotvalidexception handling 안돼서 bindexception으로 처리
 
2/7 화 로그아웃 메모
https://joalog.tistory.com/81 로그아웃 메모
2/8 수
엔진x origin null?
2/10 금 aws bulk insert
load data local infile "/home/ubuntu/sample_data/users.csv" into table USER fields terminated by "," ignore 1 lines (email, password, nickname);
load data local infile "/home/ubuntu/sample_data/posts.csv" into table POST fields terminated by "," ignore 1 lines (user_id, create_date, update_date, content, model_id);
load data local infile "/home/ubuntu/sample_data/images.csv" into table IMAGE fields terminated by "," ignore 1 lines (post_id, image_url);
load data local infile "/home/ubuntu/sample_data/follows.csv" into table FOLLOW fields terminated by "," ignore 1 lines (follower_id, following_id);
load data local infile "/home/ubuntu/sample_data/models.csv" into table MODEL fields terminated by "," ignore 1 lines (type_id, tag);
load data local infile "/home/ubuntu/sample_data/post_hashtags.csv" into table POST_HASHTAG fields terminated by "," ignore 1 lines (post_id, tag_id);
load data local infile "/home/ubuntu/sample_data/post_likes.csv" into table POST_LIKE fields terminated by "," ignore 1 lines (user_id, post_id);
load data local infile "/home/ubuntu/sample_data/tags.csv" into table HASHTAG fields terminated by "," ignore 1 lines (tag);
load data local infile "/home/ubuntu/sample_data/types.csv" into table TYPE fields terminated by "," ignore 1 lines (tag);
sudo mysql --local-infile=1 -u root -p
set global local_infile = 1;
 
2/20 월 auto_increment 된 pk값 변경
SET @count = 0;
update POST set id=@count:=@count+1;
update POST set id=@count:=@count+1 order by create_date;
2/21 화
 
조회 성능
 
notion image
반정규화 전: 118.9 ms
notion image
반정규화 후: 103.85 ms
 
삽입 성능
 
notion image
반정규화 전: 72.25 ms
notion image
반정규화 후: 82.44 ms
 
explain select count(id) from POST_LIKE where post_id = 18 and is_deleted = false;
notion image
 
explain select like_count from POST where id = 18 and is_deleted = false;
notion image
 
  • ref : 인덱스로 지정된 컬럼끼리의 '=' , '<=>' 와 같은 연산자를 통한 비교로 수행되는 조인이다
  • const: 하나의 매치되는 행만 존재하는 경우. 하나의 행이기 때문에 상수로 간주되며, 한번만 읽어들이기 때문에 무척 빠르다.
  • rows: 이 값은 쿼리 수행에서 MySQL이 찾아야하는 데이터행 수의 예상값을 나타낸다. 추정 수치이며 항상 정확하지 않다.
    •  
update POST set like_count = (select count(POST_LIKE.id) from POST_LIKE where POST_LIKE.post_id = POST.id)
notion image
notion image
notion image
notion image
notion image
select convert(argument using utf8) as query from mysql.general_log where convert(argument using utf8) like 'SELECT id, user_id, create_date, update_date, content, model_id FROM POST%';
select convert(argument using utf8) as query from mysql.general_log where convert(argument using utf8) like 'SELECT id, user_id, create_date, update_date, content, model_id, like_count FROM POST%';
select convert(argument using utf8) as query from mysql.general_log where convert(argument using utf8) like 'insert into POST_LIKE(user_id, post_id) values %';
 
select count(convert(argument using utf8)) as selectQueryCnt from mysql.general_log where convert(argument using utf8) like 'SELECT id, user_id, create_date, update_date, content, model_id, like_count FROM POST%';
 
select count(convert(argument using utf8)) as insertQueryCnt from mysql.general_log where convert(argument using utf8) like 'insert into POST_LIKE(user_id, post_id) values %';
 
set global general_log='on';
set global log_output='table';
 
 
 
수민 개발 메모
2/6 (월)
  • 키워드(해시태그)를 통한 게시물 검색 SQL
    • public List<Post> searchByHashtags(List<Integer> hashtagIds, int size, int index) { String whereStatement = createWhereStatement(hashtagIds); String query = "SELECT p.id, p.user_id, p.create_date, p.update_date, p.content " + "FROM POST AS p " + "INNER JOIN POST_HASHTAG AS ph " + "ON p.id = ph.post_id WHERE " + whereStatement + " ORDER BY p.create_date DESC LIMIT ?, ?"; return jdbcTemplate.query(query, postRowMapper(), index, size); }
[Database] SQL SELECT 쿼리문의 문법 순서와 실행 순서
JOIN 에서 WHERE 와 ON 의 차이, 그리고 OUTER JOIN
2/7 (화)
  • 여러 개의 repostory를 사용하여 쿼리를 여러 개 보내기 VS 여러 개의 테이블을 다중 조인하여 하나의 sql을 보내기
    • 여러 repository를 만들어 객체의 책임을 분리하였는데, 쿼리를 여러 개 보내면 성능 저하의 문제가 있지 않을까? 테이블을 다중 조인하여 하나의 sql을 보내는 게 낫지 않을까?
  • 여러 개의 테이블을 다중 조인하여 하나의 sql을 보내기 로 결정!
    • 쿼리를 여러 개 보내면 네트워크로 통신하기 때문에 속도가 심각하게 저하될 수 있다.
2/8 (수) - AWS 배포

스크립트로 배포하는 방법

main.yml
name: Deploy to Amazon EC2 on: push: branches: [ "main" ] jobs: deploy: name: Deploy runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Run Script on EC2 Server uses: appleboy/ssh-action@master with: key: ${{ secrets.SSH_KEY }} host: ${{ secrets.HOST }} username: ubuntu script: | /home/ubuntu/run.sh
Github의 Settings → secrets and variables → actions → New repository secret
SSH_KEY
-----BEGIN RSA PRIVATE KEY-----
….
----END RSA PRIVATE KEY-----
 
run.sh
#!/bin/bash REPOSITORY=/home/ubuntu PROJECT_NAME=Team2-CarBook cd $REPOSITORY/$PROJECT_NAME/ echo "> Git Pull" git pull cd backend/carbook echo "> 프로젝트 Build 시작" ./gradlew build echo "> step1 디렉토리로 이동" cd $REPOSITORY echo "> Build 파일 복사" cp $REPOSITORY/$PROJECT_NAME/backend/carbook/build/libs/*.jar $REPOSITORY/ echo "> 현재 구동중인 애플리케이션 pid 확인" CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar) echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID" if [ -z "$CURRENT_PID" ]; then echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다." else echo "> kill -15 $CURRENT_PID" kill -15 $CURRENT_PID sleep 5 fi echo "> 새 애플리케이션 배포" JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) echo "> JAR Name: $JAR_NAME" nohup java -jar \ -Dspring.config.location=/home/ubuntu/Team2-CarBook/backend/carbook/src/main/resources/application.properties \ -Dspring.profiles.active=real \ $REPOSITORY/$JAR_NAME 2>&1 &
 
JAVA_HOME의 시스템 변수 설정이 필요하다면 다음과 같이 ~/.bashrc 파일에 아래 내용을 추가합니다.
  • vim ~/.bashrc 마지막에 다음을 추가
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH="$PATH:$JAVA_HOME/bin"
  • java가 설치 경로 확인: ls -l /usr/lib/jvm
  • source ~/.bashrc
  • echo JAVA_HOME
 
📌
~/Team2-CarBook/backend/carbook/src/main/resources/application.properites 파일 직접 넣어주기!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://3.39.1.92:3306/carbook?serverTimezone=UTC&characterEncoding=UTF-8 spring.datasource.username=carbook spring.datasource.password=carbook1234

문제

  • 정상적인 경우에 foreground에서 동작되어서 github action이 끝나지 않음

해결방법

background에서 실행되도록 run.sh 변경
nohup java -jar \ -Dspring.config.location=/home/ubuntu/Team2-CarBook/backend/carbook/src/main/resources/application.properties \ -Dspring.profiles.active=real \ $REPOSITORY/$JAR_NAME > back.log 2> back.err < /dev/null &
run.sh
#!/bin/bash REPOSITORY=/home/ubuntu PROJECT_NAME=Team2-CarBook cd $REPOSITORY/$PROJECT_NAME/ echo "> Git Pull" git pull cd backend/carbook echo "> 프로젝트 Build 시작" ./gradlew build echo "> step1 디렉토리로 이동" cd $REPOSITORY echo "> Build 파일 복사" cp $REPOSITORY/$PROJECT_NAME/backend/carbook/build/libs/*.jar $REPOSITORY/ echo "> 현재 구동중인 애플리케이션 pid 확인" CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar) echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID" if [ -z "$CURRENT_PID" ]; then echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다." else echo "> kill -15 $CURRENT_PID" kill -15 $CURRENT_PID sleep 5 fi echo "> 새 애플리케이션 배포" JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) echo "> JAR Name: $JAR_NAME" nohup java -jar \ -Dspring.config.location=/home/ubuntu/Team2-CarBook/backend/carbook/src/main/resources/application.properties \ -Dspring.profiles.active=real \ $REPOSITORY/$JAR_NAME > back.log 2> back.err < /dev/null & echo "backend server start"
2/9 (목) - AWS 배포(+프론트)

문제

  • AWS 우분투 서버 접속이 안됨
ubuntu@ec2-3-37-102-17.ap-northeast-2.compute.amazonaws.com: Permission denied (publickey).

해결방법

새로운 EC2 인스턴스를 파기로 결정함 ㅠㅠ
 
  • vim ~/.bashrc 마지막에 JAVA_HOME 추가 X
프론트엔드 코드도 같이 빌드
run.sh
#!/bin/bash REPOSITORY=/home/ubuntu PROJECT_NAME=Team2-CarBook cd $REPOSITORY/$PROJECT_NAME/ echo "> Git Pull" git pull cd backend/carbook echo "> 백엔드 프로젝트 Build 시작" ./gradlew build echo "> step1 디렉토리로 이동" cd $REPOSITORY/$PROJECT_NAME echo "> Build 파일 복사" cp $REPOSITORY/$PROJECT_NAME/backend/carbook/build/libs/*.jar $REPOSITORY/ cd frontend/carbook echo "> 프론트엔드 프로젝트 Build 시작" npm install npm run build echo "> 현재 구동중인 애플리케이션 pid 확인" CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar) echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID" if [ -z "$CURRENT_PID" ]; then echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다." else echo "> kill -15 $CURRENT_PID" kill -15 $CURRENT_PID sleep 5 fi echo "> 새 애플리케이션 배포" JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1) echo "> JAR Name: $JAR_NAME" nohup java -jar \ -Dspring.config.location=/home/ubuntu/Team2-CarBook/backend/carbook/src/main/resources/application.properties \ -Dspring.profiles.active=real \ $REPOSITORY/$JAR_NAME > backend.log 2>&1 &
프론트엔드는 nginx 웹 서버 사용
vim /etc/nginx/sites-available/default
프론트랑 백엔드 같은 EC2 서버에 있음
→ 프론트에서 백엔드로 요청 보낼때 로컬호스트로 접근가능하고, 외부 요청은 막을 수 있다.
server { listen 80; location / { root /home/ubuntu/Team2-CarBook/frontend/carbook/dist; index index.html index.htm; try_files $uri /index.html; }
2/10 (금)
📌
게시글 좋아요 개수는 필드로 저장하면 안된다!
→ 동시성 문제
→ 따로 테이블로 빼서 저장함(POST_LIKE 테이블)
2/11 (토)
 
ORM 안 쓰고 jdbcTemplate 쓰니까 뭔가 객체지향프로그래밍과 데이터베이스 설계와 충돌
  • 기능이 추가되거나 변경되면 DB 수정이 불가피함
  • 또는 DB 컬럼 이름이 바뀌거나 하면 쿼리문 찾아서 다 수정해야 함
  • 객체지향프로그래밍이 힘듦
2/13 (월) - AWS S3
2/14 (화) - 좋아요 개수 최적화하기 (soft delete 구현)
"insert into POST_LIKE(user_id, post_id) values (?, ?) " + "on DUPLICATE KEY update is_deleted = false", userId, postId);
좋아요 최적화
게시글에 대한 ‘좋아요’ 기능인데요, 현재 구조에서는 게시글을 조회할 때, ‘좋아요’ 테이블과 함께 조인을 해서 가져오고 있습니다. 단순히 좋아요 개수만 저장하지 않는 이유는, 한번 좋아요 버튼을 누른 사람은 두 번째 누를 때 취소할 수 있어야 하기 때문입니다.
하지만 이런 방식이라면 만약 게시글 하나에 좋아요가 100만개가 된다면 게시글을 조회 한번 할때마다 100만개를 조인하게되어 오버헤드가 커지게 됩니다.
그래서 저희는 반정규화를 선택했습니다. post 테이블에 like_count 컬럼을 추가함으로서 likes 테이블을 조인하지 않고도 해당 게시글의 좋아요 개수를 알 수 있습니다. 이렇게 해서 해당 게시글의 좋아요 개수를 가져오는데 시간을 0.0013으로 줄일 수 있었습니다.
하지만 이렇게 되면 좋아요 개수에 대한 정보가 두 테이블에 나타나게 되어 데이터 정합성을 맞추는 것이 중요한 문제가 됩니다.

Sync Schedule

특정 주기마다 좋아요 개수를 맞추어 주는 것
처음에 문제였던 것이 동시에 사용자 요청이 들어올 때 같은 likecount 를 얻어 좋아요 개수가 맞지 않는다는 것인데, 이렇게 맞지 않았던 데이터를 주기적으로 한번씩 맞추어 주는 것입니다.
Sync Schedule 방법은 업데이트 주기가 오기 전에는 데이터의 정합성이 맞지 않아 사용자에게 좋아요 개수가 부정확하게 표시될 수 있습니다. 하지만, 속닥속닥은 좋아요 개수가 일시적으로 부정확하게 표기 되더라도 큰 문제가 되지 않는다고 생각했습니다.
실제 현업에서는 서버를 하나만 두고 서비스를 운영한다는 것은 상당히 리스크가 있는 운영이기 때문에 하나가 죽더라도 문제 없이 서비스 하기 위해 다중서버를 구성합니다.
redis를 활용하는 방안을 생각했습니다. 기존에 글로벌 캐시로 활용하던 redis서버가 있었고 redis는 싱글쓰레드이기 때문에 여러 요청에 대해서 트랜잭션을 보장해줄 수 있을 것이라고 생각했습니다. (이 생각이 잘못되었다는 것은 밑에서 나옵니다.)
Redis 캐시 전략 중 write-through전략을 사용

왜 안됐을까?(redis의 특징)

redis의 특징을 다시 살펴보았습니다.우선 대표적인 특징으로 redis는 싱글스레드 입니다.
레디스는 Event Loop를 이용하여 요청을 수행합니다.즉 실제 명령에 대한 작업은 커널 I/O 레벨에서 멀티플렉싱을 통해 처리하여 동시성을 보장합니다.따라서 유저 레벨에서는 싱글스레드로 동작하지만, 커널 I/O 레벨에서는 스레드 풀을 이용합니다.
하지만 이 동시성을 보장한다는 의미 자체가 동시성으로 인한 문제가 발생할 수 있다는 뜻을 의미했습니다. 따라서 동시성 문제에 대한 처리가 필요했습니다.
락을 사용하기 위한 Redisson 사용
트랜잭션 격리 수준
2/15 (수) - 무한 스크롤 구현, null 비교
무한 스크롤
  • 인덱스 대신(페이지가 없이) 응답 body에 next_max_id 값을 넘김
    • sections/
    • 요청 form data에 max_id
    • 응답 body에 next_max_id
  • 피드를 요청한 API의 응답 값인데요, next_max_id라는 값이 존재합니다. 이 값이 현재 응답받은 피드의 기준이 되는 값입니다.
  • 그래서 다음 피드를 불러오는 API를 요청할 때 max_id에 그 값을 담아 요청을 보내게 됩니다.
select * from pont from limit 20 offset 700000
select * from pont from where name > 70000 limit 20
  • where 조건절을 타고 들어가는 건, 해당 컬럼에 인덱스가 걸려있어야합니다.
  • 그래야만 index range scan으로 조회가 되기 때문에, 유의미한 결과를 낼 수 있습니다.
No-Offset은 정렬되고 중복이 존재하지 않는 인덱스 값을 가지는, 무한 스크롤 방식에 유용하다고, 생각됩니다.
Offset 기반 페이지네이션은 우리가 원하는 데이터가 ‘몇 번째’에 있다는 데에 집중하고 있다면, 커서 기반 페이지네이션(여기서는 No Offset 이라 칭함) 은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는데에 집중합니다.
객체 == null VS Object.isNull()
  • Object의isNull(), isEmpty() 모두 내부적으로 == 연산자를 사용한다.isNull()은 null 체크를 위해 만든 메서드가 아니며 Stream의 filter 메서드처럼 인자를 Predicate 인터페이스로 받는 메서드에 사용하라고 있는 메서드인 것이다. isEmpty()는 null을 검사하는 것인지 명확하지 않다.
  • String
    • str ≠ null
      • null인지 아닌지 판단
    • !str.isEmpty()
      • 빈 문자열인지 즉 “” 형태 값인지 아닌지 판단
    • containsText(strt)
      • 문자열의 길이가 0보다크고, 공백으로만 이루어져 있어도 false를 리턴한다.
      • 즉 null이 아니고, 빈문자열도 아니며, 공백으로만 이루어져 있는 문자열도 아닌 문자열인 경우에만 true를 retrun해준다. 단순 null체크를 넘어서 유효한 문자열인지 검사해주는 메서드이다.
따라서 null 체크할 때 == 연산자를 직접 사용하는 것을 지향하려고 한다. String 타입인 경우에는 StringUtils.hasText() 사용을 지향하고자 한다.
 
Objects.isNull은 if문에 사용하라고 있는 메서드가 아니다. javaDoc을 보면 Objects.isNull의 설명은 아래와 같이 쓰여 있다.
ApiNoteThis method exists to be used as a Predicate, filter(Objects::isNull)
Objects.isNull 메서드는 Stream의 filter 메서드처럼 인자를 Predicate 인터페이스로 받는 메서드에 사용하라고 있는 메서드인 것이다. 아래의 예제와 같이 사용하는 것이 적절하다.
즉, Objects.isNull, Objects.nonNullif문에서 사용하면 가독성을 오히려 해칠 수 있고 용도에 맞지 않는다. Predicate 인터페이스를 인자로 받는 메서드에 사용하자.
📌
Redis 서버가 잘 죽는다고 함
인 메모리 DB
  • 꺼지면 죽는게 아니라 디스크를 따로 쓴다고 함 → 영속성
2/16 (목) - 인메모리 DB
S3 CORS 헤더 관련 이슈
인메모리 DB
검색 기능 오류 → 2/16에 해결 완료
  • 키워드를 통한 게시물 검색
    • 키워드가 무조건 있다고 가정하고 들어오는 것이기 때문에… 수정할 필요가 없을 수도 있음
 
  • 한글 입력 → 1번째 요청은 잘 되면서, 2번째 요청부터 모든 태그가 응답에 담김
    • 2번째 요청부터 쿼리 파라미터로 빈 문자열이 들어옴
    • Postman 문제였던 것 같다. 프론트와 통신할 때 잘만 돌아감
  • &, # → 빈 문자열
    • URL에 사용하는 거라서 그냥 빈 문자열이 들어오는듯 하다
  • ^, {, }, [, ], \, | → 400 bad request 이상한 html문을 응답으로 보냄 → controller로 아예 안들어오는 듯
  • % → null 값으로 들어옴
    • 해시태그/모든 태그 검색 시 % → 400 bad request (null값)
      • WARN 3314 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'hashtag' for method parameter type String is not present]
    • 문자를 인코딩할 때 %를 쓰기 때문에 ‘%’만 들어오면 오류 생성

인코딩하지 않고 보내 발생한 문제

 
접두어로 검색할 때 쿼리문 수정
  • LIKE 절의 와일드 문자는 '%'와 '_'이므로 이 두 문자가 검색어로 들어올 경우를 처리하기 위해 쿼리문을 수정함.
  • 와일드 문자가 들어가는 부분은 ESCAPE 옵션을 사용해야 한다.
public List<Hashtag> searchHashtagByPrefix(String keyword) { return jdbcTemplate.query("SELECT h.id, h.tag FROM HASHTAG h WHERE tag LIKE '" + convertWildCharToRealChar(keyword), hashtagRowMapper()); } public String convertWildCharToRealChar(String oldStr) { String newStr = oldStr.replaceAll("%", "!%"); newStr = newStr.replaceAll("_", "!_"); newStr += "%' ESCAPE '!'"; return newStr; }
 
 
메인페이지 로드 시에 여러 서비스가 필요한데 어떻게 해야할까
// cookie를 통해 로그인 유저인지 확인 > userservice
// 팔로잉 중인 유저 리스트 불러오기 > followservice
// 리스트 별로 게시물 불러오기 > postservice
 
 
git 은 빈 폴더는 추적하지 않는다!!
.keep 파일을 추가해서 add commit 해주면 된다.
git clean -nd | sed s/'^Would remove '// | xargs -I{} touch "{}.keep"
하위폴더 한방에 추가도 가능
 
 
비밀번호 해싱 BCrypt 사용?
 
Post vs Delete?
별개로 굳이 DELETE 메서드를 고집할 필요가 없다면, Request body에 필요한 정보를 담고, POST 메서드 요청 메세지를 보내는 것도 나쁘지 않은 방법입니다.  DELETE 메서드 자체를 허용하지 않는 서버, 방화벽이 많아서 편의성 측면에서 더 좋을 수도 있습니다. 실제로 TOSS의 결제 시스템의 경우, PG 연동 개발자들의 편의를 위해 POST 메서드로 DELETE 요청에 해당하는 API를 디자인한 사례가 있습니다. 관련 내용은 이 링크에 요약해뒀어요 :)
 
무한스크롤 구현 관련 자료글
 
aws elastic ip
 
jwt 토큰 io. 으로 import 해서 사용하면 어떨까?
 
이미지 저장 방식에 대한 고민
이미지 저장 방식을 프론트에서 firebase 사용하면 구조, 성능 면에서 좋다는 의견
아마존 서버 성능 <<<<< 프론트 성능
프론트에서 firebase에 저장하고 url을 백에 넘겨 주고 백은 url을 db에 저장
 
Spring Data JDBC 관련 자료
 
인텔리제이 Warning:(1, 14) Unsupported characters for the charset 'ISO-8859-1’ 경고가 뜰 시 처리하는 방법
 
valid 예외처리 MethodArgumentNotValidException 썼는데 ExceptionHandler가 못잡음 ㅡㅡBindException 써서 해결
 
REST API 관점에서 바라보는 HTTP 상태 코드
 
 
ResponseEntity 관련
 
amazon s3 에 이미지 저장
 
User의 필드를 final로 선언하는 것에 대해서 어떻게 생각?
비밀번호, 닉네임 변경 시 DTO로 받아서 바로 dao(repository)로 넘기면 되지 않나?
 
생성자 vs setter vs builder
Setter
setter의 무분별한 사용은 코드의 의도를 가지기 어렵다.
객체의 일관성이 깨진다.
Initializer & Builder
대안으로 생성자나 빌더를 사용할 수 있다.
하지만 생성자의 경우 채워야할 필드가 무엇인지 명확하게 지정할 수 없다.
따라서 builder의 사용을 권장한다.
 
 
git fetch —all —prune
 

SQL

[Database] SQL SELECT 쿼리문의 문법 순서와 실행 순서
JOIN 에서 WHERE 와 ON 의 차이, 그리고 OUTER JOIN
mysql 데이터 존재 여부 쿼리
 
 
 

CI/CD

Docker + Github Action + Spring Boot 자동배포환경 만들기
 
 

Filter/Interceptor/AOP

  • 현재 이미지 저장소에 url decode를 하고 있는데 이거를 filter로 적용해서 모든 request 들어올 때 바로 decode 해버리기?
로그인 체크 → 인터셉터
인터셉터 구현 관련(JWT)
>>>PreHandle: 컨트롤러 직전에 실행, true false로 진입 여부 판단
PostHandle: view 렌더링 전에 실행, 화면 data 조작 가능
afterComplete: view 렌더링 이후 실행
afterConcurrentHandlingStarted: 비동기 요청 시 사용
로그인 체크를 하고 있는 api 정리
/profile/logout 로그아웃
/profile/modify/{nickname} 닉네임 변경
/profile/modify/password 비번 변경
/profile 사용자 프로필 페이지
/profile/follow 팔로우/팔로우취소
/profile/follower 팔로워 삭제
 
/posts/m getPosts >> 좀 애매함
 
/post 글 상세 페이지 조회
/post/{postId} 글 삭제
/post 글 생성
/post 글 수정
/post/like 글 좋아요/좋아요취소
 
 
Dispatcher-Servlet의 정적 자원 처리

애플리케이션 요청을 탐색하고 없으면 정적 자원 요청으로 처리

두번째 방법은 Dispatcher Servlet이 요청을 처리할 컨트롤러를 먼저 찾고, 요청에 대한 컨트롤러를 찾을 수 없는 경우에, 2차적으로 설정된 자원(Resource) 경로를 탐색하여 자원을 탐색하는 것입니다. 이렇게 영역을 분리하면 효율적인 리소스 관리를 지원할 뿐 아니라 추후에 확장을 용이하게 해준다는 장점이 있습니다.