Image를 업로드 하고 나서 해당 'User page'가 응답된다.
이때, '업로드한 이미지'가 view에 뿌려져야 한다.
그럴려면 profile 페이지를 응답할 때, 해당 데이터들을 담아서 같이 뿌려주어야 한다
'프로필페이지'를 응답해주는 UserController로 돌아가서 데이터를 Model에 담아주자.
package com.cos.photogramstart.web;
import com.cos.photogramstart.config.PrincipalDetails;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
@GetMapping("/user/{id}") //해당 페이지의 주인아이디
public String profile(@PathVariable int id, Model model) { //번호를 바꿔도 들어갈 수 있게
User userEntity = userService.회원프로필(id);
model.addAttribute("user", userEntity);
return "user/profile";
}
@GetMapping("/user/{id}/update")
public String update(@PathVariable int id,
@AuthenticationPrincipal PrincipalDetails principalDetails,
Model model) {
//1. 추천
System.out.println("세션 정보 :" + principalDetails.getUser());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//2. 극혐
PrincipalDetails mPrincipalDetails = (PrincipalDetails)auth.getPrincipal();
System.out.println("직접 찾은 세션 정보 : " + mPrincipalDetails.getUser());
//model.addAttribute("principal", principalDetails.getUser());
return "user/update";
}
}
//Principal은 접근주체(인증주체)로써 인증된 유저의 '오브젝트'로 흔히 사용된다
그리고 UserService에서 '프로필페이지'에서 데이터를 뿌려줄 로직을 만들어주자.
프로필에서 받아줄 데이터는 우선 userId인데, 이 userId는 해당 페이지 주인의 ID이다.
왜냐하면, 로그인한 유저의 id(principal)라면 어떤 페이지를 가더라도 로그인한 유저가 올린 이미지만 나올것이기 때문이다.
그리고 해당 User가 올린 모든 Image 데이터들을 받아와야 한다.
package com.cos.photogramstart.service;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User 회원프로필(int userId) { //해당 페이지 주인의 ID를 받아준다
//SELECT * FROM image WHERE userId =:userId;
User userEntity = userRepository.findById(userId).orElseThrow(()-> {
throw new CustomException("해당 프로필 페이지는 없는 페이지입니다");
});
return userEntity;
}
@Transactional
public User 회원수정(int id, User user) {
//1.영속화
//User userEntity = userRepository.findById(id).get(); //1.무조건 찾았다 걱정마 get() 2. 못찾았어 Excetption 발동시킬게
// orElseThrow()
User userEntity = userRepository.findById(id).orElseThrow(() -> {
return new CustomValidationApiException("찾을 수 없는 아이디 입니다."); //생성자 새로 추가
});
//2.영속화된 오브젝트를 수정 - DirtyChecking (업데이트 완료)
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword); //password 암호화를 해주어야 한다
userEntity.setName(user.getName());
userEntity.setPassword(user.getPassword());
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
return userEntity; //DirtyChecking이 일어나서 업데이트가 완료경
}
}
//영속화된 Object를 수정하면 자동으로 DB에 반영이 된다
-> 우선 해당 User에 대한 정보를 받아와야 하는데 이렇게 받으면 에러가 뜬다
UserId를 못찾을 수도 있기 때문이다
따라서 이전시간에 배웠던 Optional<T>를 이용하여 orElseThrow()로 Excepion을 걸어주자.
그러기 위해선 우선 이 역할을 해줄 Exception을 만들어 주어야 한다.
여태까지 우리가 만든 Exception은
1. CustomApiException 2. CustomValidationApiException 3. CustomValidationException 인데, Api에 응답할 것도 아니고 Validation Check를 위한것도 아닌 Exception이기 때문이다. 이 Exception은 단순히 message만 응답할 것이기 때문에 간단하게 만들어주자.
package com.cos.photogramstart.handler.ex;
public class CustomException extends RuntimeException {
//객체를 구분할때 쓰는 시리얼번호 - 우리한테 중요하지 않다. JVM한테 중요
private static final long serialVersionUID = 1L;
public CustomException(String message) {
super(message);
}
}
당연하게도 handler가 타야 내가 Custom한 Exception이 발동되기 때문에 handler도 만들어주자.
package com.cos.photogramstart.handler;
import com.cos.photogramstart.handler.ex.CustomApiException;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.util.Script;
import com.cos.photogramstart.web.dto.CMRespDTO;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
@RestController
@ControllerAdvice //모든 Exception을 다 낚아챈다
public class ControllerExceptionHandler {
//JavaScript로 응답하는 Handler
@ExceptionHandler(CustomValidationException.class)
public String validationException(CustomValidationException e) {
//CMRespDto, Script 비교
//1. 클라이언트에게 응답할때는 Script 좋음
//2. Ajax통신을 하거나 Android 통신을 하게되면 CMRespDto가 좋다
//즉, 개발자를 위한 응답에는 CMRespDto, 클라이언트를 위해서는 Script가 좋다
if (e.getErrorMap() == null) {
return Script.back(e.getMessage());
} else {
return Script.back(e.getMessage());
}
//자바스크립트로 짜는 부분까지 2가지 방향으로 갔을때 사용자에게 어떤것이 좋을지 판단해보라고 나눈것
}
//JavaScript로 응답하는 Handler
@ExceptionHandler(CustomException.class)
public String exception(CustomException e) {
return Script.back(e.getMessage());
}
//CMRespDto 오브젝트를 응답하는 핸들러
@ExceptionHandler(CustomValidationApiException.class)
public ResponseEntity<?> validationApiException(CustomValidationApiException e) {
return new ResponseEntity<>(
new CMRespDTO<>(-1, e.getMessage(), e.getErrorMap()),
HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<?> apiException(CustomApiException e) {
return new ResponseEntity<>(
new CMRespDTO<>(-1, e.getMessage(),null),
HttpStatus.BAD_REQUEST);
}
// <?>를 사용하면 제네릭 타입이 결정이 된다
// BAD_REQUEST는 400번대 오류이다 -> 너가 요청을 잘못했다
}
-> CustomException과 Handler를 설정해주었으니, Service로 돌아가서 throw로 던져주자.
package com.cos.photogramstart.service;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User 회원프로필(int userId) { //해당 페이지 주인의 ID를 받아준다
//SELECT * FROM image WHERE userId =:userId;
User userEntity = userRepository.findById(userId).orElseThrow(()-> {
throw new CustomException("해당 프로필 페이지는 없는 페이지입니다");
});
return userEntity;
}
@Transactional
public User 회원수정(int id, User user) {
//1.영속화
//User userEntity = userRepository.findById(id).get(); //1.무조건 찾았다 걱정마 get() 2. 못찾았어 Excetption 발동시킬게
// orElseThrow()
User userEntity = userRepository.findById(id).orElseThrow(() -> {
return new CustomValidationApiException("찾을 수 없는 아이디 입니다."); //생성자 새로 추가
});
//2.영속화된 오브젝트를 수정 - DirtyChecking (업데이트 완료)
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword); //password 암호화를 해주어야 한다
userEntity.setName(user.getName());
userEntity.setPassword(user.getPassword());
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
return userEntity; //DirtyChecking이 일어나서 업데이트가 완료경
}
}
//영속화된 Object를 수정하면 자동으로 DB에 반영이 된다
-> 이 '회원 프로필' 로직이 잘 동작하는지 테스트해보자
테스트 하기위해 UserController에서 DI를 하여 메서드를 호출해보겠다.
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
@GetMapping("/user/{id}") //해당 페이지의 주인아이디
public String profile(@PathVariable int id, Model model) { //번호를 바꿔도 들어갈 수 있게
User userEntity = userService.회원프로필(id);
model.addAttribute("user", userEntity);
return "user/profile";
}
-> User 오브젝트를 받았으니, images라는 이름은 옳지 않다. user로 바꾸어준다.
'프로필 페이지'를 응답할때, 해당 user의 정보를 담아서 왔다.
그런데 이 '프로필 페이지'에서 뿌려주어야 할 정보는 'User의 정보'만이 아니다. 당연히 'image 정보'도 함께 들고 와주어야 한다. 그 외 '구독자'는 몇명인지에 대한 데이터도 함께 담아야 한다.
이러한 데이터를 담으려면 User 오브젝트에 함께 담아 가야한다.
우리는 User정보를 DB에서 SELECT할 때, image 정보도 같이 호출되도록 해줄것이다.
그러기 위해서는 '양방향 매핑'을 해주어야 한다.
우선 User 클래스로 가서 Image 오브젝트를 List로 받아주자.
package com.cos.photogramstart.domain.user;
//JPA - Java Persistence API (자바로 데이터를 영구적으로 저장(DB)할 수 있는 API 제공)
import com.cos.photogramstart.domain.Image.Image;
import lombok.*;
import org.springframework.stereotype.Controller;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity // DB에 테이블을 생성
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //번호 증가 전략이 데이터베이스를 따라간다.
private int id; //Primary 키
@Column(length = 20, unique = true) //회원가입 아이디 중복방지
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
private String website; //웹 사이트
private String bio; //자기 소개
@Column(nullable = false)
private String email;
private String phone;
private String gender;
private String profileImageUrl; //사진
private String role; //권한
// 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지마
// User를 Select할때 해당 User id로 등록된 image들을 전부 가져와
// Lazy = User를 Select할때 해당 User id로 등록된 image들을 가져오지마 - 대신 getImages() 함수의 image들이 호출될때 가져와
// Eager = User를 Select할때 해당 User id로 등록된 image들을 전부 Join해서 가져와
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Image> images; // 양방향 매핑 - 프로필페이지를 응답할때, 같이 담아올 image의 정보
private LocalDateTime createDate;
@PrePersist // DB에 INSERT 되기 직전에 실행
public void createDate() {
this.createDate = LocalDateTime.now();
}
}
-> 이렇게 되면 DB 입장에서는 User 오브젝트안에 Image 오브젝트도 담기게 된다. 그런데 RDB에서는 '오브젝트' 안에 '오브젝트'를 담을 수 없다. 때문에, 연관관계를 설정해주면서 '오브젝트'를 만들지 말라는것을 알려주어야 한다.
Lazy -> User를 SELECT할 때, 해당 User id로 등록된 image들을 가져오지마. 대신 getImages() 함수의 image들이 호출될 때 가져와.
1명의 유저는 여러개의 Image를 생성할 수 있기 때문에 @OneToMany가 걸리게 되고,
public class Image { //N:1
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String caption; //해당 Image를 설명하는 영역
private String postImageUrl; //Image을 전송받아서 그 사진을 서버의 특정 폴더에 저장 - DB에 그 저장된 경로를 INSERT
@JoinColumn(name = "userId") // 1:1
@ManyToOne
private User user; //1명의 User는 여러개의 Image를 만들어낼수 있다. - Image를 누가 올렸는지 알기위해 받은 USerObject
-> 양방향 연관관계에서 '연관관계의 주인'은 Image테이블에 있는 user이다.
-> Image 오브젝트에 담겨있는 user오브젝트의 이름을 mappedBy에 명시해주어, '양방향 매핑'에서 '연관관계의 주인'이 아니라는 것을 알려주어야 한다.
즉, mappedBy에 적힌 Image오브젝트에 있는 user가 연관관계의 주인이 된다. 이렇게 해주면 DB는 User테이블을 형성할때, images 테이블을 생성하지 않게 된다. 따라서 User 정보를 SELECT할때, 해당 userId로 등록된 image 파일들을 모두 가져올 수 있게 된다.
이때 userId는 User 테이블이 가지고 있는 id이다.
// 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지마
// User를 Select할때 해당 User id로 등록된 image들을 전부 가져와
// Lazy = User를 Select할때 해당 User id로 등록된 image들을 가져오지마 - 대신 getImages() 함수의 image들이 호출될때 가져와
// Eager = User를 Select할때 해당 User id로 등록된 image들을 전부 Join해서 가져와
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Image> images; // 양방향 매핑 - 프로필페이지를 응답할때, 같이 담아올 image의 정보
private LocalDateTime createDate;
1. Image오브젝트와의 '양방향 매핑'
2. 연관관계의 주인은 User가 아니다
3. 따라서, User 테이블을 생성할 때, images 테이블을 만들지 않겠다.
4. User오브젝트를 Select할 때, 해당 userId로 Upload된 모든 images들을 같이 가져와야 한다.
5. LAZY : User 오브젝트를 Select할 때, 해당 userId로 등록된 모든 image들을 가져오지 않겠다.
6. EAGER : User 오브젝트를 Select할 때, 해당 userId로 등록된 모든 image들을 가져오겠다.
그런데 @OneToMany 관계일때, Fetch 타입을 Lazy로 설정해 주어야 한다.
위에 주석으로 적은 설명을 보면 Lazy가 아닌 Eager로 설정해주어야 하는것 아닌가?
아니다.
Lazy는 또 다른 기능이 있는데, 바로 getImages() 메서드가 호출될 때, image들을 가져올 수 있다는 것이다.
따라서, User를 Select할 때, 무조건 image들을 가져오는 것이 아니라 필요할 때 getImages() 메서드에서 image들이 호출될 때만 가져올 수 있게 하는 것이다.
이게 왜 필요한지에 대해 이해하려면 '양방향 매핑'에 대해서 이해를 해야 한다
우리가 User를 Select할 때, User가 가지고 있는 Image도 호출된다.
그런데 Image 안에도 User가 있기 때문에, User도 호출된다.
User가 호출되었으니 또다시 Image가 호출되고, 또 User가 호출되는데 이러한 상황이 '무한'으로 실행되면서 과부하가 걸려서 Stack Overflow 에러가 발생하게 된다.
이 상황을 테스트 하기 위해 UserService 회원프로필 메서드에서 getImages() 메서드를 실행시켜보자.
getImages() 메서드를 호출할 때, 객체로 호출해주어야 하기 때문에, get(0)까지 넣어주자.
-> FetchType이 LAZY이면 User 오브젝트가 호출될 때는 User의 정보만 가져왔는데 : SELECT user
'구분선'을 기준으로 getImages() 메서드가 실행되었을때, image 정보를 가져왔다. : SELECT images
-> 이렇게 EAGER로 바꾸어서 해보면 getImages() 메서드가 호출되지 않은 상태에서도 SELECT user -> SELECT images -> SELECT user 가 반복되고, User 오브젝트가 SELECT 될 때 image 정보들도 가져오게 된다
'메타코딩 SNS프로젝트' 카테고리의 다른 글
32. 프로필 페이지 - image업로드 에러 해결 및 Open In View 개념 잡기 (0) | 2022.06.27 |
---|---|
31. 프로필 페이지 - Image 프로필페이지에 View 렌더링하기 (0) | 2022.06.24 |
29. 프로필 페이지 - Image upload 로직 유효성 검사하기 (0) | 2022.06.21 |
28. 프로필 페이지 - Image 파일경로를 DB에 INSERT하기 (0) | 2022.06.21 |
27. 프로필 페이지 - Image 업로드 경로를 프로젝트 외부에 두는 이유 (0) | 2022.06.20 |