[EC-Spring] 5주차-게시판 기능 구현하기

umtuk·2022년 5월 7일
0

EC-Spring

목록 보기
5/6

텍스트 매체를 가지는 게시판 구현하기
01_Total_Structure

https://github.com/umtuk/ec-spring/tree/master/forum

gradle

plugins {
    id 'org.springframework.boot' version '2.6.7'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'org.ec'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

디렉토리 구조

03_Dir_Structure

Entity

04_Entity

Forum class

package org.ec.forum.domain.forum.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Table(name = "Forum")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Forum {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false) private Long createdAt;
    @Column(nullable = false) private Long updatedAt;

    @Column(nullable = false) private Long views;

    @Column(nullable = false, length = 30) private String writer;
    @Column(nullable = false, length = 40) private String title;
    @Column(nullable = false, columnDefinition = "TEXT") private String content;

    public Forum(Long createdAt, Long updatedAt, Long views, String writer, String title, String content) {
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.views = views;
        this.writer = writer;
        this.title = title;
        this.content = content;
    }
}

@@NoArgsConstructor, @AllArgsConstructor : lombok을 활용해 생성자 코드 생성
@Getter, @Setter lombok을 활용해 Getter / Setter 코드 생성

@GeneratedValue(strategy = GenerationType.IDENTITY) : 기본 키 생성을 DB에 위임

@Column에서 각 변수의 nullable, length 등의 속성을 지정

Repository

ForumRepository interface

package org.ec.forum.domain.forum.repository;

import org.ec.forum.domain.forum.entity.Forum;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ForumRepository extends PagingAndSortingRepository<Forum, Long> {

}

PagingAndSortingRepository을 사용하면 Entity를 모두 불러오는 과정에서 Pagingsort를 적용할 수 있다.

DTO

02_DTO_Entity

package org.ec.forum.domain.forum.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@AllArgsConstructor
@Getter
public class ForumRegistrationForm {

    @NotNull @Size(min = 4, max = 30)
    private String writer;

    @NotNull @Size(min = 4, max = 40)
    private String title;

    @NotNull @Size(max = 3000)
    private String content;
}
package org.ec.forum.domain.forum.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class ForumResponseForm {

    private Long id;

    private Long createdAt;
    private Long updatedAt;

    private Long views;

    private String writer;
    private String title;
    private String content;
}
package org.ec.forum.domain.forum.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@AllArgsConstructor
@Getter
public class ForumUpdateForm {

    @NotNull
    private Long id;

    @NotNull @Size(max = 3000)
    private String content;
}

ForumRegistrationForm 클래스는 게시판을 등록할 때 필요한 정보를 담은 객체
ForumResponseForm 클래스는 EntityForum에 혹시 모를 내부적으로 감춰야 할 것들을 제외한 정보를 담은 클래스 (이 예제에서는 두 정보가 동일하다)
ForumUpdateForm 클랫므는 게시판의 안의 내용을 수정하고자 할 때 필요한 정보를 담은 객체
세 객체에 모두 Validation Annotation을 적용해 Null이 아니고 적절한 Size를 가지고 있는지를 판별

ForumEntityConverter class

package org.ec.forum.domain.forum.util.converter;

import org.ec.forum.domain.forum.dto.ForumRegistrationForm;
import org.ec.forum.domain.forum.dto.ForumResponseForm;
import org.ec.forum.domain.forum.entity.Forum;

public class ForumEntityConverter {

    public static Forum convert(ForumRegistrationForm form) {
        long createdAt = System.currentTimeMillis();
        long updatedAt = -1L;
        long views = 0;
        String writer = form.getWriter();
        String title = form.getTitle();
        String content = form.getContent();

        return new Forum(createdAt, updatedAt, views, writer, title, content);
    }

    public static ForumResponseForm convert(Forum forum) {
        long id = forum.getId();
        long createdAt = forum.getCreatedAt();
        long updatedAt = forum.getUpdatedAt();
        long views = forum.getViews();
        String writer = forum.getWriter();
        String title = forum.getTitle();
        String content = forum.getContent();

        return new ForumResponseForm(id, createdAt, updatedAt, views, writer, title, content);
    }
}

EntityDTO 객체 사이의 변환 기능을 수행하는 메서드

Service

이 예제에서는 한 개의 Service Layer를 갖도록 구현

ForumService interface

package org.ec.forum.domain.forum.service;

import org.ec.forum.domain.forum.dto.ForumRegistrationForm;
import org.ec.forum.domain.forum.dto.ForumResponseForm;
import org.ec.forum.domain.forum.dto.ForumUpdateForm;
import org.springframework.data.domain.Pageable;

public interface ForumService {

    public ForumResponseForm readById(Long id);
    public Iterable<ForumResponseForm> readAll(Pageable pageable);
    public ForumResponseForm create(ForumRegistrationForm form);
    public ForumResponseForm updateById(Long id, ForumUpdateForm form);
    public void deleteById(Long id);
}

ForumServiceImpl class

package org.ec.forum.domain.forum.service.impl;

import lombok.RequiredArgsConstructor;
import org.ec.forum.domain.forum.dto.ForumRegistrationForm;
import org.ec.forum.domain.forum.dto.ForumResponseForm;
import org.ec.forum.domain.forum.dto.ForumUpdateForm;
import org.ec.forum.domain.forum.entity.Forum;
import org.ec.forum.domain.forum.exception.ForumNotFoundException;
import org.ec.forum.domain.forum.repository.ForumRepository;
import org.ec.forum.domain.forum.service.ForumService;
import org.ec.forum.domain.forum.util.converter.ForumEntityConverter;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ForumServiceImpl implements ForumService {

    private final ForumRepository forumRepository;

    @Override
    public ForumResponseForm readById(Long id) {
        Forum forum = forumRepository.findById(id)
                .orElseThrow(() -> new ForumNotFoundException(id));

        forum.setViews(forum.getViews() + 1);

        return ForumEntityConverter.convert(forumRepository.save(forum));
    }


    @Override
    public Iterable<ForumResponseForm> readAll(Pageable pageable) {
        Iterable<Forum> forums = forumRepository.findAll(pageable);
        List<ForumResponseForm> forms = new ArrayList<>();

        for (Forum forum : forums)
            forms.add(ForumEntityConverter.convert(forum));

        return forms;
    }

    @Override
    public ForumResponseForm create(ForumRegistrationForm form) {
        Forum forum = forumRepository.save(ForumEntityConverter.convert(form));
        return ForumEntityConverter.convert(forum);
    }

    @Override
    public ForumResponseForm updateById(Long id, ForumUpdateForm form) {
        Forum forum = forumRepository.findById(id)
                .orElseThrow(() -> new ForumNotFoundException(id));

        forum.setUpdatedAt(System.currentTimeMillis());
        forum.setContent(form.getContent());

        return ForumEntityConverter.convert(forumRepository.save(forum));
    }

    @Override
    public void deleteById(Long id) {
        if (forumRepository.existsById(id))
            forumRepository.deleteById(id);
        else throw new ForumNotFoundException(id);
    }
}

ForumRepository의 메서드를 불러와 ForumServiceImpl의 메서드를 구현
아직 Exception에 관련된 코드가 없어 에러가 발생합니다.

Custom Exception

package org.ec.forum.domain.forum.exception;

public class ForumNotFoundException extends RuntimeException {

    public ForumNotFoundException(Long id) {
        super("Forum is not found: " + id);
    }
}
package org.ec.forum.domain.forum.exception;

public class ForumRegistrationInvalid extends RuntimeException {

    public ForumRegistrationInvalid() {
        super("Forum Registration is invalid");
    }
}
package org.ec.forum.domain.forum.exception;

public class ForumUpdateInvalid extends RuntimeException {

    public ForumUpdateInvalid() {
        super("Forum update is invalid");
    }
}

해당 Exception들은 추후에 Controller Advice에서 처리할 예정

Controller

package org.ec.forum.domain.forum.controller;

import lombok.RequiredArgsConstructor;
import org.ec.forum.domain.forum.dto.ForumRegistrationForm;
import org.ec.forum.domain.forum.dto.ForumResponseForm;
import org.ec.forum.domain.forum.dto.ForumUpdateForm;
import org.ec.forum.domain.forum.exception.ForumNotFoundException;
import org.ec.forum.domain.forum.exception.ForumRegistrationInvalid;
import org.ec.forum.domain.forum.exception.ForumUpdateInvalid;
import org.ec.forum.domain.forum.service.ForumService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
@RequestMapping("/forum")
public class ForumController {

    private final ForumService forumService;

    @GetMapping("/{id}")
    public ResponseEntity getById(@PathVariable Long id) throws ForumNotFoundException {
        ForumResponseForm form = forumService.readById(id);
        return ResponseEntity.ok(form);
    }

    @GetMapping("/page")
    public ResponseEntity getAll(@PageableDefault() Pageable pageable) {
        Iterable<ForumResponseForm> forms = forumService.readAll(pageable);
        return ResponseEntity.ok(forms);
    }

    @PostMapping
    public ResponseEntity postForum(@RequestBody @Valid ForumRegistrationForm form, BindingResult errors) {
        if (errors.hasErrors())
            throw  new ForumRegistrationInvalid();

        ForumResponseForm responseForm = forumService.create(form);
        return ResponseEntity.ok(responseForm);
    }

    @PutMapping("/{id}")
    public ResponseEntity putForum(@PathVariable Long id, @RequestBody @Valid ForumUpdateForm form, BindingResult errors) {
        if (errors.hasErrors())
            throw new ForumUpdateInvalid();

        ForumResponseForm responseForm = forumService.updateById(id, form);
        return ResponseEntity.ok(responseForm);
    }

    @DeleteMapping("/{id}")
    public  ResponseEntity deleteForum(@PathVariable Long id) {
        forumService.deleteById(id);
        return ResponseEntity.ok("success");
    }
}

@ValidBindingResult errors를 통해 DTO에 적용한 Validation Annotation에 잘 맞는지를 확인 후 아니면 ForumRegistrationInvalidForumUpdateInvalid Exception을 던짐

Controller Advice

이 예제에서 설정한 Custom Exception으로 오는 예외를 처리

package org.ec.forum.domain.forum.controller;

import org.ec.forum.domain.forum.exception.ForumNotFoundException;
import org.ec.forum.domain.forum.exception.ForumRegistrationInvalid;
import org.ec.forum.domain.forum.exception.ForumUpdateInvalid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@ControllerAdvice
public class ForumControllerAdvice {

    @ResponseBody
    @ExceptionHandler(ForumNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    String forumNotFoundException(ForumNotFoundException e) {
        return e.getMessage();
    }

    @ResponseBody
    @ExceptionHandler(ForumRegistrationInvalid.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    String forumRegistrationInvalid(ForumRegistrationInvalid e) {
        return e.getMessage();
    }

    @ResponseBody
    @ExceptionHandler(ForumUpdateInvalid.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    String forumUpdateInvalid(ForumUpdateInvalid e) {
        return e.getMessage();
    }
}

실행 결과

비어있는 게시판에 id=1 인 게시판 불러오기

SEND  
METHOD: GET  
http://localhost:8080/forum/1  
RESPONSE: 404  
HEADERS: 
    Content-Type:	text/plain;charset=UTF-8
    Content-Length:	21 bytes
    Date:	
    Sat, 07 May 2022 14:06:17 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    Forum is not found: 1

비어있는 게시판에 page=0 인 게시판 모두 불러오기

SEND  
METHOD: GET  
http://localhost:8080/forum/page/  
QUERY PARAMETERS
    page=0
RESPONSE: 404  
HEADERS: 
    Content-Type:	application/json
    Transfer-Encoding:	chunked
    Date:	
    Sat, 07 May 2022 14:08:58 GMT
    Keep-Alive:	timeout=60
Connection:	keep-alive
BODY: 
    []

게시판 하나 추가하기

SEND  
METHOD: POST  
http://localhost:8080/forum/  
HEADERS:
    Content-Type : application/json
BODY: 
    {
        "writer" : "user",
        "title" : "title",
        "content" : "content"
    }
RESPONSE: 200  
HEADERS: 
    Content-Type:	application/json
    Transfer-Encoding:	chunked
    Date:	
    Sat, 07 May 2022 14:11:43 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    {
        "id": 1,
        "createdAt": 1651932703181,
        "updatedAt": -1,
        "views": 0,
        "writer": "user",
        "title": "title",
        "content": "content"
    }

게시판 수정하기

SEND  
METHOD: PUT  
http://localhost:8080/forum/1  
HEADERS:
    Content-Type : application/json
BODY: 
    {
        "id" : "1",
        "content" : "content"
    }
RESPONSE: 200  
HEADERS: 
    Content-Type:	application/json
    Transfer-Encoding:	chunked
    Date:	
    Sat, 07 May 2022 14:13:26 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    {
        "id": 1,
        "createdAt": 1651932703181,
        "updatedAt": 1651932806628,
        "views": 0,
        "writer": "user",
        "title": "title",
        "content": "content"
    }

게시판 삭제하기

SEND  
METHOD: DELETE  
http://localhost:8080/forum/1  
RESPONSE: 200  
HEADERS: 
    Content-Type:	application/json
    Transfer-Encoding:	chunked
    Date:		
    Sat, 07 May 2022 14:14:31 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    success

id가 잘못된 게시판 삭제 요청

SEND  
METHOD: DELETE  
http://localhost:8080/forum/1  
RESPONSE: 404  
HEADERS: 
    Content-Type:	text/plain;charset=UTF-8
    Content-Length:	21 bytes
    Date:	
    Sat, 07 May 2022 14:15:05 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    Forum is not found: 1

id가 잘못된 게시판 수정 요청

SEND  
METHOD: PUT 
http://localhost:8080/forum/1  
HEADERS:
    Content-Type : application/json
BODY: 
    {
        "id" : "1",
        "content" : "content"
    }
RESPONSE: 404  
HEADERS: 
    Content-Type:	text/plain;charset=UTF-8
    Content-Length:	21 bytes
    Date:		
    Sat, 07 May 2022 14:16:20 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    Forum is not found: 1

10개 이상의 게시판 생성 후 page=0 인 게시판 모두 불러오기

SEND  
METHOD: GET  
http://localhost:8080/forum/page/  
QUERY PARAMETERS
    page=0
RESPONSE: 200  
HEADERS: 
    Content-Type:	application/json
    Transfer-Encoding:	chunked
    Date:	
    Sat, 07 May 2022 14:17:46 GMT
    Keep-Alive:	timeout=60
    Connection:	keep-alive
BODY: 
    [
        {
            "id": 2,
            "createdAt": 1651933018240,
            "updatedAt": -1,
            "views": 0,
            "writer": "user",
            "title": "title",
            "content": "content"
        },
        {
            "id": 3,
            "createdAt": 1651933018370,
            "updatedAt": -1,
            "views": 0,
            "writer": "user",
            "title": "title",
            "content": "content"
        },

        ...

        {
            "id": 11,
            "createdAt": 1651933019636,
            "updatedAt": -1,
            "views": 0,
            "writer": "user",
            "title": "title",
            "content": "content"
        }
    ]

참고

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html
https://spring.io/guides/tutorials/rest/

profile
https://github.com/umtuk

0개의 댓글