---
description: 프로젝트 구조 만들기
---

# 4장 : Spring MVC/JPA/Thymeleaf 연습

Todo

* 프로젝트의 계층별 구조와 객체들의 구성
* Querydsl을 이용해서 동적으로 검색 조건을 처리하는 방법
* Entity 객체와 DTO의 구분
* 화면에서의 페이징 처리

## 4.1 프로젝트의 와이어 프레임

### 4.1.1 프로젝트의 화면 구성

* 목록 화면 : 전체 목록을 페이징 처리해서 조회할 수 있고, 제목/내용/작성자 항목으로 검색과 페이징 처리를 가능하게 함
* 등록 화면 : 새로운 글을 등록할 수 있고, 등록 처리 후 다시 목록 화면으로 이동 함
* 조회 화면 : 목록 화면에서 특정한 글을 선택하면 자동으로 조회 화면으로 이동함. 조회화면에서는 수정/삭제가 가능한 화면으로 버튼을 클릭해서 이동
* 수정/삭제 화면 : 수정 화면에서 삭제가 가능하고 삭제 후에는 목록 페이지로 이동함. 글을 수정하는 경우에는 다시 조회 화면으로 이동해서 수정된 내용을 확인할 수 있음

### 4.1.2 프로젝트 생성

**방명록 프로젝트**

* Spring Data JPA를 활용해서 계층을 처리하고 Thymeleaf를 이용하는 화면을 처리

Spring Initializer

* Gradle
* Java
* Spring Boot : 2.6.5
* Group : com.springweb
* Artifact : guestbook

build.gradle

```bash
buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

plugins {
	id 'org.springframework.boot' version '2.6.4'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'war'
	id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}

group = 'com.springweb'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
//		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"

	testCompileOnly 'junit:junit:4.12'
	testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.8.1'

	implementation 'com.querydsl:querydsl-jpa'

}

tasks.named('test') {
	useJUnitPlatform()
}

def querydslDir = "$buildDir/generated/querydsl"
querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}

sourceSets {
	main.java.srcDir querydslDir
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}
```

application.properties

```bash
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guestbook
spring.datasource.username=본인유저네임ㄱㄱ
spring.datasource.password=본인비밀번호ㄱㄱ 

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

spring.thymeleaf.cache=false
```

4.1.3 컨트롤러/화면 관련 준비

GuestbookController

```java
package com.springweb.guestbook.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/guestbook")
@Log4j2
public class GuestbookController {

    @GetMapping({"/", "/list"})
    public String list(){
        log.info("list .............. ");

        return "/guestbook/list";
    }

}
```

화면 → 생략 (앞장에 있음)

### 4.2 자동으로 처리되는 날짜/시간 설정

BaseEntity

```java
package com.springweb.guestbook.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class})
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name = "moddate")
    private LocalDateTime modDate;
}
```

* @MappedsuperClass

JPA

* JPA는 JPA만의 고유한 메모리 공간(이하 Context)을 이용해서 엔티티 객체들을 관리

MyBatis를 이용하는 경우에는 SQL을 위해서 전달되는 객체는 모두 SQL처리가 끝난 후에는 어떻게 되든 상관이 없는 객체들인 반면 JPA에서 사용하는 엔티티 객체들을 영속 콘텍스트라는 곳에서 관리되는 객체.

이 객체들이 변경되면 결과적으로 데이터베이스에 이를 반영하는 방식.

GuestbookApplication

```java
package com.springweb.guestbook;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class GuestbookApplication {

	public static void main(String[] args) {
		SpringApplication.run(GuestbookApplication.class, args);
	}

}
```

* @EnableJpaAuditing

## 4.3 엔티티 클래스와 Querydsl 설정

GuestbookApplication

```java
package com.springweb.guestbook.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;

    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;
}
```

GuestbookRepository 인터페이스

```java
package com.springweb.guestbook.repository;

import com.springweb.guestbook.entity.Guestbook;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

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

### 4.3.1 동적 쿼리 처리를 위한 Querydsl 설정

**Querydsl**

* 복잡한 조함을 이용하는 경우의 수가 많은 상황에서느 동적으로 쿼리를 생성해서 처리할 수 있는 기능이 필요할 때 이를 처리할 수 있는 기술

build.gradle은 위에와 동일 (위에서 이미 querydsl 설정하고 올림)

추가적으로

**compileQuerydsl 오류**

[https://www.inflearn.com/questions/355723](https://www.inflearn.com/questions/355723)

ㅅㅂ ㅠㅜ

단계들은 차례로

* plugins 항목에 querydsl 관련 부분을 추가
* dependencies 항목에 필요한 라이브러리 추가
* Gradle에서 사용할 추가적인 task를 추가
* GuestbookRepository 인터페이스에서 QuerydslPredicateExcutor 인터페이스 추가 상속

### 4.3.2 엔티티의 테스트

GuestbookRepositoryTests

```java
package com.springweb.guestbook.repository;

import com.springweb.guestbook.entity.Guestbook;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class GuestbookRepositoryTest {

    @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));
        });
    }

}
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FbrWRDBWfWv3b2FHygsVW%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%208.49.55.png?alt=media\&token=acec89e8-72f4-488e-9594-f002cfc9ced7)

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2F3N32VKxYoUJQI5zDeIrZ%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%208.50.49.png?alt=media\&token=414ab90d-73b8-44cd-8969-9b71a40ef49b)

요런식 &#x20;

엔티티 클래스는 가능하면 setter 관련 기능을 만들지 않는 것이 권장 사항이지만, 필요에 따라서 수정 기능을 만들기도

(하지만 엔티티 객체가 애플리케이션 내부에서 변경되면 JPA를 관리하는 쪽이 복잡해질 우려가 있기 때문에 가능하면 최소한의 수정이 가능하도록 하는 것을 권장)





Guestbook

```java
package com.springweb.guestbook.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;

    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;

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

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

GuestBookRepositoryTest

```java
@Test
    public void updateTest() {
        Optional<Guestbook> result = guestbookRepository.findById(300L);

        if(result.isPresent()) {
            Guestbook guestbook = result.get();

            guestbook.changeTitle("Changed Title . . .");
            guestbook.changeContent("Changed Content . . .");

            guestbookRepository.save(guestbook);
        }
    }
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2F10t6x1ieySn5WfOXwci5%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.14.19.png?alt=media\&token=67963bdb-83a8-4e8f-bfbc-0ea372b030c6)

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FOaGGokcZuVBX0xSlqPWF%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.14.48.png?alt=media\&token=b79b13c4-3ce0-443d-a4e1-ccc270c2a4dc)

update 시 moddate도 잘 업데이트 됨\~

### 4.3.3 Querydsl 테스트

Querydsl 실습

* ‘제목/내용/작성자’와 같이 단 하나의 항목으로 검색하는 경우
* ‘제목 + 내용’ / ’내용 + 작성자’ / ‘제목 + 작성자’와 같이 2개의 항목으로 검색하는 경우
* ‘제목 + 내용 + 작성자’ 같이 3개의 항목으로 검색하는 경우

Querydsl의 사용법

* BooleanBuilder 생성
* 조건에 맞는 구무은 Querydsl에서 사용하는 Predicate 타입의 함수를 생성
* BooleanBuilder에 작성된 Predicate를 추가하고 실행

**단일 항목 검색 테스트**

‘제목(title)’에 ‘1’이라는 글자가 있는 엔티티들 검색

GuestbookRepositoryTest 중 일부

```java
@Test
    public void testQuery1() {
        Pageable pageable = PageRequest.of(0,10, Sort.by("gno").descending());

        QGuestbook qGuestbook = QGuestbook.guestbook; //1

        String keyword = "1";

        BooleanBuilder builder = new BooleanBuilder(); //2

        BooleanExpression expression = qGuestbook.title.contains(keyword); //3

        builder.and(expression); //4

        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable); //5

        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });
    }
```

1. 가장 먼저 동적으로 처리하기 위해서 Q도메인 클래스를 얻어옴. Q도메인 클래스를 이용하면 엔티티 클래스에 선언된 title, content같은 필드들을 변수로 활용할 수 있음
2. BooleanBuilder는 where문에 들어가는 조건들을 넣어주는 컨테이너
3. 원하는 조건은 필드 값과 같이 결합해서 생성. BooleanBuilder 안에 들어가는 값은 com.querydsl.core.types.Predicate 타입이어야 함
4. 만들어진 조건은 where문에 and나 or 같은 키워드와 결합
5. BooleanBuilder는 GuestbookRepository에 추가된 QueryPredicateExcutor 인터페이스의 findAll()을 사용할 수 있음ㅁ

이를 통해 페이지 처리와 동시에 검색 처리가 가능해짐&#x20;

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FLbTCZ5nENMQDK1qSig6k%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.35.03.png?alt=media\&token=ebcd559b-90e5-4952-a336-ec185a7faa35)



**다중 항목 검색 테스트**

복합 조건은 여러 조건이 결합된 형태로 예를 들면,

‘제목(title) 혹은 내용(content)’에 특정한 키워드가 있고 ‘gno가 0보다 크다'와 같은 조건 처리

GuestbookRepositoryTest 중 일부

```java
@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);

        builder.and(exAll);

        builder.and(qGuestbook.gno.gt(0L));

        Page<Guestbook> result = guestbookRepository.findAll(builder, pageable);

        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });
    }
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FvZklfX8NCUEtFcjK18Lw%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.44.52.png?alt=media\&token=0e25a4bd-32e0-4dd2-87ff-b7f68ef38861)



## 4.4 서비스 계층과 DTO

JPA를 이용하게 되면 엔티티 객체는 단순히 데이터를 담는 객체가 아니라 실제 데이터베이스와 관련이 있고, 내부적으로 엔티티 매니저(entity manager)가 관리하는 객체이다.

DTO 가 일회성으로 데이터를 주고받는 용도로 사용되는 것과 달리 생명주기(life cycle)도 전혀 다르기 때문에 분리해서 처리하는 것을 권장한다.

! 웹 애플리케이션을 제작할 때는 HttpServletRequest나 HttpservletResponse를 서비스 계층으로 전달하지 않는 것을 원칙으로 한다. 유사하게 엔티티 객체가 JPA에서 사용하는 객체이므로 JPA 외에서 사용하지 않는 것을 권장한다.

GuestbookDTO

```java
package com.springweb.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, modate;
}
```

GuestbookService 인터페이스

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;

public interface GuestbookService {
    Long register(GuestbookDTO dto);
}
```

GuestbookServiceImpl

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service
@Log4j2
public class GuestbookServiceImpl implements GuestbookService{
    @Override
    public Long register(GuestbookDTO dto) {
        return null;
    }
}
```

### 4.4.1 등록과 DTO를 엔티티로 변환하기

서비스 계층에서는 파라미터를 DTO Type으로 받기 때문에 이를 JPA로 처리하기 위해서 Entity Type의 객체로 변환해야 하는 작업이 필요

* ModelMapper
* MapStruct
* 여기서는 직접

GuestbookService 인터페이스

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import com.springweb.guestbook.entity.Guestbook;

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;
    }
}
```

GuestbookServiceImpl

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import com.springweb.guestbook.entity.Guestbook;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service
@Log4j2
public class GuestbookServiceImpl implements GuestbookService{
    @Override
    public Long register(GuestbookDTO dto) {
        log.info("DTO-------------------------");
        log.info(dto);

        Guestbook entity = dtoToEntity(dto);

        log.info(entity);
        
        return null;
    }
}
```

GuestbookServiceTest

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class GuestbookServiceTest {

    @Autowired
    private GuestbookService service;

    @Test
    public void testRegister() {

        GuestbookDTO guestbookDTO = GuestbookDTO.builder()
                .title("Sample Title...")
                .content("Sample Content...")
                .writer("user0")
                .build();
        System.out.println(service.register(guestbookDTO));
    }
}
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FfWLkM2nIBicdChZe8gLh%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2010.04.42.png?alt=media\&token=4ca1abdc-b276-4ec2-9b31-cba133f82a7d)

GuestbookServiceImpl

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import com.springweb.guestbook.entity.Guestbook;
import com.springweb.guestbook.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service
@Log4j2
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService{

    private final GuestbookRepository repository;

    @Override
    public Long register(GuestbookDTO dto) {
        log.info("DTO-------------------------");
        log.info(dto);

        Guestbook entity = dtoToEntity(dto);

        log.info(entity);

        repository.save(entity);

        return entity.getGno();
    }
}
```

* GuestbookServiceImpl 클래는 JPA 처리를 위해서 GuestbookRepository를 주입
* 클래스 선언 시에 @RequiredArgsConstructor를 이용해서 자동으로 주입
* register()의 내부에서는 save()를 통해서 저장
* 저장된 후 해당 엔티티가 가지는 gno값을 반환

testRegister() ㄱ ㄱ

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2Froxr81qbZvN05pEkH32t%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2010.19.45.png?alt=media\&token=0457945d-ad77-46e4-a8e0-effb67716ab8)

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FhlpK2fKJW42qPbaq8lRb%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2010.20.57.png?alt=media\&token=ddab6ed1-7eb3-4498-9b29-ff0f407aa075)





## 4.5 목록 처리

* 화면에서 필요한 목록 데이터에 대한 DTO 생성
* DTO를 Pageable 타입으로 전환
* Page\<Entity>를 화면에서 사용하기 쉬운 DTO의 리스트 등으로 변환
* 화면에 필요한 페이지 번호 처리

### 4.5.1 목록 처리를 위한 DTO

<mark style="color:purple;">페이지 요청 처리 DTO</mark>

PageRequestDTO

```java
package com.springweb.guestbook.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {

    private int page;
    private int size;

    public PageRequestDTO() {
        this.page = 1;
        this.size = 10;
    }

    public Pageable getPageable(Sort sort) {
        return PageRequest.of(page - 1, size, sort);
    }
}
```

<mark style="color:purple;">페이지 결과 처리 DTO</mark>

PageResultDTO

```java
package com.springweb.guestbook.dto;

import lombok.Data;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
public class PageResultDTO<DTO, EN> {

    private List<DTO> dtoList;

    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn) {
        dtoList = result.stream().map(fn).collect(Collectors.toList());
    }
}
```

### 4.5.2 서비스 계층에서는 목록 처리

GuestbookService 인터페이스

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import com.springweb.guestbook.dto.PageRequestDTO;
import com.springweb.guestbook.dto.PageResultDTO;
import com.springweb.guestbook.entity.Guestbook;

public interface GuestbookService {

    Long register(GuestbookDTO dto);

    PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO); // ***

    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 GuestbookDTO entityToDto(Guestbook entity) {

        GuestbookDTO dto = GuestbookDTO.builder()
                .gno(entity.getGno())
                .title(entity.getTitle())
                .content(entity.getContent())
                .writer(entity.getWriter())
                .regDate(entity.getRegDate())
                .modDate(entity.getModDate())
                .build();
        return dto;
    }
}
```

GuestbookserviceImpl

```java
package com.springweb.guestbook.service;

import com.springweb.guestbook.dto.GuestbookDTO;
import com.springweb.guestbook.dto.PageRequestDTO;
import com.springweb.guestbook.dto.PageResultDTO;
import com.springweb.guestbook.entity.Guestbook;
import com.springweb.guestbook.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.function.Function;

@Service
@Log4j2
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService{

    private final GuestbookRepository repository;

    @Override
    public Long register(GuestbookDTO dto) {
        log.info("DTO-------------------------");
        log.info(dto);

        Guestbook entity = dtoToEntity(dto);

        log.info(entity);

        repository.save(entity);

        return entity.getGno();
    }

    @Override
    public PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO) {

        Pageable pageable = requestDTO.getPageable(Sort.by("gno"));
        
        Page<Guestbook> result = repository.findAll(pageable);

        Function<Guestbook, GuestbookDTO> fn = (entity -> entityToDto(entity));

        return new PageResultDTO<>(result, fn);
    }
}
```

<mark style="color:purple;">목록 처리 테스트</mark>

GusetbookServiceTest

```java
@Test
    public void testList(){
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
        PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);
        for (GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
            System.out.println(guestbookDTO);
        }

    }
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2F337VGemmtHV1NKmSe3kT%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2010.51.48.png?alt=media\&token=515f25db-440f-4ab4-bb75-37413f932cbe)



목록 데이터 페이지 처리

* 화면에서 시작 페이지 번호(start)
* 화며에서 끝 페이지 번호(end)
* 이전/다음 이동 링크 여부(prev, next)
* 현재 페이지 번호(page)

PageResultDTO

```java
package com.springweb.guestbook.dto;

import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Data
public class PageResultDTO<DTO, EN> {

    private List<DTO> dtoList;

    private int totalPage;

    private int page;

    private int size;

    private int start, end;

    private boolean prev, next;

    private List<Integer> pageList;

    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn) {
        dtoList = result.stream().map(fn).collect(Collectors.toList());

        totalPage = result.getTotalPages();

        makePageList(result.getPageable());
    }

    public void makePageList(Pageable pageable) {
        this.page = pageable.getPageNumber() + 1;
        this.size = pageable.getPageSize();

        int tempEnd = (int)(Math.ceil(page/10.0))*10;

        start = tempEnd - 9;
        prev = start > 1;
        end = totalPage > tempEnd ? tempEnd: totalPage;

        next = totalPage > tempEnd;

        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
    }
}
```

GuestbookServiceTest

```java
@Test
    public void testList() {

        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();

        PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);

        System.out.println("PREV: " + resultDTO.isPrev());
        System.out.println("NEXT: " + resultDTO.isNext());
        System.out.println("TOTAL: " + resultDTO.getTotalPage());
        System.out.println("----------------------------------");
        for (GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
            System.out.println(guestbookDTO);
        }

        System.out.println("==================================");
        resultDTO.getPageList().forEach(i -> System.out.println(i));
    }
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FlrQnM3uJ3uJbM5Lc12i8%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2011.33.46.png?alt=media\&token=79b6bfee-80ce-4ed5-b481-7c4b7378913b)



## 4.6 컨트롤러와 화면에서의 목록 처리

GuestbookController

```java
package com.springweb.guestbook.controller;

import com.springweb.guestbook.dto.PageRequestDTO;
import com.springweb.guestbook.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    private final GuestbookService service;

    @GetMapping("/")
    public String index() {

        return "redirect:/guestbook/list";
    }

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model) {
        log.info("list..............." + pageRequestDTO);
        model.addAttribute("result", service.getList(pageRequestDTO));
    }
}
```

list.html

```java
package com.springweb.guestbook.controller;

import com.springweb.guestbook.dto.PageRequestDTO;
import com.springweb.guestbook.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    private final GuestbookService service;

    @GetMapping("/")
    public String index() {

        return "redirect:/guestbook/list";
    }

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model) {
        log.info("list..............." + pageRequestDTO);
        model.addAttribute("result", service.getList(pageRequestDTO));
    }
}
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2Fsq5eqpgYT1sfWsN1Iy9P%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2011.46.09.png?alt=media\&token=eb13702f-8324-49db-9b8d-a2a46b74fef6)



### 4.6.1 목록 페이지 처리

* /guestbook/list 혹은 /guestbook/list?page=1의 경우 아래와 같이 1페이지 출력

list.html

```java
<!DOCTYPE html>
<html lang="en" xmlns:th="<http://www.thymeleaf.org>">
<th:block th:replace="~{/layout/basic :: setContent(~{this :: content})}">
    <th:block th:fragment="content">
        <h1 class=""mt-4> Guest Book List Page</h1>

        <table class="table table-striped">
            <thread>
                <th scope=""col>#</th>
                <th scope=""col>Gno</th>
                <th scope=""col>Tittle</th>
                <th scope=""col>Regdate</th>
            </thread>
            <tbody>
            <tr th:each="dto : ${result.dtoList}">
                <th scope="row">[[${dto.gno}]]</th>
                <td>[[${dto.title}]]</td>
                <td>[[${dto.writer}]]</td>
                <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
            </tr>
            </tbody>
        </table>
        <ul class="pagination h-100 justify-content-center align-item-center">
            <li class="page-item " th:if="${result.prev}">
                <a class="page-link" href="#" tabindex="-1">Previous</a>
            </li>

            <li th:class=" 'page-item ' + ${result.page == page?'active':''}" th:each="page: ${result.pageList}">
                <a class="page-link" href="#">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item " th:if="${result.next}">
                <a class="page-link" href="#">Next</a>
            </li>
        </ul>>
    </th:block>
</th:block>
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2F9T5s6uixKqEUKVGNUbzc%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2011.55.01.png?alt=media\&token=a29acce7-351d-4fe8-af5f-2133ba0e3b60)



페이지별 링크 처리

list.html

```java
<!DOCTYPE html>
<html lang="en" xmlns:th="<http://www.thymeleaf.org>">
<th:block th:replace="~{/layout/basic :: setContent(~{this :: content})}">
    <th:block th:fragment="content">
        <h1 class=""mt-4> Guest Book List Page</h1>

        <table class="table table-striped">
            <thread>
                <th scope=""col>#</th>
                <th scope=""col>Gno</th>
                <th scope=""col>Tittle</th>
                <th scope=""col>Regdate</th>
            </thread>
            <tbody>
            <tr th:each="dto : ${result.dtoList}">
                <th scope="row">[[${dto.gno}]]</th>
                <td>[[${dto.title}]]</td>
                <td>[[${dto.writer}]]</td>
                <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
            </tr>
            </tbody>
        </table>
        <ul class="pagination h-100 justify-content-center align-item-center">
            <li class="page-item " th:if="${result.prev}">
                <a class="page-link" th:href="@{/guestbook/list(page=${result.start -1})}" tabindex="-1">Previous</a>
            </li>

            <li th:class=" 'page-item ' + ${result.page == page?'active':''}" th:each="page: ${result.pageList}">
                <a class="page-link" th:href="@{/guestbook/list(page = ${page})}">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item " th:if="${result.next}">
                <a class="page-link" th:href="@{/guestbook/list(page=${result.end + 1})}">Next</a>
            </li>
        </ul>
    </th:block>
</th:block>
```

![](https://2669959621-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxncaDDrGpdjhVy86HbwM%2Fuploads%2FRgpnMePpRqrXJsTkgNO9%2F%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-03-26%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.03.10.png?alt=media\&token=41ccaff1-8342-46c5-b545-ee556b9209d5)
