-
게시판 프로젝트 vue.js + java spring boot (일기장)project 2022. 1. 11. 14:49
게시판을 만들려고 한다. vue + spring boot 로 msa 구성을 할 거고, 프론트 보다 백엔드에 좀 더 집중에서 만들어볼 생각이다.
글로 정리하는게 기억에 더 남아서 정리해보자.
첫 웹 프로젝트이고 프로젝트 자체 퀄리티 보다, jpa 와 spring 개념을 정리하는 데에 집중해서 만들자.
[22.01.01.~12]
프론트와 디비에서 출발해서 언젠간 맞닿을 것이다 ...!!
프론트 화면을 대충 만들었다. 로그인 화면, 회원가입 화면, 게시글 썸네일 리스트 화면, 단일 게시글 화면으로 구성되었고 백엔드에 get 을 보내 401 unauthorized response 가 오면 login 페이지로 redirect 할 생각이다.
자바스크립트를 잘 사용해야 사용성이 좋을거 같은데 일단 ui/ux 는 적당히 만들자.
[22.01.13]
먼저 로그인 기능을 구현하자. Spring security 가 아닌 세션을 사용해서 먼저 해보고 싶다. (지난 주에 이걸 배웠기 때문이다!) 나중에 spring security 를 사용하는 것도 알아보려고 한다.
LoginController 라는 restcontroller 를 만들고, /login/* 으로 들어오는 request 를 처리할 것이다. Postman 으로 테스팅을 하고 이후 자동화 테스트 코드를 만들어 놓자.
먼저 버전 1 이다. 로그인 기능이 있고, 로그인 한 사람만 글 목록을 볼 수 있다. 로그인 안 한 사람은 로그인 페이지로 리다이렉트 되고 로그인 한 사람은 글 목록으로 리다이렉트 된다. 로그아웃 버튼을 통해 로그아웃 할 수 있다.
세션으로 이 기능을 만들었는데, 보안 취약점이 있나?
스니핑 문제가 있을 것이다. -> tls 로 encrpyt 하면 된다.
자바스크립트로 세션을 꺼내오는 방법도 있을거 같다. -> 찾아보니까 xss 라는 방법이 있다고 한다. 이건 해결방법을 모르겠다. CORS 와 연관도 있다고 한다. 하나씩 알아봐야 겠다.
지금은 http + session refresh 로 해결하고 나중에 spring security 에 대해 알아보자.
[22.01.16]
DDD 라는 걸 알게 되었다. Domain 먼저 만들고 시작하는 건데, 이렇게 하면 버그 잡기가 쉬운거 같다.
백엔드쪽 코드를 처음부터 다시 짰다. 잘 작동하는 코드를 노드로 만들고 깃에 브랜치를 새로 만들었는데, 어설퍼서 깃 브랜치 전략이 뭔지 공부해야 겠다.
exception 을 어떻게 handle 할지, 오류 코드를 어떻게 적을 지, Controller 가 많아졌는데 url 과 설명은 어떻게 할 지, 테이블 공유, visualize 는 어떻게 할지 고민했다. 좋은 툴들이 있더라 (swagger, datagrip). 혼자 할 땐 몰라도 프론트엔드 등 다른 사람이랑 하려면 잘 파악되게 해야 할 것 같다. 지금은 일단 연습해 보는거다.
로그인 / 회원가입 / 글 작성 / 좋아요 / 댓글 기능까지 추가해서 마스터 브랜치 하나를 만들자.
좋아요 기능은 별개의 테이블을 만들어서 fk 두 개로 글, 좋아요 누른 사람 을 column 으로 갖게 만들었다. 인덱스를 걸면 좋을 거 같다. 프론트는 어떻게 구현하지? 토글 버튼처럼 만들어 놓고 request 는 비동기로 만들어야 겠다. 근데 request 를 어느 시점에 보내지? 사용자가 좋아요 버튼을 계속 누르면 어떻게 해야되지? -> 고민할 거리가 있다. -> 좋아요 를 눌렀을 때 request 를 보내고, response 가 ok 면 좋아요 개수를 하나 추가하는 방식으로 만들었다. 좋아요를 누를 때마다 서버에 request 가 가면 안되므로, 여러 해결 방법이 있고 그 중 하나로 해결했다.
자동화된 테스트 케이스의 중요성을 다시 느끼게 되었다. 기능 추가 / 수정할 때마다 postman 으로 다 찍어볼 수는 없으니까. 테스트 케이스는 기능 하나 하나가 오류를 만들지 않도록 작성하는 것이 좋을 것 같다. Edge case 도 잡아줘야 할 것 같다.
[22.01.17]
테스트 케이스를 몇 개 추가했는데, 실제로 해보니까 잡아내지 못한 오류가 또 나온다. 꼼꼼하게 만들어야 겠다.
Controller 에서의 exception handling 지식이 부족하다. 깃허브를 통해 좋은 exception handling 은 어떻게 하는 것인지 공부해보자. -> 그냥 프론트엔드랑 규칙을 정하면 되는건가? 서비스회사 api 를 보니까 언어적 제약 없이 임의로 정하는듯 하다. 참고해서 공부하자.
vue 를 쓰니까 생각보다 빨리 만들 수 있는거 같다.
다음 기능은 뭘 추가할지 생각해 보자.
[22.01.18]
글 수정 / 삭제 기능
junit5 test 를 쓰면 exception test 를 assertThrows 로 더 편하게 할 수 있다.
junit4 에서는 try catch, fail 이랑 @Test(expected = Exception.class) 같은걸 써야 했다.
org.junit.jupiter.api.Assertions.assertThrows(Exception.class, () -> { articleService.deleteArticleById(testArticleId); });mysql 에서 한글이 안되는 거 해결하느라 시간이 좀 걸렸다. utf-8 로 설정해야 한다.
delete 를 할 때 변경감지로 삭제가 되는지 궁금했다. 사용자는 1차 캐시 영속성 컨테스트 에 있는 entity 를 변경하면 jpa 가 transaction commit 등의 flush 시점에 자동으로 쿼리를 날려주는 방식인데, field 를 그냥 바꾸면 되는 update 와 달리 delete 는 em.remove() 로 할 수 있다.
또, cascade 로 좋아요 를 게시글에 넣어놨는데, 게시글 삭제를 누르면 좋아요 도 삭제된다.
게시글 가져오는 쿼리를 날리면 게시글 갯수만큼 좋아요 테이블에 select 쿼리가 나간다. LAZY fetch 기능을 써서 이렇게 되는건데, LAZY 란 field 를 쓸 때 까지 쿼리를 날리기를 보류하는 것이다. 이런 경우에 처음에 join 한 번만 하면 더 쿼리를 날릴 이유가 없는데, jqpl 을 따로 짜야 하나? -> fetch join 기능을 쓰면 된다. -> distinct left join fecth 로 해결했다.
->좋아요 개수를 세는데 꼭 join 을 해야 할까? 게시글 field 에 column 을 하나 만들어서 개수만 표시하면 조회시에 join 이 필요 없다. Tradeoff 로 update 쿼리가 더 나간다. 조회가 좋아요를 누르는 경우보다 훨씬 더 많으므로 이렇게 하는게 옳지 않을까?
페이징 기능
Spring data jpa 를 쓰면 더 편하게 할 수 있는데, 지금은 그냥 jpa 로 할거라서 이건 나중에 공부해 보자.
Repository 에 .setFirstResult, .setMaxResult 를 적어놓고 페이징 숫자를 적어주면 되는데, 페이징 정보를 넘길 때 Pageable, PageRequest 를 쓸 수 있다. (여기선 의미가 없지만)
axios 는 단방향으로 props 가 흐른다. 예를 들어 board.vue 에서 navbar.vue 를 component 로 부르면서 props 에 데이터를 넣어주면, navbar.vue 에서 props 데이터를 수정하려고 하면 오류가 난다. 수정하고 싶다면 예를 들어 navbar.vue 에서 this.$parent.method() 처럼 부모의 메소드를 콜하면 된다.
jpql 짜다 보니 결국 sql 이 중요하다고 느낀다. 게시글을 크롤링으로 가져오고, 검색 필터를 만들어보면서 쿼리 짜는 연습을 해 보자. spring data jpa 가 crud 를 제공해준다 하니 그동안 만든 crud를 그걸로 바꿔보자.
[22.01.19]
테스터 데이터를 추가하려고 하다가 stack overflow 에서 api 를 제공해줘서 그걸 쓰려고 한다. 데이터 형식이 비슷해서 그냥 stack overflow 클론 프로젝트로 바꿔야 겠다. 쿼리 연습도 이걸로 하는게 더 나을 것 같다.
front 코드를 다시 짰다. (리팩토링?) 코드가 지저분하기도 하고, routing 기능을 잘못 이해하고 짠 거 같아서 다시 만들었고, 코드 양이 훨씬 줄어든 거 같다. (중복이 줄어들어서 그렇다)
다음으로 구현할 내용이다.
front: 페이징, hash tag, 댓글 기능
back: hash tag, 댓글 기능
프론트를 더 못해서 그런가 백엔드를 먼저 만들고 하는게 편하다.
[22.01.20]
자바스크립트 비동기/동기 -> 콜백, promise, async/await 이 있다. event loop / threading 차이 -> spring webflux 는 event loop
stack overflow api 를 통해 api 규격에 힌트를 얻어보자.
N+1 문제가 계속 나오는데, 정리해서 공부할 필요가 있다. -> https://jojoldu.tistory.com/165
어떨 때 N+1 문제가 발생하는지, 해결책과 장단점 글에서 mappedby 를 안쓰고 joincolumn 을 두번 썼는데 이건 테스트를 위해서 이렇게 한 거 같다.
(스택 오버플로우 보다가 1년 반전에 c++ 처음 공부할 때 질문을 올린게 있다. 저땐 static 을 몰랐나 보다. 그동안 사람들이 욕을 달아놨다..)
어떤 유저가 어떤 게시글에 좋아요를 했는지 알 방법이 뭐가 있을까? (좋아요는 한 사람이 한 게시글에 한 번만 할 수 있다.)
->User entity 에서 좋아요 field(list) 를 콜해서 loop 를 돈다. => LAZY 에 양방향 인데 N+1 문제가 생긴다.
->User 테이블과 좋아요 테이블을 left fetch join 한다. => 궂이?
->좋아요 table 에서 쿼리를 한다. select from (select from 좋아요.user = user).article = article 처럼 2중 from 을 쓴다. => jpa 에서 이런 문법을 지원하지 않는다.
->좋아요 table 에서 self join 을 해서 쿼리를 한다.
->좋아요 table 에서 where 를 2개 돌려서 쿼리를 한다.
->Spring data jpa 를 쓰자. findByUseridAndArticleid 라고 declare 만 하면 구현이 다 된다.
->좋아요 개수만 column 을 따로 만들어서 하면 조회시에 좋아요 테이블을 조회할 필요가 없다.
자바로 코딩을 하다 보니까 sql 으로 생각하는게 바로 안떠오르는 것 같다. 이리저리 생각하면서 좋은 방법을 찾는게 중요할 것 같다.
조회수 기능 -> getArticle 할 때마다 transaction 걸고 조회수를 1 올린다. 이렇게 해도 될 지 모르겠다.
여러가지 생각할 점이 생겼다.
1. 양방향을 쓰는 이유가 뭘까? 그냥 다 단방향으로 하는게 편하지 않나?
2. 쿼리 최적화를 하기에 앞서 프론트에서 Controller 에 보내는 request 를 최적화 해야 할 거같다.
3. 스프링에서 캐시를 이용하면 성능 개선이 있을텐데, persistence context 가 해주나? 아님 redis 같은거 기능이 있나?
4. read 가 많은 거랑, write/update 가 많은 거랑 디자인을 다르게, tradeoff 를 생각해서 해야 한다.점점 궁금한 것이 늘어나는 것 같다.
[22.01.21]
vue 에서 interceptor 기능 -> axios 에서 추가하면 된다. vue axios 로 exception handling 을 공통적으로 할 수 있다. plugin 이란 기능이 있다.
vue mvvm 이랑 spring mvc 랑 차이가 뭐지? 비슷한 점이 많다.
generatedvalue 는 어떤식으로 값을 생성하는 걸까
[22.01.22]
Tag 기능을 추가했다. Article entity 에 List<String> tags; 처럼 구현했다.
인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편 값 타입 컬렉션 부분을 참고해서,
Db 에 List<String> 이 불가능 하므로, Tag 라는 entity 를 만들고, Article 의 field 로 @OneToMany(cascade = All, orphanRemoval = true) List<Tag> tags; 로 만들었다.
검색시 parameter 로 tag 를 넘겨주면 그 tag 를 가진 게시글만 조회하는 기능을 만들었다.
-> jpql 로 join 을 짜서 했는데, 더 좋은 방법이 없을까?
jpa 인강을 하루종일 들었는데, 처음 들었을 때 보다 와닿는게 많아진 것 같다.
[22.01.23]
Stack overflow 에서 tag 는 다음과 같이 동작한다. -> https://stackoverflow.com/ 을 참고
1. 클릭시 tag 를 가진 게시글만 불러온다.
2. 마우스를 1초 대고 있으면 설명창이 뜬다. 설명창의 기능은 그림과같고,설명이 없을 경우 excerpt 페이지 링크가 뜬다.

3. Questions tab, 단일 게시글, Tags tab 에서 위와 같이 동일하게 작동한다. Users tab 에서는 다르게 작동하는데, 다 동일하게 만들어보자.
Query parameter 가 변경될 때 페이지를 re-load 해야 하는데, path 가 같으므로 re-load 되지 않는다. <router-view> 에 :key="$route.query" 를 추가해서 해결했다. https://hj-tilblog.tistory.com/99
Tooltip 이 뜨는 것을 jquery 로 한다. -> vue 를 써서 만드려면 어떻게 해야 할까?
Tooltip 안에 watch tag, ignore tag 는 지금 구현하지 말고, tooltip 에 설명을 서버에서 받아서 나타내는 방식으로 만들자.
vue 의 component 가 많아짐에 따라 vuex 의 필요성이 있다.
[22.01.24]
게시판 데이터 생성을 위해 Stack exchange api 를 consume 하는 새로운 앱을 만들었다. Proxy 서버 역할을 할 것이고, 나누어서 만들면 필요할 때 쓰고 필요없을땐 안쓰고 할수 있을거 같아서이다. 서버는 request 를 api 에 보내고, response 로 내 프로젝트에서 쓰일 dto 를 반환해준다. -> 스펙 변경
여러가지 문제점이 있었다.
api 가 gzip 으로 response 를 encode 해서 준다. 따라서 client 가 decode 해야 하는데, 간단하게 spring rest template 을 이용할 경우 https://stackoverflow.com/questions/34415144/how-to-parse-gzip-encoded-response-with-resttemplate-in-spring-web 이렇게 하면 해결된다.
api 에서 datetime 을 epoch time 으로 제공한다. LocalDateTime 으로 바꾸었다.
api 를 하루에 300번 이상 호출할 수 없다. 라이센스를 얻을수도 있지만, 300번이 부족할 때 그렇게 하자.
jpa + mysql 로 캐시 스토리지를 만들었는데, 이런 특수한 경우에는 (테스트 데이터 생성 용도) 성능에 대한 고민이 없기 때문에 최대한 간편하게 구성하자.

먼저 데이터를 get 해서 db에 저장하는 기능을 만들었다. (생각보다 오래 걸렸다)
pk 를 api 에서 보내준 id 값으로 설정했으므로, 중복저장 문제가 없다. 다만 글 내용이 없는데, 우선 없는 상태로 하자. (다른 api 에서 가져와야 할 것 같다.)
데이터를 게시판 서버에 전달해야 하는데, 로직을 두 가지 생각했다.
1. 캐시 서버에 GET 으로 신호를 보내면 POST 를 로직에 따라 게시판 서버에 넣어준다. (게시판 서버 api consume)
2. 게시판 서버에서 CommandLineRunner 로 캐시 서버에서 받아온다.

이런 식으로 게시판 서버에 저장했다. 1번 방법으로 구현하였다. (장단점? 게시판 서버를 변경하면 캐시 서버도 바꿔야 한다. )
api documentation library 로 swagger 대신 openapi springdoc 을 쓸 수 있다. 버전이 안맞아서 그게 더 좋다고 한다. gradle 추가를 해 주고 http://localhost:8080/v3/api-docs.yml 접속하면 볼 수 있다. swagger 로 어노테이션으로 설명을 만들 수 있다.
쿼리를 만들어 보려니까 잘 모르겠으므로 오늘은 jpa 인강을 봐야겠다. 꼼꼼하게 안봐서 잘 안되는 부분이 있는거 같다.
spring data jpa repository 로 바꾸고 test case 만들 환경을 만들었다.
생각해보니 tag 는 article 과 N:M 관계인거 같다. 평범한 string 으로 처리하기엔 아쉽다.
[22.01.25]
Repository test 환경을 위해 테스트 db 설정을 만들었다. 그동안 테스트와 메인에서 db 를 공유하고 했는데, 이제 데이터를 만들었으므로 따로 하는게 맞는거 같다.
@Test public void checkDbName() throws SQLException { org.hibernate.engine.spi.SessionImplementor sessionImp = (org.hibernate.engine.spi.SessionImplementor) em.getDelegate(); DatabaseMetaData metadata = sessionImp.connection().getMetaData(); System.out.println(metadata.getDatabaseProductName()); }이 코드로 적용된 db 이름을 알 수 있고, (예를 들어 h2 / mariadb) 테스트 클래스에 @ActiveProfile("test"), 테스트 resources 에 application-test.properties 를 만들어 설정을 적으면 테스트와 메인에서 다른 설정을 적용할 수 있다.
테스트 뿐 아니라 임이의 클래스에서 설정을 적용하려면 https://www.baeldung.com/spring-testing-separate-data-source https://www.baeldung.com/spring-jpa-test-in-memory-database 이런 글을 읽는게 좋을 것 같다.
또, @TestPropertySource 를 사용하면 class 에서 application properties 를 override 해서 쓸 수 있다. (method 는 안된다고 한다.)
@DynamicPropertySource 를 사용하면 같은 기능을 dynamic 하게 할 수 있다. (예를 들어 properties = something )
https://stackoverflow.com/questions/27919270/set-override-spring-spring-boot-properties-at-runtime api 로 properties 를 수정하는 방법도 있다.
또, 로깅을 insert method 에서만 끄고 싶다. 이 경우엔 두 가지 방법이 생각났는데, 1. method level 에서 application.properties 를 override 한다.(가능?) 2. (pseudocode) get Logger, turn off -> do something -> turn on 이런 식으로
1번 방법은 찾아봤는데 spring cloud 로 외부에서 파일 자체를 수정하는 방법이 있다. 나중에 필요할 수 있으니 기록해 놓자. -> 엉뚱한 생각이었던거 같다.
2번 방법은 여러 방법이 있다. 처음엔 hibernate logging level 을 메소드마다 바꾸는 방법을 찾았는데, 해결하지 못했다. 그냥 간접적으로 spring logging level 를 바꾸면 되는데 이럴 경우 sql query 뿐 아니라 다른 log 도 level 이 바뀐다. 테스트용으로 쓰기엔 무리가 없으므로 일단 이렇게 하자. https://stackoverflow.com/questions/35232827/spring-boot-unit-test-ignores-logging-level
public static void setLoggingLevel(Level level) { ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); root.setLevel(level); }setLoggingLevel(Level.DEBUG);로깅의 구현체가 뭐냐에 따라 바뀔 수 있을거 같다.
application.properties 는 메모장이나 다름 없다. 스프링이 파일을 읽어 와서 적용하는 거고, (실시간으로 변동된 걸 읽어오나?) 읽어 와서 적용시킨다. 즉, 메소드를 실행할 시점에 hibernate logging level 이 debug 로 설정이 되어 있으므로, programmatical 하게 logging 을 껐다 켰다 하기 위해선 그 level 을 설정하는 방법밖에 없다. 즉, 1번 방법은 explicit 방법이고 간접적으로 해결하는 별로 안좋은 방법이다.
또, 로깅은 system out 보다 효율적으로 print 한다는 것을 알게 되었다. logger 를 쓰지 않고 print 를 쓰면 성능 저하가 있을 수 있다.

로그를 끕니다 / 켭니다 사이에 insert 쿼리가 표시가 안되게 했다.
이제 jpql 로 쿼리를 만들어 보자. Stack overflow 는 tagged/java+python?filter = ~,~,~&sort=... 이다. Tag 를 따로 뺀 이유를 모르겠고, tag=...+...+... 으로 하는 것도 좋을거 같다.
Spring data jpa 는 조금 특이한데, findById 라고 declare 만 해주면 메소드를 자동으로 만들어 준다. 아마 컴파일러에서 AST 등을 수정해서 bytecode 를 만드는 방식일 거다.
@Query("... :id") Page<Entity> method(@Param("id") Long id, Pageable pageable) 이렇게 해주면 정적 쿼리 생산이 가능하고 파라미터 바인딩, 페이징이 다 처리된다.
만든 쿼리들: (꾸준히 연습해야 겠다. 알고리즘 문제 풀듯 sql / jpql 연습을 해야 할 듯)
1. Tag 를 많이 가진 article 순서대로 정렬 하고 싶다. (tag 는 값 타입 컬렉션으로 구현되어 있다.)
sql: (mysql 은 이거랑 동일한 특별한 방법이 있다고 한다. )
select *, count(article.article_id) from article left join tag t on article.article_id = t.article_id group by article.article_id order by count(article.article_id) desc;jpql:
findAllByOrderByIdAsc 이거 안된다. 왜지? findAll 이랑 sort 로 대신 하자.
mariadb 를 쓰는데, 예전에 썼던 sqlite3 와 쿼리가 많이 다르다.
이제 만든 쿼리 repository 를 service - controller layer 에서 consume 해야 할 텐데, api 를 어떻게 만드는게 좋을까?
학생이면 intellij ultimate 가 무료인걸 몰랐다. (educational 이라고 따로 있었는데?) 당분간 이걸 쓸 수 있을듯
[22.01.26]
테이블을 바꾸려고 한다. Tag 가 값 타입 컬렉션이라 한계가 있기 때문이다. Tag 를 중심으로 쿼리할 때나, tag 에 explanation 을 적을 경우, 수정/삭제할 경우 등등 따로 테이블을 만드는 게 좋을 것 같다.
즉, 지금은 Tag table schema 가 (pk, fk(article_id), tag_name) 이런데, 이걸 (pk, tag_name(unique), ...) 이렇게 바꾸고 싶다. 중간 테이블을 fk 두 개 갖는 걸로 만들어서 이어줄 것이다. 또, cascade 를 설정하지 않는게 좋을 것 같다.
여기서 발생한 어려움이, 이미 db에 데이터가 존재한다는 것이다(가짜 데이터긴 하지만.. 공부를 위해 데이터라고 생각하자). jpa entity 로 테이블을 만든다면 drop / create 를 사용해서 데이터가 다 지워진다. 테이블을 수정하고 entity 를 매핑할 경우, 기존에 tag table 과 연관된 모든 테이블을 다 수정해야 할 듯하다. -> 이래서 다른거 보다 테이블을 먼저 만드나 보다. 테이블을 설계하는 것 보다 이미 만들어진 테이블과 데이터를 수정하는게 훨씬 어렵다.
다음과 같이 작업을 했다.
1. 먼저 Entity 를 만든다. hibernate.ddl.auto=validate 로 설정하고, 실행해 본다. 테이블이 잘 만들어졌다면 잘 실행될 것이다.
2. ddl 로 테이블을 만든다. 이 경우엔 외래키가 article 바깥에 있어 article 을 수정할 필요가 없다. 복합키 매핑 테이블, 태그 테이블 두 개를 생성하고 서버를 실행해 본다. -> 오류 없이 뜨면 잘 만들었다는 거다. (만능은 아니고 적당히 확인하는 용도..)
3. 기존 테이블에서 꺼내서 새 테이블에 바꿔서 넣고 잘 됬는지 확인 후 기존 테이블을 드랍한다. 기존 엔티디를 지운다.
여기서 만났던 이슈 정리 (article, tag 두 개의 테이블에서 article, article_tag, hashtag 세 개의 테이블로 바꾸는 과정이다. 제2정규화라고 하는거 같다.)
@GeneratedValue startegy 를 auto 로 할 경우 jpa 가 hibernate_sequence 테이블에서 값을 읽어 그걸 id 값으로 넣는다. 이걸 sql 로 할 경우 비슷하게 할 수 있을 거 같은데, 일단 strategy 를 identity 로 새로운 테이블을 생성해야 겠다.
https://github.com/HomoEfficio/dev-tips/blob/master/JPA-GenerationType-%EB%B3%84-INSERT-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90.mdauto 와 identity 성능 차이를 실험해본 글이다.
tag name 을 unique 로 했는데, insert select 과정에서 duplicate 오류가 났다. 트랜잭션이 rollback 되어 insert 쿼리를 해도 한 개도 들어가지 않았다. distinct 를 추가하면 된다.
insert into hash_tag (tag_name) select distinct tag from tag;먼저 tag name 만 뽑아서 새로운 테이블에 넣고,
insert into article_tag (article_id, hash_tag_id) select t.article_id, h.hash_tag_id from tag t join article a on t.article_id = a.article_id join hash_tag h on h.tag_name = t.tag;중간 테이블을 만들어서foreign key integrity 때문에 조인을 여러 번 해야 했다. (이게 맞나?)
중간 테이블을 만들어서 각 row 가 article_id, hash_tag_id 를 갖도록 했다. hash_tag_id 는 새로운 테이블의 primary key 이다.
alter table article_tag add unique(article_id, hash_tag_id);게시글은 같은 tag 를 가질 수 없으므로 위의 조건도 추가했다.
이렇게 했을 경우 tag row 에 explanation 같은 새로운 데이터를 넣을 수 있다. 만약 기존의 방법에서 컬럼을 추가하려면 논리적으로 explanantion 한 개를 넣으려면 physical 한 데이터를 여러개 만들어서 넣어야 했을 것이다.
이걸 보니까 jpa 가 좀 더 이해되는 것 같다. 또 datagrip 을 쓰면 sql 을 더 쉽게 짤 수 있어 좋은거 같다. (자바를 intellij 로 하듯이.. 자동완성 기능 같은게 매우 도움된다.)
인강에서 들은 내용 중에 운영 서버에는 hibernate.ddl-auto= 를 validate 또는 none 만 사용하라고 한다. validate 가 뭔가 해봤는데 entity 에 있는 테이블/컬럼이 없으면 서버가 꺼진다. 테이블을 드랍하고 validate 를 켜면 서버가 꺼지고, update 를 켜면 테이블이 다시 생긴다. validate 로 서버가 꺼질 경우 로그에 친절하게 뭐 때문에인지 설명해 준다.
Entity 를 많이 변경했다. Repository 도 spring data repository 로 전부 바꾸었는데, 페이징이 확실히 쉬운 거 같다.
좋아요 기능을 Article 로부터 cascade 로 만들었었는데, aggregate root 개념을 쓰면 좋아요 repository 를 만들지 않는게 좋다. 그러나 이렇게 하면 조회시 불필요한 join 이 필요할 수 있다. 여기에도 장단점이 있는 것 같다. -> root repository 에 @Query 로 별도 쿼리만 적으면 되지 않나? -> 이렇게 바꾸니까 코드가 훨씬 적어진다.
이게 디자인 패턴의 힘인가 보다..
이제 필터를 만들어 봐야겠다. 아마 이 부분이 이 프로젝트에서 제일 어려울 거 같다.
[22.01.28]
검색 필터를 stack overflow 의 questions, users, tags 탭을 보고 각각 만들었다. Query parameter 로 몇 가지 보내주면 적용되는 구조로 했다.
(생각한 과정)
검색 필터를 어디에서 처리할까?
Service 에서 처리하기엔 service 코드가 너무 길어진다. 필터의 모든 경우에 따라 분류하는 것을 service 에 다 넣어야 하기 때문이다. 따라서 무엇이 되었던, service 에서는 로직을 작성하지 말고 다른 객체에게 해줘 하는게 좋다고 생각했다.
그럼 어떤 객체에게 해달라고 해야 할까? DBMS 에서 본 팩토리 패턴이 생각났다. Query Engine 이라는 Component 를 만들고, execute 라는 메소드를 가진다. Query engine 은 Factory 를 각 쿼리 대상에 대해 가지고, (게시글 factory, 태그 factory, user factory) 각 factory 는 repository 의 메소드를 조합하여 쿼리를 작동시킨다. 이렇게 하면 service 에서는 execute 만 콜하면 되기 때문에 편리하다. Factory 들은 사용자가 보낸 query dto 를 받아 spring data jpa (jparepository) 로 만들어진 메소드를 적절히 조합하거나 선택해서 불러주면 된다.
이렇게 만들다가 QueryDsl 코드를 보니까 이런 작업을 repository 에서 할 수 있었다. Query dto 처리를 위한 별도의 객체도 필요 없이 repository 로 abstract 할 수 있어 더 깔끔하다. 다만 jpa repository 로 만든 메소드를 활용하지 못한다는 (방법이 있나?) 단점이 있는데 검색 필터 같은 경우엔 querydsl 로 쿼리 짜는게 훨씬 편한거 같다.
Deprecated 라고
이렇게표시된 기능이 보이는데, 페이징을 구현할때fetchResults를 쓰는데fetchResults는 count 쿼리가 한 번 더 나가서 querydsl 5.0.0에서 deprecated 라고한다. .fetch 를 쓰고 반환된 list 로 Page 객체를 새로 만들자. count 가 필요할 땐 별도로 count 쿼리를 보내자.검색 쿼리에 단어를 입력하고 엔터를 누르면 해당 단어가 포함된 대상만 쿼리하기로 만들었다. 이걸 필터링이라고 할 때,
1. 엔터를 누를 때 마다 서버에 새로운 쿼리를 보내는 방법
2. 프론트서버에 쿼리 결과를 다 가져와 놓고 선택적으로 렌더링하는 방법
두 가지를 생각해 봤는데, 같은 내용을 구현하지만 장단점이 있을 듯 하다. 1번은 서버에 더 많은 쿼리가 간다는 단점이 있고, 2번 방법은 프론트서버 메모리에 더 많은 것들이 올라간다는 단점이 있다. 1번이 좋은 선택인거 같아서 1번으로 구현하였다.
데이터를 어디에서 처리할지, DB에서, 백엔드 서버에서, 프론트 서버에서 중요한 점인거 같다. Entity 를 DB 에서 조회해서 백엔드에서 필요한 데이터만 꺼내 오는 것과 DB에서 꺼내올 때 필요한 것만 백엔드에 가져오는 것, 프론트에 가져올 때 필요한 것만 가져오는 것과 자바스크립트로 가져온 것을 분류해서 렌더링하는 것 다 중요한 최적화 요소인 것 같다.
[22.01.30]
프론트를 stack overflow 처럼 만들었다. Stack exchange 에서 게시글 body 도 가져와서 v-html 로 렌더링했다. vue 를 이렇게 쓰면 xss 취약점이 있다는데, 다른 방법을 찾아봐야 겠다.
기능을 하나씩 다듬고 그 다음에 쿼리 최적화를 해야 겠다.
로그인 중복 처리를 session 에 IP address 를 추가해서 할 수 있다.
vue 에서 객체지향적으로 코드를 바꾸었는데 이게 맞는지 모르겠다. 재사용 가능성이 있는 컴포넌트는 따로 함수처럼 props 를 넘겨주는 식으로 만들었다. 이렇게 하니까 코드의 양이 많이 줄어든다. 코드 가독성도 많이 늘어나고 구조 파악하기도 쉬운 거 같다. 이렇게 만든 데에 오버헤드도 있지 않을까?
프론트엔드에 필요한 디테일이 매우 많다. 이걸 다 할 수 없으니 남겨두자. 적당히 깔끔하게 만드는 게 목표이다.
spring 에서 entity 를 많이 바꿨다. Comment, follows 테이블을 추가했고, 좋아요 를 article, answer, comment 세 가지 entity 마다 따로 entity 를 정의해서 cascade 로 만들었는데, 한 개의 테이블로 뭉쳐놓았다. 쿼리시의 성능이 저하될 수 있지만 대신에 코드가 간결해진다. 쿼리 성능이 영향을 미칠지 모르겠다. 나중에 큰 데이터를 가져와서 실험해봐야 겠다. 인덱스 걸면 괜찮지 않을까?
모든게 trade-off 인 거 같다. 선택지가 있을 때 각각이 장단점이 존재한다. 좀 더 만들기 쉬운 선택을 하고, 성능 문제가 바뀌면 개선하는 방식도 좋을 것 같다.
또, 좋아요 함수를 각각 service 에 따로 심어놨는데 이를 좋아요 service 를 뽑아서 하나의 메소드로 만들었다. Enum 을 활용했고, 코드 양이 원래 코드의 1 / 3 정도로 줄었다. 공통되는 부분을 줄일 수 있었기 때문이다. 객체지향 원리중 하나의 interface 는 하나의 기능만 담당한다는 분리원칙에 부합하는 거 같다(맞나?).
Transaction 을 걸지 않고 좋아요를 누르다가 deadlock 이 걸렸다. 예전에 bustub DBMS 를 구현할 때 daemon 에서 일정 시간마다 deadlock 을 검사하고 감지하면 일정 시간 후에 해결해 주는 방식으로 만들었는데, mysql 도 비슷한가 보다. 일정 시간 있다가 deadlock 을 감지했다고 오류가 뜬다.
[22.01.31]
테스트 데이터를 좀 더 구체화했다. ( article ( comment )* ( answer ( comment ) *) *) * 와 같이 구성했고, 좋아요와 생성일자 등등 proxy server db 에 저장해 놓았다.
이렇게 한 이유는 다음과 같다. 테스트용 데이터를 수집할 여러 방법이 있는데,
1. 프로젝트 서버에서 직접 api 를 호출하여 받는다.
2. Proxy server 에서 api 를 호출하여 데이터를 저장한다. 프로젝트 서버에서 proxy server 의 api 를 호출한다.
3. Proxy server 에서 api 를 호출하여 db 에 저장해 놓고 그 db 에서 프로젝트 db 로 옮긴다.
2번 방법으로 했지만, 생각해 보니까 테스트 데이터를 생성하는 목적이면 3번이 맞는 것 같다. Json 을 보고 테이블을 설계해서 데이터를 저장하고, 그 테이블에서 꺼내서 프로젝트 스키마로 변환해서 insert 한다. Sql 로 짜도 되고 jpa 로 각 db 를 연결해서 해도 된다. https://www.baeldung.com/spring-data-jpa-multiple-databases
Proxy server db 에 request 를 보내면 response 로 게시글, 유저 데이터를 보내 준다. 이는 stack exchange api 에서 보내주는 response 와 동일한데, 이렇게 만든 이유는 연습을 위한 용도가 크다. 학교 네트워크 수업에서 배운 Proxy server 를 구현해보는 것과, 외부 api 를 consume 하는 것, 또 api 를 콜하는데 제한사항이 있는 이유도 있다. 하루에 300번 이상 콜하지 못하는 것과, 짧은 시간 내에 여러 번 request 를 보내지 못하는 것이 그것이다.
Json 형식을 그대로 db 에 저장하고자 했지만, 여기에도 jpa 를 써서 dto 로 받은 후 저장용 entity 로 변환하는 작업을 거쳤다. Dto 와 entity 가 대부분 비슷하지만 다른 몇 개 때문에 변환이 생각보다 까다로웠다. 앞으로도 대부분의 경우에 dto 를 따로 만드는 방식을 취해야 겠다.
이제 데이터를 프로젝트에 적용시켜 보자. 프로젝트의 Entity 로 변환해서 실제 유저가 생성한 것 같이 저장할 수 있다. 일단 500 개 정도의 게시글을 저장해 놓았고, 쿼리를 작성해서 성능 문제가 있는지 실험해 보자. (jpa 쿼리도 공부할 겸!) 필요하면 더 늘려보자.
데이터가 이렇게 작은데도 쿼리가 몇 초 이상 걸리는 게 있다. Jpql 을 잘못 짜서 그런거 같다.
[22.02.01]
쿼리 최적화를 하자.
인강에서 들은 대로 fetch = FetchType.LAZY 로 다 설정해 놓고 필요할 때 fetch join 을 할 것이다.
먼저 게시글 조회를 할 때 유저 정보도 표시하는데, 여기서 N+1 문제가 발생한다. QueryDsl 에 .leftjoin(article.user, user).fetchJoin() 을 추가하면 해결된다.
Tag 를 fetch join 하는 것이 꽤 어렵다. Article 과 Tag 는 N:M 관계이고 중간 테이블을 엔티티로 설정했는데, List 도 있고 어떻게 해야 할지 모르겠다. 다시 생각해 보자. Sql 스럽게 짜는건 했었는데 querydsl 로 하고 필터도 같이 적용하고 하니까 많이 복잡하다. 알고리즘 문제 풀듯이 풀어보자.. 페이징도 해야 하는데 batch size 로 하라는데 이건 뭘까 ...
Jpa 생각보다 어려운 거 같다. 러닝커브 우매함의 봉우리에서 내려온 걸까 ..
코드 짜면서 궁금했던 부분중 상당수가 인프런 jpa2 강의에 있다.. 참 신기한 수업인거 같고 들어봐야 겠다
-> 그냥 위의 내용 그대로 수업에 나와있다.
로그인 상태 관리를 vuex 로 했다. props 로 하는것 보다 더 편한 것 같다.
[22.02.02]
인강에서 들은 대로 쿼리를 다시 짰다.
컬렉션을 fetch join 하면 페이징이 안된다. @ManyToOne, @OneToOne 은 fetch join 을 하고, @OneToMany 는 조인을 하지 말고 설정을 바꾸자.
Article list 를 조회할 때, user 는 fetch join 하고, tags 는 lazy 로 설정하고 application.properties 에 hibernate.default_batch_fetch_size=100 을 추가했다. 이렇게 하니까 쿼리가 4번 나간다. 1번은 article, user 를 조인한 테이블 조회, 1번은 count 조회, 1번은 article_tag 라는 중간 테이블 조회고 1번은 tag 테이블 조회다.
기존에는 페이징 개수가 10개고 한 게시글당 태그가 5개까지 있으니까 최대 1번 + 50번 + 10번 + 50번 나갈 수 있었다.
이 옵션만 적용했는데 쿼리가 주룩주룩 나가던 거에서 한개로 바뀌었다..
(팀프로젝트를 시작해서 여기서 끝)
'project' 카테고리의 다른 글
테스트 코드의 중요성, TDD (0) 2022.04.30 Spring security > 5.1 jwt 인증하는 방법 (0) 2022.04.25 spring security oauth2 로그인 구현 (개발 로그) (0) 2022.03.30 Spring Db, jpa 를 사용하며 고민들 (0) 2022.03.29 협업 준비 (백엔드, 인프라) (0) 2022.02.02