[Spring Boot] ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

DANIยท2023๋…„ 10์›” 21์ผ
0
post-thumbnail

๐Ÿ’ป ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ!

๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ„์† ์Œ“์—ฌ๊ฐ€๋Š” ๊ฒŒ์‹œํŒ์ด๋ฏ€๋กœ ๊ฒ€์ƒ‰๊ธฐ๋Šฅ์€ ํ•„์ˆ˜๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฒ€์ƒ‰ํ•  ๋‚ด์šฉ์ด ์งˆ๋ฌธ์˜ ์ œ๋ชฉ, ์งˆ๋ฌธ์˜ ๋‚ด์šฉ, ์งˆ๋ฌธ ์ž‘์„ฑ์ž, ๋‹ต๋ณ€์˜ ๋‚ด์šฉ, ๋‹ต๋ณ€ ์ž‘์„ฑ์ž์— ์กด์žฌํ•˜๋Š”์ง€ ์ฐพ์•„๋ณด๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ์ถœ๋ ฅํ•ด๋ณด์ž!


๐Ÿ“ "์Šคํ”„๋ง"์„ ํฌํ•จํ•œ ๊ธ€์ž ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ

select
    distinct q.id,
    q.author_id,
    q.content,
    q.create_date,
    q.modify_date,
    q.subject 
from question q 
left outer join site_user u1 on q.author_id=u1.id 
left outer join answer a on q.id=a.question_id 
left outer join site_user u2 on a.author_id=u2.id 
where
    q.subject like '%์Šคํ”„๋ง%' 
    or q.content like '%์Šคํ”„๋ง%' 
    or u1.username like '%์Šคํ”„๋ง%' 
    or a.content like '%์Šคํ”„๋ง%' 
    or u2.username like '%์Šคํ”„๋ง%' 


Specification

์œ„์˜ ์ฟผ๋ฆฌ์—์„œ ๋ณธ ๊ฒƒ๊ณผ ๊ฐ™์ด ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•ด์•ผ ํ•  ๊ฒฝ์šฐ์—๋Š” JPA๊ฐ€ ์ œ๊ณตํ•˜๋Š” Specification ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ํŽธ๋ฆฌํ•˜๋‹ค.

์ฐธ๊ณ  : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications




๐Ÿ’พ QuestionService์— search ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€

import com.mysite.sbb.answer.Answer;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;

private Specification<Question> search(String kw) {
		
        return new Specification<>() {
            private static final long serialVersionUID = 1L;
            @Override
            // Root<Question> q : ๊ธฐ์ค€์„ ์˜๋ฏธํ•˜๋Š” Question ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐ์ฒด
            public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
                query.distinct(true);  // ์ค‘๋ณต์„ ์ œ๊ฑฐ 
                
                /* 
                 * from question q
                 * left outer join site_user u1 on q.author_id = u1.id
                 */
                // Question, SiteUser ์—”ํ‹ฐํ‹ฐ๋Š” author ์†์„ฑ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— q.join("author")์™€ ๊ฐ™์ด ์กฐ์ธ
                Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
                
                /* 
                 * from question q
                 * left outer join answer a on q.id=a.question_id
                 */
                // Question๊ณผ Answer ์—”ํ‹ฐํ‹ฐ๋Š” "answerList"๋กœ ์—ฐ๊ฒฐ๋˜์–ด์žˆ๋‹ค.
                Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
                
                /* 
                 * left outer join site_user u2 on a.author_id = u2.id
                 */
                // Answer, SiteUser ์—”ํ‹ฐํ‹ฐ๋Š” "author"๋กœ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋‹ค.
                Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);
                
                // ์ž…๋ ฅ๋ฐ›์€ ๋ฌธ์ž "kw"๋ฅผ ํฌํ•จ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ฆฌํ„ด
                return cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // ์งˆ๋ฌธ ์ œ๋ชฉ 
                        cb.like(q.get("content"), "%" + kw + "%"),      // ์งˆ๋ฌธ ๋‚ด์šฉ 
                        cb.like(u1.get("username"), "%" + kw + "%"),    // ์งˆ๋ฌธ ์ž‘์„ฑ์ž 
                        cb.like(a.get("content"), "%" + kw + "%"),      // ๋‹ต๋ณ€ ๋‚ด์šฉ 
                        cb.like(u2.get("username"), "%" + kw + "%"));   // ๋‹ต๋ณ€ ์ž‘์„ฑ์ž 
            }
        };
    }



๐Ÿ’พ QuestionRepository์— ์ˆ˜์ •(Specification์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด)

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface QuestionRepository extends JpaRepository<Question, Integer> {
    Question findBySubject(String subject);
    Question findBySubjectAndContent(String subject, String content);
    List<Question> findBySubjectLike(String subject);
    Page<Question> findAll(Pageable pageable);
    Page<Question> findAll(Specification<Question> spec, Pageable pageable);
}

โœ… ์ถ”๊ฐ€ : Page<Question> findAll(Specification<Question> spec, Pageable pageable);



๐Ÿ’พ QuestionService getList ๋ฉ”์†Œ๋“œ ์ˆ˜์ •

    // ๊ฒ€์ƒ‰์–ด๋ฅผ ์˜๋ฏธํ•˜๋Š” kw ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ถ”๊ฐ€ 
	public Page<Question> getList(int page, String kw){
		List<Sort.Order> sorts = new ArrayList<>();
		sorts.add(Sort.Order.desc("createDate"));
		Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
		Specification<Question> spec = search(kw); // ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ
		return this.questionRepository.findAll(spec, pageable); // ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋ฆฌํ„ด
	}


๐Ÿ’พ QuestionController list ๋ฉ”์†Œ๋“œ ์ˆ˜์ •

@GetMapping("/list")
	// ๊ฒ€์ƒ‰์–ด์— ํ•ด๋‹นํ•˜๋Š” kw ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๊ณ  ๋””ํดํŠธ๊ฐ’์œผ๋กœ ๋นˆ ๋ฌธ์ž์—ด์„ ์„ค์ •
	public String list(Model model, @RequestParam(value="page", defaultValue="0") int page, @RequestParam(value="kw", defaultValue="") String kw) {
		Page<Question> paging = this.questionService.getList(page, kw); 
		model.addAttribute("paging", paging);
		
		// ํ™”๋ฉด์—์„œ ์ž…๋ ฅํ•œ ๊ฒ€์ƒ‰์–ด๋ฅผ ํ™”๋ฉด์— ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด model.addAttribute("kw", kw)๋กœ kw ๊ฐ’์„ ์ €์žฅ
		model.addAttribute("kw", kw);
		return "question_list";
	}


๐Ÿ’พ question_list.html ์ˆ˜์ •ํ•˜๊ธฐ

โœ… ์›ํ•˜๋Š” ์œ„์น˜์— ๊ฒ€์ƒ‰์ฐฝ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž!

   <div class="row my-3">
        <div class="col-6">
            <div class="input-group">
                <input type="text" id="search_kw" class="form-control" th:value="${kw}">
                <button class="btn btn-outline-secondary" type="button" id="btn_search">์ฐพ๊ธฐ</button>
            </div>
        </div>
    </div>

โœ… <input type="text" id="search_kw" class="form-control" th:value="${kw}">

์ž๋ฐ” ์Šคํฌ๋ฆฝํŠธ์—์„œ ์ด ํ…์ŠคํŠธ์ฐฝ์— ์ž…๋ ฅ๋œ ๊ฐ’์„ ์ฝ๊ธฐ ์œ„ํ•ด ๋‹ค์Œ์ฒ˜๋Ÿผ ํ…์ŠคํŠธ์ฐฝ id ์†์„ฑ์— "search_kw"๋ผ๋Š” ๊ฐ’์„ ์ถ”๊ฐ€ํ•˜์˜€๋‹ค.

id ์†์„ฑ์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค.



โœ… page์™€ kw๋ฅผ ๋™์‹œ์— GET์œผ๋กœ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋Š” searchForm ์ถ”๊ฐ€

(... ์ƒ๋žต ...)
    <!-- ํŽ˜์ด์ง•์ฒ˜๋ฆฌ ๋ -->
    <form th:action="@{/question/list}" method="get" id="searchForm">
        <input type="hidden" id="kw" name="kw" th:value="${kw}">
        <input type="hidden" id="page" name="page" th:value="${paging.number}">
    </form>
</div>
</html>

GET ๋ฐฉ์‹์œผ๋กœ ์š”์ฒญํ•ด์•ผ ํ•˜๋ฏ€๋กœ method ์†์„ฑ์— "get"์„ ์„ค์ •ํ–ˆ๋‹ค. kw์™€ page๋Š” ์ด์ „์— ์š”์ฒญํ–ˆ๋˜ ๊ฐ’์„ ๊ธฐ์–ตํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ value์— ๊ฐ’์„ ์œ ์ง€ํ• ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

๐Ÿ’ก GET ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 


๋งŒ์•ฝ GET์ด ์•„๋‹Œ POST ๋ฐฉ์‹์œผ๋กœ ๊ฒ€์ƒ‰๊ณผ ํŽ˜์ด์ง•์„ ์ฒ˜๋ฆฌํ•œ๋‹ค๋ฉด ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ "์ƒˆ๋กœ๊ณ ์นจ" ๋˜๋Š” "๋’ค๋กœ๊ฐ€๊ธฐ"๋ฅผ ํ–ˆ์„ ๋•Œ "๋งŒ๋ฃŒ๋œ ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค."๋ผ๋Š” ์˜ค๋ฅ˜๋ฅผ ์ข…์ข… ๋งŒ๋‚˜๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค. ์™œ๋ƒํ•˜๋ฉด POST ๋ฐฉ์‹์€ ๋™์ผํ•œ POST ์š”์ฒญ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ์ค‘๋ณต ์š”์ฒญ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด "๋งŒ๋ฃŒ๋œ ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค." ๋ผ๋Š” ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก์„ ์กฐํšŒํ•  ๋•Œ๋Š” GET ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.


โœ… ๊ธฐ์กด ํŽ˜์ด์ง•์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์„ ์ง์ ‘ URL์„ ๋งํฌํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹Œ ๊ฐ’์„ ์ฝ์–ด ํผ์— ์„ค์ •ํ•˜๋„๋ก ์ˆ˜์ •ํ•ด์•ผํ•œ๋‹ค.

why? ๊ฒ€์ƒ‰์–ด๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๊ฒ€์ƒ‰์–ด์™€ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ํ•จ๊ป˜ ์ „์†กํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

โœ… ์ˆ˜์ • ์ „ : th:href="@{|?page=${paging.number-1}|}"
โœ… ์ˆ˜์ • ํ›„ : href="javascript:void(0)" th:data-page="${paging.number-1}"



โœ… page, kw ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋™์‹œ์— ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถ”๊ฐ€

<script layout:fragment="script" type='text/javascript'>

const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});

const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 0;  // ๊ฒ€์ƒ‰๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ 0ํŽ˜์ด์ง€๋ถ€ํ„ฐ ์กฐํšŒํ•œ๋‹ค.
    document.getElementById('searchForm').submit();
});

</script>

const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});

โœ” ์ด class ์†์„ฑ๊ฐ’์œผ๋กœ "page-link"๋ผ๋Š” ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋งํฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด
์ด ๋งํฌ์˜ data-page ์†์„ฑ๊ฐ’์„ ์ฝ์–ด searchForm์˜ page ํ•„๋“œ์— ์„ค์ •ํ•˜์—ฌ searchForm์„ ์š”์ฒญํ•œ๋‹ค.


const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 0;  // ๊ฒ€์ƒ‰๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ 0ํŽ˜์ด์ง€๋ถ€ํ„ฐ ์กฐํšŒํ•œ๋‹ค.
    document.getElementById('searchForm').submit();
});

โœ” ๊ฒ€์ƒ‰๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๊ฒ€์ƒ‰์–ด ํ…์ŠคํŠธ์ฐฝ์— ์ž…๋ ฅ๋œ ๊ฐ’์„ searchForm์˜ kw ํ•„๋“œ์— ์„ค์ •ํ•˜์—ฌ searchForm์„ ์š”์ฒญํ•˜๋„๋ก ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ๊ฒ€์ƒ‰๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์ƒˆ๋กœ์šด ๊ฒ€์ƒ‰์— ํ•ด๋‹น๋˜๋ฏ€๋กœ page์— ํ•ญ์ƒ 0์„ ์„ค์ •ํ•˜์—ฌ ์ฒซ ํŽ˜์ด์ง€๋กœ ์š”์ฒญํ•˜๋„๋ก ํ–ˆ๋‹ค.





๐Ÿ’ก Specification ๋Œ€์‹  ์ง์ ‘ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•


๐Ÿ’พ QuestionRepository์— ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€

โœ… @Query ์• ๋„ˆํ…Œ์ด์…˜์ด ์ ์šฉ๋œ findAllByKeyword ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Query("select "
            + "distinct q "
            + "from Question q " 
            + "left outer join SiteUser u1 on q.author=u1 "
            + "left outer join Answer a on a.question=q "
            + "left outer join SiteUser u2 on a.author=u2 "
            + "where "
            + "   q.subject like %:kw% "
            + "   or q.content like %:kw% "
            + "   or u1.username like %:kw% "
            + "   or a.content like %:kw% "
            + "   or u2.username like %:kw% ")
Page<Question> findAllByKeyword(@Param("kw") String kw, Pageable pageable);

โœ… QuestionService์—์„œ Page getList ๋ฉ”์†Œ๋“œ ์ˆ˜์ •ํ•˜๊ธฐ

โœ” ์ˆ˜์ • ์ „ : return this.questionRepository.findAll(spec, pageable);
โœ” ์ˆ˜์ • ํ›„ : return this.questionRepository.findAllByKeyword(kw, pageable);

@Query๋ฅผ ์ž‘์„ฑํ•  ๋•Œ์—๋Š” ๋ฐ˜๋“œ์‹œ ํ…Œ์ด๋ธ” ๊ธฐ์ค€์ด ์•„๋‹Œ ์—”ํ‹ฐํ‹ฐ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค. ์ฆ‰, site_user์™€ ๊ฐ™์€ ํ…Œ์ด๋ธ”๋ช… ๋Œ€์‹  SiteUser์ฒ˜๋Ÿผ ์—”ํ‹ฐํ‹ฐ๋ช…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ณ  ์กฐ์ธ๋ฌธ์—์„œ ๋ณด๋“ฏ์ด q.author_id=u1.id์™€ ๊ฐ™์€ ์ปฌ๋Ÿผ๋ช… ๋Œ€์‹  q.author=u1์ฒ˜๋Ÿผ ์—”ํ‹ฐํ‹ฐ์˜ ์†์„ฑ๋ช…์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  @Query์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•  kw ๋ฌธ์ž์—ด์€ ๋ฉ”์„œ๋“œ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜์— @Param("kw")์ฒ˜๋Ÿผ @Param ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๊ฒ€์ƒ‰์–ด๋ฅผ ์˜๋ฏธํ•˜๋Š” kw ๋ฌธ์ž์—ด์€ @Query ์•ˆ์—์„œ :kw๋กœ ์ฐธ์กฐ๋œ๋‹ค.







โœจ ์ด๋ฒˆ ์ฑ•ํ„ฐ์—์„œ ๋ฐฐ์šด ๋ถ€๋ถ„

โœ… ๋ฐ์ดํ„ฐ ์กฐํšŒ์‹œ์— ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” Specification ์ธํ„ฐํŽ˜์ด์Šค
โœ… ๋ฐ์ดํ„ฐ ์กฐํšŒ์‹œ POST๋ฐฉ์‹์ด ์•„๋‹Œ GET๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

๐Ÿ“ ๊ณต๋ถ€ํ•  ๋ถ€๋ถ„

โœ… Specification ์ธํ„ฐํŽ˜์ด์Šค / JPA๊ณต๋ถ€ํ•˜๊ธฐ
โœ… ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ณต๋ถ€

0๊ฐœ์˜ ๋Œ“๊ธ€