본문 바로가기

스프링 부트/웹MVC

업데이트, 더티 체킹

반응형

더티 체킹은 Transaction 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터베이스에 반영하는 JPA 특징이다

 

트랜잭션이 끝나는 시점에 최초 조회 상태와 비교해 변화가 있는 모든 엔티티 객체를 DB에 자동으로 반영해준다

JPA에서는 엔티티를 조회했을 때 조회 상태 그대로를 스냅샷으로 만들어놓는다

그리고 트랜잭션이 끝나는 시점에 이 스냅샷과 비교해 다른점이 있으면 업데이트 쿼리를 DB에 전달한다

 

더티 체킹으로 생성되는 업데이트 쿼리는 기본적으로 모든 필드를 업데이트시키므로, 필드가 많아질 경우 전체 필드 업데이트가 부담이 된다

 

@DynamicUpdate 어노테이션으로 변경 필드만 변경이 가능하다

 

일단 info.html에 userName을 표시해주어야하는데, 2가지 방법이 있다

 

 

이 방법은 현재 하는 연습용 프로젝트에선 작업량에 비해 비효율적일 수 있다

UserDetails 인터페이스를 구현하여서 Principal을 직접 만들면서 여기에 userName을 넣고

package com.cos.web01.config.auth;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.cos.web01.domain.user.Users;

import lombok.Getter;

@Getter
public class PrincipalDetail implements UserDetails {

	private Users users;
	private ArrayList<GrantedAuthority> authorities;

	public PrincipalDetail(Users users, ArrayList<GrantedAuthority> authorities) {
		this.users = users;
		this.authorities = authorities;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return users.getPassword();
	}

	@Override
	public String getUsername() {
		return users.getUserId();
	}

	public String getUserName() {
		return users.getUserName();
	}

//	계정이 만료되지 않았는지 리턴 (true : 만료안됨)
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

//	계정이 잠겨있지 않았는지 리턴 (true : 잠기지 않음)
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

//	비밀번호가 만료되지 않았는지 리턴 (true : 만료안됨)
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

//	계정이 활성화인지 리턴 (true: 활성화)
	@Override
	public boolean isEnabled() {
		return true;
	}

}

 

 

UserSecurityService.java의 loadByUsername 메소드에 리턴 값으로 전달한다

package com.cos.web01.service;

import java.util.ArrayList;
import java.util.Optional;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.cos.web01.config.auth.PrincipalDetail;
import com.cos.web01.domain.dto.UserSaveRequestDto;
import com.cos.web01.domain.user.UserRepository;
import com.cos.web01.domain.user.Users;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class UserSecurityService implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Transactional // DB에 접근하는 여러 연산을 하나의 트랜잭션으로 처리하여 오류가 발생한 경우 롤백을 도와줌
	public Long accountUser(UserSaveRequestDto saveDto) {
//		회원가입에 대한 메소드

//		클라이언트에서 전달한 패스워드를 해쉬 암호화 작업
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		saveDto.setPassword(passwordEncoder.encode(saveDto.getPassword()));

		return userRepository.save(saveDto.toEntity()).getId();
	}

	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		// 로그인 요청이 들어왔을 때 유저 정보의 조회하는 메소드
		// 유저의 ROLE 부여
		// 매개 변수로 userId를 받지만, form의 input 태그에서 username으로 서버에 전달해주어야함

		Optional<Users> userEntity = userRepository.findByUserId(userId);
		Users findUser = userEntity.get();

//		권한은 하나만 갖고 있는게 여러개를 갖고 있을 수도 있음
		ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

//		import org.springframework.security.core.userdetails.User;
//		return new User(findUser.getUserId(), findUser.getPassword(), authorities);
		
		return new PrincipalDetail(findUser, authorities);
	}

	@Transactional
	public void changeUserName(String userId, String changeName) {
		Optional<Users> users = userRepository.findByUserId(userId);

		Users user = users.get();

		user.changeUserName(changeName);
	}

}

 

 

 

그리고 info.html에서

						<h3>
							<span th:text="${userName}"></span>
						</h3>

 

 

 

 

 

다른 방법은 간단한 방법으로

WebController.java의 /info 부분을 수정하는 것이다

	@GetMapping("/info")
	public String info(Principal principal, Model model) {
		Optional<Users> users = userRepository.findByUserId(principal.getName());
		Users user = users.get();

		model.addAttribute("userName", user.getUserName());

		return "contents/info";
	}

 

						<h3>
							<!-- <span sec:authentication="principal.userName"></span> -->
							<span th:text="${userName}"></span>
						</h3>

 

 

info.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{common/layout}">

<section layout:fragment="content">
	<div class="container wrap__content">
		<div class="row text-center">
			<div class="col-sm-8 col-md-8 col-md-offset-2">
				<div class="thumbnail">
					<div class="caption">
						<h3>
							<!-- <span sec:authentication="principal.userName"></span> -->
							<span th:text="${userName}"></span>
						</h3>
						<div>
							<button type="button" class="btn btn-primary" data-toggle="modal"
								data-target="#chageUserModal" id="btn-change-modal">회원 정보 변경</button>
							<button class="btn btn-default" role="button" id="btn-delete">회원탈퇴</button>
						</div>
					</div>
				</div>
			</div>
		</div>

		<div class="modal fade" id="chageUserModal" tabindex="-1" role="dialog"
			aria-labelledby="chageUserLabel" aria-hidden="true">
			<div class="modal-dialog" role="document">
				<div class="modal-content">
					<div class="modal-header">
						<h5 class="modal-title" id="accountUserLabel">회원 가입</h5>
						<button type="button" class="close" data-dismiss="modal" aria-label="Close">
							<span aria-hidden="true">&times;</span>
						</button>
					</div>
					<div class="modal-body">
						<form id="changeName-frm">
							<div class="form-group">
								<label for="user_name">이름</label> <input type="text" class="form-control" id="changeName"
									th:placeholder="${userName}">
							</div>
						</form>
					</div>
					<div class="modal-footer">
						<button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
						<button type="button" class="btn btn-primary" id="btn-change">변경</button>
					</div>
				</div>
			</div>
		</div>
	</div>


	<script src="/js/lib/jquery.min.js"></script>
	<script src="/js/lib/bootstrap.min.js"></script>
	<script>
		const userInfo = {
			init : function() {
				$("#btn-change").on("click", ()=>{
					this.clickChangeName();
				})
			},
			csrf : {
				token : $("meta[name='_csrf']").attr("content"),
				header : $("meta[name='_csrf_header']").attr("content")
			},
			clickChangeName : function() {
				const data = {
						changeName : $("#changeName").val(),
				}
				
				// console.log(data);
				
				const {header, token} = this.csrf;
				
				$.ajax({
					type:  "POST",
					url: "user/changeName",
					dataType: "json",
					contentType: "application/json; charset=utf-8",
					data: JSON.stringify(data),
					beforeSend: function(xhr){
						 xhr.setRequestHeader(header, token);
					}
				}).done(function(result){
					if(result.msg === "success"){
						alert("변경 완료");
						location.reload();
					}
				}).fail(function(){
					alert("에러");
					console.log(error);
				})
			}
		}

		userInfo.init();
	</script>
</section>

 

 

 

 

 

 

 

info.html의 자바스크립트 부분에 삭제를 위한 코드 추가

		...
        
		const userInfo = {
			init : function() {
				$("#btn-change").on("click", ()=>{
					this.clickChangeName();
				})
				$("#btn-delete").on("click", ()=>{
					this.clickDeleteUser();
				})
			},
			csrf : {
				token : $("meta[name='_csrf']").attr("content"),
				header : $("meta[name='_csrf_header']").attr("content")
			},
			clickChangeName : function() {
				const data = {
						changeName : $("#changeName").val(),
				}
				
				// console.log(data);
				
				const {header, token} = this.csrf;
				
				$.ajax({
					type:  "POST",
					url: "user/changeName",
					dataType: "json",
					contentType: "application/json; charset=utf-8",
					data: JSON.stringify(data),
					beforeSend: function(xhr){
						 xhr.setRequestHeader(header, token);
					}
				}).done(function(result){
					if(result.msg === "success"){
						alert("변경 완료");
						location.reload();
					}
				}).fail(function(){
					alert("에러");
					console.log(error);
				})
			},
			clickDeleteUser: function(){
				const {header, token} = this.csrf;
				
				$.ajax({
					type:  "DELETE",
					url: "/user",
					dataType: "json",
					contentType: "application/json; charset=utf-8",
					data: JSON.stringify(data),
					beforeSend: function(xhr){
						 xhr.setRequestHeader(header, token);
					}
				}).done(function(result){
					alert("삭제 완료");
					location.href="/user/logout";
				}).fail(function(){
					alert("에러");
					console.log(error);
				})
			}
		}

		userInfo.init();
        
        ...

 

 

UserRepository.java

package com.cos.web01.domain.user;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<Users, Long> {

//	userId로 검색할 수 있는 메소드
//	Optional을 사용하여 null을 사용하지 않고 Optional 인스턴스로 대체하여 값이 없음에 대한 에러를 안전하게 처리
	Optional<Users> findByUserId(String userId);

	// 삭제 메소드
	Long deleteByUserId(String userId);
}

 

 

UserSecurityService.java

package com.cos.web01.service;

import java.util.ArrayList;
import java.util.Optional;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.cos.web01.config.auth.PrincipalDetail;
import com.cos.web01.domain.dto.UserSaveRequestDto;
import com.cos.web01.domain.user.UserRepository;
import com.cos.web01.domain.user.Users;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class UserSecurityService implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Transactional // DB에 접근하는 여러 연산을 하나의 트랜잭션으로 처리하여 오류가 발생한 경우 롤백을 도와줌
	public Long accountUser(UserSaveRequestDto saveDto) {
//		회원가입에 대한 메소드

//		클라이언트에서 전달한 패스워드를 해쉬 암호화 작업
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		saveDto.setPassword(passwordEncoder.encode(saveDto.getPassword()));

		return userRepository.save(saveDto.toEntity()).getId();
	}

	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		// 로그인 요청이 들어왔을 때 유저 정보의 조회하는 메소드
		// 유저의 ROLE 부여
		// 매개 변수로 userId를 받지만, form의 input 태그에서 username으로 서버에 전달해주어야함

		Optional<Users> userEntity = userRepository.findByUserId(userId);
		Users findUser = userEntity.get();

//		권한은 하나만 갖고 있는게 여러개를 갖고 있을 수도 있음
		ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

//		import org.springframework.security.core.userdetails.User;
		return new User(findUser.getUserId(), findUser.getPassword(), authorities);

//		return new PrincipalDetail(findUser, authorities);
	}

	@Transactional
	public void changeUserName(String userId, String changeName) {
		Optional<Users> users = userRepository.findByUserId(userId);

		Users user = users.get();

		user.changeUserName(changeName);
	}

	@Transactional
	public void deleteUser(String userId) {
		userRepository.deleteByUserId(userId);
	}

}

 

 

WebRestController.java에 @DeleteMapping 어노테이션으로 삭제 메소드 추가

package com.cos.web01.controller;

import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.web01.domain.dto.UserSaveRequestDto;
import com.cos.web01.domain.user.UserRepository;
import com.cos.web01.service.UserSecurityService;

import lombok.AllArgsConstructor;

@RestController
@AllArgsConstructor
public class WebRestController {

	@Autowired
	private UserSecurityService userSecurityService;

	@PostMapping("/users/signup")
	public ResponseEntity<Map<String, Object>> saveUsers(@RequestBody UserSaveRequestDto dto) {

		userSecurityService.accountUser(dto);

		Map<String, Object> map = new HashMap<>();

		map.put("msg", "save");
		return new ResponseEntity<>(map, HttpStatus.OK);
	}

	@GetMapping("/user/userName")
	public ResponseEntity<Map<String, Object>> getUserName(Principal principal) {
		Map<String, Object> map = new HashMap<String, Object>();

		// UserSecurityService에서 laodUserByUsername 메소드에
		// 리턴 값으로 전달한 User 객체에서 설정한 Id 값을 가져옴
		map.put("userName", principal.getName());

		return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
	}

	@PostMapping("/user/changeName")
	public ResponseEntity<Map<String, Object>> changeUserName(@RequestBody Map<String, Object> map,
			Principal principal) {

		String changeName = map.get("changeName").toString();
		String userId = principal.getName();

		userSecurityService.changeUserName(userId, changeName);

		Map<String, Object> responseMap = new HashMap<String, Object>();

		responseMap.put("msg", "success");

		return new ResponseEntity<Map<String, Object>>(responseMap, HttpStatus.OK);

	}

	@DeleteMapping("/user")
	public ResponseEntity<Map<String, Object>> deleteUser(Principal principal) {
		String userId = principal.getName();

		userSecurityService.deleteUser(userId);

		Map<String, Object> responseMap = new HashMap<String, Object>();

		return new ResponseEntity<Map<String, Object>>(responseMap, HttpStatus.OK);
	}

}

 

 

 

 

 

 

 

 

 

반응형

'스프링 부트 > 웹MVC' 카테고리의 다른 글

글 등록, 글 목록 불러오기, 해당 글의 상세 페이지로 이동하기  (0) 2021.12.09
스프링 시큐리티 적용  (0) 2021.12.03
thymeleaf 적용  (0) 2021.12.01
User 생성 작업  (0) 2021.12.01
셋업  (0) 2021.12.01