Super Kawaii Cute Cat Kaoani 스프링 부트와 AWS로 혼자 구현하는 웹 서비스(4)

스프링 부트와 AWS로 혼자 구현하는 웹 서비스(4)

2024. 1. 22. 01:36
728x90
SMALL

📌 머스테치로 화면 구성하기

⚡ 서버 템플릿 엔진과 머스테치 소개

템플릿 엔진 : 지정된 템플릿 양식과 데이터가 합쳐서 HTML 문서를 출력하는 소프트웨어

  • 클라이언트 템플릿 엔진 : 리액트, 뷰의 View 파일 등
    • 브라우저에서 화면 생성
    • 서버에서 이미 코드가 벗어남
  • 서버 템플릿 엔진 : JSP, Freemarker 등
    • 서버에서 구동됨
    • 서버 템플릿 엔진을 이용한 화면 생성 : 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달

 

⚡ 머스테치

✔ 머스테치 : 수많은 언어를 지원하는 가장 심플한 템플릿 엔진

  • 루비, 자바스크립트, 파이썬, PHP, 자바, 펄, Go, ASP 등 현존하는 대부분 언어를 지원함
    • 자바에서 사용할 때는 서버 템플릿엔진, 자바스크립트에서 사용될 때는 클라이언트 템플릿 엔진으로 둘 다 사용 가능

템플릿 엔진 단점

  • JSP, Velocity : 스프링 부트에서는 권장하지 않는 템플릿 엔진
  • Freemarker : 템플릿 엔진으로는 너무 과하게 많은 기능을 지원
  • Thymeleaf : 스프링 진영에서 적극적으로 밀고 있지만 문법이 너무 어려움

 

머스테치 장점

  • 문법이 다른 템플릿 엔진보다 심플
  • 로직 코드를 사용할 수 없어 View의 역할과 서버의 역할이 명확하게 분리됨
  • Mustache.js와 Mustache.java 2가지가 다 있어, 하나의 문법으로 클라이언트/서버 템플릿으로 모두 사용 가능
  • 인텔리제이 커무니티 버전을 사용해도 플러그인 사용 가능
    • 머스테치의 문법 체크, HTML 문법 지원, 자동완성 등 지원

❗ 템플릿 엔진은 화면 역할에만 충실해야 한다고 생각

 

플러그인에서 mustache 검색 후 설치

 

⚡ 기본 페이지 만들기

✔ 머스테치 스타터의 의존성 build.gradle에 등록

implementation('org.springframework.boot:spring-boot-starter-mustache')

✔ 머스테치의 파일 위치는 기본적으로 resources/templates 아래에 둠

 

index.mustache

🔶 main/.../templates 아래에 추가

<!DOCTYPE HTML>
<html>
<head>
    <title>스프링 부트 웹서비스</title>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
</head>
<body>
<H1>스프링 부트로 시작하는 웹 서비스</H1>
</body>
</html>

 

IndexController

🔶 main/.../web 아래에 추가

package com.yerin.book.springbootwebservice.web;

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

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(){
        return "index";
    }
}
  • URL 매핑
  • 머스태치 스타터 덕분에 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정됨
    • src/main/resources/templates/index.mustache로 전환되어 View Resolver가 처리

 

IndexControllerTest

📎 단축키 command + shift + T
🔶 test/.../web/IndexControllerTest.java

package com.yerin.book.springbootwebservice.web;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.junit.jupiter.SpringExtension;

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

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void 메인페이지_로딩(){

        //when
        String body = this.restTemplate.getForObject("/", String.class);

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

}
  • 실제로 URL 호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트
  • TestRestTemplate을 통해 "/"로 호출했을 때 index.mustache에 포함된 코드들이 있는지 확인하면 됨

 

-> Application을 실행해보면 정상적으로 코드가 수행되는 것을 확인할 수 있음

 

 

 

⚡ 게시글 등록 화면 만들기

부트스트랩을 이용하여 화면 만듦

  • 부트스트랩, 제이쿼리 등 프론트엔드 라이브러리를 사용할 수 있는 방법은 크게 2가지 있음
    • 외부 CDN사용
    • 직접 라이브러리를 받아서 사용

-> 우리는 외부 CDN 사용
-> HTML/JSP/Mustache에 코드만 한 줄 추가하면 됨

 

  • 2개의 라이브러리 부트스트랩과 제이쿼리를 index.mustache에 추가
  • 레이아웃 방식으로 추가할 것임
    • 레이아웃 방식 : 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식

 

header.mustache

<!DOCTYPE HTML>
<html>
<head>
   <title>스프링부트 웹서비스</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>

 

footer.mustache

	<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

	<!--index.js 추가-->
	<script src="/js/app/index.js"></script>
</body>
</html>
  • resources/templates 아래에 layout 패키지 생성
  • 그 아래에 header.mustache, footer.mustache 생성
    • 코드를 보면 css와 js의 위치가 서로 다름
      • 페이지 로딩 속도를 높이기 위해 css는 header, js는 footer에 둠
      • HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body 실행
      • head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출됨
      • js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어야 함(화면이 다 그려진 후에 호출하는 것이 좋음)
      • 잘못하면 사용자가 css가 적용되지 않은 깨진 화면을 볼 수도 있음
      • bootstrap.js의 경우 제이쿼리가 꼭 있어야만 함
        • 부스트스탭보다 먼저 호출되도록 코드 작성(의존)
  •  

 

index.mustache 수정

    • 라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가됨
    • 필요한 코드만 남게 됨
{{>layout/header}}

<h1> 스프링 부트로 시작하는 웹 서비스</h1>

{{>layout/footer}}

{{>layout/header}}

  • {{>}}현재 머스테치 파일을 기준으로 다른 파일을 가져옴

-> 레이아웃으로 파일 분리

 

🔶 글 등록 버튼 추가

{{>layout/header}}

<h1> 스프링 부트로 시작하는 웹 서비스</h1>
<div class = "col-md-12">
    <div class = "row">
        <a href="/posts/save" role = "button" class = "btn btn-primary">글 등록</a>
    </div>
</div>
{{>layout/footer}}

<a>태그를 이용해 글 등록 페이지로 이동하는 글 등록 버튼 생성
-> posts/save

 

posts-save.mustache 생성

🔶 resources/templates 아래에 생성


<h1>게시글 등록</h1>

<div class="col-md-12">
    <div class="col-md-4">
        <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>
        <a href="/" role="button" class="btn btn-secondary">취소</a>
        <button type="button" class="btn btn-primary" id="btn-save">등록</button>
    </div>
</div>

{{>layout/footer}}

 

index.js

🔶 등록 버튼 기능 생성
🔶 main/resources/static/js/app/index.js 

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: '/api/v1/posts',
            dataType: 'json',
            contentType: 'application/json;charset=utf-8',
            data: JSON.stringify(data)
        }).done(function(){
            alert('글이 등록되었습니다.');
            window.location.href = '/';
        }).fail(function(error){
            alert(JSON.stringify(error));
        })
    }
}

main.init();

window.location.href='/'

  • 글 등록이 성공하면 메인페이지(/)로 이동

✔ `var main = {..}

    • 굳이 index 라는 변수의 속성으로 function을 추가한 이유는?
    • ex)
      var init = function(){
      ...
      };
      var save = function(){
        ...
      };
      init();
    • index.mustache에서 a.js가 추가되어도 a.js만의 init과 function이 있다면 브라우저 스코프는 공용 공간으로 쓰이므로 나중에 로딩된 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 됨
    • 여러 사람이 참여하는 프로젝트에서는 중복된 함수 이름은 자주 발생할 수 있음!
    • 모든 function 이름을 확인하면서 만들기는 어렵기 때문에 index.js만의 유효 범위를 만들어서 사용
    • var index라는 객체를 만들어서 해당 객체에서 필요한 모든 function 선언
    • index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹칠 위험 없음

 

footer.mustache 수정

✔ 생서된 index.js를 머스테치 파일이 쓸 수 있도록 footer.mustache에 코드 추가

<script src = "/js/app/index.js"></script>
    • index.js 호출코드를 보면 절대 경로(/)로 바로 시작
    • 스프링 부트는 기본적으로 src/main/resources/static에 위치한 자바스크립트, CSS, 이미지 등 정적 파일들은 URL에서 /로 설정

 

반응형

📌 전체 조회 화면 만들기

🔶 전체 조회를 위해 index.mustache 수정

{{>layout/header}}

<h1> 스프링 부트로 시작하는 웹 서비스</h1>
<div class = "col-md-12">
    <div class = "row">
        <div class="col-md-6">
            <a href="/posts/save" role = "button" class = "btn btn-primary">글 등록</a>
        </div>
    </div>
    <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">
        {{#posts}}
            <tr>
                <td>{{id}}</td>
                <td>{{title}}</td>
                <td>{{author}}</td>
                <td>{{modifiedDate}}</td>
            </tr>
        {{/posts}}
        </tbody>
    </table>
</div>
{{>layout/footer}}

{{#posts}}

  • posts라는 List를 순회
  • Java의 for문과 동일

{{#id}}

  • List에서 뽑아낸 객체의 필드 사용

 

 

⚡ Controller, Repository, Service 코드 작성

Repository

🔶 기존에 있던 PostRepository 인터페이스에 쿼리 추가

package com.yerin.book.springbootwebservice.domain.posts;

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

import java.util.List;

public interface PostsRepository extends JpaRepository<Posts, Long> {

    @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
}
🌟 cf) 규모가 있는 프로젝트에서의 데이터 조회는 FK의 조인, 복잡한 조건 등으로 인해 Entity클래스만으로 처리하기 어려워서 조회용 프레임워크 추가로 사용
-> querydsl(가장 추천), jooq,MyBatis 등

 

Service

🔶 기존에 있던 PostsService 코드 수정

@RequiredArgsConstructor
@Service
public class PostsService {

    ...

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

@Transactional(readOnly = true)

    • 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선
    • 등록, 수정, 삭제 기능이 전혀 없는 서비스에 사용 추천

 

.map(PostsListResponseDto::new)

  • .map(posts -> new PostsListResponseDto(posts))와 같은 코드
  • PostsRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto 변환 -> List로 변환

 

PostsListResponseDto

package com.yerin.book.springbootwebservice.service.posts;

import com.yerin.book.springbootwebservice.domain.posts.Posts;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostsListResponseDto {

    private Long id;
    private String title;
    private String author;
    private LocalDateTime modifiedDate;

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

}

 

Controller

🔶 기존에 있던 IndexController 코드 변경

package com.yerin.book.springbootwebservice.web;


import com.yerin.book.springbootwebservice.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@RequiredArgsConstructor
@Controller
public class IndexController {

    private final PostsService postsService;

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

Model

  • 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있음
  • postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달

 

 

📌 게시글 수정, 삭제 화면 만들기

⚡ 게시글 수정

Controller

    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id,@RequestBody PostsUpdateRequestDto requestDto){
        return postsService.update(id,requestDto);
    }

-> 전에 수정 API를 미리 만들어 둠

 

posts-update.mustache

{{>layout/header}}
<h1>게시글 수정</h1>

<div class = "col-md-14">
    <div class = "col-md-4">
        <form>
            <div class="form-group">
                <label for = "id">글 번호</label>
                <input type="text" class = "form-control" id = "id" value = "{{posts.id}}" readonly>
            </div>
            <div class="form-group">
                <label for="title">제목</label>
                <input type="text" class="form-control" id="title" value="{{post.title}}">
            </div>
            <div class="form-group">
                <label for="author"> 작성자 </label>
                <input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
            </div>
            <div class="form-group">
                <label for="content"> 내용 </label>
                <textarea class="form-control" id="content">{{post.content}}</textarea>
            </div>
        </form>

        <a href="/" role="button" class="btn btn-secondary">취소</a>
        <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>

    </div>
</div>

{{>layout/footer}}

{{post.id}}

    • 머스테치는 객체의 필드 접근 시 점(Dot)으로 구분
    • Post 클래스의 id에 대한 접근은 post.id로 사용 가능

readonly

  • input 태그에 읽기 가능만 허용하는 속성
  • id와 author는 수정할 수 없도록 읽기만 허용하도록 추가

 

 

index.js 수정

...
        $('#btn-update').on('click',function () {
            _this.update();
        });
...
...

    update : function(){
      var data = {
          title : $('#title').val(),
          content : $('#content').val()
      };

      var id = $('#id').val();

      $.ajax({
          type: 'PUT',
          url: '/api/v1/posts'+id,
          dataType: 'json',
          contentType: 'application/json;charset=utf-8',
          data:JSON.stringify(data)
      }).done(function (){
          alert('글이 수정되었습니다.');
          window.location.href='/';
      }).fail(function (error){
          alert(JSON.stringify(error));
      });
    }
...

✔ `$('#btn-update').on('click')

  • btn-update : id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하도록 이벤트 등록

update : function()

  • 신규로 추가될 update function

type: 'PUT

    • 여러 HTTP Method 중 PUT 메소드 선택
    • PostsApiController에 있는 API에서 이미 @PutMapping으로 선언했기 때문에 PUT 사용해야 함
    • Rest 규약에 맞기 설정된 것
    • REST에서 CRUD는 다음과 같이 HTTP Method에 매핑됨
      • 생성 : POST
      • 읽기 : GET
      • 수정 : PUT
      • 삭제 : DELETE

url:'/api/v1/posts/' + id

  • 어느 게시글을 수정할지 URL Path로 구분하기 위해 Path에 id 추가

 

 

index.mustache 코드 수정

✔ 수정 페이지로 이동할 수 있게 기능 추가

        <tbody id = "tbody">
        {{#posts}}
            <tr>
                <td>{{id}}</td>
                <td><a href="/post/update/{{id}}">{{title}}</a></td>
                <td>{{author}}</td>
                <td>{{modifiedDate}}</td>
            </tr>
        {{/posts}}

 

 

IndexController 코드 수정

@RequiredArgsConstructor
@Controller
public class IndexController {

    ...

    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model){
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);
        return "post-update";
    }
}

 

 

⚡ 게시글 삭제

posts-update.mustache 코드 수정

<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>

코드 추가

btn-delete

  • 삭제 버튼을 수정 완료 버튼 옆에 추가
  • 해당 버튼 클릭 시 JS에서 이벤트를 수신할 예정

 

index.js

...

        $('#btn-delete').on('click',function () {
            _this.delete();
        })
...
...

    delete : function(){
        var id = $('#id').val();

        $.ajax({
            type: 'DELETE',
            url: '/api/v1/posts'+id,
            dataType: 'json',
            contentType: 'application/json;charset=utf-8',
        }).done(function (){
            alert('글이 삭제되었습니다.');
            window.location.href='/';
        }).fail(function (error){
            alert(JSON.stringify(error));
        });
    }
...

 

 

PostsService

    @Transactional
    public void delete(Long id){
        Posts posts = postsRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("해당 게시글이 없습니다. id="+ id));
        postsRepository.delete(posts);
    }

postsRepository.delete(posts)

  • JpaRepository에서 이미 delete 메소드를 지원하고 있어서 이를 활용함
  • 엔티티를 파라미터로 삭제할 수도 있고, deleteById 메소드를 이용하면 id로 삭제할 수도 있음
  • 존재하는 Posts인지 확인을 위해 엔티티 조회 후 그대로 삭제

 

 

PostsApiController

    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id){
        postsService.delete(id);
        return id;
    }

 

 

 

 

 

 

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스_이동욱 지음 책을 읽고 정리하는 글입니다

 

 

 

 

 

 

 


PREV

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스(3)

📌 JPA JPA : Java 표준 ORM (Object Relational Mapping) ⚡ 패러다임 불일치 문제 관계형 데이터베이스 : 어떻게 데이터를 저장할지에 초점이 맞춰진 기술 객체지향 프로그래밍 : 언어는 메시지를 기반으로

nyeroni.tistory.com

 

NEXT

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스(5)

📌 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 ✔ 스프링 시큐리티 : 막강한 인증과 인가 기능을 가진 프레임워크 스프링 기반의 애플리케이션에서는 보안을 위한 표준 ⚡ 구글 서비

nyeroni.tistory.com

 

728x90
LIST

BELATED ARTICLES

more