[스프링] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 머스테치로 화면 구성하기

June·2021년 3월 20일
0

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

일반적으로 웹 개발에 있어 템플릿 엔진이란, 지정된 템플릿 양식과 데이터가 합쳐서 HTML 문서를 출력하는 소프트웨어를 이야기한다. JSP, Freemarker, 리액트, 뷰의 view 파일 등이 해당한다. 둘 모두 지정된 템플릿과 데이터를 이용하여 HTML을 생성하는 템플릿 엔진이다.

다만 조금의 차이가 있는데 전자는 서버 템플릿 엔진이고, 후자는 클라이언트 템플릿 엔진이다. 프론트엔드의 자바스크립트가 작동하는 영역과 JSP가 작동하는 영역은 다른데, JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동된다.

JSP는 명확하게는 서버 템플릿 엔진은 아니지만, View의 역할만 하도록 구성할 때는 템플릿 엔진으로써 사용할 수 있다. 이 경우엔 Spring + JSP로 사용한 경우다.

서버 템플릿 엔진을 이용한 화면 생성은 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달한다.

반면 자바스크립트는 브라우저 위에서 작동한다. 앞에서 작성된 자바스크립트 코드가 실행되는 장소는 서버가 아닌 브라우저이다. 즉, 브라우저에서 작동될 때는 서버 템플릿 엔진의 손을 벗어나 제어할 수가 없다.

Vue.js나 React.js를 이용한 SPA(Single Page Application)은 브라우저에서 화면을 생성한다. 즉, 서버에서 이미 코드가 벗어난 경우다. 그래서 서버에서는 Json 혹은 Xml 형식의 데이터만 전달하고 클라이언트에서 조립한다.

최근에는 리액트나 뷰와 같은 자바스크립트 프레임워크에서 서버 사이드 렌더링을 지원하는 모습도 보인다.

머스테치란

머스테치란 수많은 언어를 지원하는 가장 심플한 템플릿 엔진이다.

자바 진영에서는 JSP, Velocity, Freemarker, Thymeleaf등 다양한 서버 템플릿 엔진이 존재한다.

  • JSP, Velocity: 스프링 부트에서는 권장하지 않는 템플릿 엔진이다.

  • Freemarker: 템플릿 엔진으로는 너무 과하게 많은 기능을 지원한다. 높은 자유도로 인해 숙련도가 낮을 수록 Freemarker안에 비즈니스 로직이 추가될 확률이 높다.

  • Thymeleaf: 스프링 진영에서 적극적으로 밀고 있지만 문법이 어렵다. HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 기존 개발자에게 어렵다. 실제로 사용하는 사람은 자바스크립트 프레임워크를 배우는 기분이라 한다.

머스테치의 장점

  • 문법이 다른 템플릿 엔진보다 심플하다.
  • 로직 코드를 사용할 수 없어 View의 역할과 서버의 역할이 명확하레 분리된다.
  • Mustache.js와 Mustache.java 2가지가 다 있어, 하나의 문법으로 클라이언트.서버 템플릿 모두 사용 가능하다.

템플릿 엔진은 화면 역할에만 충실해야한다.

머스테치 플러그인 설치

머스테치의 또 다른 장점은 인텔리제이 커뮤니티 버전에서도 사용 가능하다는 것이다.

Thymeleaf나 JSP 등은 커뮤니티 버전에서 지원하지 않고 얼티메이트 버전에서만 가능하다.

기본 페이지 만들기

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

스프링 부트에서 공식 지원하는 템플릿 엔진이다. 의존성 하나만 추가하면 다른 스타터 패키니와 마찬가지로 추가 설정 없이 설치가 끝이다.

머스테치의 파일 위치는 기본적으로 src/main/resources/templates이다. 이 위치에 머스테치 파일을 두면 스프링 부트에서 자동으로 로딩한다. 첫 페이지를 담당할 index.mustache를 src/min/resources/templates에 생성한다.

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

이 머스테치에 URL을 매핑하는데, URL 매핑은 Controller에서 진행한다. web 패키지 안에 IndexController를 생성한다.

package com.jojoldu.book.springboot.web;

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

@Controller
public class IndexController {

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

머스테치 스타터 덕분에 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다. 앞의 경로는 src/main/resources/templates로, 뒤의 파일 확장자는 .mustache가 붙는다. 여기서 View Resolver가 처리한다. View Resolver는 URL 요청의 결과를 전달할 타입과 값을 지정하는 관리자 격이다.

IndexControllerTest

package com.jojoldu.book.springboot.web;

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 IndexControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

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

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

이번 테스트는 실제로 URL 호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트이다. HTML도 결국은 규칙이 있는 문자열이다. TestRestTemplate을 통해 "/"로 호출했을 때, index.mustache에 포함된 코드들이 있는지 확인하면 된다.

게시글 등록 화면 만들기

오픈소스인 부트스트랩을 이용하여 화면을 만들어보자. 부트스트랩, 제이쿼리 등 프론트엔드 라이브러리를 사용할 수 있는 방법은 크게 2가지가 있다. 하나는 외부 CDN을 사용하는 것이고, 다른 하나는 직접 라이브러리를 받아서 사용하는 것이다.

외부 CDN을 사용하면, 본인의 프로젝트에서 직접 내려받아 사용할 필요도 없고, 사용 방법도 HTML/JSP/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>

페이지 로딩 속도를 높이기 위해 css는 header에, js는 footer에 두었다. HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행된다. 즉, head가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출된다. 특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다. 반면 css는 화면을 그리는 역할이므로 head에서 불러오는 것이 좋다. 그렇지 않으면, css가 적용되지 않아서 깨진 화면을 볼 수 있다.

추가로, bootstrap.js의 경우 제이쿼리가 꼭 있어야만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성했다. 보통 앞선 상황을 bootstrap.js가 제이쿼리에 의존한다고 한다.

라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가되니 이제 index.mustache에는 필요한 코드만 남게 된다.

{{>layout/header}} // 1

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

{{>layout/footer}}
    1. {{> }}는 현재 머스테치 파일(index.mustache)을 기준으로 다른 파일을 가져온다.
{{>layout/header}}

    <h1>스프링부트로 시작하는 웹 서비스 Ver.2</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>
    </div>
{{>layout/footer}}

<a> 태그를 이용해 글 등록 페이지로 이동하는 글 등록 버튼이 생성되었다. 이동할 페이지의 주소는 /posts/save이다.

@Controller
public class IndexController {

	...

    @GetMapping("/posts/save")
    public String postsSave() {
        return "posts-save";
    }
}

/posts/save를 호출하면 posts-save.mustache를 호출한다.

posts-save.mustache

{{>layout/header}}

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

게시글 등록 화면에 등록 버튼은 기능이 없다. API를 호출하는 JS가 없기 때문이다. src/main/resources에 static/js/app 디렉토리를 생성하자.

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

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

        $('#btn-delete').on('click', function () {
            _this.delete();
        });
    },
    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));
        });
    },
    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));
        });
    },
    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 = '/'; // 1
        }).fail(function (error) {
            alert(JSON.stringify(error));
        });
    }

};

main.init();
    1. window.location.href = "/"
    • 글 등록이 성공하면 메인페이지(/)로 이동한다.

index.js의 첫 문장에 var main = {...}라는 코드를 선언했다. 굳이 index라는 변수의 속성으로 function을 추가한 이유가 무러까?

예를 들어 아래와 같이 function을 작성했다고 가정하자.

var init = function() {
    ...
  };
  
  var save = function () {
      ...
  };
  
  init();

index.mustache에서 a.js가 추가되어 a.js도 a.js만의 init과 save function이 있다면 어떻게 될까? 브라우저의 스코프는 공용 공간으로 쓰이기 때문에 나중에 로딩된 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 된다.

여러 사람이 참여하는 프로젝트에서는 중복된 함수 이름은 자주 발생한다. 이런 문제를 피하려고 index.js만의 유효범위를 만들어 사용한다.
방법은 var index이란 객체를 만들어 해당 객체에서 필요한 모든 function을 선언하는 것이다. 이렇게 하면 index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹칠 위험이 사라진다.

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

그래서 다음과 같이 파일이 위치하면 위치에 맞게 호출이 가능하다.

전체 조회 화면 만들기

{{>layout/header}}

<h1>스프링부트로 시작하는 웹 서비스 Ver.2</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}} // 1
      <tr>
        <td>{{id}}</td> // 2
        <td><a href="/posts/update/{{id}}">{{title}}</a></td>
        <td>{{author}}</td>
        <td>{{modifiedDate}}</td>
      </tr>
    {{/posts}}
    </tbody>
  </table>
</div>

{{>layout/footer}}
    1. {{#posts}}
    • posts라는 List를 순회한다.
    • Java의 for문과 동일하다
    1. {{id}}등의 {{변수명}}
    • List에서 뽑아낸 객체의 필드를 사용한다.

PostsRepository 인터페이스

package com.jojoldu.book.springboot.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("SELCT p from Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
}

SpringDataJpa에서 제공하지 않는 메서드를 위처럼 쿼리로 작성해도 된다는 것을 보여주고자 @Query를 사용했다. 실제로 앞의 코드는 SpringDataJpa에서 제공하는 기본 메소드만으로 해결 할 수 있기는 하나, 가독성이 떨어진다.

규모가 있는 프로젝트에서의 데이터 조회는 FK의 조인, 복잡한 조건등으로 인해 Entity 클래스 만으로 처리하기 어려워 조회용 프레임워크를 추가로 사용한다. 대표적으로 querydsl, jooq, MyBatis등이 있다. 조회는 위 3가지 프레임워크 중 하나를 통해 조회하고, 등록/수정/삭제 등은 SpringDataJpa를 통해 진행한다.

Querydsl 장점

  1. 타입 안정성이 보장된다.
    단순한 문자열로 쿼리를 생성하는 것이 아니라, 메소드를 기반으로 쿼리를 생성하기 때문에 오타나 존재하지 않는 칼럼명을 명시할 경우 IDE에서 자동으로 검출된다. 이 장점은 Jooq에서도 마찬가지지만, MyBatis에서는 지원되지 않는다.
  2. 국내 많은 회사들이 사용중이다.
    쿠팡, 배민 등 JPA를 사용하는 회사는 Querydsl을 사용한다.
  3. 레퍼런스가 많다.

PostsService

public class PostsService extends BaseTimeEntity {

    private final PostsRepository postsRepository;

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

findAllDesc 메소드의 트랜잭션 어노테이션에 옵션이 추가되었는데, (readOnly = true)를 주면 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도가 개선되기 때문에 등록, 수정, 삭제 기능이 전혀 없는 서비스 메소드에서 사용하는 것을 추천한다.

.map(PostsListsResponseDto::new)

위의 코드는 실제로 아래와 같다.

.map(posts -> new PostsListResponseDto(posts))

postsRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto 변환 -> List로 반환하는 메소드이다.

PostsListResponseDto

package com.jojoldu.book.springboot.web.dto;

import com.jojoldu.book.springboot.domain.posts.Posts;
import java.time.LocalDateTime;
import lombok.Getter;

@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 변경

import org.springframework.ui.Model;

@RequiredArgsConstructor
@Controller
public class IndexController {

    private final PostsService postsService;

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


    @GetMapping("/posts/save")
    public String postsSave() {
        return "posts-save";
    }
}

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

게시글 수정 API는 이미 3.4절에서 만들어 두었다.

@RequiredArgsConstructor
@RestController
public class PostsApiController {

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

게시글 수정
src/main/resources/templates/posts-update.mustache

  
{{>layout/header}}

<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="id" value="{{post.id}}" readonly> // 1
            </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> // 2
            </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>
        <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
    </div>
</div>

{{>layout/footer}}
    1. {{post.id}}
    • 머스테치는 객체의 필드 접근 시 점(Dot)으로 구분한다.
    • 즉, Post 클래스의 id에 대한 접근은 post.id이다.
    1. readonly
    • Input 태그에 읽기 가능만 허용하는 속성이다
    • id와 author는 수정할 수 없도록 읽기만 허용하도록 추가한다.

그리고 btn-update 버튼을 클리갛면 update 기능을 호출할 수 있게 index.js 파일에도 update function을 추가하겠다.

    init : function () {
        var _this = this;
		...

        $('#btn-update').on('click', function () { // 1
            _this.update();
        });
    },
    save : function () {
		...
    },
    update : function () { // 2
        var data = {
            title: $('#title').val(),
            content: $('#content').val()
        };

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

        $.ajax({
            type: 'PUT', // 3
            url: '/api/v1/posts/'+id, // 4
            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));
        });
    }
	...
  1. $('btn-update').on('click')
    • btn-update란 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하도록 이벤트를 등록한다.
  2. update: function()
    • 신규로 추가될 update function이다.
  3. type: 'PUT'
    • 여러 HTTP Method 중 PUT 메소드를 선택한다.
    • PostsApiController에 있는 API에서 이미 @PutMapping으로 선언했기 때문에 PUT을 사용해야 한다. 참고로 이는 REST API 규약에 맞게 설정된 것이다.
    • REST에서 CRUD는 다음과 같이 HTTP Method에 매핑된다.
      • 생성 (Create) - POST
      • 읽기 (Read) - GET
      • 수정 (Update) - PUT
      • 삭제 (Delete) - DELETE
  4. url: '/api/v1/posts/'+id
    • 어느 게시글을 수정할지 URL Path로 구분하기 위해 Path에 id를 추가한다.

마지막으로 전체 목록에서 수정 페이지로 이동할 수 있게페이지 이동 기능을 추가해보자.

index.mustache

    <tbody id="tbody">
    {{#posts}}
      <tr>
        <td>{{id}}</td>
        <td><a href="/posts/update/{{id}}">{{title}}</a></td> // 1
        <td>{{author}}</td>
        <td>{{modifiedDate}}</td>
      </tr>
    {{/posts}}
    </tbody>
  1. <a href = "/posts/update/{{id}}"></a>
    • 타이틀(title)에 a tag를 추가해준다.
    • 타이틀을 클리갛면 해당 게시글의 수정화면으로 이동한다.

화면쪽 작업이 다 끝났으니 수정 화면을 연결한 Conroller를 작업하자
IndexController에 아래와 같이 메서드를 추가한다.

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 "posts-update";
    }
}

게시글 삭제

posts-update.mustache


<div class="col-md-12">
  <div class="col-md-4">
	... 
    <a href="/" role="button" class="btn btn-secondary">취소</a>
    <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
    <button type="button" class="btn btn-danger" id="btn-delete">삭제</button> // 1
  </div>
</div>
  1. btn-delete
    • 삭제 버튼을 수정 완료 버튼 옆에 추가한다.
    • 해당 버튼 클릭시 JS에서 이벤트를 수신한다.

index.js

var main = {
    init : function () {
		...

        $('#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));
        });
    }

};

main.init();

PostsService

package com.jojoldu.book.springboot.service.posts;

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

        postsRepository.delete(posts); // 1
    }
	... 
}
  1. postsRepository.delete(posts)
    • JpaRepository에서 이미 delete 메서드를 지원하니 활용한다.
    • 엔티티를 파라미터로 삭제할 수도 있고, deleteById 메소드를 이용하면 id로 지운다.
    • 존재하는 posts인지 확인을 위해 엔티티 조회 후 삭제한다.

서비스에서 만든 delet 메소드를 컨트롤러가 사용하도록 코드를 추가한다.

PostsApiController

public class PostsApiController {
	...

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

0개의 댓글