텍스트 매체를 가지는 게시판 구현하기
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()
}
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
등의 속성을 지정
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
를 모두 불러오는 과정에서 Paging
및 sort
를 적용할 수 있다.
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
클래스는 Entity
인 Forum
에 혹시 모를 내부적으로 감춰야 할 것들을 제외한 정보를 담은 클래스 (이 예제에서는 두 정보가 동일하다)
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);
}
}
Entity
와 DTO
객체 사이의 변환 기능을 수행하는 메서드
이 예제에서는 한 개의 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
에 관련된 코드가 없어 에러가 발생합니다.
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
에서 처리할 예정
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");
}
}
@Valid
와 BindingResult errors
를 통해 DTO
에 적용한 Validation Annotation
에 잘 맞는지를 확인 후 아니면 ForumRegistrationInvalid
및 ForumUpdateInvalid
Exception
을 던짐
이 예제에서 설정한 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();
}
}
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
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
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
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
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/