[Spring] - ์ข‹์•„์š” ๊ธฐ๋Šฅ ๊ตฌํ˜„

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

๐ŸŽจ IDE : Intellij Ultimate

๐Ÿƒ Spring : Spring Boot 2.7.x + Spring Security

๐Ÿ–ฅ๏ธ View : Thymeleaf

๐Ÿ› ๏ธ Java : Amazon corretto 11

Ajax๋ž€?

Ajax๋ž€, Asynchronous JavaScript and XML์˜ ์•ฝ์ž์ด๋‹ค.
์šฐ๋ฆฌ๋Š” ๋ณดํ†ต ์ด ๊ธฐ๋Šฅ์„ JS๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„์™€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ  ๋ฐ›๋Š”๋ฐ ์‚ฌ์šฉํ•œ๋‹ค.

์‹๋‹น์„ ์˜ˆ์‹œ๋กœ ๋“ค์–ด๋ณด์ž!

  1. ์†๋‹˜์ด ๋ฐฅ์„ ๋จน๊ธฐ ์œ„ํ•ด ์‹๋‹น์— ๋“ค์–ด์™”๋‹ค.
  2. ์†๋‹˜์€ ํ™€ ๋งค๋‹ˆ์ €์—๊ฒŒ ์ œ์œก ๋ณถ์Œ์„ ์ฃผ๋ฌธํ–ˆ๋‹ค.
  3. ํ™€ ๋งค๋‹ˆ์ €๋Š” ์ฃผ๋ฐฉ์žฅ์—๊ฒŒ ์ œ์œก ๋ณถ์Œ ์ฃผ๋ฌธ์„ ์•Œ๋ฆฐ๋‹ค.
  4. ์ฃผ๋ฐฉ์žฅ์ด ๋š๋”ฑ ๋งŒ๋“ค์–ด์„œ ํ™€ ๋งค๋‹ˆ์ €์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.
  5. ํ™€ ๋งค๋‹ˆ์ €๊ฐ€ ์†๋‹˜์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ์†๋‹˜์„ ์‚ฌ์šฉ์ž, ํ™€ ๋งค๋‹ˆ์ €๋ฅผ ์„œ๋ฒ„, ์ฃผ๋ฐฉ์žฅ์„ DB๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค!
์ฆ‰, JS๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„(Spring)๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ , ์›ํ•˜๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

HTML

HTML ์ฝ”๋“œ๋Š” ๋‹จ์ˆœํ•˜๋‹ค. ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์œ ์ €๊ฐ€ ์ข‹์•„์š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด์ง€ ๋ชปํ•˜๋„๋ก ๊ตฌ์„ฑํ•˜์˜€๋‹ค.
๋งŒ์•ฝ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๊ฒฝ์šฐ JS์˜ modifyPostLike ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜์˜€๊ณ , ํƒ€์ž„๋ฆฌํ”„ ๋ฆฌํ„ฐ๋Ÿด์„ ์ด์šฉํ•ด ํ˜„์žฌ ๊ฒŒ์‹œ๊ธ€์˜ ๋ฒˆํ˜ธ๋ฅผ ๋„˜๊ฒจ์ฃผ์—ˆ๋‹ค.
์ด ์™ธ์˜ ๋ถ€๋ถ„์€ ๊ฐ€์žฅ ์ž๋ฐ” ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์ดํ•ด๊ฐ€ ๋  ๊ฒƒ์ด๋‹ค.

<!-- ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž๋งŒ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑ -->
<th:block sec:authorize="isAuthenticated()">
  <div class="post-meta" id="post-ajax-form">
    <!-- ํ•ด๋‹น ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ ํ˜„์žฌ ๊ฒŒ์‹œ๊ธ€์˜ ๋ฒˆํ˜ธ๋ฅผ JS์˜ addPostLike.modifyPostLike ํ•จ์ˆ˜๋กœ ๋„˜๊ฒจ์ค€๋‹ค. -->
    <button class="btn" id="post-react-btn" th:onclick="|addPostLike.modifyPostLike(${post.getId()})|">
      <!-- likeFlag๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๋‹ค. -->
      <th:block th:if="${!likeFlag}">
        <i class="icofont-thumbs-up"></i>
        <span class="mx-2">์ข‹์•„์š”</span>
      </th:block>
      <!-- likeFlag๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋‹ค. -->
      <th:block th:if="${likeFlag}">
        <i class="icofont-thumbs-up text-primary"></i>
        <span class="mx-2 text-primary">์ข‹์•„์š” ์ทจ์†Œ</span>
      </th:block>
      <!-- ํ˜„์žฌ ์ข‹์•„์š” ์ˆ˜๋ฅผ ํ‘œ์‹œ -->
      <span class="badge rounded-pill bg-primary text-white" th:text="${post.getPostLike().size()}"></span>
    </button>
  </div>
</th:block>

JavaScript

์ž๋ฐ” ์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋„ ๋‹จ์ˆœํ•˜๋‹ค.
์œ ์ €๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด์„œ ๋„˜๊ฒจ์ค€ postId๋ฅผ ๋ฐ›์•„์˜จ ๋‹ค์Œ, ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ api ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ๋ฉด ๋์ด๋‹ค.

let addPostLike = {
  modifyPostLike: function (postId) {
    // ์„œ๋ฒ„์— ๋„˜๊ฒจ์ค„ ๋ฐ์ดํ„ฐ : ๊ฒŒ์‹œ๊ธ€ ์•„์ด๋””๋งŒ ์ „๋‹ฌ
    let param = {
      postNum: postId
    };
    $.ajax({
      type: "PUT",
      url: "/post/modify/like",
      data : param,
      success: function (flag) {
        // ๋ฐ์ดํ„ฐ ์š”์ฒญ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ
        // id๊ฐ€ post-ajax-form์ธ div๋งŒ ์ƒˆ๋กœ๊ณ ์นจ ํ•ด์ค€๋‹ค.
        $('#post-ajax-form').load(location.href+' #post-ajax-form');
      },
    })
  },
}

Java

Entity

์–ด๋–ค ์œ ์ €๊ฐ€ ์–ด๋–ค ๊ฒŒ์‹œ๊ธ€์˜ ์ข‹์•„์š”๋ฅผ ๋ˆŒ๋ €๋Š”์ง€ ์•Œ์•„์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ–ˆ๋‹ค.

@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Like {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

}

๋ฉค๋ฒ„๊ฐ€ ํšŒ์›์„ ํƒˆํ‡ดํ•˜๊ฑฐ๋‚˜ ๊ฒŒ์‹œ๊ธ€์ด ์ง€์›Œ์งˆ ๊ฒฝ์šฐ, ๊ทธ์— ํ•ด๋‹นํ•˜๋Š” Like๋„ ์ง€์›Œ์ ธ์•ผ๊ธฐ ๋•Œ๋ฌธ์— Member์™€ Post์—๋„ ์—ฐ๊ด€ ๊ด€๊ณ„ ๋งคํ•‘์„ ํ•ด์ค€๋‹ค.
Set์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” ํ•œ ํšŒ์›์ด ์ค‘๋ณต๋œ ๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
๋กœ์ง์„ ์ž˜ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด List๋กœ ์ˆ˜์ •ํ•ด๋„ ์ƒ๊ด€์—†๋‹ค!

public class Member {
    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Like> postLike;
}
public class Post {
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Like> postLike;
}

Controller

Ajax์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น ์ •๋ณด์— ๋งž์ถฐ์„œ ๋งŒ๋“ค์–ด์ค€๋‹ค.

type: "PUT",
url: "/post/modify/like",

Security๋ฅผ ์ด์šฉํ•ด User ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” SecurityUser๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

@RestController
@RequiredArgsConstructor
public class LikeApiController {

    private final LikeApiService likeApiService;

    @PutMapping("/post/modify/like")
    public boolean modifyPostLike(@AuthenticationPrincipal SecurityUser securityUser,
                                  @RequestParam Map<String, String> params) {
        // Post ์—”ํ‹ฐํ‹ฐ์˜ id ํ•„๋“œ๊ฐ€ Long์ด๊ธฐ ๋•Œ๋ฌธ์— Long์œผ๋กœ ๋ณ€ํ™˜
        Long postId = Long.valueOf(params.get("postNum"));

        // ์ข‹์•„์š”๊ฐ€ ์ƒˆ๋กœ ์ƒ๊ฒผ๋‹ค๋ฉด true, ๊ธฐ์กด์— ์ข‹์•„์š”๊ฐ€ ์žˆ์—ˆ๋‹ค๋ฉด false
        return likeApiService.modifyLikeStatus(securityUser.getMember(), postId);
    }
}

Service

ํ•ด๋‹น ๊ธฐ๋Šฅ์ด ์ง„ํ–‰๋˜๋Š” ๋„์ค‘ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ๋กค๋ฐฑ์„ ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— @Transactional์„ ํ™œ์šฉํ–ˆ๋‹ค.

@Service
@RequiredArgsConstructor
public class LikeApiService {
    private final LikeRepository likeRepository;
    private final PostService postService;

    @Transactional
    public boolean modifyLikeStatus(Member member, Long id) {
        // postService๋ฅผ ํ†ตํ•ด ์•„์ด๋””์— ๋Œ€ํ•œ ์—”ํ‹ฐํ‹ฐ ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์˜ด
        Post currentPost = postService.findById(id);

        // DB์—์„œ ํ•ด๋‹น ํšŒ์›์ด ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅธ ์ƒํƒœ์ธ์ง€ ํ™•์ธ
        if (existLikeFlag(member, currentPost)) {
            // ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅธ ์ƒํƒœ์ผ ๊ฒฝ์šฐ
            // ์ข‹์•„์š” ์ทจ์†Œ๋ฅผ ์œ„ํ•ด DB์—์„œ ์‚ญ์ œ
            Like currentPostLike = likeRepository.findByMemberAndPost(member, currentPost);
            likeRepository.delete(currentPostLike);
            return false;
        } else {
            // ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅด์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ
            // DB์— ์ €์žฅ
            createNewPostLike(member, currentPost);
            return true;
        }
    }

    @Transactional(readOnly = true)
    public boolean existLikeFlag(Member member, Post post) {
        return likeRepository.existsByMemberAndPost(member, post);
    }

    @Transactional
    public void createNewLike(Member member, Post currentPost) {
        Like newPostLike = Like.builder()
                .member(member)
                .post(currentPost)
                .build();
        likeRepository.save(newPostLike);
    }
}

Repository

public interface LikeRepository extends JpaRepository<Like, Long> {
    boolean existsByMemberAndPost(Member member, Post post);
    Like findByMemberAndPost(Member member, Post post);
}

๋งˆ๋ฌด๋ฆฌ

๋จธ๋ฆฌ์†์œผ๋กœ๋Š” ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ• ์ง€ ์ •๋ฆฌ๊ฐ€ ๋˜์—ˆ์ง€๋งŒ, ๊ธ€๋กœ ๋‚จ๊ฒจ๋†“์œผ๋ ค๋‹ˆ ์ˆœ์„œ๊ฐ€ ๋’ค์ฃฝ๋ฐ•์ฃฝ ๋œ ๊ฒƒ ๊ฐ™๋‹ค.
๊ทธ๋ž˜๋„ ์ด๋ฒˆ ๊ธฐํšŒ์— ๋น„๋™๊ธฐ ํ†ต์‹ ์— ๋Œ€ํ•ด ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด์„œ ๋‹คํ–‰์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

โžก๏ธ ์ฐธ๊ณ  ๋ธ”๋กœ๊ทธ : ๊น€๋ฏธ์ธ์ฝ”๋”ฉ