아무거나

[springboot] SpringBoot & Handlebars로 화면 생성 [퍼옴] 본문

Java/Spring

[springboot] SpringBoot & Handlebars로 화면 생성 [퍼옴]

전봉근 2019. 7. 4. 11:26
반응형

[SpringBoot & Handlebars로 화면 생성]

Handlebars는 흔히 사용하는 Freemarker, Velocity와 같은 서버 템플릿 엔진이다.

-> 현재 Freemarker, Velocity는 몇년동안 업데이트가 되지 않아서 사실상 springboot에서 권장하지 않는다. 

   Freemarker는 프리뷰버전은 계속나오고 있지만 릴리즈가 2015년이다. 현재까지 꾸준하게 업데이트 되고있는

   Handlebars나 Thymeleaf이다. spring에선 Thymeleaf를 밀고 있음.

   [Handlebars 특징]

   (1) 문법이 다른 템플릿엔진보다 간단하고 

   (2) 로직 코드를 사용할 수 없어 View의 역할과 서버의 역할을 명확하게 제한할 수 있으며 

   (3) Handlebars.js와 Handlebars.java 2가지가 다 있어, 하나의 문법으로 클라이언트 템플릿/서버 템플릿을 모두 사용할 수 있습니다.

 

1. gradle에 의존성 추가(Handlebars는 아직 정식 SpringBoot starter 패키지가 존재하진 않지만, 많은 분들이 사용중이신 라이브러리인 handlebars-spring-boot-starter 추가)

   [build.gradle]

   compile 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.2.15'

    

   - 다른 서버 템플릿 스타터 패키지와 마찬가지로 Handlebars도 기본 경로는 src/main/resources/templates가 됩니다.

     Tip) 혹시 IntelliJ를 사용중이시라면 아래와 같이 Handlebars 플러그인을 설치하면 문법체크 등과 같이 많은 지원을 받을 수 있습니다. (handlebars으로 검색)

          

2. 메인 페이지 생성

   - src/main/resources/templates에 main.hbs 파일을 생성

     [main.hbs]

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
    <h1>스프링부트로 시작하는 웹 서비스</h1>
</body>
</html>

 

   - main.hbs 파일을 호출하기 위해 컨트롤러를 생성합니다. src/main/java/com/bk/webservice/web패키지안에 webController을 만든다.

     [WebController.java]

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@AllArgsConstructor
public class WebController {
    @GetMapping("/")
    public String main() {
		return "main";
    }
}

 

Spring4.3부터는 @RequestMapping을 대체하는 @GetMapping이 나왔다(=@RequestMapping(value="/", method = RequestMethod.GET)와 동일)

        또한 handlebars-spring-boot-starter 덕분에 컨트롤러에서 문자열을 반환할때 앞의 path와 뒤의 파일 확장자는 자동으로 지정됩니다. 

        (prefix: src/main/resources/templates, suffix: .hbs)  즉 여기선 "main"을 반환하니, 

src/main/resources/templates/main.hbs로 전환되어 View Resolver가 처리하게 됩니다. 

        (ViewResolver는 URL 요청의 결과를 전달할 타입과 값을 지정하는 관리자 격으로 보시면 됩니다.)

 

3. 메인 페이지 테스트 코드 생성 후 확인

   - src/test/java/com/bk/webservice/web에 WebControllerTest 클래스를 생성

     [WebControllerTest.java]

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class WebControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void 메인페이지_로딩() {
		//when
		String body = this.restTemplate.getForObject("/", String.class);

		//then
		assertThat(body).contains("스프링부트로 시작하는 웹 서비스");
    }
}

 

   - 테스트를 통과하면 부트앱을 실행하여 localhost:8080 에서 확인한다.

 

4. 게시글 등록 기능 생성

   - Service 메소드를 생성하여 트랜잭션까지 관리할 것이다.

     [PostsService.java] 

// src/main/java/com/bk/webservice/
import com.bk.webservice.domain.posts.PostsRepository;
import com.bk.webservice.dto.posts.PostsSaveRequestDto;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@AllArgsConstructor
@Service
public class PostsService {
    private PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto dto){
		return postsRepository.save(dto.toEntity()).getId();
    }
}

 

Tip) 

Controller에서 Dto.toEntity를 통해서 바로 전달해도 되는데 굳이 Service에서 Dto를 받는 이유는 간단합니다. 

Controller와 Service 의 역할을 분리하기 위함입니다. 

비지니스 로직 & 트랜잭션 관리는 모두 Service에서 관리하고, View 와 연동되는 부분은 Controller에서 담당하도록 구성합니다.

 

Tip) 트랜잭션? 

일반적으로 DB 데이터를 등록/수정/삭제 하는 Service 메소드는 @Transactional를 필수적으로 가져갑니다. 

이 어노테이션이 하는 일은 간단합니다. 

메소드 내에서 Exception이 발생하면 해당 메소드에서 이루어진 모든 DB작업을 초기화 시킵니다. 

즉, save 메소드를 통해서 10개를 등록해야하는데 5번째에서 Exception이 발생하면 앞에 저장된 4개까지를 전부 롤백시켜버립니다. 

(정확히 얘기하면, 이미 넣은걸 롤백시키는건 아니며, 모든 처리가 정상적으로 됐을때만 DB에 커밋하며 그렇지 않은 경우엔 커밋하지 않는것입니다.)

 

   - Service 메소드가 생성되었으니 작동여부 확인을 위하여 간단한 테스트 코드를 추가하자.

     [PostServiceTest.java] 

// src/test/java/com/bk/webservice/service 패키지 생성
import com.jojoldu.webservice.domain.posts.Posts;
import com.jojoldu.webservice.domain.posts.PostsRepository;
import com.jojoldu.webservice.dto.posts.PostsSaveRequestDto;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostServiceTest {

    @Autowired
    private PostsService postsService;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void cleanup () {
		postsRepository.deleteAll();
    }

    @Test
    public void Dto데이터가_posts테이블에_저장된다 () {
		//given
		PostsSaveRequestDto dto = PostsSaveRequestDto.builder()
		.author("jojoldu@gmail.com")
		.content("테스트")
		.title("테스트 타이틀")
		.build();

		//when
		postsService.save(dto);

		//then
		Posts posts = postsRepository.findAll().get(0);
		assertThat(posts.getAuthor()).isEqualTo(dto.getAuthor());
		assertThat(posts.getContent()).isEqualTo(dto.getContent());
		assertThat(posts.getTitle()).isEqualTo(dto.getTitle());
    }
}

 

   - 위의 테스트 코드를 돌리시려면 PostsSaveRequestDto에 Builder가 필요합니다. 추가해줍시다.

     [PostsSaveRequestDto.java]

// src/main/java/com/bk/webservice/dto/posts/
@Getter
@Setter
@NoArgsConstructor
public class PostsSaveRequestDto {

    private String title;
    private String content;
    private String author;

    @Builder
    public PostsSaveRequestDto(String title, String content, String author) {
		this.title = title;
		this.content = content;
		this.author = author;
    }

    public Posts toEntity(){
		return Posts.builder()
		.title(title)
		.content(content)
		.author(author)
		.build();
    }
}

 

   - 위의 테스트가 잘 통과되었음을 확인하고 WebRestController의 save 메소드를 service의 save로 교체한다.

     [WebRestController.java]

import com.jojoldu.webservice.dto.posts.PostsSaveRequestDto;
import com.jojoldu.webservice.service.PostsService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@AllArgsConstructor
public class WebRestController {

    private PostsService postsService;

    @GetMapping("/hello")
    public String hello() {
		return "HelloWorld";
    }

    @PostMapping("/posts")
    public Long savePosts(@RequestBody PostsSaveRequestDto dto){
		return postsService.save(dto);
    }
}

 

5. 게시글 등록 화면 생성(bootstrap)

   - https://getbootstrap.com/ 들어가서 부트스트랩을 다운받아서 아래 경로로 저장한다.

     # dist 폴더 아래에 있는 css폴더에서 bootstrap.min.css를 src/main/resources/static/css/lib로 복사합니다.

     # 마찬가지로 dist 폴더 아래에 있는 bootstrap.min.js를 src/main/resources/static/js/lib로 복사합니다.

 

   - https://jquery.com/download/ Download the compressed, production jQuery 3.2.1 을 클릭해 다운받습니다.

     jquery-3.2.1.min.js -> jquery.min.js 로 이름을 변경

     src/main/resources/static/js/lib 로 복사합니다.

 

   - 파일을 다 복사하셨으면 main.hbs 파일에 아래와 같이 라이브러리를 추가합니다.

     [main.hbs]   

// src/main/resources
<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- bootstrap css add -->
    <link rel="stylesheet" href="/css/lib/bootstrap.min.css" />
</head>
<body>
    <h1>스프링부트로 시작하는 웹 서비스</h1>

    <!-- bootstrap js, jquery add -->
    <script src="/js/lib/jquery.min.js"></script>
    <script src="/js/lib/bootstrap.min.js"></script>
</body>
</html>

 

css와 js 호출 주소를 보면 /로 바로 시작하는데요. 

SpringBoot는 기본적으로 src/main/resources/static은 URL에서 / 로 지정됩니다. 

그래서 src/main/resources/static/js/..., src/main/resources/static/css/... 등은 URL로 호출시엔 도메인/js/..., 도메인/css/... 로 호출할 수 있습니다.

 

여기서 css와 js의 위치가 서로 다릅니다. 

css는 <head>에, js는 <body> 최하단에 두었습니다. 

이렇게 하는 이유는 페이지 로딩속도를 높이기 위함입니다.

 

HTML은 최상단에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행됩니다. 

즉, head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출됩니다. 

특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는것이 좋습니다.

 

반면 css는 화면을 그리는 역할을 하기 때문에 head에서 불러오는것이 좋습니다.

추가로, bootstrap.js의 경우 jquery가 꼭 있어야만 하기 때문에 bootstrap보다 먼저 호출되도록 코드를 작성했습니다.

 

   - 라이브러리는 추가되었으니 입력화면을 만듭니다.

     [main.hbs]   

// src/main/resources
<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- bootstrap css add -->
    <link rel="stylesheet" href="/css/lib/bootstrap.min.css" />
</head>
<body>
    <h1>스프링부트로 시작하는 웹 서비스</h1>

    <div class="col-md-12">
    	<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#savePostsModal">글 등록</button>
    </div>

    <!-- Modal 영역 -->
    <div class="modal fade" id="savePostsModal" tabindex="-1" role="dialog" aria-labelledby="savePostsLabel" aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="savePostsLabel">게시글 등록</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">×</span>
                    </button>
                </div>
                <div class="modal-body">
                <form>
                    <div class="form-group">
                    <label for="title">제목</label>
                    <input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
                    </div>
                    <div class="form-group">
                    <label for="author"> 작성자 </label>
                    <input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요">
                    </div>
                    <div class="form-group">
                    <label for="content"> 내용 </label>
                    <textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
                    </div>
                </form>
                </div>
                <div class="modal-footer">
                	<button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
                	<button type="button" class="btn btn-primary" id="btn-save">등록</button>
                </div>
            </div>
        </div>
    </div>

    <!-- bootstrap js, jquery add -->
    <script src="/js/lib/jquery.min.js"></script>
    <script src="/js/lib/bootstrap.min.js"></script>
</body>
</html>

    

 

   - 등록 버튼 기능 스크립트 작성

     [main.js]

     // src/main/static/js/app

var main = {
    init : function () {
		var _this = this;
		$('#btn-save').on('click', function () {
    		_this.save();
		});
    },
    save : function () {
        var data = {
            title: $('#title').val(),
            author: $('#author').val(),
            content: $('#content').val()
        };

        $.ajax({
            type: 'POST',
            url: '/posts',
            dataType: 'json',
            contentType:'application/json; charset=utf-8',
            data: JSON.stringify(data)
        }).done(function() {
            alert('글이 등록되었습니다.');
            location.reload();
        }).fail(function (error) {
            alert(error);
        });
    }
};

main.init();

 

   - 작성한 main.js를 main.hbs에 추가하자.

     [main.hbs]   

	// src/main/resources
    <!-- bootstrap js, jquery add -->
    <script src="/js/lib/jquery.min.js"></script>
    <script src="/js/lib/bootstrap.min.js"></script>

    <!-- custom js add -->
    <script src="/js/app/main.js"></script>
</body>

 

   - localhost:8080에 들어가서 확인하여 게시글 등록후 localhost:8080/h2-console에 접속해서 데이터가 들어갔는지 확인한다. 

 

6. 게시글 목록

   현재 사용중인 로컬 DB는 H2입니다. 

   H2 DB는 메모리 DB인지라 프로젝트를 실행할때마다 스키마가 새로 생성되어 테이블 구조 변경시 일일이 alter table과 같이 수정할 필요가 없습니다. 

   또한, 항상 테이블을 초기화하기 때문에 깨끗한 상태로 로컬 개발을 진행할수있다는 장점도 있습니다. 

   하지만 이로 인해, 프로젝트 코드를 수정하고 다시 실행시키면 이전에 저장해놓은 데이터가 초기화되버립니다.

   

   - src/main/resources/ 아래에 data-h2.sql 파일을 생성한다.

     [data-h2.sql]

insert into posts (title, author, content, created_date, modified_date) values ('테스트1', 'test1@gmail.com', '테스트1의 본문', now(), now()); 
insert into posts (title, author, content, created_date, modified_date) values ('테스트2', 'test2@gmail.com', '테스트2의 본문', now(), now());

 

   - 그리고 위 insert sql파일을 프로젝트 실행시에 자동으로 수행되도록 설정을 추가하겠습니다. 

     application.yml의 코드를 아래와 같이 변경합니다.

     [application.yml] src/main/resources/

spring:
  profiles:
    active: local # 기본 환경 선택

# local 환경

---

spring:
  profiles: local
  datasource:
    data: classpath:data-h2.sql # 시작할때 실행시킬 script
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true

 

spring.profile 옵션이 추가되었는데요. 

이 옵션은 특별히 어플리케이션 실행시 파라미터로 넘어온게 없으면 active 값을 보게됩니다. 

운영 환경에선 real 혹은 production 등과 같은 profile을 보도록 jar 실행시점에 파라미터를 변경합니다. 

(이 부분은 앞으로 배포 환경 구축시에 자세히 설명할 예정이니, 대충 그렇구나 정도로만 보시면 됩니다.) 

local profile에선 data-h2.sql을 초기 데이터 실행 스크립트로 지정하였습니다. 

그외 환경에선 해당 스크립트가 실행되지 않기 위해 local에 직접 등록한 것입니다.

 

Tip) 

application.yml 에서 --- 를 기준으로 상단은 공통 영역이며, 하단이 각 profile의 설정 영역입니다. 

공통영역의 값은 각 profile환경에 동일한 설정이 있으면 무시되고, 없으면 공통영역의 설정값이 사용됩니다. 

그렇다보니 공통영역에 설정값을 넣는것에 굉장히 주의가 필요합니다. 

만약 공통영역에 jpa.hibernate.ddl-auto:create-drop가 있고 운영 profile에 해당 설정값이 없다면 운영환경에서 배포시 모든 테이블이 drop -> create 됩니다. 

이때문에 datasource, table 등과 같은 옵션들은 공통영역엔 두지 않고 각 profile마다 별도로 두는것을 추천합니다

 

** 앱 실행후 localhost:8080/h2-console 들어가서 자동으로 데이터가 들어갔는지 확인하자.

 

   - 글 목록을 표시하기 위해 main.hbs를 수정하자

     [main.hbs]   src/main/resources

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- bootstrap css add -->
    <link rel="stylesheet" href="/css/lib/bootstrap.min.css" />
</head>
<body>
    <h1>스프링부트로 시작하는 웹 서비스</h1>

    <div class="col-md-12">
        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#savePostsModal">글 등록</button>
        <br/>
        <br/>

        <!-- 목록 출력 영역 -->
        <table class="table table-horizontal table-bordered">
        <thead class="thead-strong">
        <tr>
        <th>게시글번호</th>
        <th>제목</th>
        <th>작성자</th>
        <th>최종수정일</th>
        </tr>
        </thead>
        <tbody id="tbody">
        {{#each posts}}
        <tr>
        <td>{{id}}</td>
        <td>{{title}}</td>
        <td>{{author}}</td>
        <td>{{modifiedDate}}</td>
        </tr>
        {{/each}}
        </tbody>
        </table>
        </div>

        <!-- Modal 영역 -->
        <div class="modal fade" id="savePostsModal" tabindex="-1" role="dialog" aria-labelledby="savePostsLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="savePostsLabel">게시글 등록</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">×</span>
                        </button>
                    </div>
                    <div class="modal-body">
                    <form>
                        <div class="form-group">
                            <label for="title">제목</label>
                            <input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
                        </div>
                        <div class="form-group">
                            <label for="author"> 작성자 </label>
                            <input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요">
                        </div>
                        <div class="form-group">
                            <label for="content"> 내용 </label>
                            <textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
                        </div>
                    </form>
                    </div>
                    <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
                    <button type="button" class="btn btn-primary" id="btn-save">등록</button>
                </div>
            </div>
        </div>
    </div>

    <!-- bootstrap js, jquery add -->
    <script src="/js/lib/jquery.min.js"></script>
    <script src="/js/lib/bootstrap.min.js"></script>

    <!-- custom js add -->
    <script src="/js/app/main.js"></script>
</body>
</html>

 

{{#each posts}}는 posts라는 리스트를 순회하는 하나씩 꺼내 각각의 필드값을 채워서 테이블에 출력시킵니다.

 

   - Controller, Service, Repository 코드를 작성하겠습니다. 먼저 Repository부터, 기존에 있던 PostsRepository 인터페이스에 쿼리가 추가됩니다.

     [PostsRepository.java] src/main/bk/webservice/domain/posts/

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.stream.Stream;

public interface PostsRepository extends JpaRepository<Posts, Long> {
    @Query(
      "SELECT p " +
      "FROM Posts p " +
      "ORDER BY p.id DESC"
    )
    Stream<Posts> findAllDesc();
}

 

     실제로 위 코드는 SpringDataJpa에서 제공하는 기본 메소드만으로 해결할 수 있는데요. 

     굳이 @Query를 쓴 이유는, SpringDataJpa에서 제공하지 않는 메소드는 위처럼 쿼리로 작성해도 되는것을 보여주기 위함입니다.

 

     Tip) 

     규모가 있는 프로젝트에서의 데이터 조회는 FK의 조인, 복잡한 조건등으로 인해 이런 Entity 클래스만으로 처리하기 어려워 조회용 프레임워크를 추가로 사용합니다. 

     대표적 예로 querydsl, jooq, MyBatis 등이 있습니다. 

     조회는 위 3가지 프레임워크중 하나를 통해 조회하고, 등록/수정/삭제 등은 SpringDataJpa를 통해 진행합니다. (개인적으로는 querydsl를 강추합니다.) 

     http://www.yes24.com/24/goods/19040233 // 참고

 

   - Repository 다음으로 PostsService 코드를 변경하겠습니다.

     [PostsService.java] src/main/bk/webservice/service/

import com.jojoldu.webservice.domain.posts.PostsRepository;
import com.jojoldu.webservice.dto.posts.PostsMainResponseDto;
import com.jojoldu.webservice.dto.posts.PostsSaveRequestDto;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@AllArgsConstructor
@Service
public class PostsService {
    private PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto dto){
		return postsRepository.save(dto.toEntity()).getId();
    }

    @Transactional(readOnly = true)
    public List<PostsMainResponseDto> findAllDesc() {
		return postsRepository.findAllDesc()
				.map(PostsMainResponseDto::new)
				.collect(Collectors.toList());
    }
}

 

findAllDesc 메소드의 트랜잭션 어노테이션(@Transactional)에 옵션이 하나 추가되었습니다. 

옵션(readOnly = true)을 주면 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선되기 때문에 특별히 등록/수정/삭제 기능이 없는 메소드에선 사용하시는걸 추천드립니다. 

메소드 내부의 코드에선 람다식을 모르시면 조금 생소한 코드가 있으실텐데요.

 

.map(PostsMainResponseDto::new)는 실제로는 .map(posts -> new PostsMainResponseDto(posts))와 같습니다. 

repository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsMainResponseDto로 변환 -> List로 반환하는 메소드입니다.

 

아직 PostsMainResponseDto 클래스가 없기 때문에 이 클래스 역시 생성합니다. 

위치는 dto패키지입니다.

 

   - PostsMainResponseDto 추가 

     [PostsMainResponseDto.java] src/main/bk/webservice/dto/posts/

import com.jojoldu.webservice.domain.posts.Posts;
import lombok.Getter;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

@Getter
public class PostsMainResponseDto {
    private Long id;
    private String title;
    private String author;
    private String modifiedDate;

    public PostsMainResponseDto(Posts entity) {
        id = entity.getId();
        title = entity.getTitle();
        author = entity.getAuthor();
        modifiedDate = toStringDateTime(entity.getModifiedDate());
    }

    /**
     * Java 8 버전
     */
    private String toStringDateTime(LocalDateTime localDateTime){
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return Optional.ofNullable(localDateTime)
        .map(formatter::format)
        .orElse("");
    }

    /**
     * Java 7 버전
     */
    private String toStringDateTimeByJava7(LocalDateTime localDateTime){
        if(localDateTime == null){
            return "";
        }

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return formatter.format(localDateTime);
    }
}

 

     modifiedDate는 LocalDate를 하지 않고, String을 사용하였습니다. 

     View영역에선 LocalDateTime 타입을 모르기 때문에 인식할수 있도록 toStringDateTime을 통해 문자열로 날짜형식을 변경해서 등록하였습니다. 

     (Java8이 생소하신 분들이 계실것 같아 Java7코드도 아래에 추가하였습니다.)

 

     Tip) 

     Entity가 toDto와 같은 메소드로 dto를 반환하면 되지 않나? 라고 의문이 드실수 있습니다. 

     그렇게 하시면 절대 안됩니다. 

     DTO는 Entity를 사용해도 되지만, Entity는 DTO에 대해 전혀 모르게 코드를 구성해야합니다. 

     Entity는 말 그대로 가장 core한 클래스인 반면, DTO는 View 혹은 외부 요청에 관련 있는 클래스입니다. 

     Entity가 DTO를 사용하게 되면, 그때부터 View/외부요청에 따라 DTO뿐만 아니라 Entity까지 변경이 필요하게 됩니다. 

     또한, 다른 DTO도 필요하다고 하면 다시 Entity에 toDto2와 같은 메소드가 추가되는데, 모든 변화에 맞춰 Entity 변경이 필요하게 됩니다. 

     프로젝트 규모가 커져 프로젝트를 분리해야할때도 Entity가 DTO를 의존하고 있으면 분리하기가 굉장히 어렵기 때문에 DTO가 Entity에 의존하도록 코드를 꼭꼭 작성하시길 바랍니다.

 

   - 마지막으로 Controller를 변경하겠습니다. 기존에 생성해둔 WebController 클래스를 아래와 같이 변경합니다.

     [WebController.java] src/main/bk/webservice/web/

import com.jojoldu.webservice.service.PostsService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@AllArgsConstructor
public class WebController {
    private PostsService postsService;

    @GetMapping("/")
    public String main(Model model) {
        model.addAttribute("posts", postsService.findAllDesc());
        return "main";
    }
}

 

   - 시간이 된다면 목록출력에 사용될 Repository와 Service의 테스트 코드를 작성해보자.​ 

 

 

참조: https://jojoldu.tistory.com/ ( 해당 포스팅은 거의 기재된 사이트에서 복사한거라 참조 사이트에서 확인하시면 좋습니다. )

반응형
Comments