ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코드로 배우는 스프링부트 웹 프로젝트 Day 5
    Spring/코드로 배우는 스프링부트 웹 프로젝트 2022. 8. 17. 11:51

    /* Annotation들은 스프링부트 프로젝트의 Annotation 정리 페이지에 따로 정리해두었습니다. */

     

    public interface GuestbookRepository extends JpaRepository<Guestbook, Long>, QuerydslPredicateExecutor<Guestbook> {
    }
    

    Querydsl을 이용하므로 GuestRepository인터페이스가 QuerydslPredicateExecutor 추가 상속하도록 함

     

    [엔티티의 테스트]

     

    test 폴더에 repository/GuestbookRepositoryTests 생성

    package org.zerock.guestbook.repository;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.zerock.guestbook.entity.Guestbook;
    
    import java.util.stream.IntStream;
    
    @SpringBootTest
    public class GuestbookRepositoryTests {
        
        @Autowired
        private GuestbookRepository guestbookRepository;
        
        @Test
        public void insertDummies(){
    
            IntStream.rangeClosed(1, 300).forEach(i -> {
                Guestbook guestbook = Guestbook.builder()
                        .title("Title...." + i)
                        .content("Content..." + i)
                        .writer("user" + (i % 10))
                        .build();
                System.out.println(guestbookRepository.save(guestbook));
            });
        }
    }

    300개의 테스트 데이터 생성

     

    자동으로 moddate, regdate 채워짐

     

    [수정 시간 테스트]

    (엔티티 클래스는 setter 관련 기능을 만들지 않는 것을 권장 - 필요에 따라서 최소한으로 수정 기능을 만들기도 함)

    Guestbook클래스에 changeTitle(), changeContent() 메서드를 추가

    public void changeTitle(String title){
        this.title = title;
    }
    
    public void changeContent(String content){
        this.content = content;
    }

    modDate는 엔티티를 수정하고 save() 했을 경우에 동작함

     

    modDate 갱신 정상 동작 확인

    @Test
    public void updateTest() {
    
        Optional<Guestbook> result = guestbookRepository.findById(300L);
        
        if (result.isPresent()){
            Guestbook guestbook = result.get();
            
            guestbook.changeTitle("Change Title....");
            guestbook.changeContent("Change Content...");
            
            guestbookRepository.save(guestbook);
        }
    }

    300을 id 로 갖는 엔티티를 result로 받음

    result 값이 있으면 title과 content를 수정한 후, save()

    정상적으로 처리됨

     

    [Querydsl 테스트]

     

    검색 조건의 경우의 수

    1. '제목/내용/작성자'
    2. '제목 + 내용' / '내용 + 작성자' / '제목 + 작성자'
    3. '제목 + 내용 + 작성자'

    멤버 변수들이 많을수록 조합의 수는 많아지게 됨

    이를 대비하여 상황에 맞게 쿼리를 처리할 수 있는 Querydsl이 필요함

     

    📌Querydsl 사용법

        • BooleanBuilder를 생성

        • 조건에 맞는 구문은 Querydsl에서 사용하는 Predicate 타입의 함수를 생성

        • BooleanBuilder에 작성된 Predicate를 추가하고 실행

     

    [단일 항목 검색 테스트]

    제목(title)에 '1'이 있는 엔티티들을 검색하는 경우 예시

    package org.zerock.guestbook.repository;
    
    import com.querydsl.core.BooleanBuilder;
    import com.querydsl.core.types.dsl.BooleanExpression;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Sort;
    import org.zerock.guestbook.entity.Guestbook;
    import org.zerock.guestbook.entity.QGuestbook;
    
    
    import java.util.Optional;
    import java.util.stream.IntStream;
    
    @SpringBootTest
    public class GuestbookRepositoryTests {
        ```
        @Test
        public void testQuery1() {
    
            Pageable pageable = PageRequest.of(0, 10, Sort.by("gno").descending());
    
            QGuestbook qGuestbook = QGuestbook.guestbook;
    
            String keyword = "1";
    
            BooleanBuilder builder = new BooleanBuilder();
    
            BooleanExpression expression = qGuestbook.title.contains(keyword);
    
            builder.and(expression);
    
            Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);
    
            result.stream().forEach(guestbook -> {
                System.out.println(guestbook);
            });
        }
    }

    • 동적 처리를 위해 Q도메인 클래스(QGuestbook)를 얻어옴 (엔티티 클래스에 선언된 title, content 같은 필드들을 변수로 활용 가능해짐)

    • 만든 조건은 where문에 and나 or같은 키워드와 결합시킴

     

    Page<T>: 페이지 정보를 담는 인터페이스

    Pageable: 페이지 처리에 필요한 정보를 담게 되는 인터페이스

     

    과정

    PageRequest에 의해 Pageable에 페이징 정보가 담겨 객체화됨

    Pageable이 JpaRepository가 상속된 인터페이스의 메서드에 파라미터로 전달됨

    앞의 메서드의 return으로 Page<T>가 전달됨

    전달된 Page<T>에 담겨진 Page정보를 바탕으로 로직을 처리

     

    PageRequest의 메서드

    of(int page, int size): 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음

    of(int page, int size, Sort sort): 페이지 번호와 개수, 정렬 관련 정보

     

    BooleanBuilder

    쿼리를 조건별로 쌓아서 동적 쿼리로 쓸 수 있음

    where문에 들어가는 조건들을 넣어주는 컨테이너

    GuestbookRepository에 추가된 QuerydslPredicateExecutor 인터페이스의 findAll() 사용 가능

    and나 or 사용 가능

     

    BooleanExpression

    메서드가 늘어나지만 메서드 명을 통해 어떤 기능인지 확인이 가능다는 것 등 가독성이 증가하며, 메서드를 이용하므로 재사용성이 증가

    Predicate의 구현체

    null일 때 무시될 수 있고, and 또는 or절을 통해 조합 가능

     

    BooleanExpression ⇨ qGuestbook.title.contains(keyword) 같이 표현식의 결과로 반환되는 값

    BooleanBuilder  이러한 표현식을 모아서 사용할 수 있도록 도와주는 도구

     

    참고: Querydsl - 동적쿼리(BooleanBuilder, BooleanExpression) : 네이버 블로그 (naver.com)

     

    Hibernate: 
        select
            guestbook0_.gno as gno1_0_,
            guestbook0_.moddate as moddate2_0_,
            guestbook0_.regdate as regdate3_0_,
            guestbook0_.content as content4_0_,
            guestbook0_.title as title5_0_,
            guestbook0_.writer as writer6_0_ 
        from
            guestbook guestbook0_ 
        where
            guestbook0_.title like ? escape '!' 
        order by
            guestbook0_.gno desc limit ?
    Hibernate: 
        select
            count(guestbook0_.gno) as col_0_0_ 
        from
            guestbook guestbook0_ 
        where
            guestbook0_.title like ? escape '!'

     where조건절에서 title에 대한 처리가 온전히 처리되는 것을 볼 수 있음

     

    [다중항목 검색 테스트]

     

    앞서 정리한 것 처럼 BoolenaBuilder는 and() 혹은 or()의 파라미터로 BooleanBuilder를 전달 가능하기 때문에 복합적인 쿼리 생성 가능

    @Test
    public void testQuery2() {
    
        Pageable pageable = PageRequest.of(0, 10, Sort.by("gno").descending());
    
        QGuestbook qGuestbook = QGuestbook.guestbook;
    
        String keyword = "1";
    
        BooleanBuilder builder = new BooleanBuilder();
    
        BooleanExpression exTitle = qGuestbook.title.contains(keyword);
    
        BooleanExpression exContent = qGuestbook.content.contains(keyword);
    
        BooleanExpression exAll = exTitle.or(exContent); // exTitle과 exContent 조건 결합
    
        builder.and(exAll); // BooleanBuilder에 exAll 추가
    
        builder.and(qGuestbook.gno.gt(0L)); // 'gno가 0보다 크다' BooleanBuilder에 추가
    
        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);
        
        result.stream().forEach(System.out::println);
    }

     

    쿼리문 일부

    Hibernate: 
        select
            count(guestbook0_.gno) as col_0_0_ 
        from
            guestbook guestbook0_ 
        where
            (
                guestbook0_.title like ? escape '!' 
                or guestbook0_.content like ? escape '!'
            ) 
            and guestbook0_.gno>?

    where절 내부에 or로 생성된 쿼리 확인 가능

     

    [서비스 계층과 DTO]

    실제 프로젝트를 작성할 경우, 엔티티 객체를 영속 계층 바깥쪽에서 사용하는 방식 보다는 DTO(Data Transfer Object)를 이용하는 방식을 권장함

     

    DTO

    • 엔티티 객체와 달리 각 계층끼리 주고받는 우편물이나 상자의 개념

    순수하게 데이터를 담고 있다는 점에서 엔티티 객체와 유사

    목적이 데이터의 전달이므로, 읽고 쓰는 것이 허용되는 것이 가능하고, 일회성으로 사용하는 성격이 강하다는 점이 엔티티 객체와 다름

     

    DTO는 일회성으로 데이터를 주고받는 용도인 반면, 엔티티 객체는 실제 데이터 베이스와 관련있고, 엔티티 매니저가 관리하는 객체이므로, 생명주기가 전혀 다르기 때문에 분리해서 처리하는 것을 권장함

     

    *웹 애플리케이션을 제작할 때는 HttpServletRequest나 HttpServletResponse를 서비스 계층으로 전달하지 않는 것이 원칙

    *엔티티 객체가 JPA에서 사용하는 객체이므로 JPA외에서 사용하지 않는 것을 권장

     


     

    서비스 계층에서 DTO로 파라미터 리턴 타입을 처리하도록 구성

    DTO 사용시 이점: 엔티티 객체의 범위를 한정할 수 있기 때문에 좀 더 안전한 코드 작성 가능 + 화면과 데이터 분리 취지에도 더 부합함

    DTO 사용시 단점: Entity와 유사한 코드를 중복으로 개발함 + 엔티티 객체 → DTO객체 또는 DTO객체  엔티티 객체의 변환 과정 필요

     

    dto/GuestbookDTO 생성

    package org.zerock.guestbook.dto;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.time.LocalDateTime;
    
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class GuestbookDTO {
        
        private Long gno;
        private String title;
        private String content;
        private String writer;
        private LocalDateTime regDate, modDate;
    }

    Guestbook 필드와 거의 비슷한 필드를 가짐

    getter/setter를 통해 자유롭게 값을 변경 가능하게 구성함 (@Data)

     

    서비스 계층: GuestbookDTO를 이용해서 필요한 내용을 전달받고, 반환하도록 처리

    GuestbookService 인터페이스

    package org.zerock.guestbook.service;
    
    import org.zerock.guestbook.dto.GuestbookDTO;
    
    public interface GuestbookService {
        Long register(GuestbookDTO dto);
    }

    GuestbookServiceImpl 클래스

    package org.zerock.guestbook.service;
    
    import lombok.extern.log4j.Log4j2;
    import org.springframework.stereotype.Service;
    import org.zerock.guestbook.dto.GuestbookDTO;
    
    @Service
    @Log4j2
    public class GuestbookServiceImpl implements GuestbookService {
        
        @Override
        public Long register(GuestbookDTO dto) {
            return null;
        }
    }

    스프링에서 빈으로 처리되도록 @Service 추가

     

    [등록과 DTO를 엔티티로 변환하기]

    서비스 계층에서는 파라미터를 DTO로 타입으로 받음 → JPA로 처리하려면 엔티티 타입의 객체로 변환하는 작업이 필수!

     

    해당 기능 구현 방법

        • DTO 클래스에 해당 기능 적용

        • ModelMapper라이브러리 이용

        • MapStruct라이브러리 이용

        • 직접 처리 ✓

     

    기존에 만든 DTO와 엔티티 클래스를 변경하지 않기 위해 예제에서는 GuestbookServie인터페이스에 default 메서드를 이용해서 이를 처리하는 형태로 작업

    GuestbookService 인터페이스 수정

    public interface GuestbookService {
        Long register(GuestbookDTO dto);
        
        default Guestbook dtoToEntity(GuestbookDTO dto) {
            Guestbook entity = Guestbook.builder()
                    .gno(dto.getGno())
                    .title(dto.getTitle())
                    .content(dto.getContent())
                    .writer(dto.getWriter())
                    .build();
            return entity;
        }
    }

    default를 이용해서 추상 클래스를 생략하고 구현 클래스에서 동작 가능한 dtoToEntity() 생성

     

    GuestbookServiceImpl 클래스

    @Override
    public Long register(GuestbookDTO dto) {
        
        log.info("DTO----------------------");
        log.info(dto);
    
        Guestbook entity = dtoToEntity(dto);
        
        log.info(entity);
        
        return null;
    }

    GuestbookDto 타입의 dto를 Guestbook타입의 entity로 변환 (dto entity)

     

     

Designed by Tistory.