LostCatBox

SpringProject-Board-CH06

Word count: 4kReading time: 25 min
2022/12/24 Share

Spring 게시판 프로젝트 6편 (vue로 post+login구현)

Created Time: July 23, 2022 7:31 PM
Last Edited Time: August 3, 2022 11:07 AM
Tags: Java, Spring, Computer

vue로 로그인 관련 프론트 만들기

전체 폴더 구조

스크린샷 2022-07-31 오후 6.56.51.png

로그인 로직

로그인은 vuex를 사용하였고, 다음과같다

  1. 아이디(이메일), 패스워드 입력 후 로그인 버튼 클릭
  2. 클라이언트는 사용자가 입력한 정보를 vuex의 store(Vuex는 상태관리 패턴 + 라이브러리)를 통해 로그인 API에 전달한다.
  3. API 에서는 요청 시 넘겨받은 데이터를 가지고 존재하는 회원인지, 존재한다면 비밀번호가 일치하는지 검사한다.
  4. 해당 과정을 통과한다면 서버는 사용자 정보와 함께 해당 사용자에 대한 인증값(토큰)을 발급한다.
  5. 사용자 정보와 인증토큰을 클라이언트에게 응답한다.
  6. 클라이언트는 토큰과 사용자 정보를 정상적으로 응답 받았다면 해당 데이터들을 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.jsapp.use(***store)*** 하여 사용한다. 이후에는 Vue 컴포넌트에서 this.$store로 스토어 접근가능하다.

Backend - spring

SignController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Api(tags = "1. SignUp / LogIn")
@RequiredArgsConstructor
@RestController("/")
@CrossOrigin(origins="*", allowedHeaders = "*")
public class SignController {

private final UserService userService;
private final JwtProvider jwtProvider;
private final ResponseService responseService;
private final PasswordEncoder passwordEncoder;

@ApiOperation(value = "로그인", notes = "이메일로 로그인을 합니다.")
@PostMapping("/login")
public SingleResult<String> login(
@ApiParam(value = "로그인 아이디 : 이메일", required = true) @RequestBody Map<String, String> loginMap) {
String username = loginMap.get("username");
String password = loginMap.get("password");
UserLoginResponseDto userLoginDto = userService.login(username, password);

String token = jwtProvider.createToken(String.valueOf(userLoginDto.getId()), userLoginDto.getRoles());

return responseService.getSingleResult(token);
}
}
  • 기본적으로 CORS 정책을 지켜야함으로 @CrossOrigin 으로 응답가능하도록함
  • login @PostMapping 수정
    (API로 동작하므로 로그인시에도 RequestBody로 요청 데이터 받아서 처리 위해서)
  • login로직에서 @requestBody로 json값으로 username, password사용

Frontend - vue

Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<form @submit.prevent="handleSubmit"> //@submit.prevent로 handleSubmit 함수를 호출
<h3>로그인</h3>
<input type="username" placeholder="Username" v-model="username">
<input type="password" placeholder="Password" v-model="password">
<div v-if="error" class="error">{{ error }}</div>
<button v-if="!isPending">로그인</button>
<button v-if="isPending" disable>Loading</button>
</form>
</template>

<script>
import useLogin from '@/composables/useLogin' //1
import { useStore } from 'vuex' //2
import { ref } from 'vue' //3
import { useRouter } from 'vue-router' //4

export default {
setup() {
const{ error, login, isPending } = useLogin()
const store = new useStore()
const router = useRouter()

const username = ref('')
const password = ref('')

const handleSubmit = async () => { //5
//사용자 로그인 처리
await login(username.value, password.value)

if (store.state.auth.status.loggedIn) {
router.push({ name: 'BoardList' })
}
}

return { username, password, handleSubmit, error, isPending }
}
}
</script>

<style>

</style>
  1. 아래 userLogin.js 에서 useLogin 변수를 import함.
  2. userStore()를 사용하여 저장소 객체 주입받아 사용하기
  3. ???
  4. useRouter를 사용하여, 현재 접속할수있는 router등록된 경로로 push 가능
  5. js funtion문으로 화살표를 사용하였고, async()매개변수로 구현문이 실행된다.
    async()로 await로 login()함수의 응답을 기다리며, 응답후, store의 인자를 확인하고 있다면, 있다면 router사용하여, BoardList로 등록된 경로로 push함

composables/useLogin.js

실제 서버와의 통신을 통하여서 로그인 성공 시 응답 값을 받아서 Local Storage에 저장하는 역할과 vuex를 이용하여서 사용자의 로그인 여부를 state에 저장 함으로써 다른 컴포넌트에서도 사용자의 로그인 여부를 판단할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {ref} from 'vue';
import store from '../store/index.js'; //store가 정의되어있는곳 import

const error = ref(null)
const isPending = ref(false)

const login = async (username, password) => {

error.value = null
isPending.value = true

try {
await store.dispatch('auth/login', {username, password}) // store.dispatch로 store에 auth/login 함수가 실행된다.

error.value = null
isPending.value = false
} catch(err) {
error.value = '로그인 정보가 올바르지 않습니다.'
isPending.value = false
}
}

const useLogin = () => {
return { error, login, isPending }
}

export default useLogin

store/index.js

  • store의 핵심은 state, mutations, actions, getters이다. auth에 구현되어있다.
1
2
3
4
5
6
7
8
9
10
11
// store객체를 생성하고 모듈을 관리한다.
import { createStore } from "vuex";
import { auth } from "./auth.module";

const store = createStore({
modules: {
auth,
},
});

export default store;

store/auth.module.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//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 };

export const auth = {
namespaced: true,
state: initialState,
actions: {
login({ commit }, {username, password}) {
return AuthService.login(username, password).then( //AuthService에 로그인 함수 호출. 결과값받아냄
user => {
commit('loginSuccess', user);
return Promise.resolve(user);
},
error => {
commit('loginFailure');
return Promise.reject(error);
}
);
},
logout({ commit }) {
AuthService.logout();
commit('logout');
}
},
mutations: {
loginSuccess(state, user) {
state.status.loggedIn = true;
state.user = user;
},
loginFailure(state) {
state.status.loggedIn = false;
state.user = null;
},
logout(state) {
state.status.loggedIn = false;
state.user = null;
}
},
getters: {
isLoggedIn: state => state.loginSuccess
}

};

service/auth.service.js

  • login, logout 요청 보내고 응답 반환하는곳
  • 요청 받으면, response.data에서 내가 발급해준 jwt token을 user의 값으로 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import axios from 'axios';

const API_URL = 'http://localhost:8080/'; //해당 API주소

class AuthService {
/**
* 로그인
*/
login(username, password) {
return axios.post('/login',
{
username: username,
password: password
})
.then(response => {
console.log(response.data)
if (response.data.data) {
localStorage.setItem('user', JSON.stringify(response.data.data)); //localStorage에 User데이터 저장
}

return response.data;
});
}

/**
* 로그아웃
*/
logout() {
// LocalStorage 사용자 정보
let user = JSON.parse(localStorage.getItem('user'))

let data = {
username: user.username
}

return axios.post(API_URL + 'signout', JSON.stringify(data), {
headers: {
"Content-Type": 'application/json',
},
})
.then(response => {
console.log(response)
localStorage.removeItem('user');
});
}
}

export default new AuthService();

정리

  • 로그인 과정이 성공한다면 localStorage에 user의 값은 token이 남게된다. 앞으로 로그인시에는 header에 “X-AUTH-TOKEN”값으로 넣어주면 SpringSecurity에서 검증후 API응답을 해준다.

vue로 게시판 관련 프론트 만들기

Backend -spring

PostController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("basic/post")
@CrossOrigin(origins="*", allowedHeaders = "*")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
private final ResponseService responseService;

@GetMapping
public ListResult<PostDto> postList(){
List<PostDto> postDtoList = postService.getPostList();
return responseService.getListResult(postDtoList);
}

@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);

return responseService.getSingleResult(postService.savePost(email, postDto));

frontend - vue

요청시에는 항상 header에 {“X-AUTH-TOKEN”:token} 이 필요하다.

views/board

  • BoardDetail.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div class="board-detail">
<div class="common-buttons">
<button type="button" class="w3-button w3-round w3-blue-gray" v-on:click="fnUpdate">수정</button>&nbsp;
<button type="button" class="w3-button w3-round w3-red" v-on:click="fnDelete">삭제</button>&nbsp;
<button type="button" class="w3-button w3-round w3-gray" v-on:click="fnList">목록</button>
</div>
<div class="board-contents">
<h3>{{ title }}</h3>
<div>
<strong class="w3-large">{{ author }}</strong>
<br>
<span>{{ created_at }}</span>
</div>
</div>
<div class="board-contents">
<span>{{ contents }}</span>
</div>
<div class="common-buttons">
<button type="button" class="w3-button w3-round w3-blue-gray" v-on:click="fnUpdate">수정</button>&nbsp;
<button type="button" class="w3-button w3-round w3-red" v-on:click="fnDelete">삭제</button>&nbsp;
<button type="button" class="w3-button w3-round w3-gray" v-on:click="fnList">목록</button>
</div>
</div>
</template>

<script>
export default {
data() { //변수생성
return {
requestBody: this.$route.query, //boardlist에서 push한것 대한 쿼리 값!
id: this.$route.query.id,
token: `${localStorage.getItem("user").replace(/^"(.*)"$/, '$1')}`,

title: '',
author: '',
contents: '',
created_at: ''
}
},
mounted() {
this.fnGetView()
},
methods: {
fnGetView() {
this.$axios.get(this.$serverUrl + '/basic/post/' + this.id, {
params: this.requestBody,
headers: {
"X-AUTH-TOKEN": this.token
}
}).then((res) => {
console.log(res.data)
this.title = res.data.data.postName
this.author = res.data.data.user.nickname
this.contents = res.data.data.content
this.created_at = res.data.data.createdDate
}).catch((err) => {
if (err.message.indexOf('Network Error') > -1) {
alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
}
})
},
fnList() {
delete this.requestBody.id
this.$router.push({
path: './list',
query: this.requestBody
})
},
fnUpdate() {
this.$router.push({
path: './write',
query: this.requestBody
})
},
fnDelete() {
if (!confirm("삭제하시겠습니까?")) return

this.$axios.delete(this.$serverUrl + '/basic/post/' + this.id, {
headers: {
"X-AUTH-TOKEN": this.token
}
})
.then(() => {
alert('삭제되었습니다.')
this.fnList();
}).catch((err) => {
console.log(err);
})
}
}
}
</script>
<style scoped>

</style>
  • BoardList.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<template>
<div class="board-list">
<div class="common-buttons">
<button type="button" class="w3-button w3-round w3-blue-gray" v-on:click="fnWrite">등록</button>
</div>
<table class="w3-table-all">
<thead>
<tr>
<th>No</th>
<th>제목</th>
<th>내용</th>
<th>작성자</th>
<th>등록일시</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in list" :key="idx">
<td>{{ row.id }}</td>
<td><a v-on:click="fnView(`${row.id}`)">{{ row.postName }}</a></td>
<td>{{ row.content }}</td>
<td>{{ row.user.username}}</td>
<td>{{ row.createdDate }}</td>
</tr>
</tbody>
</table>
</div>
</template>

<script>
export default {
data() { //변수생성
return {
requestBody: {}, //리스트 페이지 데이터전송
list: {}, //리스트 데이터
token : `${localStorage.getItem("user").replace(/^"(.*)"$/, '$1')}`
};
},

mounted() {
this.fnGetList()
},
methods: {
fnGetList() {
this.requestBody = { // 데이터 전송
keyword: this.keyword,
page: this.page,
size: this.size,
}

this.$axios.get(this.$serverUrl + "/basic/post", {
params: this.requestBody,
headers: {
"X-AUTH-TOKEN": this.token
}
}).then((res) => {
console.log(res.data);

this.list = res.data.list //서버에서 데이터를 목록으로 보내므로 바로 할당하여 사용할 수 있다.

}).catch((err) => {
if (err.message.indexOf('Network Error') > -1) {
alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
}
})
},
fnView(id){
this.requestBody.id = id
this.$router.push({
path: './detail',
query: this.requestBody,
headers: {
"X-AUTH-TOKEN": this.token
}
})
},
fnWrite(){
this.$router.push({
path: './write'
})
},

},
}
</script>
  • BoardWrite
    글을 수정시에 사용하는 vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<!--id값 있다면, update, 없다면 post생성-->
<template>
<div class="board-detail">
<div class="common-buttons">
<button type="button" class="w3-button w3-round w3-blue-gray" v-on:click="fnSave">저장</button>&nbsp;
<button type="button" class="w3-button w3-round w3-gray" v-on:click="fnList">목록</button>
</div>
<div class="board-contents">
<h2>{{author}}</h2>
<input type="text" v-model="postName" class="w3-input w3-border" placeholder="제목을 입력해주세요.">
</div>
<div class="board-contents">
<textarea id="" cols="30" rows="10" v-model="content" class="w3-input w3-border" style="resize: none;">
</textarea>
</div>
<div class="common-buttons">
<button type="button" class="w3-button w3-round w3-blue-gray" v-on:click="fnSave">저장</button>&nbsp;
<button type="button" class="w3-button w3-round w3-gray" v-on:click="fnList">목록</button>
</div>
</div>
</template>

<script>
export default {
data() { //변수생성
return {
requestBody: this.$route.query,
id: this.$route.query.id,
token : `${localStorage.getItem("user").replace(/^"(.*)"$/, '$1')}`,

postName: '',
content: '',
author: '',
created_at: ''
}
},
mounted() {
this.fnGetView()
},
methods: {
fnGetView() {
if (this.id !== undefined) {
this.$axios.get(this.$serverUrl + '/basic/post/' + this.id, {
params: this.requestBody,
headers: {
"X-AUTH-TOKEN":this.token
}
}).then((res) => {
console.log(res.data)
this.postName = res.data.data.postName
this.author = res.data.data.user.nickname
this.content = res.data.data.content
this.created_at = res.data.data.createdDate
}).catch((err) => {
console.log(err)
})
}
},
fnList() {
delete this.requestBody.id
this.$router.push({
path: './list',
query: this.requestBody
})
},
fnView(id) {
this.requestBody.id = id
this.$router.push({
path: './detail',
query: this.requestBody
})
},
fnSave() {
let apiUrl = this.$serverUrl + '/basic/post'
this.form = {
"id": this.id,
"postName": this.postName,
"content": this.content,
}

if (this.id === undefined) {
//INSERT
this.$axios.post(apiUrl,
this.form,{
headers: {
"X-AUTH-TOKEN": this.token
}
})
.then((res) => {
alert('글이 저장되었습니다.')
this.fnView(res.data.data.id)
}).catch((err) => {
if (err.message.indexOf('Network Error') > -1) {
alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
}
})
} else {
//UPDATE
this.$axios.put(apiUrl, this.form,{
headers: {
"X-AUTH-TOKEN": this.token
}
})
.then((res) => {
alert('글이 저장되었습니다.')
this.fnView(res.data.data.id)
}).catch((err) => {
if (err.message.indexOf('Network Error') > -1) {
alert('네트워크가 원활하지 않습니다.\n잠시 후 다시 시도해주세요.')
}
})
}
}
}
}
</script>
<style scoped>

</style>

회원가입 구현(signup)

backend- spring

SignController

  • 회원가입을 처리하기위해서 json으로 vue와 통신할 것이므로 @RequestBody중요
  • 따라서 Map을 통해 해당 키-값 받은후 get()으로 처리 … 하지만 여기엔 다 required가 되었고, 아니면 serverError가 날텐데…???
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ApiOperation(value = "회원가입", notes = "회원가입을 합니다.")
@PostMapping("/signup")
public SingleResult<Long> signup( @RequestBody Map<String, String> signupMap){
String email = signupMap.get("email");
String password = signupMap.get("password");
String nickname = signupMap.get("nickname");

UserSignupRequestDto userSignupRequestDto = UserSignupRequestDto.builder()
.email(email)
.password(passwordEncoder.encode(password))
.nickname(nickname)
.build();
Long signupId = userService.signup(userSignupRequestDto);
return responseService.getSingleResult(signupId);
}

frontend- vue

view/auth/signup.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
<form @submit.prevent="handleSubmit">
<label>Email :</label>
<input type="username" name="username" v-model="email" required>

<label>Password :</label>
<input type="password" name="password" v-model="password" required>

<label>Nickname :</label>
<input type="nickname" name="nickname" v-model="nickname" required>

<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해므로 문제가없었을것이다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)/* JPA에게 해당 Entity는 Auditing 기능을 사용함을 알립니다. */
public class Post {
@Id
@GeneratedValue
private Long id;

@Column(length =10,nullable = false)
private String postName;

@Column(columnDefinition ="TEXT", nullable = false)
private String content;

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime modifiedDate;

@ManyToOne(fetch =FetchType.LAZY)
@JoinColumn(name="user_id")
private User user;

@OneToMany(mappedBy="post", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
@OrderBy("id asc") //댓글 정렬
private List<Comment> comments;

@Builder //매개변수를 builder 패턴으로 편리성,유연함을 줌
public Post(Long id, String postName, String content, User user, List<Comment> comments){
this.id = id;
this.postName = postName;
this.content = content;
this.user = user;
this.comments =comments;
}
}

해결책

보통 이럴때 해결책 3가지이다.

  1. application.properties에서
    spring.jackson.serialization.fail-on-empty-beans=false
    하지만 근본적인 해결방법이 아닌 표면적인 방법이다. 근본적인 해결을 위해선 JSON형식으로 변환할지 말지에 대해 정해야한다.(결과가 hibernateLaztInitializer:{} 로 나온다)
  2. 결과를 반환 getlist시 JSON response에서 figure를 제외하고 보낸다.(해당 필드를 @JsonIgnore사용) (추천, Lazy유지 + 오류 원인 제거가능)
  3. fetchType을 EAGER로 변경한다. 하지만 EAGER 이기에 post단순조회를 해도 user경보까지 항상 모두 조회하게된다.

시도

1번은 근본적해결방법이 아니므로, 3번 방법을 활용하였다. 프록시가 안되도록 lazy에서 eager로 즉시 만들어주었다. 하지만 다시 문제가생겼다.

1
2
3
@ManyToOne(fetch =FetchType.EAGER) //수정
@JoinColumn(name="user_id")
private User user;
  • 하지만 multiplebagfetchException이 나타났다.

Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [hello.postpractice.domain.User.roles, hello.postpractice.domain.Post.comments]

multiplebagfetchException

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 비교해 본후 이 응답이 유효한 응답이 아닌지를 결정한다.!!!!

문제해결

  • 해당문제는 cors로 크롬 개발자 탭에서확인하였고, 이는 springSecurity에서 요청이 들어오자마자. CORS를 허용하지않았으므로 발생한문제이다.
  • login 관련 controller에 대해서 다음과같이 에너테이션을 붙여서 해결하였다
1
2
3
4
@CrossOrigin(origins="*", allowedHeaders = "*")
public class SignController {
...
}
  • 또는 WebMvcConfigurer를 구현하는 것도 가능하다
1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowCredentials(true);
}
}

인증요구해야 jwt로 user infor받아올수있음..

당연.

anyRequest().authenticated() 필요햇싸..인증을 api테스트한다고 다 permitall()놧둔점이 …문제였음

하지만 인증을 요구한 후에도…

SecurityContextHolder.*getContext*().getAuthentication().getName()

아래내용출력시, 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가지를 만족해야만 써야한다.

  1. 등록 삭제 등 라이프 사이클이 똑같을 때
  2. 단일 엔티티에 완전히 종속적일때만 사용 가능하다.
    parent-child라는 연관관계를 맺고있을 때 child를 다른곳에서도 관계를 맺고있다면 사용하면 안된다.

user와 comments 1:N관계 였고 post와 comments에 1:N 관계였고, 이를 삭제하려면 cascade.all을 해놨다. … 그래서

CATALOG
  1. 1. Spring 게시판 프로젝트 6편 (vue로 post+login구현)
  2. 2. vue로 로그인 관련 프론트 만들기
    1. 2.1. 전체 폴더 구조
    2. 2.2. 로그인 로직
    3. 2.3. Vuex란?
      1. 2.3.1. store란?
    4. 2.4. vuex 필요
    5. 2.5. Backend - spring
      1. 2.5.1. SignController
    6. 2.6. Frontend - vue
      1. 2.6.1. Login.vue
      2. 2.6.2. composables/useLogin.js
      3. 2.6.3. store/index.js
      4. 2.6.4. store/auth.module.js
      5. 2.6.5. service/auth.service.js
      6. 2.6.6. 정리
  3. 3. vue로 게시판 관련 프론트 만들기
    1. 3.1. Backend -spring
      1. 3.1.1. PostController
    2. 3.2. frontend - vue
      1. 3.2.1. views/board
  4. 4. 회원가입 구현(signup)
    1. 4.1. backend- spring
      1. 4.1.1. SignController
    2. 4.2. frontend- vue
      1. 4.2.1. view/auth/signup.vue
  5. 5. 문제 해결 기록
    1. 5.1. postList 조회시 에러
      1. 5.1.1. 문제점
      2. 5.1.2. 해결책
      3. 5.1.3. 시도
      4. 5.1.4. multiplebagfetchException
      5. 5.1.5. 해결
    2. 5.2. CORS 이슈
      1. 5.2.1. CORS문제(Cross-Origin Resource Sharing)
      2. 5.2.2. 문제해결
    3. 5.3. 인증요구해야 jwt로 user infor받아올수있음..
    4. 5.4. JPA 1:N 삭제 시