본문 바로가기
Spring

Ajax 사용해 put 요청 / Validation Check / 영속화, 영속성 컨텍스트

by 쌩욱 2021. 8. 19.

Ajax를 사용한 put 요청

Ajax란?

Ajax란 Asynchronous JavaScript and XML의 약자입니다.

Ajax는 빠르게 동작하는 동적인 웹 페이지를 만들기 위한 개발 기법의 하나입니다.

 

Ajax는 웹 페이지 전체를 다시 로딩하지 않고도, 웹 페이지의 일부분만을 갱신할 수 있습니다.

즉 Ajax를 이용하면 백그라운드 영역에서 서버와 통신하여, 그 결과를 웹 페이지의 일부분에만 표시할 수 있습니다.

 

이때 서버와는 다음과 같은 다양한 형태의 데이터를 주고받을 수 있습니다.

 - JSON

 - XML

 - HTML

 - 텍스트 파일 등

 

html form 태그는 put 요청을 할 수 없습니다.

그래서 현재 회원 정보를 수정하기 위한 form태그에 제출 버튼 클릭 시 Javascript 함수를 실행하도록 하겠습니다.

<form id="profileUpdate" onsubmit="update(${principal.user.id}, event)">
// (1) 회원정보 수정
function update(userId, event) {
	event.preventDefault(); //폼태그 액션 막기?
	
	let data = $("#profileUpdate").serialize(); 
    //해당 아이디의 폼태그를 찾아서 모든 정보를 시리얼라이즈해줌, 
	
	$.ajax({ //js object가 들어가야함
		type:"put",
		url:`/api/user/${userId}`,
		data: data,
		contentType: "application/x-www-form-urlencoded; charset=utf-8", //데이터의 타입, 키-벨류 타입임
		dataType: "json" /*반환받을 타입?*/
		
	}).done(res=>{  
    /*javascript object로 파싱해줌, res는 javascript object가 됨, 위 요청의 응답을 담음*/
	//HttpStatus 상태코드 200번대
		console.log("update 성공", res);
		location.href=`/user/${userId}`; //성공하면 이 페이지로 돌아가라
		
	}).fail(error=>{
		// HttpStatus 상태코드 200번대 아닐 때
		if(error.data == null){
			alert(JSON.stringify(error.responseJSON.message)); 
            //JSON.stringify 함수 사용하면 js 오브젝트를 json 문자열로 변환해줌
		}else{
			alert(JSON.stringify(error.responseJSON.data)); 
		}
		
		console.log("update 실패", error);
	});
	
	
}

 

$("#profileUpdate").serialize(); -> 이 코드로 data 변수에 저희가 만든 form 태그의 모든 정보를 키-벨류 형식으로 시리얼라이즈 할 수 있습니다.

$.ajax() -> javascript object를 만들어 요청을 보냅니다.

          -> type : 요청 method

          -> url : 요청할 url

          -> data : 보낼 데이터

          -> contentType : 보내는 데이터의 타입

          -> dataType : 응답받을 데이터의 타입

 

.done() -> 해당 요청이 성공적으로 완료되면 실행합니다. (HttpStatus 상태코드 200번대)

          -> ajax 요청의 응답을 javascript object로 만들어줌

          -> location.href : 해당 위치로 이동

 

.fail() -> 해당 요청이 실패하면 실행합니다. (HttpStatus 상태코드 200번대가 아닐 때)

 

이제 해당 요청을 받을 컨트롤러와 데이터를 옮길 DTO, 예외처리, 서비스를 구현합니다. 


Validation Check

1) 프론트단

input tag에 required="required" 붙이기

form tag에 action을 적을 수 없기 때문에 디폴트액션으로 summit 버튼을 누르면 자기 자신으로 되돌아옴

-> update.js에 event를 넘기고 event.preventDefault()로 막음

2) 유효성 검사

회원수정 페이지의 name과 password는 필수로 입력받도록 구현하기 위해 Validation의 @NotBlank를 사용합니다.

컨트롤러에서 검증할 dto에 @Valid 어노테이션을 붙이고 바로 다음에 검증 오류를 담을 BindingResult를 넣어줍니다. (중요!)

BindingResult 변수에 에러가 담겨있으면 예외을 던지게 됩니다.

이 때 예외처리 핸들러에서는 HttpStatus도 같이 반환해야 하기 때문에 ResponseEntity 클래스 사용

@RequiredArgsConstructor
@RestController
public class UserApiController {
	
	private final UserService userService;
	
	@PutMapping("/api/user/{id}")
	public CMRespDto<?> update(
			@PathVariable int id, 
			@Valid UserUpdateDto userUpdateDto,
			BindingResult bindingResult, //바인딩 리설트는 @Valid가 적혀있는 다음 파라미터에 적어야함
			@AuthenticationPrincipal PrincipalDetails principalDetails) {
		
		
		//@Valid에서 오류가 발생하면 BindingResult에 오류를 다 모아줌 -> getFieldErrors 콜렉션에 다 모아줌
		if(bindingResult.hasErrors()) {
			Map<String, String> errorMap = new HashMap<>();
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(), error.getDefaultMessage());
			}
			throw new CustomValidationApiException("유효성 검사 실패함", errorMap);
			//유효성 검사 실패 -> BindingResult -> errorMap -> throw CustomValidationApiException -> ControllerExceptionHandler -> validationException함수 -> CMRespDto 리턴
		}
		else {
			User userEntity = userService.회원수정(id, userUpdateDto.toEntity());
			principalDetails.setUser(userEntity);
            //세션정보까지 수정 , 이거 안하면 디비만 변경되고 세션은 변경 안됨
			return new CMRespDto<>(1, "회원수정완료", userEntity);
			
		}
	}
}

 

@Data
public class UserUpdateDto {
	@NotBlank
	private String name; //필수
	@NotBlank
	private String password; //필수
	
	private String website;
	private String bio;
	private String phone;
	private String gender;

	public User toEntity() {
		return User.builder()
				.name(name)
				.password(password)
				.website(website)
				.bio(bio)
				.phone(phone)
				.gender(gender)
				.build();
	}
}

 

@RestController // 데이터 응답을 위해
@ControllerAdvice  //모든 exception을 낚아채 처리함
public class ControllerExceptionHandler {

	...

	@ExceptionHandler(CustomValidationApiException.class) //Ajax통신 - 데이터 응답
	public  ResponseEntity<?> validationApiException(CustomValidationApiException e) { 
		//update.js에서 fail 부분을 실행하기 위해 http 상태코드를 같이 넘겨줌
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()), HttpStatus.BAD_REQUEST ); 
	}

}

 

3) DB validation check (뒷단)

ex) 10번 유저 회원정보 수정해 -> DB에 10번 유저가 없을 시 -> 예외처리

 

유저 서비스 클래스의 회원수정 함수를 살펴보면

레파지토리에서 요청받은 아이디의 회원을 찾고 없을 시 예외를 발생시킵니다.

.orElseThrow가 반환값이 null이면 예외를 던지는 함수입니다.

예외가 발생하면 따로 구현해놓은 핸들러가 동작해 처리하게 됩니다.

 

- 비밀번호는 암호화 시켜야하기 때문에 BCryptPasswordEncoder 클래스 함수를 사용합니다.

@RequiredArgsConstructor
@Service
public class UserService {
	
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Transactional
	public User 회원수정(int id, User user) {
		//1. 영속화
		
		User userEntity = userRepository.findById(id).orElseThrow( () -> { // 인자가 잘못된 경우
				return new CustomValidationApiException("찾을 수 없는 id입니다.");
		}); //리턴값이 optional이라  
		// 영속성 컨텍스트 : 스프링부트 서버와 데이터베이스 사이에 이 객체가 영속화되어 들어옴
		// 영속화된 오브젝트는 변경하면 바로 데이터베이스에 적용이 된다!
		

		//2. 영속화된 오브젝트를 수정
		userEntity.setName(user.getName());
		
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword); //비밀번호 암호화
		userEntity.setPassword(encPassword);
		
		userEntity.setBio(user.getBio());
		userEntity.setWebsite(user.getWebsite());
		userEntity.setPhone(user.getPhone());
		userEntity.setGender(user.getGender());
		
		return userEntity;
	} //더티 체킹이 일어나서 업데이트가 완료됨.

}

영속화, 영속성 컨텍스트

데이터베이스를 접근해 얻은 데이터는 스프링부트 서버와 데이터베이스 사이에 있는 영속성 컨텍스트라는 공간에 저장되고 이를 영속화되었다고 합니다.

 

영속화된 오브젝트는 변경하면 바로 데이터베이스에 반영이 됩니다.

따라서 회원정보를 수정하고 싶으면 내가 수정하고 싶은 회원정보를 영속화시키고 영속화된 오브젝트를 수정하면 자동으로 데이터베이스에 반영이 됩니다.

 

수정된 오브젝트를 리턴하면 더티 체킹이 일어나서 업데이트가 완료됩니다!

 

 

전체 코드 : https://github.com/ysu96/photogram