스프링컨테이너가 들고 있는 필터를 '인터셉터'라고 부른다. 인터셉터 또한 권한을 체크한다.
어노테이션 & 리플렉션
어노테이션 어노테이션은 사전뜻대로는 주석이지만 스프링에서 어노테이션은 컴파일러가 무시하지 않고 체킹을 한다. 스프링은 어노테이션으로 객체를 생성하기도 한다. ex) @Component: 해당 어노테이션이 붙어있는 클래스를 메모리에 로딩해 특정 클래스에 해당 어노테이션이 붙어있으면 스프링은 그 클래스를 스캔해서 자기가 들고있는 메모리공간(heap영역, 컨테이너)에 로드한다. => IOC @Autowired: 스프링 컨테이너에 있는 클래스를 사용하고 싶을 때사용
리플렉션 - 스프링이 B클래스를 스캔할 때 B클래스 내부에 어떤 것들이 있는지 분석하는 기법을 리플랙션이라 한다. - 리플랙션은 어떤 메서드, 필드, 어노테이션이 있는지를 체킹한다. 또한 있는지를 체킹하는것을 넘어서 어떤 동작을 할지를 설정할수도 있다. - 만약 @Autowired 를 발견하면 스프링 컨테이너를 쭉 읽어어서 A랑 동일한 타입의 객체가 있는지를 확인한다. 만약 A가 없다면 null을 반환, 있다면 A를 DI한다. - 리플렉션은 런타임때 동작(분석)한다.
- 주도권이 스프링에게 있다. - 클래스: 설계도 - Object: 실체화가 가능한 것 ex) 롤 챔프 누누(Class)는 게임할때 튀어나오니까 실체화가 가능하다 => Object다. 하지만 캐릭터(abstract Class)는 추상적인 의미다. (애쉬가 될수도 있고, 케이틀린이 될수도 있다.) 캐릭터는 추상적인 것이기때문에 실체화가 불가능하다, 게임에서 등장 불가능 => Object가 아니다. - Instance: 실체화가 된것 (누누가 전장에서 돌아다니면, 게임속에서 존재하는것)
주도권이 스프링에게 있다는것은 개발자가 직접 Object를 new해서 heap 공간에 올리게 되면 해당 레퍼런스의 주소를 각각의 메소드가 관리하게된다. 예를 들어보자
Public void make(){
User user = new User();
}
예컨데 user의 주소는 make() 메소드가 실행되는 순간에 메모리에 뜬다. 그래서 다른 메소드에서 user를 사용하고 싶다면 아래와 같이 또 새로 new해야할 텐데 그럼 heap메모리에 또다른 주소가 생기게 된다. 또한 make메소드에서의 user와 make2 메소드에서의user는 다른 존재다.(주소가 다르기 때문)
Public void make2(){
User user = new User();
}
이렇게 되면 다른 매소드에서 같은 user를 공유를 하기 힘들어진다.
스프링는 수많은 Object들을 스캔해서 그 객체들을 heap 메모리 공간에 올려준다. 이것을 IOC라고한다.
DI란
스프링이 IOC를 통해 Object들을 메모리에 띄웠기 때문에, 스프링이 관리하기 때문에 스프링이 관리하는 객체들을 개발자가 원하는 모든곳(메소드)에서 가져와 쓸 수 있는것을 말한다. make 메소드의 user나, make2 메소드에서의 user는 같은 user다. 한마디로 싱글톤으로 관리된다. => 스프링이 오브젝트를 스캔하면 user가 딱 한번 힙메모리에 뜨고 그것을 다양한 곳에서 공유해서 쓴다.
한줄 요약
Ioc: 제어의역전, 스프링이 싱글톤으로 빈을 관리하는것 Di: 인스턴스 생성시 IOC 컨터에너에서 싱글톤으로 관리하는 빈들을 변수에 의존성 주입
ValidationAdvice - 유효성 검사를 공통으로 처리하는 곳 - @Aspect가 있어야 AOP를 처리할 수 있는 핸들러가 된다. - api쪽 advice와 그냥 advice를 만든다.
1. @Before(): 특정 함수 실행직전에 실행 @After(): 특정 함수 실행 직후에 실행 @Around(): 특정 함수 실행직전부터 직후에 관여
2. - ....web.api의 모든 컨트롤러의 모든 메소드의 모든 매개변수 가 실행될때 작동된다. - proceedingJoinPoint : api의 Controller중에 특정 함수가 실행됐으면 proceedingJoinPoint가 그 함수의 매개변수 뿐만 아니라 내부의 모든 정보에 접근할 수 있다. - 예를들어 profile함수가 실행되는 순간 그 함수의 모든 정보를 proceedingJoinPoint가 담고 profile함수 실행 이전에 apiAdvice가 먼저 실행된다. - 그 이후 return proceedingJoinPoint.proceed(); 가 실행되며 profile함수가 실행된다.
3. proceedingJoinPoint.getArgs() 를 통해 함수의 매개변수에 접근해서 리스트에 담는다. 만약에 매개변수 중에서 BindingResult라는 타입이 있으면 -> 해당 arg를 BindingResult형으로 다운 캐스팅 -> 만약 bindingResult에 에러가 있다면 -> 유효성 검사 실행
@Component
@Aspect
public class ValidationAdvice {
@Around("execution(* com.jghan.instaclone.web.api.*Controller.*(..))")
public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// System.out.println("==================web api 컨트롤러===================");
Object[] args = proceedingJoinPoint.getArgs();
for(Object arg: args){
if(arg instanceof BindingResult){
BindingResult bindingResult = (BindingResult) arg;
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
}
throw new CustomValidationApiException("유효성 검사 실패함", errorMap);
}
}
}
return proceedingJoinPoint.proceed(); //prfofile함수가 실행됨
}
@Around("execution(* com.jghan.instaclone.web.*Controller.*(..))")
public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();
for(Object arg: args){
if(arg instanceof BindingResult){
BindingResult bindingResult = (BindingResult) arg;
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
}
throw new CustomValidationException("유효성 검사 실패함", errorMap);
}
}
}
return proceedingJoinPoint.proceed();
}
}
- Comment 와 User는 n:1관계다 (1명의 유저는 여러개의 댓글을 쓸 수 있으므로 - 또한 하나의 이미지에 여러 댓글이 달릴 수 있으므로 Comment 와 Image는 n:1이다. - ManyToOne에서는 EAGER 가 디폴트 =>왜냐하면 1개 댓글에는 user나 image 정보가 1개뿐이라서 join해서 가져와도 db에 무리가 없다.
@RequiredArgsConstructor
@Service
public class CommentService {
private final CommentRepository commentRepository;
private final UserRepository userRepository;
@Transactional
public Comment commentSave(String content, int imageId, int userId){
Image image = new Image();
image.setId(imageId);
User userEntity = userRepository.findById(userId).orElseThrow(()->{
return new CustomApiException("유저 아이디를 찾을 수 없습니다.");
});
Comment comment = new Comment();
comment.setContent(content);
comment.setImage(image);
comment.setUser(userEntity);
return commentRepository.save(comment);
}
CommentApiController.class - CommentDto commentDto만 쓰는건 x-www-urlencoded 로 받는거기 때문에 json 데이터 받으려면 @RequestBody 붙여줘야한다.
@RequiredArgsConstructor
@RestController
public class CommentApiController {
private final CommentService commentService;
@PostMapping("/api/comment")
public ResponseEntity<?> commentSave(@RequestBody CommentDto commentDto,
@AuthenticationPrincipal PrincipalDetails principalDetails){
Comment comment = commentService.commentSave(commentDto.getContent(), commentDto.getImageId(), principalDetails.getUser().getId());
return new ResponseEntity<>(new CMRespDto<>(1, "댓글쓰기성공", comment), HttpStatus.CREATED);
}
}
story.js - 댓글을 달고자하는 이미지의 id를 전달해줘서 클릭시 addComment() 함수가 실행되도록 한다. - 댓글 작성하고 DB에 댓글이 저장되고, 그 내용이 storyCommentList에 append되야한다
function addComment(imageId) {
let commentInput = $(`#storyCommentInput-${imageId}`);
let commentList = $(`#storyCommentList-${imageId}`);
let data = {
imageId: imageId,
content: commentInput.val()
};
if (data.content === "") {
alert("댓글을 작성해주세요!");
return;
}
$.ajax({
type:"post",
url:"/api/comment",
data:JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(res=>{
console.log("성공", res);
let comment = res.data;
console.log(comment);
let content = `
<div class="sl__item__contents__comment" id="storyCommentItem-${comment.id}">
<p>
<b>${comment.user.username} :</b>
${comment.content}
</p>
<button><i class="fas fa-times"></i></button>
</div>
`;
commentList.prepend(content);
}).fail(error=>{
console.log("오류", error);
});
commentInput.val(""); //인풋필드를 깨끗하게 비워준다.
}
CommentDto
- addComment()가 전달하는 값이 imageId와 content 뿐이기에 이 데이터를 받는 dto가 필요하다.
@Data
public class CommentDto {
private String content;
private int imageId;
}
ImageApiController의 imageStory 함수가 등록한 댓글을 뿌려줘야 하는데,
Image.class 현재 Image 객체에는 댓글관련 정보를 들고 있지 않기 때문에 관련 컬럼을 만들어주고, 양방향 매핑을 해줘야한다. - 한 이미지에 여러 comments가 붙기때문에 OneToMany 어노테이션을 붙여준다. - 이미지를 가져올때 Comment의 이미지 정보를 가져올 필요가 없으므로 JsonIgnore해준다.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
...
//댓글
@OrderBy("id DESC")
@JsonIgnoreProperties({"image"})
@OneToMany(mappedBy = "image")
private List<Comment>comments;
...
}
profile.js - 페이지주인 아이디와 로그인 유저 아이드를 받는다. - 페이지 유저 아이디랑 로그인 유저가 동일하다면 자동으로 userProfileImageInput 부분을 클릭시킨다. 1. 서버에 이미지 전송 - userProfileImageForm의 0번째 요소를 찾아서 profileImageForm변수에 넣어준다. - ajax로 이 데이터를 전송하려면 FormData로 감싸야 한다. =>profileImageForm은 폼테그 그 자체고그것을FormData에 넣으면 값들만 담긴다고 생각하면 된다.
function profileImageUpload(pageUserId, principalId) {
if(pageUserId != principalId){
alert("프로필 사진을 수정할 수 없는 유저입니다.");
return;
}
$("#userProfileImageInput").click();
$("#userProfileImageInput").on("change", (e) => {
let f = e.target.files[0];
if (!f.type.match("image.*")) {
alert("이미지를 등록해야 합니다.");
return;
}
// 서버에 이미지를 전송
let profileImageForm = $("#userProfileImageForm")[0];
console.log(profileImageForm);
// FormData 객체를 이용하면 form 태그의 필드와 그 값을 나타내는 일련의 key/value 쌍을 담을 수 있다.
let formData = new FormData(profileImageForm);
$.ajax({
type: "put",
url: `/api/user/${principalId}/profileImageUrl`,
data: formData,
contentType: false, // 필수 : x-www-form-urlencoded로 파싱되는 것을 방지
processData: false, // 필수: contentType을 false로 줬을 때 QueryString 자동 설정됨. 해제
enctype: "multipart/form-data",
dataType: "json"
}).done(res=>{
// 사진 전송 성공시 이미지 변경
let reader = new FileReader();
reader.onload = (e) => {
$("#userProfileImage").attr("src", e.target.result);
}
reader.readAsDataURL(f); // 이 코드 실행시 reader.onload 실행됨.
}).fail(error=>{
console.log("오류", error);
});
});
}
UserApiController - 사진데이터도 받기 때문에 MultipartFile도 받는데, html에서의 input 아이디와 똑같은 이름으로 받아야한다. - userService에 principalId와 파일정보를 넘긴다. - 회원사진이 변경되면 세션값이 변경되야하기 때문에 변경된 userEntity를 받아서 세션변경을 해준다.
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
private final FollowService followService;
@PutMapping("/api/user/{principalId}/profileImageUrl")
public ResponseEntity<?> profileImageUrlUpdate(@PathVariable int principalId, MultipartFile profileImageFile,
@AuthenticationPrincipal PrincipalDetails principalDetails){
User userEntity = userService.profileImageUrlUpdate(principalId, profileImageFile);
principalDetails.setUser(userEntity); // 세션 변경
return new ResponseEntity<>(new CMRespDto<>(1, "프로필사진변경 성공", null), HttpStatus.OK);
}
..
}
UserService
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final FollowRepositoy followRepositoy;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Value("${file.path}") //application.properties에서 가져옴
private String uploadFolder;
@Transactional
public User profileImageUrlUpdate(int principalId, MultipartFile profileImageFile){
UUID uuid = UUID.randomUUID(); // uuid
String imageFileName = uuid+"_"+profileImageFile.getOriginalFilename(); // 1.jpg
System.out.println("이미지 파일이름 : "+imageFileName);
Path imageFilePath = Paths.get(uploadFolder+imageFileName);
try {
Files.write(imageFilePath, profileImageFile.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
User userEntity = userRepository.findById(principalId).orElseThrow(()->{
// throw -> return 으로 변경
return new CustomApiException("유저를 찾을 수 없습니다.");
});
userEntity.setProfileImageUrl(imageFileName);
return userEntity;
}