Light Blue Pointer
본문 바로가기
개발일지

2024-01-11, Today I Learned

by 개발바닥곰발바닥!!! 2024. 1. 11.

오늘 한 일

1. Custom annotation 이어서 해봄

2. 글 삭제하기 기능 구현(⛏️안 해도 될 고생을... 함⛏️)

3. Thymeleaf 환경설정

 

1. Custom annotation 이어서 해봄!

https://www.baeldung.com/spring-mvc-custom-validator

baeldung을 읽어보겠음!!

 

1. The New Annotation

@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

With the @Constraint annotation, we defined the class that is going to validate our field. The message() is the error message that is showed in the user interface. Finally, the additional code is mostly boilerplate code to conform to the Spring standards.

 

일단 이렇게 만들어봄

@Documented
@Constraint(validatedBy = CategoryValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Category {
    String message() default "Invalid category";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

 

2. Creating a Validator

public class ContactNumberValidator implements 
  ConstraintValidator<ContactNumberConstraint, String> {

    @Override
    public void initialize(ContactNumberConstraint contactNumber) {
    }

    @Override
    public boolean isValid(String contactField,
      ConstraintValidatorContext cxt) {
        return contactField != null && contactField.matches("[0-9]+")
          && (contactField.length() > 8) && (contactField.length() < 14);
    }

}

The validation class implements the ConstraintValidator interface, and must also implement the isValid method; it’s in this method that we defined our validation rules.

Naturally, we’re going with a simple validation rule here in order to show how the validator works.

ConstraintValidator defines the logic to validate a given constraint for a given object. Implementations must comply with the following restrictions:

  • the object must resolve to a non-parametrized type
  • generic parameters of the object must be unbounded wildcard types

 

일단 이렇게 해봄

public class CategoryValidator implements ConstraintValidator<Category,String> {

    @Override
    public void initialize(Category category) {
    }

    @Override
    public boolean isValid(String contactField,
        ConstraintValidatorContext cxt) {
        return contactField != null && contactField.matches("[0-9]+")
            && (contactField.length() > 8) && (contactField.length() < 14);
    }

}

나는 CategoryEnum 안에 있는 값인지 확인하고 싶은건데 저건 전화번호 확인하는 거 같음

isValid 로직을 바꿔야 하는데 약간 감이 안 잡혀서 일단 저 파일에 들어가서 isValid가 뭔지 살펴봄

public interface ConstraintValidator<A extends Annotation, T> {

/**
	 * Initializes the validator in preparation for
	 * {@link #isValid(Object, ConstraintValidatorContext)} calls.
	 * The constraint annotation for a given constraint declaration
	 * is passed.
	 * <p>
	 * This method is guaranteed to be called before any use of this instance for
	 * validation.
	 * <p>
	 * The default implementation is a no-op.
	 *
	 * @param constraintAnnotation annotation instance for a given constraint declaration
	 */
	default void initialize(A constraintAnnotation) {
	}

	/**
	 * Implements the validation logic.
	 * The state of {@code value} must not be altered.
	 * <p>
	 * This method can be accessed concurrently, thread-safety must be ensured
	 * by the implementation.
	 *
	 * @param value object to validate
	 * @param context context in which the constraint is evaluated
	 *
	 * @return {@code false} if {@code value} does not pass the constraint
	 */
	boolean isValid(T value, ConstraintValidatorContext context);

저거 읽고 감 잡아서 적용해봤다

public class CategoryValidator implements ConstraintValidator<Category, CategoryEnum> {

    @Override
    public void initialize(Category category) {
    }

    @Override
    public boolean isValid(CategoryEnum categoryEnum,
        ConstraintValidatorContext cxt) {
        return categoryEnum.equals(CategoryEnum.ALL)
            ||categoryEnum.equals(CategoryEnum.ASIAN)
            ||categoryEnum.equals(CategoryEnum.BURGER)
            ||categoryEnum.equals(CategoryEnum.CHICKEN)
            ||categoryEnum.equals(CategoryEnum.CHINESE)
            ||categoryEnum.equals(CategoryEnum.JAPANESE)
            ||categoryEnum.equals(CategoryEnum.KOREAN)
            ||categoryEnum.equals(CategoryEnum.LUNCHBOX)
            ||categoryEnum.equals(CategoryEnum.PIZZA)
            ||categoryEnum.equals(CategoryEnum.SNACK)
            ||categoryEnum.equals(CategoryEnum.WESTERN)
            ;
    }

}

 

3. Applying Validation Annotation

@ContactNumberConstraint
private String phone;
@PostMapping("/addValidatePhone")
    public String submitForm(@Valid ValidatedPhone validatedPhone,
      BindingResult result, Model m) {
        if(result.hasErrors()) {
            return "phoneHome";
        }
        m.addAttribute("message", "Successfully saved phone: "
          + validatedPhone.toString());
        return "phoneHome";
    }

우리 프로젝트는 BindingResult 없이 그냥 GlobalExceptionHandler 에서 MethodArgumentNotValidException 을 AOP로 처리를 해서 저 부분은 안 넣었다.

@ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<List<String>> handleMethodArgumentNotValidException(
        BindingResult bindingResult) {
        List<String> errors = bindingResult.getFieldErrors().stream()
            .map((FieldError fieldError) -> fieldError.getField() +" "+ fieldError.getDefaultMessage())
            .toList();
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "입력값이 잘못되었습니다", errors);
    }

적용해봄

import com.moayo.moayoeats.domain.post.exception.validator.Category;
import com.moayo.moayoeats.domain.post.entity.CategoryEnum;

public record PostCategoryRequest (
    @Category
    CategoryEnum category
){
}

테스트해봄

일단 존재하는 걸 테스트하면 어제와는 다르게 잘 됨

{
    "category":"ALL"
}
{
    "status": 200,
    "message": "글 카테고리별 조회에 성공했습니다.",
    "data": [
        {
            "id": 1,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:48"
        },
        {
            "id": 2,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 10000,
            "deadline": "2024-01-11T00:41:49"
        },
        {
            "id": 3,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:50"
        }
    ]
}

존재하지 않는 카테고리를 날리면

{
    "category":"PLS"
}
{
    "status": 400,
    "message": "에러가 발생했습니다",
    "data": "JSON parse error: Cannot deserialize value of type `com.moayo.moayoeats.domain.post.entity.CategoryEnum` from String \\"PLS\\": not one of the values accepted for Enum class: [ALL, WESTERN, ASIAN, BURGER, PIZZA, CHINESE, SNACK, LUNCHBOX, CHICKEN, JAPANESE, KOREAN]"
}

그냥 에러처리로 들어감 ㅜㅜ

GlobalExceptionHandler에서 에러 받는 부분 빼버리고 진행함

콘솔에는 HttpMessageNotReadableException 이 발생했다고 뜨고

모든 에러를 다 메시지로 돌려보내는게 생각보다 정보가 적고 더 불편한거 같아서 저 부분 걍 빼기로 함 ㅋㅋ

그리고 그냥 String으로 바꿔서 진행해봄

public record PostCategoryRequest (
    @Category
    String category
){
}
public class CategoryValidator implements ConstraintValidator<Category, String> {

    @Override
    public void initialize(Category category) {
    }

    @Override
    public boolean isValid(String categoryEnum,
        ConstraintValidatorContext cxt) {
        return categoryEnum.equals(CategoryEnum.ALL.toString())
            ||categoryEnum.equals(CategoryEnum.ASIAN.toString())
            ||categoryEnum.equals(CategoryEnum.BURGER.toString())
            ||categoryEnum.equals(CategoryEnum.CHICKEN.toString())
            ||categoryEnum.equals(CategoryEnum.CHINESE.toString())
            ||categoryEnum.equals(CategoryEnum.JAPANESE.toString())
            ||categoryEnum.equals(CategoryEnum.KOREAN.toString())
            ||categoryEnum.equals(CategoryEnum.LUNCHBOX.toString())
            ||categoryEnum.equals(CategoryEnum.PIZZA.toString())
            ||categoryEnum.equals(CategoryEnum.SNACK.toString())
            ||categoryEnum.equals(CategoryEnum.WESTERN.toString())
            ;
    }

}

toString()으로 하면 Enum값 이름대로 String화가 될지 value값대로 String화가 될지 모르겠지만 아무튼 저렇게 해봄

service랑 repository도 수정해봄

  			List<Post> posts;
        if(postCategoryReq.category().equals(CategoryEnum.ALL.toString())){
            posts = findAll();
        }else{
            posts = postRepository.findAllByCategoryEquals(postCategoryReq.category()).orElse(null);
        }
Optional<List<Post>> findAllByCategoryEquals(String category);
{
    "category":"PLS"
}
{
    "status": 400,
    "message": "입력값이 잘못되었습니다",
    "data": [
        "category Invalid category"
    ]
}

와~~~ 잘 됨!

GlobalExceptionHandler의 이 부분이 잘 동작한다

@ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<List<String>> handleMethodArgumentNotValidException(
        BindingResult bindingResult) {
        List<String> errors = bindingResult.getFieldErrors().stream()
            .map((FieldError fieldError) -> fieldError.getField() +" "+ fieldError.getDefaultMessage())
            .toList();
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "입력값이 잘못되었습니다", errors);
    }

이제 되는 값 보내서 JPA Query랑 이런거 잘 되는지 보면 됨

{
    "category":"ALL"
}
{
    "status": 200,
    "message": "글 카테고리별 조회에 성공했습니다.",
    "data": [
        {
            "id": 1,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:48"
        },
        {
            "id": 2,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 10000,
            "deadline": "2024-01-11T00:41:49"
        },
        {
            "id": 3,
            "author": "가나다라",
            "address": "주소주소",
            "store": "storestore",
            "minPrice": 20000,
            "sumPrice": 5000,
            "deadline": "2024-01-11T00:41:50"
        }
    ]
}

와~~ 잘 된다!!

에러 메시지 수정해봄

@Documented
@Constraint(validatedBy = CategoryValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Category {
    String message() default "has to be one of the CategoryEnum";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

이렇게 바꿔봤다

{
    "category":"YAY"
}
{
    "status": 400,
    "message": "입력값이 잘못되었습니다",
    "data": [
        "category has to be one of the CategoryEnum"
    ]
}

와~~ 잘 됨

 

 

2. 글 삭제하기 기능 구현

글 삭제하기 진행!

시작할땐 내가 이렇게까지 삽질하게될줄 몰랐다....

@Override
    public void deletePost(PostIdRequest postIdReq, User user) {
        Post post = getPostById(postIdReq.postId());
        
    }

이거 짜다가 다른 이미 있는 메서드 쓰려고 보니 메서드가 너무 한 기능에만 특정적이라

refactoring함

private String getAuthor(List<UserPost> userPosts){
        for(UserPost userpost : userPosts ){
            if(userpost.getRole().equals(UserPostRole.HOST)){
                return userpost.getUser().getNickname();
            }
        }
        throw new GlobalException(UserPostErrorCode.NOT_FOUND_HOST);
    }

private User getAuthor(List<UserPost> userPosts){
        for(UserPost userpost : userPosts ){
            if(userpost.getRole().equals(UserPostRole.HOST)){
                return userpost.getUser();
            }
        }
        throw new GlobalException(UserPostErrorCode.NOT_FOUND_HOST);
    }

이미 만들어둔 메서드만으로 구현하기 성공!! 메서드 재사용성이 좋은거같아서 약간 뿌듯했음

@Override
    public void deletePost(PostIdRequest postIdReq, User user) {
        //check if the post exists
        Post post = getPostById(postIdReq.postId());
        //check if the user has a relation with the post
        List<UserPost> userPosts = getUserPostsByPost(post);
        //check if the user is the host of the post
        User host = getAuthor(userPosts);
        if(!host.getId().equals(user.getId())){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
        postRepository.delete(post);
    }

 

 

에러코드 만들다가 궁금해졌는데 저 경우에는 BAD_REQUEST가 맞을까 FORBIDDEN이 맞을까?

NOT_FOUND_POST(HttpStatus.FORBIDDEN.value(), "작성자만 글을 수정/삭제할 수 있습니다."),

찾다보니 UnAuthorized도 있었음

UnAuthorized : 익명의 유저가 특정한 권한이 필요한 API를 호출

FORBIDDEN : 인증은 하였으나(Authorization) 접근권한이 없는 상태

FORBIDDEN이 맞는거같음

//403
    FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN.value(), "작성자만 글을 수정/삭제할 수 있습니다."),

이렇게 함!

테스트해봄

{
    "postId":1
}

 

⛳문제 : Request method 'DELETE' is not supported

{
    "status": 400,
    "message": "에러가 발생했습니다",
    "data": "Request method 'DELETE' is not supported"
}

왜죠

//글 삭제하기
    @DeleteMapping("/posts")
    public ApiResponse<Void> getPostsByCategory(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody PostIdRequest postIdReq
    ){
        postService.deletePost(postIdReq, userDetails.getUser());
        return new ApiResponse<>(HttpStatus.OK.value(), "글 삭제 성공했습니다.");
    }

Controller 코드를 가보니 카테고리별 글 전체조회 메서드랑 메서드 이름을 똑같이 해둠

저래서 못 찾은 거 같다

메서드 이름 바꿈

//글 삭제하기
    @DeleteMapping("/posts")
    public ApiResponse<Void> deletePost(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody PostIdRequest postIdReq
    ){
        postService.deletePost(postIdReq, userDetails.getUser());
        return new ApiResponse<>(HttpStatus.OK.value(), "글 삭제 성공했습니다.");
    }

그게 문제가 아니었다

바꾸고 나서도 아까랑 똑같은 에러가 떴는데

내가 경로를 잘못 잡은거였음

조회 경로 복붙해서 posts/1이었음

🚩해결 : url을 잘 입력하니까 해결됨

{
    "status": 200,
    "message": "글 삭제 성공했습니다.",
    "data": null
}

엥 근데 내가 에러 메시지 보려고 다시 한번 날리니까 post가 없다고 안 뜨고 또 성공메시지가 뜸

{
    "status": 200,
    "message": "글 삭제 성공했습니다.",
    "data": null
}

이러시면 안되거든요…

⛳문제 : 삭제한게 table에 반영이 안 됨

일단 Repository에 안 써뒀던 것들 명시적으로 쓰고 Optional로 다 만들어봄

postRepository.findById(postId).orElseThrow(()-> new GlobalException(
            PostErrorCode.NOT_FOUND_POST));

원래 이렇게 쓰고있긴 했는데 PostRepository에는 안 만들어놔서 Optional 이 아니라 orElseThrow가 실행되지 않는건가 싶어졌음

Optional<Post> findById(Long postId);

ㅋㅋ 아직도

{
    "status": 200,
    "message": "글 삭제 성공했습니다.",
    "data": null
}

이렇게 뜸

Transaction이 안 일어난 거 같음

읽기만 해오고 delete를 안 한거같음

이런 상황에서는 별 의미 없겠지만 @Transactional을 한번 달아봄

@Override
    @Transactional
    public void deletePost(PostIdRequest postIdReq, User user) {
        //check if the post exists
        Post post = getPostById(postIdReq.postId());
        //check if the user has a relation with the post
        List<UserPost> userPosts = getUserPostsByPost(post);
        //check if the user is the host of the post
        User host = getAuthor(userPosts);
        if(!host.getId().equals(user.getId())){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
        postRepository.delete(post);
    }

JPARepository wont update entity on save

이거 전 프로젝트에서 User part개발할때도 비슷한 이슈가 있었음

근데 그떄는 그냥 save() 쓰는걸로 영속성 다시 연결했었는데?

이번엔 delete()를 쓰니까 영속성이 끊어진게 문제가 아니고 다른 이슈인거 같다

예측 : Post가 key값으로 연결된 것들이 있어서 삭제가 안 되나?

 

나랑 똑같은 상황을 겪는 다른 사람의 블로그 참고함

연관관계를 끊어보라고 함

 

Post에 이렇게 만듦

public void delete(){

        for(Menu menu : menus){
            menus.remove(menu);
            menu.deletePost();
        }
        menus.clear();//-> 위에서 다 remove했는데 또 해줘야하나?
        
        for(Offer offer: offers){
            offers.remove(offer);
            offer.deletePost();
        }
        offers.clear();
    }

Menu랑 Offer에 이렇게 만듦

public void deletePost(){
        this.post = null;
    }
@Override
    public void deletePost(PostIdRequest postIdReq, User user) {
        //check if the post exists
        Post post = getPostById(postIdReq.postId());
        //check if the user has a relation with the post
        List<UserPost> userPosts = getUserPostsByPost(post);
        //check if the user is the host of the post
        User host = getAuthor(userPosts);
        if(!host.getId().equals(user.getId())){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
        post.delete();
        postRepository.delete(post);
    }

이렇게 해봄

{
    "postId":1
}

에러가 발생함

java.util.ConcurrentModificationException: null
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) ~[na:na]
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967) ~[na:na]
	at org.hibernate.collection.spi.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:920) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at com.moayo.moayoeats.domain.post.entity.Post.delete(Post.java:76) ~[main/:na]
	at com.moayo.moayoeats.domain.post.service.impl.PostServiceImpl.deletePost(PostServiceImpl.java:111) ~[main/:na]

ConcurrentModificationException: null

List를 순회하며 삭제할때 발생하는 오류라고 함

삭제하자마자 break하면 발생하지 않는다

for(Menu menu : menus){
            menus.remove(menu);
            menu.deletePost();
        }

→ 순회 로직 수정함

while(menus.size()>0){
            menus.get(0).deletePost();
            menus.remove(0);
        }
        menus.clear();

다시 테스트

{
    "postId":1
}
{
    "status": 200,
    "message": "글 삭제 성공했습니다.",
    "data": null
}

ㅋㅋ

계속 삭제하게 해줌

테이블에 가봐도 전혀 삭제가 되어있지 않다!

public void delete(){

        for(Menu menu : menus){
            menu.getPost().getMenus().remove(menu);
            menu.deletePost();
        }
        menus.clear();

        for(Offer offer: offers){
            offer.getPost().getOffers().remove(offer);
            offer.deletePost();
        }
        offers.clear();
    }

저렇게 거쳐거쳐가는게 불필요한 로직같아서 나는 걍 임의로 줄였었는데

블로그에서 굳이 저렇게 거쳐거쳐 remove를 한데에 의미가 있나 싶어서 따라해봄

아무 효과없음~

다시 내 코드로 돌아옴

public void delete(){
        while(menus.size()>0){
            menus.get(0).deletePost();
            menus.remove(0);
        }
        menus.clear();

        while(offers.size()>0){
            offers.get(0).deletePost();
            offers.remove(0);
        }
        offers.clear();
    }

 

저 블로그에서 글 읽다가 Soft Delete를 알게 됨

삭제를 해도 실제로 삭제하지는 않고 삭제 여부를 나타내는 칼럼에다 삭제 여부 표시한 후 나중에 삭제하는 방식이라고 함

soft delete도 나중에 한번 적용해보기!! → 일단 softdelete후 스케쥴러가 순회하면서 삭제하는 방식으로 구현하고 싶다!!

 

일단 post가 삭제될때 연관된 menu들이랑 offer도 삭제되어야 함!

그거 명시하려고 offer 양방향 관계로 만들어줌

@OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Menu> menus;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Offer> offers;

이렇게 하고 테스트 해봤는데 또 안된다

ㅋㅋ 환장해요!!

구글링해서 자료들 읽어봄

나랑 비슷하게 하고있는데?

근데 그냥 null을 하지 않네

연관관계 해제 메서드 로직을 살짝 변경해봄

public void dismissPost(){
        this.post.dismissMenu(this);
        this.post = null;
    }
public void dismissMenu(Menu menu){
        this.menus.remove(menu);
    }
public void dissmissPost(){
        this.post.dismissOffer(this);
        this.post = null;
    }
public void delete(){
        this.menus.forEach(menu->menu.dismissPost());
        this.menus.clear();

        this.offers.forEach(offer->offer.dismissPost());
        offers.clear();
    }

음 이거 해놔도 또 안됨 ㅋㅋ

cascade를 삭제해봄

@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Menu> menus;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Offer> offers;
@Override
    public void deletePost(PostIdRequest postIdReq, User user) {
        //check if the post exists
        Post post = getPostById(postIdReq.postId());
        //check if the user has a relation with the post
        List<UserPost> userPosts = getUserPostsByPost(post);
        //check if the user is the host of the post
        User host = getAuthor(userPosts);
        if(!host.getId().equals(user.getId())){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
        post.delete();
        //postRepository.delete(post);
    }

그리고 주석처리해봄

저건 상관없는듯

You need to add PreRemove function ,in the class where you have many object as attribute e.g in Education Class which have relation with UserProfile Education.java

private Set<UserProfile> userProfiles = new HashSet<UserProfile>(0);

@ManyToMany(fetch = FetchType.EAGER, mappedBy = "educations")
public Set<UserProfile> getUserProfiles() {
    return this.userProfiles;
}

@PreRemove
private void removeEducationFromUsersProfile() {
    for (UsersProfile u : usersProfiles) {
        u.getEducationses().remove(this);
    }
}

@PreRemove 를 달아봄

그래도 안됨 ㅋㅋㅋ

One way is to use cascade = CascadeType.ALL like this in your userAccount service:

@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;

Then do something like the following (or similar logic)

@Transactional
public void deleteUserToken(Token token){
    userAccount.getTokens().remove(token);
}

Notice the @Transactional annotation. This will allow Spring (Hibernate) to know if you want to either persist, merge, or whatever it is you are doing in the method. AFAIK the example above should work as if you had no CascadeType set, and call JPARepository.delete(token).

fetch = FetchType.*LAZY*

이게 문젠가 싶어서 없애봄

그리고 cascade다시 넣음

넣어도 안됨 ㅋㅋ

@Transactional 붙임

싹 다 붙여봄

@Transactional
    public void dismissPost(){
        this.post.dismissOffer(this);
        this.post = null;
    }

전혀 해결이 안 됨

Delete Code:

@Transactional
    @PreRemove
    public void deleteMenus(){
        this.menus.forEach(menu->menu.dismissPost());
        this.menus.clear();
    }

    @Transactional
    @PreRemove
    public void deletePosts(){
        this.offers.forEach(offer->offer.dismissPost());
        offers.clear();
    }

일단 쪼개봄

      	post.deleteMenus();
        postRepository.save(post);
        post.deletePosts();
        postRepository.save(post);
        postRepository.delete(post);

이렇게 해봄 제발 영속성 연결됐으면 ㅜㅜ

에러남

Caused by: jakarta.persistence.PersistenceException: You can only annotate one callback method with jakarta.persistence.PreRemove in bean class: com.moayo.moayoeats.domain.post.entity.Post
	at org.hibernate.jpa.event.internal.CallbackDefinitionResolverLegacyImpl.resolveEntityCallbacks(CallbackDefinitionResolverLegacyImpl.java:85) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.internal.EntityBinder.bindCallbacks(EntityBinder.java:1160) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.internal.EntityBinder.bindEntityClass(EntityBinder.java:251) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.internal.AnnotationBinder.bindClass(AnnotationBinder.java:423) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.source.internal.annotations.AnnotationMetadataSourceProcessorImpl.processEntityHierarchies(AnnotationMetadataSourceProcessorImpl.java:256) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess$1.processEntityHierarchies(MetadataBuildingProcess.java:279) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:322) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1432) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1503) ~[hibernate-core-6.4.1.Final.jar:6.4.1.Final]
	at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75) ~[spring-orm-6.1.2.jar:6.1.2]
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:376) ~[spring-orm-6.1.2.jar:6.1.2]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-6.1.2.jar:6.1.2]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-6.1.2.jar:6.1.2]
	at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:352) ~[spring-orm-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1820) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1769) ~[spring-beans-6.1.2.jar:6.1.2]

You can only annotate one callback method with jakarta.persistence.PreRemove in bean class: com.moayo.moayoeats.domain.post.entity.Post

PreRemove는 한번만 쓸 수 있나봄

PreRemove가 뭔데

https://www.baeldung.com/jpa-entity-lifecycle-events

2. JPA Entity Lifecycle Events

JPA specifies seven optional lifecycle events that are called:

  • before persist is called for a new entity – @PrePersist
  • after persist is called for a new entity – @PostPersist
  • before an entity is removed – @PreRemove
  • after an entity has been deleted – @PostRemove
  • before the update operation – @PreUpdate
  • after an entity is updated – @PostUpdate
  • after an entity has been loaded – @PostLoad
post.delete();
        postRepository.save(post);
        postRepository.delete(post);

이거 하다보니 생각났는데 저번 프로젝트에서는 update가 안돼서 Entity객체의 값을 바꾼다음에 다시 save()해서 해결해 줬었던 거 같다

근데 이번엔 delete()인데 왜 안되는지 모르겠다 ㅎㅎㅎ

현재 상태

PostService

@Override
    public void deletePost(PostIdRequest postIdReq, User user) {
        //check if the post exists
        Post post = getPostById(postIdReq.postId());
        //check if the user has a relation with the post
        List<UserPost> userPosts = getUserPostsByPost(post);
        //check if the user is the host of the post
        User host = getAuthor(userPosts);
        if(!host.getId().equals(user.getId())){
            throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS);
        }
        post.delete();
        postRepository.save(post);
        postRepository.delete(post);
    }

Post

@Transactional
    public void delete(){
        deleteMenus();
        deleteOffers();
    }
    @Transactional
    public void deleteMenus(){
        this.menus.forEach(menu->menu.dismissPost());
        this.menus.clear();
    }

    @Transactional
    public void deleteOffers(){
        this.offers.forEach(offer->offer.dismissPost());
        offers.clear();
    }

    @Transactional
    public void dismissMenu(Menu menu){
        this.menus.remove(menu);
    }

    @Transactional
    public void dismissOffer(Offer offer){
        this.offers.remove(offer);
    }

Offer

@Transactional
    public void dismissPost(){
        this.post.dismissOffer(this);
        this.post = null;
    }

Menu

@Transactional
    public void dismissPost(){
        this.post.dismissMenu(this);
        this.post = null;
    }

아 혹시 영속성이 연결이 안돼서 저거 하고 나서 menu를 menuRepository에서delete()해야하나?

그렇게 한번 해봄

//post.delete();
        List<Offer> offers = post.getOffers();
        for(Offer offer: offers){
            offer.dismissPost();
            offerRepository.delete(offer);
        }
        offers.clear();
        List<Menu> menus = post.getMenus();
        for(Menu menu: menus){
            menu.dismissPost();
            menuRepository.delete(menu);
        }
        menus.clear();
        postRepository.save(post);
        postRepository.delete(post);

서비스단에서 이렇게 해봄

왜 저번 프로젝트부터 @Transactional 이 안 먹히지

ㅋㅋ 아직도 무한대로 삭제됨

삭제가 반영이 안 돼

menu는 삭제가 됐는지 한번 봄

음 메뉴도 삭제가 안 됨

controller로 메뉴삭제 날리면 메뉴가 삭제되는지 한번 봄

{
    "menuId":4
}
{
    "status": 201,
    "message": "메뉴를 삭제했습니다.",
    "data": null
}
{
    "status": 404,
    "message": "메뉴를 찾을 수 없습니다.",
    "data": null
}

삭제가 잘 됨 ㅋㅋㅋ 삭제하자마자 db에 반영됨

public void deleteMenu(MenuDeleteRequest menuDeleteReq, User user) {
        Menu menu = findMenuById(menuDeleteReq.menuId(), user);

        menuRepository.delete(menu);
    }

진심 억울하다 ㅋㅋ 저기서는 저것만 하면 삭제가 되는데요 ㅋㅋㅋ

걍 양방향 관계를 끊어봄

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Menu> menus;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    private List<Offer> offers;

없애버림

그러면 단방향관계인가?

그리고 menuRepository 돌아서 postid인거 들고오고

offerRepository돌아서 offerid인거 들고옴

그거 다 delete하고 나서 이걸 delete를 하자 ㅎㅎ

private List<NickMenusResponse> getAllMenus(List<UserPost> userposts){

        List<NickMenusResponse> menus =
            //List<UserPost> -> List<NickMenusResponse>
            userposts.stream().map((UserPost userpost)->
            new NickMenusResponse(userpost.getUser().getNickname(),
                //List<Menu> menus -> List<MenuResponse>
                userpost.getPost()**.getMenus()**.stream().map((Menu menu)->new MenuResponse(menu.getMenuname(),menu.getPrice())).toList()
            )).toList();
        return menus;
    }

저부분이 없어져서 수정해야됨

private List<NickMenusResponse> getAllMenus(List<UserPost> userposts){

        List<NickMenusResponse> menus =
            //List<UserPost> -> List<NickMenusResponse>
            userposts.stream().map((UserPost userpost)->
            new NickMenusResponse(userpost.getUser().getNickname(),
                //List<Menu> menus -> List<MenuResponse>
                getUserMenus(userpost.getUser(),userpost.getPost()).stream().map((Menu menu)->new MenuResponse(menu.getMenuname(),menu.getPrice())).toList()
            )).toList();
        return menus;
    }

이렇게 수정함

private List<Menu> getUserMenus(User user, Post post){
        return menuRepository.findAllByUserAndPost(user,post);
    }

이건 이미 아래에 있었음

Optional<List<Offer>> findAllByPost(Post post);

최후의 방침으로 싹 다 Service단에서 수동삭제 시도해봄 ㅎㅎ

      	List<Offer> offers = getOffersByPost(post);
        offerRepository.deleteAll(offers);

        List<Menu> menus = getMenusByPost(post);
        menuRepository.deleteAll(menus);
        
        postRepository.delete(post);
private List<Offer> getOffersByPost(Post post){
        return offerRepository.findAllByPost(post).orElse(null);
    }

    private List<Menu> getMenusByPost(Post post){
       return menuRepository.findAllByPost(post).orElse(null);
    }
{
    "status": 200,
    "message": "글 삭제 성공했습니다.",
    "data": null
}

ㅎㅎ아직도 무한반복임

메뉴는 사라졌는지 가봄

메뉴 삭제도 안 됐음

 

menu 테이블이 비어있고

offer 테이블도 비어있는 상태에서도 삭제가 안 됨

postId가 당연히 Menu테이블이랑 Offer테이블에만 있다고 생각하고 터널비전으로 하루종일 헤맸는데

문제는 다른곳에 있었다…

UserPost에서 postid를 가지고 있어서 그런거였다…

 

🚩해결 : 생각지도 못했던 곳에서 postId를 fk로 가진 Entity 발견..!

그 후 UserPost 테이블에서 postId를 fk로 가지고 있는 모든 Entity 삭제

	      post.delete();
        userPostRepository.deleteAll(userPosts);
        postRepository.delete(post);

userpost에서 삭제해줬더니 잘 삭제된다~~

 

주문 마감?을 할때

Review 를 발생시키고!

모두 다 수령 완료를 누르면

Review 의 어떤 boolean 값 true로 바꾸면서 거기다 postId 넣어놨다가 빼고 삭제하면서

이 연관관계 삭제 로직 쓰자!!

연관관계 삭제 로직!

Post

    public void deleteMenus(){
        while(this.menus.size()>0){
            menus.get(0).dismissPost();
        }
        this.menus.clear();
    }

    public void dismissMenu(Menu menu){
        this.menus.remove(menu);
    }
Child

    public void dismissPost(){
        this.post.dismissMenu(this);
        this.post = null;
    }

테스트 새로 해봤는데

cascade잘 되고 orphanremoval도 잘 된다 ㅋㅋ… 아까운 내 하루

{
    "status": 404,
    "message": "글을 찾지 못하였습니다.",
    "data": null
}

잘 됐다!

 

3. 프론트 구현 환경설정

이제 thymeleaf랑 ajax쓰기로 해서 spring에 연동 시작함!

일단 차근차근 해봄

https://www.thymeleaf.org/doc/tutorials/3.1/thymeleafspring.html

  1. build.gradle에 thymeleaf 추가
  2. resources→static→index.html 파일 생성
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
메인 페이지 입니다.
</body>
</html>

근데 나는 다르게도 해봄

application.yml 설정

spring:
  thymeleaf:
    prefix: classpath:templates/
    suffix: .html
    cache: false

HomeController 생성

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @GetMapping(value = "/")
    public ModelAndView home() {
        ModelAndView modelAndView = new ModelAndView();
        
        modelAndView.setViewName("home");
        
        Map<String, Object> map = new HashMap<>();
        map.put("name", "Bamdule");
        map.put("date", LocalDateTime.now());
        
        modelAndView.addObject("data", map);
        
        return modelAndView;
    }
}

 

home.html 생성

src/main/resources/templates/ -> home.html을 생성

<!DOCTYPE html>
<html>
    <head>
        <title>title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div th:text="${data.name}"></div>
        <th:block th:text="${data.date}"/>
    </body>
</html>

 

http://localhost:8080/ 들어갔더니 저거 뜬다!

MoayoEats 2024-01-12T00:03:43.986423500

이렇게 뜸

'개발일지' 카테고리의 다른 글

2024-01-13, Today I Learned  (0) 2024.01.14
2024-01-12, Today I Learned  (0) 2024.01.12
2023-01-10, Today I Learned  (0) 2024.01.10
2024-01-09, Today I Learned @IdClass Composite key in Spring  (0) 2024.01.09
2024-01-08, Today I Learned  (1) 2024.01.08