클라이언트는 사용자가 입력한 정보를 vuex의 store(Vuex는 상태관리 패턴 + 라이브러리)를 통해 로그인 API에 전달한다.
API 에서는 요청 시 넘겨받은 데이터를 가지고 존재하는 회원인지, 존재한다면 비밀번호가 일치하는지 검사한다.
해당 과정을 통과한다면 서버는 사용자 정보와 함께 해당 사용자에 대한 인증값(토큰)을 발급한다.
사용자 정보와 인증토큰을 클라이언트에게 응답한다.
클라이언트는 토큰과 사용자 정보를 정상적으로 응답 받았다면 해당 데이터들을 store를 통해 클라이언트에서 저장한 후 약속된 화면이동을 실행한다.
이후 로그인 결과는 accessToken을 클라이언트의 Local Storage에 가지고있는것을 볼수있을것이다.(나는 user정보는 따로 없이 jwt token으로만 springSecurity에서 인증하여 사용하고있으므로 X-AUTH-TOKEN으로 저장하고 활용할것이다.)
Vuex란?
Vue.js 애플리케이션에 사용할 수있는 상태 관리 라이브러리다. 모든 컴포넌트에 대한 중앙 집중식 저장소 역할
store란?
vuex로 상태관리를 하기 위해서는 store가 필요하다. 애플리케이션의 공유된 상태를 보유하고 있는 전역변수다!
다른 라이브러리 설치후 종속성 추가(main.js)하는 것보다 src/store아래에 js파일을 만들어서 관리하는것이, 나중에 전역변수도 여러개 되는 경우 모듈화 간편화가가능하다.
vuex 필요
프론트에서 vuex로 변수 관리 필요함.
npm install vuex --save
vuex는 비동기와 데이터 보존이며, 보통 src/store/index.js에서 index.js에 import Vuex from vuex 와 src/main.js에서 import store from ./store/index.js와 app.use(***store)*** 하여 사용한다. 이후에는 Vue 컴포넌트에서 this.$store로 스토어 접근가능하다.
//Actions: Backend API 호출 // Mutatios: 뜻은 변이라는 뜻이지만 쉽게 말하자면 Backend API 호출 결괏값을 파라미터로 전달받은 후 state에 값을 저장하게 된다. // 위의 개념을 보고 아래 소스를 보면 이해하기 쉬울 것입니다. // 아래 소스는 Backend Login API 호출 후 결과로 사용자 정보를 담고 있는 user 객체를 받은 후 mutations에서 user 객체와 로그인 상태를 저장하게 됩니다.
import AuthService from'../service/auth.service';
const user = JSON.parse(localStorage.getItem('user')); const initialState = user ? { status: { loggedIn: true }, user } : { status: { loggedIn: false }, user: null };
@GetMapping("/{id}") public SingleResult<PostResponseDto> getpost(@PathVariable Long id){ PostResponseDto postResponseDto = postService.getResponseDtoPost(id); return responseService.getSingleResult(postResponseDto); } @PostMapping public SingleResult<PostDto> create(@RequestBody PostDto postDto){ Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String email = ((User) principal).getEmail(); return responseService.getSingleResult(postService.savePost(email, postDto)); } @PutMapping() public SingleResult<PostDto> update(@RequestBody PostDto postDto){ return responseService.getSingleResult(postService.editPost(postDto)); } @DeleteMapping("/{id}") public CommonResult delete(@PathVariable Long id){ PostDto postDto = postService.getPost(id); postService.deletePost(postDto); return responseService.getSuccessResult(); } }
CORS 정책에 따라, 해당 요청 받을수있도록 에너테이션으로 처리
특히 create()할때는 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 를 사용하여, (User) principal 형변환을 통해 getEmail()을 활용하여, savePost()시 email기준으로 저장하였다.
1 2 3 4 5
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); String email = ((User) principal).getEmail(); System.out.println(email);
<div class="button"> <button class="submit" type="submit">Sign up here</button> </div> </form> </template>
<script> import axios from "axios";
export default {
data() { return { email: '', password: '', nickname: '', passwordError: '', } }, methods:{ handleSubmit(){ //validate password field length this.passwordError = this.password.length < 5 ? '': "password should be longer than 5";
if (!this.passwordError) { console.log(this.email); console.log(this.password); console.log(this.nickname); alert("password should be longer than 6"); } else { axios.post('/signup', { "email": this.email, "password": this.password, "nickname": this.nickname }) .then((res) => { console.log(res); alert('회원가입 성공하였습니다.'); this.$router.push({ path: './login' }) }).catch((err) => { if (err.message.indexOf('Network Error') > -1) { alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.') } }); } }, } } </script>
길이 5이하면 alert을 띄웠다.
res받으면, this.$router.push로 login페이지로 돌아가도록 하였다.
문제 해결 기록
postList 조회시 에러
현재 getresult를 따로 설정해서 list를 반환하고있는데 여기에서 server error가 도출되었다. 아래 오류 코드를 보면 SerializationFeature관련 오류이며, Jackson 이야기도나왔다. 해결책은 disable SerializationFeature.FAIL_ON_EMPTY_BEANS 하라고한다.
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->hello.postpractice.domain.PostDto[“user”]->hello.postpractice.domain.User$HibernateProxy$yxBnj4Cf[“hibernateLazyInitializer”])
문제점
문제가된 Post엔티티를 보면 문제점확인가능하다. getresult는 결국 해당객체를 조회할때 Post객체 정보를 JSON으로 변환하는 과정에서 Post와 연관된 user를 serialize해주려는 순간, fetchType이 Lazy라 실제 user객체가 아닌 프록시로 감싸져있는 hibernateLazyInitializer를 serialize하려하기대문에 문제가 발생한것이다.(EAGER 옵션이였다면, 실제 매핑되어있는 user에 대한 조회가 이뤄지고 실제 user객체를 serialize해므로 문제가없었을것이다.)
application.properties에서 spring.jackson.serialization.fail-on-empty-beans=false 하지만 근본적인 해결방법이 아닌 표면적인 방법이다. 근본적인 해결을 위해선 JSON형식으로 변환할지 말지에 대해 정해야한다.(결과가 hibernateLaztInitializer:{} 로 나온다)
결과를 반환 getlist시 JSON response에서 figure를 제외하고 보낸다.(해당 필드를 @JsonIgnore사용) (추천, Lazy유지 + 오류 원인 제거가능)
fetchType을 EAGER로 변경한다. 하지만 EAGER 이기에 post단순조회를 해도 user경보까지 항상 모두 조회하게된다.
시도
1번은 근본적해결방법이 아니므로, 3번 방법을 활용하였다. 프록시가 안되도록 lazy에서 eager로 즉시 만들어주었다. 하지만 다시 문제가생겼다.
1 2 3
@ManyToOne(fetch =FetchType.EAGER) //수정 @JoinColumn(name="user_id") private User user;
Exception used to indicate that a query is attempting to simultaneously fetch multiple bag
Bag(Multiset)은 Set과 같이 순서가 없고, List와 같이 중복을 허용하는 자료구조이다.
하지만 자바 컬렉션 프레임워크에서는 Bag이 없기 때문에 하이버네이트에서는 List를 Bag으로써 사용하고 있는 것이다.
해결책으로 보통 2가지를 제시된다
OneToMany, ManyToMany인 Bag 두 개 이상을 EAGER로 fetch할 때 발생한다.
나 같은경우는 Post엔티티에 user필드가 EAGER했고 user.roles도 EAGER였고, Post엔티티에 comments필드 또한 EAGER였기때문에 문제가 발생했다.
해결
나는 getlist를 할때는 구지 comments를 EAGER 할 필요없으므로, Lazy를 통해 해결하였다. getpost를 할경우 Dto에서 해당부분 comment객체를 모두 조회해 postresponsedto.comments 에 넣어줌으로 문제없었다.
CORS 이슈
CORS문제(Cross-Origin Resource Sharing)
교차 출처 리소스 공유 → 다른 출처를 의미
origin이란 서버의 위치를 의미하는 protocol, host, 포트번호까지 모두 합친것을 의미한다.(https://www.naver.com:8080) → 즉, 서버위치를 찾아가기 위해 필요한 가장 기본적인것들.(http, https프로토콜은 기본 포트 번호정해져있다.생략가능)
웹 생태계에는 다른 출처로의 리소스 요청을 제한하는 것과 관련된 두가지 정책은 cors와 sop(Same-Origin Policy)이다
sop는 같은 출처에서만 리소스를 공유할수있다는 정책이다.
하지만 웹에서 다른 출처에 있는 리소스를 가져와서 사용하는 일은 흔한일이기때문에, 예외 조항을 두고, 리소스 요청은 철처가 다르더라도 허용했는데, 그중 하나가 CORS정책을 지킨 리소스 요청이다.
CORS정책을 지켜야 다른 리소스 요청을 사용할수잇다.
이런 출처(protocol+host+port)를 비교하는 로직은 브라우저에 구현되어있는 스펙이다.
검증
클라이언트가 리소스 요청시 http 프로토콜을 사용하여 요청을 보낼떄 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다 → 이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의 Access-Control-allow-Origin이라는 값에 “이 리소스를 접근하는 것에 허용된 출처”를 내려주고 → 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin 과 서버가 보내준 응답 Access-Control-allow-Origin 비교해 본후 이 응답이 유효한 응답이 아닌지를 결정한다.!!!!
아래내용출력시, postman으로하면 user5라고 나오는데, 왜 vue에서는 안될까? 그리고
.antMatchers(HttpMethod.*OPTIONS*,"/**").permitAll()//allow CORS option calls
애초에 token을 내가 x-auth-token으로 token찾게 해놓고, ….그래서 토큰을 못찾고있었음
"X-AUTH-TOKEN": this.$token
JPA 1:N 삭제 시
post DB를 삭제시 다음과 같은 오류가 떳다
Cannot delete or update a parent row: a foreign key constraint fails (post_db.comments, CONSTRAINT FKbqnvawwwv4gtlctsi3o7vs131 FOREIGN KEY (post_id) REFERENCES post (id))
해당 삭제하려고 하는 테이블 또는 행이 다른 곳에서 참조하고 있기 때문에 발생하는문제다( 외부에 foregin_key존재)
@OneToOne이나 @OneToMany 에서 붙여주는 영속성 전이 Cascade 때문에 일어난 문제였다. 필드에 cascade=CascadeType.ALL을 붙여주면 그 필드와 연관된 엔티팉를 persist해주지 않아도 영속성이된다. 하지만 2가지를 만족해야만 써야한다.
등록 삭제 등 라이프 사이클이 똑같을 때
단일 엔티티에 완전히 종속적일때만 사용 가능하다. parent-child라는 연관관계를 맺고있을 때 child를 다른곳에서도 관계를 맺고있다면 사용하면 안된다.
user와 comments 1:N관계 였고 post와 comments에 1:N 관계였고, 이를 삭제하려면 cascade.all을 해놨다. … 그래서