마크다운 묘하게 적용 안되서 손목 뽀사지는 줄 알았습니다...
대망의 킹시판 만들기!

 

5. 게시판 만들기

5.1. IndexController

저는 4절에서 언급했던 mustache를 사용해 화면을 구성하는 연습을 했습니다.

mustache 파일들의 기본 위치는 src/main/resources/templates로 설정해 IndexController.java의 url mapping을 도울 것입니다.

아래 코드에서는 "index"라는 문자열을 반환하므로 src/main/resources/template/index.mustache로 전환됩니다.

    @RequiredArgsConstructor
    @Controller

    public class IndexController {

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

 

메인페이지를 로딩하는 테스트 코드는 사전조건이 필요없으므로 given에 해당하는 부분도 없습니다.

또한 전체 코드를 확인할 필요가 없으니, 메인 페이지에 포함된 문자열 일부가 있는지 검사하는 코드를 다음과 같이 짰습니다.

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

        //then
        assertThat(body).contains("Spring Boot Webservice");
    }

 

5.2. Contents Delivery Network

게시글을 등록하는 화면을 예쁘게 구성하기 위해 프론트엔드 라이브러리를 사용했습니다.

라이브러리를 사용하는 방법에는 크게 두가지가 있습니다.

 

1. 직접 라이브러리 다운받기

2. CDN 사용하기

 

CDN은 Contents Delivery Network의 약자로 정적 컨텐츠(이미지, css, js파일 등)를 캐시해 전 세계에 복사합니다.

사용자가 사이트를 요청하면 사용자와 물리적으로 가장 가까운 서버가 데이터를 전송하도록 해 대기 시간을 줄입니다.

우리는 CDN을 사용해 프론트엔드 라이브러리를 간편하게 사용할 수 있습니다.

(다만 실제 서비스에서는 CDN 서버에 문제가 생기면 서비스에도 영향을 미치기 때문에 이 방법을 사용하지 않는다고 합니다.)

CDN을 사용하는 코드는 다음과 같습니다.

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

 

5.3. 게시글 등록

 

게시글 등록 화면을 만들기 위해 다음 과정을 수행했습니다.

 

1. index.mustache에 글 등록 버튼을 추가합니다.

<a href="/posts/save" role="button" class="btn btn-primary">글 등록하기</a>

 

2. 페이지가 맵핑되도록 IndexController.java를 수정해줍니다.

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

 

3. 게시글을 저장하는 역할을 하는 post-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}}

 

4. 게시글 등록 버튼이 동작할 수 있도록 index.js를 생성합니다.

경로는 src/main/resources/static/js/app 입니다.

var main 속성 안에 function을 추가함으로써 브라우저의 scope가 겹치는 문제, 여러 사람이 작성한 코드에서 함수 이름이 중복되는 문제를 미연에 방지할 수 있습니다.

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

 

5. footer.js에 index.js를 추가합니다. 버전을 지정하지 않으면 캐시 문제로 application을 구동해도 수정된 코드가 반영되지 않는 불상사가 발생할 수 있어요...

<script src="/js/app/index.js?ver=2"></script>

 

5.4. 전체 조회

 

게시글 전체 조회 화면을 만들기 위해 다음 과정을 수행했습니다.

 

1. 전체 목록을 나타낼 수 있도록 index.mustache의 UI를 변경합니다.

        <!-- 목록 출력 영역 -->

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

 

2. 가독성을 높이기 위해 @Query 어노테이션을 사용해 PostsRepository.java를 작성합니다.

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

 

3. PostRepository 결과로 넘어온 Stream을 Dto로 변환해 List로 반환하기 위해 PostsService.java를 수정합니다.

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

 

4. PostListResponseDto.java를 작성합니다.

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

 

5. PostController.java가 게시글 목록을 보여줄 수 있도록 수정합니다.

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

 

5.5. 게시글 수정

게시글을 수정하는 기능을 구현했습니다.

이 부분은 코드를 최소한으로 작성하고 어떤 과정을 거쳤는지 기록하겠습니다!

 

1. 게시글을 수정하기 위해 template 폴더 내에 posts-update.mustache를 생성했습니다.

2. update 메소드를 index.js var main 속성 내에 추가했습니다.

3. index.mustache에 게시글 수정 시각이 표시되도록 modifiedDate 속성을 추가했습니다.

4. IndexController에 update id에 따라 url을 맵핑해주는 기능을 추가했습니다.

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

            return "posts-update";
        }

 

5.6. 게시글 삭제

 

게시글을 삭제하는 기능을 구현하기 위해 다음과 같은 과정을 거쳤습니다.

 

1. 삭제 기능은 수정 페이지에서 구현되어야 하므로 posts-update.mustache 내에 삭제 버튼을 추가합니다.

2. index.js var main 속성 내에 delete 함수를 정의했습니다.

3. PostsService.java에 삭제 기능을 처리하는 API를 추가했습니다.

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

 

4. PostsService.java에서 만든 메소드를 사용할 수 있도록 PostApiController.java에 다음 코드를 추가합니다.

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

 

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://brownbears.tistory.com/408>

* <https://goddaehee.tistory.com/173>

 

3. Java Persistence Api

 

3.1. JPA가 무엇인가요?

Java Persistence Api는 자바 ORM 기술에 대한 API 입니다.
본격적으로 객체지향 개발을 하게 되면서 애플리케이션은 객체지향 언어와 관계형 DB로 구성되었습니다.

그러나 이 조합은 자바 객체를 SQL로, SQL을 자바 객체로 바꾸는 과정이 반복되어 지루한 코드가 반복된다는 문제가 있었습니다.
더불어 자바와 SQL간에는 패러다임 불일치 문제가 존재합니다.
자바는 추상화, 캡슐화, 정보 은닉을 통해 객체의 기능과 속성을 한 곳에서 관리하려는 패러다임을 가집니다.
반면 관계형 DB는 어떻게 데이터를 저장할지에 집중한 기술이므로 개발자들은 점차 데이터베이스 모델링에 치중하게 되었습니다.

JPA는 개발자가 객체지향적으로 프로그래밍한 것을 관계형 데이터베이스에 걸맞게 대신 SQL문을 생성해서 실행합니다.
따라서 JPA를 사용한 프로그램의 생산성이 높아지고 유지보수가 쉬워졌습니다.

 

3.2. JPA를 적용해봅시다

JPA는 인터페이스이므로 이를 사용하기 위해 구현체가 필요합니다(e.g., Hibernate, Eclipse Link).
또한 구현체를 더욱 쉽게 사용하기 위해 Spring Data JPA라는 모듈을 사용합니다.

JPA <- Hibernate <- Spring Data JPA

또한 모듈을 사용하면 Spring Data JPA는 Hibernate 외에 다른 구현체로 교체하는 작업과
관계형 데이터베이스 외에 다른 저장소(e.g., MongoDB)로 교체하는 작업의 오버헤드를 줄여줍니다.

 

3.3. 게시글 등록 API를 만들어봅시다

게시글 등록 기능을 만들기 위해 아래 세 파일을 만들어야 했습니다.

  • web 패키지 내에 PostApiController.java
  • web.dto 패키지 내에 PostsSaveRequestDto.java
  • service.posts 패키지 내에 PostsService.java

 

3.4. 테스트 코드로 JPA가 잘 돌아가는지 확인해볼까요?

저는 스프링 부트 프로젝트의 test 폴더 내에 앞서 연습했던 given-when-then 방식으로 테스트 코드를 작성했습니다.

 @Test
    public void Posts_등록된다() throws Exception{
        //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);
    }

 

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://velog.io/@adam2/JPA%EB%8A%94-%EB%8F%84%EB%8D%B0%EC%B2%B4-%EB%AD%98%EA%B9%8C-orm-%EC%98%81%EC%86%8D%EC%84%B1-hibernate-spring-data-jpa>

* <https://gmlwjd9405.github.io/2019/08/03/reason-why-use-jpa.html>

2. Testcode

 

2.1. 테스트코드를 왜 작성해야 하나요?

요즘의 개발에서 테스트는 필수적입니다. 이번주에 저는 Junit4를 사용해 테스트하는 방법을 익혔습니다.

  • 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.
  • 단위 테스트를 하면 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다.
  • 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.

 

2.2. Spring boot 테스트코드는 어떻게 작성해야 하나요?

2.2.1. Given-when-then

테스트 코드는 일반적으로 given-when-then 형식으로 작성합니다.
given 단계에서 테스트를 위해 준비하고 when 단계에서 실제로 액션하는 테스트를 실행한 후 then 단계에서 테스트를 검증합니다.

 

2.2.2. Given-when-then 연습

이번 주에 실습했던 게시글이 제대로 저장되었는지 확인하고 불러오는 코드입니다.

// given
String title = "테스트 게시글";
String content = "테스트 본문";

postsRepository.save(Posts.builder()
    .title(title)
    .content(content)
    .author("ei654028@gmail.com")
    .build());

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

//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);

 

2.3. Lombok으로 자바를 더 편하게 써봅시다

Lombok은 자바에서 DTO, Domain 등을 만들 때 반복적으로 만들어야 하는 멤버 필드 생성자 코드를 줄이는 라이브러리 입니다.
Getter, Setter, ToString 등 다양한 코드를 자동완성 해 줍니다.
lombok을 적용하면 코드의 가독성이 높아져 일명 코드 다이어트로 불리기도 합니다.

 

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://brunch.co.kr/@springboot/418>

* <https://martinfowler.com/bliki/GivenWhenThen.html>

* <https://woowabros.github.io/study/2018/03/01/spock-test.html>

* <https://brunch.co.kr/@springboot/292>

* <https://goddaehee.tistory.com/95>

+ Recent posts