Springboot로 디자인이 하나도 없고 매우 간단한 게시판을 구현하는 시리즈입니다.
최종 소스는 깃헙에서 확인하실 수 있습니다.
- [SpringBoot] 게시판 (1) - 준비작업
- [SpringBoot] 게시판 (2) - 게시글 추가하기
- [SpringBoot] 게시판 (3) - 게시글 조회,수정,삭제
- [SpringBoot] 게시판 (4) - 검색과 페이징
- [SpringBoot] 게시판 (5) - EC2에 배포하기
이 글에서는 게시글을 추가하는 작업을 해보도록 하겠습니다.
1. 퍼블리싱
게시글을 추가하려면 입력할 수 있는 폼이 있어야 하기 때문에, 퍼블리싱을 먼저 해보려 합니다.
1) 헤더와 푸터
templates/common/header.html
<h1>헤더 입니다.</h1>
<hr>
templates/common/footer.html
<hr>
<h1>푸터입니다.</h1>
모든 페이지에 존재하는 헤더와 푸터입니다.
2) 게시글 리스트 페이지
templates/board/list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- HEADER -->
<div th:insert="common/header.html" id="header"></div>
<a th:href="@{/post}">글쓰기</a>
<!-- FOOTER -->
<div th:insert="common/footer.html" id="footer"></div>
</body>
</html>
thymeleaf 문법을 볼 수 있습니다.
-
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
- XHTML 문서를 위한 XML 네임스페이스를 명시하는 것으로, 생략해도 정상동작합니다. ( 참고 )
- IntelliJ에서 thymeleaf 문법 사용 시, 문법 에러가 발생하여 추가하였습니다.
-
th:insert
- 헤더와 푸터처럼 다른 페이지를 현재 페이지에 삽입하는 역할을 합니다. ( 참고 )
- JSP의 include와 같습니다.
-
th:href
- 예제에서 @{/post}는 URL이 http://localhost:8080/post 가 되겠네요.
- thymeleaf에서 html 속성은 대부분 이처럼 th: 으로 바꿔서 사용할 수 있습니다.
- @{ } 의 의미는 애플리케이션이 설치된 경로를 기준으로 하는 상대경로 입니다. ( 참고 )
다음 글에서도 thymeleaf에 대한 내용을 몇 가지 더 소개할 예정이지만 모두 다룰 수 없어서, 그 밖에 thymeleaf 레퍼런스는 위 링크의 공식문서 또는 여기를 참고하면 좋을 것 같습니다.
3) 글쓰기 입력 폼 페이지
templates/board/write.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/post" method="post">
제목 : <input type="text" name="title"> <br>
작성자 : <input type="text" name="writer"> <br>
<textarea name="content"></textarea><br>
<input type="submit" value="등록">
</form>
</body>
</html>
입력 폼 페이지에서 특별한 것은 없습니다.
2. Controller
다음으로 URL을 매핑하고, 비즈니스 로직 함수를 호출하여 view에 뿌려주는 역할을 하는 컨트롤러를 구현해보겠습니다.
이 글에서는 게시글을 DB에 INSERT하는 write() 메서드만 구현하고, 나머지 메서드는 다음 글에서 작성하도록 하겠습니다.
src/main/java/com/victolee/board/controller/BoardController.java
package com.victolee.board.controller;
import com.victolee.board.dto.BoardDto;
import com.victolee.board.service.BoardService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@AllArgsConstructor
public class BoardController {
private BoardService boardService;
@GetMapping("/")
public String list() {
return "board/list.html";
}
@GetMapping("/post")
public String write() {
return "board/write.html";
}
@PostMapping("/post")
public String write(BoardDto boardDto) {
boardService.savePost(boardDto);
return "redirect:/";
}
}
-
@Controller
- 컨트롤러임을 명시하는 어노테이션입니다.
- MVC에서 컨트롤러로 명시된 클래스의 메서드들은 반환 값으로 템플릿 경로를 작성하거나, redirect를 해줘야 합니다.
- 템플릿 경로는 templates 패키지를 기준으로한 상대경로입니다.
- @RestController도 존재하는데, 이는 @Controller, @ResponseBody가 합쳐진 어노테이션입니다.
- view 페이지가 필요없는 API 응답에 어울리는 어노테이션입니다.
-
@AllArgsConstructor
- Bean 주입 방식과 관련이 있으며, 생성자로 Bean 객체를 받는 방식을 해결해주는 어노테이션입니다. 그래서 BoardService 객체를 주입 받을 때 @Autowired 같은 특별한 어노테이션을 부여하지 않았습니다. ( 참고 )
- 그 밖에, @NoArgsConstructor @RequiredArgsConstructor 어노테이션이 있습니다. ( 참고 )
- URL을 매핑해주는 어노테이션이며, HTTP Method에 맞는 어노테이션을 작성하면 됩니다. ( 참고 )
- list() 메서드는 지금 구현하지 않고, 다음 글에서 구현할 것입니다.
- dto는 Controller와 Service 사이에서 데이터를 주고 받는 객체를 의미합니다.
다음으로 비즈니스 로직을 수행하는 Service를 구현해보도록 하겠습니다.
src/main/java/com/victolee/board/service/BoardService.java
package com.victolee.board.service;
import com.victolee.board.dto.BoardDto;
import com.victolee.board.domain.repository.BoardRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@AllArgsConstructor
@Service
public class BoardService {
private BoardRepository boardRepository;
@Transactional
public Long savePost(BoardDto boardDto) {
return boardRepository.save(boardDto.toEntity()).getId();
}
}
-
@AllArgsConstructor
- Controller에서 봤던 어노테이션 입니다.
- 마찬가지로 Repository를 주입하기 위해 사용합니다.
-
@Service
- 서비스 계층임을 명시해주는 어노테이션입니다.
-
@Transactional
- 선언적 트랜잭션이라 부르며, 트랜잭션을 적용하는 어노테이션입니다.
- 필수적으로 사용할 필요는 없고, 필요한 부분에 적용하면 됩니다.
- save()
- JpaRepository에 정의된 메서드로, DB에 INSERT, UPDATE를 담당합니다.
- 매개변수로는 Entity를 전달합니다.
4. Repository
다음으로 데이터 조작을 담당하는 Repository를 구현해보겠습니다.
src/main/java/com/victolee/board/domain/repository/BoardRepository.java
package com.victolee.board.domain.repository;
import com.victolee.board.domain.entity.BoardEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}
- Repository는 인터페이스로 정의하고, JpaRepository 인터페이스를 상속받으면 됩니다.
- JpaRepository의 제네릭 타입에는 Entity 클래스와 PK의 타입을 명시해주면 됩니다.
- JpaRepository에는 일반적으로 많이 사용하는 데이터 조작을 다루는 함수가 정의되어 있기 때문에, CRUD 작업이 편해집니다.
5. Entity
다음으로 DB 테이블과 매핑되는 객체를 정의하는 Entity를 구현해보겠습니다.
Entity는 JPA와 관련이 깊습니다.
1) BoardEntity 구현
src/main/java/com/victolee/board/domain/entity/BoardEntity.java
package com.victolee.board.domain.entity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "board")
public class BoardEntity extends TimeEntity {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(length = 10, nullable = false)
private String writer;
@Column(length = 100, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Builder
public BoardEntity(Long id, String title, String content, String writer) {
this.id = id;
this.writer = writer;
this.title = title;
this.content = content;
}
}
-
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- access는 생성자의 접근 권한을 설정하는 속성이며, 최종적으로 protected BoardEntity() { }와 동일합니다.
- 파라미터가 없는 기본 생성자를 추가하는 어노테이션입니다. ( JPA 사용을위해 기본 생성자 생성은 필수 )
- protected인 이유는 Entity 생성을 외부에서 할 필요가 없기 때문입니다.
-
@Getter
- 참고로 @Getter와 @Setter를 모두 해결해주는 @Data 어노테이션도 있습니다.
- 모든 필드에 getter를 자동생성 해주는 어노테이션입니다.
- @Setter 어노테이션은 setter를 자동생성 해주지만, 무분별한 setter 사용은 안정성을 보장받기 어려우므로 Builder 패턴을 사용합니다. ( 참고 - 무분별한 Setter 남용)
-
@Entity
- 객체를 테이블과 매핑 할 엔티티라고 JPA에게 알려주는 역할을 하는 어노테이션 입니다. ( 엔티티 매핑 )
- @Entity가 붙은 클래스는 JPA가 관리하며, 이를 엔티티 클래스라 합니다.
-
@Table(name = "board")
- 엔티티 클래스와 매핑되는 테이블 정보를 명시하는 어노테이션입니다.
- name 속성으로 테이블명을 작성할 수 있으며, 생략 시 엔티티 이름이 테이블명으로 자동 매핑됩니다.
-
@Id
- 테이블의 기본 키임을 명시하는 어노테이션 입니다.
- 저는 일반적으로 Id를 대체키로 사용하는 것이 좋다는 관점이며, Long 타입을 사용합니다.
-
@GeneratedValue(strategy= GenerationType.IDENTITY)
- 기본키로 대체키를 사용할 때, 기본키 값 생성 전략을 명시합니다. ( 참고 )
-
@Column
- 컬럼을 매핑하는 어노테이션입니다.
-
@Builder
- 빌더패턴 클래스를 생성해주는 어노테이션입니다.
- @Setter 사용 대신 빌더패턴을 사용해야 안정성을 보장할 수 있습니다.
Entity 클래스는 테이블과 관련이 있는 것을 알 수 있습니다.
비즈니스 로직은 Entity를 기준으로 돌아가기 때문에 Entity를 Request, Response 용도로 사용하는 것은 적절하지 못합니다.
그래서 데이터 전달목적을 갖는 dto 클래스를 정의하여 사용합니다.
이와 관련하여 이 글과 이 글을 참고해보시면 좋을 것 같네요.
2) TimeEntity 구현
다음으로 BoardEntity는 TimeEntity를 상속하고 있는데요.
TimeEntity는 데이터 조작 시 자동으로 날짜를 수정해주는 JPA의 Auditing 기능을 사용합니다.
src/main/java/com/victolee/board/domain/entity/TimeEntity.java
package com.victolee.board.domain.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
-
@MappedSuperclass
- 테이블로 매핑하지 않고, 자식 클래스(엔티티)에게 매핑정보를 상속하기 위한 어노테이션입니다.
-
@EntityListeners(AuditingEntityListener.class)
- JPA에게 해당 Entity는 Auditing기능을 사용한다는 것을 알리는 어노테이션입니다.
-
@CreatedDate
- 속성을 추가하지 않으면 수정 시, 해당 값은 null이 되어버립니다.
- Entity가 처음 저장될때 생성일을 주입하는 어노테이션입니다.
- 이때 생성일은 update할 필요가 없으므로, updatable = false 속성을 추가합니다. ( 참고 )
-
@LastModifiedDate
- Entity가 수정될때 수정일자를 주입하는 어노테이션입니다.
package com.victolee.board;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
4) Plugin
또한 IntelliJ에서 Lombok을 사용하려면 플러그인을 설치해야 합니다.
6. DTO
마지막으로 데이터 전달 객체인 dto를 구현해보겠습니다.
src/main/java/com/victolee/board/dto/BoardDto.java
package com.victolee.board.dto;
import com.victolee.board.domain.entity.BoardEntity;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class BoardDto {
private Long id;
private String writer;
private String title;
private String content;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
public BoardEntity toEntity(){
BoardEntity boardEntity = BoardEntity.builder()
.id(id)
.writer(writer)
.title(title)
.content(content)
.build();
return boardEntity;
}
@Builder
public BoardDto(Long id, String title, String content, String writer, LocalDateTime createdDate, LocalDateTime modifiedDate) {
this.id = id;
this.writer = writer;
this.title = title;
this.content = content;
this.createdDate = createdDate;
this.modifiedDate = modifiedDate;
}
}
- toEntity()
- 필요한 Entity는 이런식으로 추가하면 됩니다.
- dto에서 필요한 부분을 빌더패턴을 통해 entity로 만듭니다.
- dto는 Controller <-> Service <-> Repository 간에 필요한 데이터를 캡슐화한 데이터 전달 객체입니다.
- 그런데 예제에서 Service에서 Repository 메서드를 호출할 때, Entity를 전달한 이유는 JpaRepository에 정의된 함수들은 미리 정의되어 있기 때문입니다. 그래서 Entity를 전달할 수 밖에 없었는데, 요점은 각 계층에서 필요한 객체전달은 Entity 객체가 아닌 dto 객체를 통해 주고받는 것이 좋다는 것입니다.
7. 테스트
스프링부트를 시작하고, http://localhost:8080/에 접속합니다.
게시글을 등록하고 MySQL에서 확인을 해보면 정상적으로 저장이되고, 등록일과 수정일 또한 자동으로 추가가 된 것을 확인할 수 있습니다.
테이블이 자동 생성된 이유는 application.yml 파일에 spring.jpa.hibernate.ddl_auto: create와 관련이 있습니다.
***error
IntelliJ에서 실행시, 위와 같이 cannot find symbol 에러가 발생한다면, 설정에서 Enable annotation processing을 체크해주도록 합니다.
이상으로 게시글 등록을 구현해보았습니다.
전반적인 내용들을 다루기 때문에 글이 길어졌네요.
다음 글에서는 게시글 조회, 수정, 삭제 방법에 대해 알아보도록 하겠습니다.
[ 참고자료 ]
https://www.thymeleaf.org/doc/articles/layouts.html
https://www.thymeleaf.org/doc/articles/standardurlsyntax.html#context-relative-urls
https://jojoldu.tistory.com/251
https://www.popit.kr/실무에서-lombok-사용법
https://blusky10.tistory.com/316
https://gmlwjd9405.github.io/2018/12/25/difference-dao-dto-entity.html