게시글 삭제

  • 수정화면에 삭제 버튼 기능 추가

FrontEnd

posts-update.mustache

  • update
    • btn-delete
      • 삭제 버튼을 수정 완료 옆에 추가
      • id가 btn-delete인 버튼의 click 이벤트가 발생하는 경우 게시글 삭제 javaScript delete 함수 호출
{{>layout/header}}

    <h1>게시글 수정</h1>

    <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>
        </div>
    </div>

{{>layout/footer}}

index.js

  • index
    • btn-delete 버튼 이벤트 등록
    • delete 함수 호출 시 /api/v1/posts/{id} URL 로 DELETE Method 방식으로 호출하여 게시글을 삭제 요청
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, update

    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();

BackEnd

PostsAPIController

  • PostsAPIController
    • 게시글의 Id를 arguements로 받아 PostsService.delete(id)를 호출
    • URL을 Delete method 방식으로 호출하는 경우 게시글 삭제
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    // ... save, update, findById

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

PostsService

  • PostsService
    • postsRepository.delete(posts)
      • JpaRepository에서 이미 delete 메서드를 지원
      • 엔티티를 파라미터를 삭제할 수도 있고, deleteById 메서드를 이용하면 id로 삭제할 수도 있다.
      • 존재하는 Posts인지 확인하기 위해 Entity 조회 후 그대로 삭제
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    // ...     save, update, findById, findAllDesc

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

화면 확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] OAuth2 Google Login  (0) 2020.05.31
[SpringBoot] Spring Security & OAuth 2.0 로그인  (0) 2020.05.31
[SpringBoot] 게시글 수정  (0) 2020.05.29
[SpringBoot] 게시글 전체 조회  (0) 2020.05.29
[SpringBoot] 게시글 등록  (0) 2020.05.29

게시글 수정

  • FrontEnd
    • posts-udate.mustache 화면 개발
    • index.js 스크립트 내에 update 함수 추가
  • BackEnd
    • IndexController 내에 update 메서드 추가
    • PostsAPIController내에 postsService의 update 메서드 호출하는 update 메서드 추가

FrontEnd

index.mustache

  • index
    • <a href="/posts/update/{id}">
      • 타이틀(title)에 a tag를 추가
      • 타이틀을 클릭하면 개당 게시글의 수정 화면으로 이동
{{>layout/header}}

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

{{>layout/footer}}

posts-update.mustache

  • 게시글 수정

    1. {{post.id}}

      • mustache는 객체의 필드 접근 시 '.'(dot)으로 구분
      • 즉, Post 클래스의 id에 대한 접근은 post.id로 사용할 수 있다.
    2. readOnly

      • input 태그에 수정이 불가능하도록 하는 속성
      • id와 author는 수정할 수 없도록 readOnly 속성 추가
{{>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>
                </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>
            <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
        </div>
    </div>

{{>layout/footer}}

index.js

  • index 객체 수정

    • update function 추가
    • title, content를 data로 설정하여 /api/v1/posts/{id} URL로 PUT 메서드 호출하는 스크립트 작성
    1. $("#btn-update").on("click")
      • btn-update란 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하도록 이벤트 등록
    2. update : function()
      • 신규로 추가될 update function()
    3. type: "PUT"
      • 여러 HTTP Method 중 PUT 메서드를 선택
      • REST에서 CRUD는 다음과 같이 HTTP Method에 매핑된다.
        • 생성(Create) - POST
        • 읽기(Read) - GET
        • 수정(Update) - PUT
        • 삭제(Delete) - DELETE
    4. URL: "/api/v1/posts/" + id
      • 어느 게시글을 수정할 지 URL path로 구분하기 위해 Path에 id 값 추가
var index = {
    init : function () {
        var _this = this;
        // ... 
        $('#btn-update').on('click', function () { _this.update(); });
    },
    // ... save
    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));
        });
    },
};

index.init();

BackEnd

IndexController

  • IndexController
    • 게시글의 id로 /posts/update/{id} postsUpdate 메서드를 호출하는 메서드를 정의
    • 게시글을 가져와 model에 넣어 templates로 전달
@RequiredArgsConstructor
@Controller
public class IndexController {

    private final PostsService postsService;

    // ... index, save 

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

PostsAPIController

  • PostsAPIController
    • update메서드 호출로 게시글 수정
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    // ... save, findById

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

브라우저 확인

게시글 수정 페이지
게시글 수정 완료
변경 완료 확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] Spring Security & OAuth 2.0 로그인  (0) 2020.05.31
[SpringBoot] 게시글 삭제  (0) 2020.05.29
[SpringBoot] 게시글 전체 조회  (0) 2020.05.29
[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] Mustache  (0) 2020.05.28

게시글 전체조회

  • View template 수정
  • BackEnd
    • Posts List Dto 생성
    • Controller, Service, Repository Query 생성

View

  • Template 수정
    • Posts를 보여주기위한 table 구성

index.mustache

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

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

{{>layout/footer}}

BackEnd

PostsListResponseDto

  • PostsListResponseDto
    • Posts의 Entity를 이용하여 필요한 필드만 Dto로 구성
@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();
    }
}

PostsRepository

  • PostsRepository

    • JpaRepository 인터페이스에 Entity에 접근하여 가져오는 findAll() 메서드가 있다.
    • Spring Data JPA에서 제공하지 않는 메서드를 사용하기 위해서 @Query를 사용하는 방법도 있다.
    • findAllDesc을 새로 작성하여 PostsListResponseDto라는 Controller와 Service에서 접근가능한 Dto를 사용하여 호출
    • @Query가 가독성이 더 좋을 수도 있다.
  • Entity 클래스만으로 처리하기 어려운 경우

    • querydsl, jooq, MyBatis 등 프레임워크를 추가하여 조회용으로 사용할 수 있다.
    • 기본적인 등록 / 수정 / 삭제는 Spring Data JPA만으로도 충분히 가능하다.
  • querydsl을 추천하는 이유

    • 타입의 안정성
      • 단순한 문자열로 쿼리를 생성하는 것이 아니라, 메서드를 기번으로 쿼리를 생성
      • 오타나 존재하지 않는 컬럼명을 명시하는 경우 IDE에서 자동으로 검증이 가능하다.
public interface PostsRepository extends JpaRepository<Posts, Long> {
    @Query("SELECT p FROM Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
}

PostsService

  • PostsService

    • PostsRepository에서 구현한 findAllDesc() 메서드를 호출
    • List<Posts> 를 PostsListResponseDto로 매핑하여 List<PostsResponseDto>로 리턴
    • findAllDesc() 메서드의 트랜잭션 어노테이션에 readOnly = true라는 옵션을 추가
      - 이는 트랜잭션 범위는 유지하되, 조회 기능만 남겨두어 조회 속도를 개선할 수 있다.
      • 등록, 수정, 삭제 기능이 전혀없는 서비스 메서드에서 사용하는 것을 추천
  • 람다 설명

    - .map(PostsListResponseDto::new)
    - .map(posts -> new PostsListResponseDto(posts))
    - postsRepository  결과로 넘어온 Posts의 Stream을 map을 통해 PostsListReponseDto로 변환 -> List로 반환하는 메서드
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    // ... save, update, findById

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

IndexController

  • IndexController
    • Service Layer에서 List<PostsListResponseDto> 를 반환하는 findAllDesc()를 model에 담아 View로 전달한다.
    • Model
      • 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장
      • postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달
@RequiredArgsConstructor
@Controller
public class IndexController {

    private final PostsService postsService;

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

    //... save
}

게시글 화면 확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 삭제  (0) 2020.05.29
[SpringBoot] 게시글 수정  (0) 2020.05.29
[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] JPA Auditing  (0) 2020.05.28

Mustache를 활용한 게시글 등록화면 만들기

  • HTML
    • Layout 방식을 활용하여 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식 활용
    • footer, header 공통 영역을 분리
    • src/main/resources/templates 디렉토리에 layout 디렉토리를 추가로 생성하여 저장
  • CSS, JavaScript
    • BootStrap, JQuery 등 프론트엔드 라이브러리를 사용
    • 여기서는 외부 CDN을 사용하여 개발, 실제 서비스에서는 직접 라이브러리를 받아서 사용
    • 페이지 로딩속도를 높이기 위해 css 태그는 header, js 태그는 footer에 위치
    • HTML은 인터프리터언어로 소스코드를 위에서 아래로 읽고 실행하기 때문에 css를 읽지않고는 깨진화면이 보여질 수 있다.
    • JavaScript의 위치가 Header에 있을 경우 script 오류가 발생하는 경우 화면자체가 나오지 않을 수 있다.

IndexController

@RequiredArgsConstructor
@Controller
public class IndexController {

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

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

View Page

  • 스프링 부트는 기본적으로 src/main/resources/static에 위치한 정적 자원을 호출한다.
    • 자바스크립트, CSS, 이미지 등 정적 파일들은 URL에서 /로 설정된다.
    • src/main/resources/static/js
    • src/main/resources/static/css
    • src/main/resources/static/image

posts-save.mustache

  • posts-save
{{>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}}

index.js

  • index
    • 프로토타입 기반의 객체지향 언어로 사용
    • index이라는 객체 리터럴에 init, save 내부 함수를 생성
    • init 함수에는 id 값이 btn-save인 부분의 click 이벤트가 발생하는 경우 save 함수를 호출
    • save 함수는 title, author, content를 data라는 객체로 저장하여 /api/v1/posts URL로 AJAX 호출하여 게시글을 등록
var index = {
    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));
        });
    },
};

index.init();

게시글 등록 페이지
게시글 등록 확인

게시글 등록 DB 확인

  • 게시글 등록확인

게시글 등록확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 수정  (0) 2020.05.29
[SpringBoot] 게시글 전체 조회  (0) 2020.05.29
[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28

템플릿 엔진

  • 템플릿 엔진

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

    • JSP(View의 역할만 하도록 구성할 경우), Freemarker 등
    • 서버에서 구동
    • 서버에서 Java 코드 문자열을 만든 뒤, 이 문자열을 HTML로 변환하여 브라우저로 전달
  • 클라이언트 템플릿 엔진

    • React, Vue, Angular
    • 브라우저에서 작동
    • SPA(Single Page Application)는 브라우저에서 화면을 생성
    • 이런 경우 서버에서 Json 또는 XML 형식의 데이터만 전달하고 클라이언트에서 조립
    • 자바스크립트 프레임워크에서 서버 사이드 렌더링(Server Side Rendering)을 지원
      (자바스크립트 프레임워크의 화면 생성 방식을 서버에서 실행하는 것)

Mustache

  • 머스테치의 장점

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

    • JSP, Velocity
      • 스프링 부트에서는 권장하지 않는 템플릿 엔진
    • Freemarker
      • 템플릿 엔진으로는 과라게 많은 기능을 지원
      • 높은 자유도로 인하여 숙련도가 낮을 수록 Freemarker안에 비즈니스 로직이 추가될 확률이 높음
    • Thymeleaf
      • HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 어려울 수 있음
      • Vue.js 태그 속성 방식과 비슷

Mustache 의존성 추가

  • spring-boot-starter-mustache 의존성 추가
    • SpringBoot에서 공식 지원하는 템플릿 엔진
dependencies {
    ...
    compile('org.springframework.boot:spring-boot-starter-mustache')

    ...
} 

Mustache Plugin 설치

  • Mustache 플러그인
    • IntelliJ community 버전에서 지원
    • 문법체크, HTML 문법 지원, 자동완성을 지원

기본 페이지 만들기

  • IndexController
@Controller
public class IndexController {
    @GetMapping
    public String index() {
        return "index";
    }
}
  • 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>
  • index.mustache
{{>layout/header}}

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

{{>layout/footer}}

View Resolver

  • Mustache starter로 인한 View Resolver가 하는 일
    • 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다.
    • prefix: src/main/resources/templates
    • suffix: .mustache
    • index를 반환하는 경우 'src/main/resources/templates/index.mustache'로 전환

IndexControlerTest

  • 기본 페이지 테스트 코드
    • restTemplate.getForObject("/")를 호출하는 경우 index.mustache에 포함된 코드 확인
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void loadMainPage() {
        // when
        String body  = this.restTemplate.getForObject("/", String.class);

        // then
        assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
    }
}
  • IndexController 브라우저 확인

index.mustache 확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 전체 조회  (0) 2020.05.29
[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28

JPA Auditing으로 생성시간 / 수정시간 자동화하기

  • Entity에는 해당 데이터의 생성시간과 수정시간을 포함한다.
  • 공통으로 사용하게 되는 필드를 JPA Auditing을 통해 재사용하도록 한다.

LocalDate

  • Java8부터 LocalDate, LocalDateTime를 사용한다.
    • 8버전 이전에는 Date와 Calendar 클래스의 문제점
      • 불변 객체가 아니므로 멀티스레드 환경에서 문제가 발생할 가능성이 높다.
      • Calendar는 월(Month) 값 설계가 잘못되었다.
        • 10월을 나타내는 Calendar.OCTOBER는 숫자 값이 '9'이다.
    • Hibernate 5.2.10버전 이후, 데이터베이스에 제대로 매핑되지 않는 이슈 해결
    • SpringBoot 1.x 버전을 사용하는 경우 Hibernate 5.2.10 버전 이상을 사용하도록 설정 필요
    • SpringBoot 2.x 버전을 사용하는 경우 별다른 설정이 없이 바로 적용가능

BaseTimeEntity

  • BaseTimeEntity 클래스

    • domain 패키지에 생성
    • 모든 Entity의 상위 클래스로 사용하여 createDate, modifiedDate를 자동으로 관리
    1. @MappedSuperclass

      • JPA Entity 클래스들이 BaseTimeEntity를 사용할 경우 필드들(createDate, modifiedDate)도 컬럼으로 인식하도록 한다.
    2. @EntityListeners(AuditingEntityListener.class)

      • BaseTimeEntity 클래스에 Auditing 기능을 포함
    3. @CreatedDate

      • Entity가 생성되어 저장될 때 자동 저장
    4. @LastModifiedDate

      • 조회한 Entity의 값을 변경할 때 시간이 자동 저장
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}
  • domain 클래스에서 BaseTimeEntity 상속
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
    ...
}
  • JPA Auditing 어노테이션들을 활성화 할 수 있도록 Application 클래스에 활성화 어노테이션 추가

@EnableJpaAuditing // JPA Auditing 활성화  
@SpringBootApplication  
public class Application {  
    public static void main(String\[\] args) {  
        SpringApplication.run(Application.class, args);  
    }  
}

JPA Auditing 테스트 코드

  • PostsRepositoryTest 클래스
    • LocalDateTime.of()로 날짜 설정
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

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

    // getBoard

    @Test
    public void regBaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2020,5,28,19,28,0);
        postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        System.out.println(">>> createDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}
  • createDate, modifiedDate 확인으로 정상적으로 JPA Auditing이 적용되었음을 알 수 있다.
    • 앞으로 생성되는 Entity들은 BaseTimeEntity를 상속받아 등록일/수정일을 자동화할 수 있다.

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
  • Spring 웹 계층알아보기
  • 각 계층에 작성해야하는 로직 구분하기
  • API 만들기

API 만들기

  • save
  • update
  • get

Spring 웹 계층

  • Web Layer

    • Controller, JSP/Freemarker 등의 View Template영역
    • Filter(@Filter), Interceptor, Controller Advice(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역
  • Service Layer

    • @Service에 사용되는 서비스 영역
    • Controller와 Dao 중간 영역
    • @Transactional이 사용되는 영역
  • Repository Layer

    • Database와 같이 데이터 저장소에 접근하는 영역
  • Dtos

    • Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체이며 Dtos는 해당 객체들의 영역을 이야기함
    • View Template Engine에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등
  • Domain Model

    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화시킨 것
    • @Entity가 사용되는 영역
    • 데이터베이스의 테이블과 관계되는 것 뿐만아니라 VO처럼 값 객체들도 이 영역에 해당
    • 비즈니스 처리를 담당하는 영역

Save API 만들기

  1. Controller와 Service에서 사용할 Dto 클래스 생성
  2. Controller -> Service 순으로 작성
  3. save기능 테스트 코드 작성

web/dto/PostsSaveRequestDto

  • Dtos

    • Request 데이터를 받은 Dto
    • 계층 간에 데이터 교환을 위한 객체
  • PostsSaveRequestDto

    • Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경되므로 Requst/Response 클래스로 사용해선 안된다.
    • Entity 클래스와 Controller에서 쓸 Dto 클래스는 분리되어 사용되야 한다.
@Getter
@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();
    }
}

web/PostsAPIController

  • Controller Layer (web)

    • 외부 요청과 응답에 대한 전반적인 로직
  • PostsAPIController

    1. @RequiredArgsConstructor
      • final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성
      • 생성자로 Bean을 주입받아 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함
      • 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드를 수정하지 않아도 된다.
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }
}

service/PostsService

  • Service Layer
    • 트랜잭션
    • 도메인 간 순서 보장 (@Transactional)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

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

PostsControllerTest

  • Posts.save 테스트
    • @WebMvcTest 대신 @SpringBootTest와 TestRestTemplate을 사용
    • @WebMvcTest는 JPA 기능이 작동하지 않는다.
    • @SpringBootTest와 TestRestTemplat을 통해 Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 사용
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsAPIControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void testSave() {
        // given
        String title = "title";
        String content = "content";

        PostsSaveRequestDto requestDto =
                PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        // when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();

        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}
  • 로그 확인
    • WebEnvironment.RANDOM_PORT를 통해 tomcat의 포트가 3421로 구동된 것을 확인
    • insert 쿼리 실행 확인

Update / findById API 만들기

PostsResponseDto

  • PostsReponseDto
    • Entity의 필드 중 일부를 사용하므로 생성자로 Entity를 받아 필드에 대입
    • 모든 필드를 가진 생성자가 필요하지 않으므로 Dto는 Entity를 받아 처리
@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

PostsAPIController

  • PostsAPIController
    • Posts의 id값과 update할 PostsUpdateRequestDto 값을 json 데이터 타입으로 전달하여 호출
    • 해당 Posts의 값을 조회하기 위한 findById 메서드를 정의
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    // save

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

    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById(@PathVariable Long id) {
        return postsService.findById(id);
    }
}

PostsService

  • PostsService
    • update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
    • JPA의 영속성 컨텍스트, 엔티티를 영구 저장하는 환경
    • JPA의 엔티티 매니저가 활성화된 상태(Spring Data JPA의 기본 옵션)로 트랜잭션안에서 데이터베이스에서 데이터를 가져오는 경우 이 데이터는 영속성 컨텍스트가 유지된 상태이다.
    • 이 상태에서 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
    • Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다. (더티 체킹)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    ...

    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다. id=" + id));
        posts.update(requestDto.getTitle(), requestDto.getContent());
        return id;
    }

    public PostsResponseDto findById(Long id) {
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다. id=" + id));
        return new PostsResponseDto(entity);
    }
}

PostsAPIControllerTest

  • PostsAPIControllerTest
    • Update 기능을 테스트 하기 위해서 savePosts를 통해 Insert 쿼리 호출
    • Update할 값을 설정하여 PostsUpdateRequestDto를 통해 데이터를 만들기
    • restTemplate.exchange(url.toString(), HttpMethod.PUT, requestEntity, Long.class)로 update 실행
    • assertThat을 통해 정상 호출 확인
    • postsRepository.findAll()를 이용하여 데이터 호출
    • 입력된 값이 기대값과 같은지 확인 후 완료
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsAPIControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    // save

    @Test
    public void testUpdate() {
        // 변경하기 전 데이터 입력
        Posts savePosts = postsRepository.save(
                Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                        .build()
        );

        Long updateId = savePosts.getId();
        String exceptedTitle = "title2";
        String exceptedContent = "content2";
        // 데이터 변경을 위한 Dto 생성
        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(exceptedTitle)
                .content(exceptedContent)
                .build();

        StringBuilder url = new StringBuilder();

        url.append("http://localhost:");
        url.append(port);
        url.append("/api/v1/posts/");
        url.append(updateId);

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        // when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url.toString(), HttpMethod.PUT, requestEntity, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(exceptedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(exceptedContent);

    }
}

update Test

H2 데이터베이스 웹 콘솔에서 확인하기

insert into posts 
(author, content, title)
values
('author', 'content', 'title');
  • GET: /api/v1/posts/1 URL 호출로 findById 기능 확인

  • PUT: /api/v1/posts/1 URL 호출로 update 기능 확인
    • insomnia라는 프로그램으로 실행하여 테스트

 

 

  • 추가 PostsUpdateRequestDto

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
[SpringBoot] lombok 설정 및 테스트  (0) 2020.05.28

application.properties를 application.yaml파일로 변경하기

  • spring-boot-starter를 사용하는 경우, SnakeYAML를 사용할 수 있다.
  • SpringApplication 클래스는 SnakeYAML라이브러리를 가지고 있다면 properties를 대체할 수 있도록 YAML을 지원한다.
  • YAML 불러오기
    • SpringFramework는 YAML document를 불러와 사용할 수 있는 클래스를 제공한다.
    • YamlPropertiesFactoryBean은 properties로써 YAML을 불러온다.
    • YamlMapFactoryBean은 YAML을 Map으로 불러온다.

application.properties

  • 서버포트 설정 및 h2 console, jpa sql, query 변경하는 설정

application.properties

application.yaml

  • 위 properties파일과 동일한 설정을 yaml 파일로 설정
# server setting
server:
  port: 8085

# jpa setting
spring:
  h2:
    console:
      enabled: true
  jpa:
    show_sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

 

application.yaml

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
[SpringBoot] lombok 설정 및 테스트  (0) 2020.05.28
[SpringBoot] UnitTest 환경 만들기  (0) 2020.05.27

+ Recent posts