🛠️Backend/Spring

[Spring Boot] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 08

뉴발자 2023. 1. 8.
728x90

 

 

 

 

 

 

 

 

 

 

 

 

 

 

CRUD

 

 

전체 조회 화면 만들기

1. index.mustache UI 변경

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

 

추가된 부분에서 Mustache의 문법이 처음으로 사용된다.

 


{{#posts}} {{/posts}}

  • posts라는 List를 가져온다
  • Java의 for문과 동일하게 생각하면 된다.

 

{{변수명}}

  • List에서 뽑아낸 객체의 필드를 사용한다.

 

 

 

 

2. PostsRepository, PostsService, IndexController 코드 수정

2-1. PostsRepository

package com.jojoIdu.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("SELECT p FROM Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();

}

 

SpringDataJpa에서 제공하지 않는 메소드@Query를 사용하여 작성한다.

 

실제로 앞의 코드는 SpringDataJpa에서 제공하는 기본 메소드만으로 해결할 수 있지만

 

@Query를 사용하면 가독성이 좋아지므로 선택해서 사용하면 된다.

 


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

 

대표적 예로 Querydsl, Jooq, MyBatis 등이 있다.

 

조회는 위 3가지 Framework중 하나를 통해 조회하고, 등록/수정/삭제 등은 SpringDataJpa를 통해 진행한다.

 

책의 저자는 Querydsl을 추천한다.

 

1. 타입 안정성이 보장된다.

단순한 문자열로 쿼리를 생성하는 것이 아니라, 소드를 기반으로 쿼리를 생성하기 때문에

 

오타나 존재하지 않는 컬럼명을 명시할 경우 IDE에서 자동으로 검출된다.

 

이 장점은 Jooq에서도 지원하는 장점이지만, MyBatis에서는 지원하지 않는다.

 

 

2. 국내 많은 회사에서 사용 중이다.

JPA를 적극적으로 사용하는 회사에서는 Querydsl을 적극적으로 사용 중이다.

 

 

3. 레퍼런스가 많다.

앞 2번의 장점에서 이어지는 것인데,

 

많은 회사와 개발자들이 사용하다보니 그만큼 국내 자료가 많다.

 

어떤 문제가 발생했을 때, 여러 커뮤니티에서 질문하고

 

그에 대한 답변을 들을 수 있다는 것은 큰 장점이다.


 

 

2-2. PostsService

...

import org.springframework.transaction.annotation.Transactional;

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

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    ...

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

}

 

findAllDest() 메소드의 @Transactional 어노테이션에 옵션이 하나 추가되었다.

 

(readOnly = true) 옵션을 주면 트랜잭션 범위는 유지하되,

 

조회 기능만 남겨두어 조회 속도가 개선되기 때문에

 

등록, 수정, 삭제 기능이 전혀 없는 서비스 메소드에서 사용하는 것을 추천한다.

 

 

메소드 내부의 코드에선 람다식을 모르면 조금 생소한 코드가 있을 수 있다

.

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

 

위의 메소드는 postsRepository 결과로 넘어온 Posts의 Stream을

 

map을 통해 PostsListResponseDto로 변환하여 List로 반환하는 메소드이다.

 

 

아직 PostsListResponseDto Class가 없기 때문에 이 클래스 역시 생성해준다.

 

 

2-3) PostsListResponseDto

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

import com.jojoIdu.book.springboot.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();
    }
    
}

 

마지막으로 IndexController를 수정해준다.

 

 

2-4. IndexController

...

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

}

 

이제 전체 조회를 출력하기 위한 모든 부분의 작성이 완료되었다.

 

http://localhost:8080/ 로 접속한 뒤 등록 화면을 이용해 하나의 데이터를 등록해보면

 

아래와 같이 조회가 되는것을 확인할 수 있다.

 

전체 테이블 조회 기능 추가 후 출력되는 화면

 

 

 

 

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

다음은 게시글 수정 및 삭제 화면을 만들어 보겠다. 게시글 수정 API는 이미 이전에 만들어 두었다.

package com.jojoIdu.book.springboot.web;

...

public class PostsApiController {

    private final PostsService postsService;

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

...

}

 

해당 API로 요청하는 화면을 개발한다.

 

 

3. 게시글 수정

우선 게시글 수정 화면 Mustache 파일을 생성한다.

 

(posts-save.mustache와 화면 구성이 비슷하기 때문에 복사해서 이름만 바꿔준 후 수정하면 빠르다.)

 

 

3-1. 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>
            </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}}

  • Mustache는 객체의 필드 접근 시 점(Dot)으로 구분한다.
  • 즉, Post Class의 id에 대한 접근은 post.id로 사용할 수 있다.

 

readonly

  • Input 태그에 읽기 가능만 허용하는 속성이다.
  • id와 author는 수정할 수 없도록 읽기만 허용하도록 추가해준다.

 

그리고 btn-update 버튼을 클릭하면 update 기능을 호출할 수 있게

 

index.js 파일에도 update function을 추가해준다.

 

 

3-2. index.js

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

    save : function() {
    
	...
        
    },

    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=utr-8',
            data: JSON.stringify(data)
        }).done(function() {
            alert('글이 수정되었습니다.');
            window.location.href='/';
        }).fail(function(error) {
            alert(JSON.stringify(error));
        });

    }

};

main.init();

 


type:'PUT'

여러 HTTPMethod 중 PUT 메소드를 선택한다.

  • PostsApiController에 있는 API에서 이미 @PutMapping으로 선언했기 때문에 PUT를 사용해야 한다.
  • 참고로 이는 REST 규약에 맞게 설정된 것이다.
  • REST에서 CRUD는 아래과 같이 HTTP Method에 매핑된다.

        생성 (Create) - POST

        조회 (Read) - GET

        수정 (Update) - PUT

        삭제 (Delete) - DELETE

 

 

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

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

 

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

 

 

3-3. index.mustache

{{>layout/header}}

	...

        <!-- 목록 출력 영역 -->
        
        ...
        
            <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}}

 

화면쪽 작업이 끝났으니 수정 화면을 연결해줄 IndexController 코드에 아래의 코드를 추가해준다. 

 

 

3-4. IndexController

package com.jojoIdu.book.springboot.web;

...

public class IndexController {

    private final PostsService postsService;

    ...

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

        return "posts-update";
    }

}

 

이제 페이지로 접속해서 수정 기능을 테스트해본다.

 

제목의 글씨를 누르면 아래와 같이 수정 화면으로 이동하고

 

수정 후 수정완료 버튼을 누르면 제목과 내용이 수정된다.

 

수정기능이 추가된 조회 화면

 

게시글 수정 페이지 - 수정할 내용 입력 및 수정완료 버튼 클릭

 

수정완료 버튼 클릭 시 나오는 알람창

 

수정 후 수정된 내용으로 출력되는 조회 화면

 

 

 

 

4. 게시글 삭제

수정 기능이 정상적으로 구현되었고, 이어서 삭제 기능을 구현해본다.

 

삭제 버튼은 본문을 확인하고 진행해야 하므로, 수정 화면에 추가해준다.

 

 

4-1. posts-update.mustache

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

 

다음으로 삭제 이벤트를 실행할 JS 코드도 추가해 준다.

 

 

4-2. index.js

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

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

    save : function() {
        
        ...

    },

    update : function() {
        
        ...

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

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

    }

};

main.init();

 

type: 'DELETE'를 제외하고는 update function과 크게 차이가 없기 때문에

 

복사해서 내용만 조금 수정해주면 된다.

 

 

다음으로 삭제 API를 생성한다.

 

먼저 PostsService에 아래의 메소드를 추가해준다. 

 

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

...

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    ...

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

}

 


postsRepository.delete(posts)

  • JpaRepository에서 이미 delete 메소드를 지원하고 있으니 이를 사용한다.
  • Entity를 파라미터로 삭제할 수도 있고, deleteById메소드를 이용하면 id값으로 삭제할 수도 있다.
  • 존재하는 Posts인지 확인을위해 Entity 조회 후 그대로 삭제한다.

 

서비스에서 만든 delete 메소드를 Controller가 사용하도록

 

PostsApiController에 아래의 코드를 추가해준다.

 

 

4-3) PostsApiController

package com.jojoIdu.book.springboot.web;

...

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    private final PostsService postsService;

    ...

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


}

 

게시글 삭제를 위한 모든 코드의 작성이 완료되었다.

 

이제 페이지에 접속해서 게시글 수정 화면으로 들어간 후 삭제 버튼을 클릭하면

 

아래와 같이 정상적으로 게시글이 삭제된다.

 

게시글 수정 페이지에 생성된 게시글 삭제 버튼 클릭

 

게시글 삭제 버튼 클릭 시 나오는 알람창

 

게시글 삭제 버튼 클릭 후 출력되는 조회 목록

 

 

 

다음번엔 Spring Security와 OAuth 2.0으로 로그인 기능을 구현하는 방법을 포스팅 하겠다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

댓글