본문 바로가기
Project/🥑JPA + SpringSecurity 게시판

[JPA + SpringSecurity 게시판] #4 글쓰기 만들기

by 로띠 2022. 4. 12.

이제 본격 게시판 CRUD 시작!

🖍️ create 글쓰기를 만들어 보자 🖍️

 

 

🥑ABoardcadoABoardcadoABoardcadoABoardcadoABoardcadoABoardcadoABoardcado🥑

 


 

👩🏻‍💻 코딩 시작

 

 

 

 

 

1. Entity

DB 테이블과 매핑되는 객체

Entity 로 테이블 column이 생성되고 Data가 들어간다

  1. domain 패키지를 생성하고
  2. entity 패키지를 생성하고
  3. 그 안에 Board 클래스를 생성

 

 

Board 내용은 아래와 같다

 

 

[ Board ]

package com.rim.aboardcado.domain.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;

@Table 
@Getter 
@Builder
@Entity 
@EntityListeners(AuditingEntityListener.class) 
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED) 
public class Board {

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

    @Column(length = 10, nullable = false)
    private String author;

    @Column(length = 50, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    @CreatedDate 
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate 
    private LocalDateTime modifiedDate;

}

  @Table  

  • Entity와 매핑할 table. 즉 DB에서 schema 를 매핑
    • application.properties 에서 Data Source 설정했던 걸 기억하자
    • 🥑ABoardcado 는 update로 설정. DB테이블과 매핑 Entity를 비교해 변경사항 수정
  • class명과 동일하다면 생략 가능 (위 코드 생략가능)

 

  @Getter  

  • 변수 값을 호출하는 getter() 메서드 자동 생성
  •  

  @Builder  

  • 필드 초기화 작업을 도와줌
    • Setter 와 비슷한 역할 this.name = name; 의 기능으로 보면된다
  • 위 코드처럼 @NoArgsConstructor 로 생성된 생성자에 설정하는 @Builder 는
  • 해당 생성자를 사용하는 Builder만 생성되어, 의미있는 객채만 생성 (무분별X)

 

  @Entity  

  • JPA가 관리하는 Entity. DB 테이블과 매핑
  • 생성자 필수
    • SpringBoot에서는 @Autowired, 빈 방식보다 생성자로 빈 주입을 권장

 

  @EntityListeners(AuditingEntityListener.class)  

  • 해당 class에 Auditing 기능을 포함 선언Audit 은 ‘감시하다’의 뜻으로 시간기록에 대해 자동으로 값을 생성✅ JPA Auditing 활성화는 main class에서 @EnableJpaAuditing 추가 설정 ❗
  • @CreatedDate ****, @LastModifiedDate ****의 기능을 사용하기 위해 선언
  • JPA에서는 JPA Audit 을 제공하는데
Audit 은 ‘감시하다’의 뜻으로 시간기록에 대해 자동으로 값을 생성
@CreatedDate ****, @LastModifiedDate ****의 기능을 사용하기 위해 선언
✅ JPA Auditing 활성화는 main class에서 @EnableJpaAuditing 추가 설정 ❗

 

  @NoArgsConstructor(access = AccessLevel.PROTECTED)  

  • 파라미터가 없는 기본 생성자 생성
  • *PROTECTED *****무분별한 생성을 막기 위함

 

  @AllArgsConstructor  

  • 모든 필드 값을 파라미터로 받는 생성자 생성
  • @Builder를 사용할 때, 생성한 생성자가 아무것도 없다면→ 위 코드는 @NoArgsConstructor 기본생성자를 넣었기때문에 명시해줘야 함
  • @AllArgsConstructor(access = AccessLevel.PACKAGE)가 자동 적용

 

  @Id  

  • JPA Entity 객체의 식별자로 사용

 

  @GeneratedValue (strategy = GenerationType.AUTO)  

  • 기본 key 생성을 DB에 위임 ( = AUTO_INCREMENT )
  • DB 마다 IDENTITY, *SEQUENCE* 등 **맞는 방식으로 해야하는데 AUTO는 알아서 적합배정

 

  @Column  

  • 객체를 DB table의 컬럼에 매핑. nullable, unique, length 등 속성 입력가능
  • 생략가능 (Entity 자체를 테이블로 보고 필드를 Column으로 생성해준다.

 

  @CreatedDate  ,  @LastModifiedDate  

  • JPA에서 생성,수정된 시간 정보 자동 생성

 


2. Application

 

@EnableJpaAuditing 추가하여 JPA Auditing 활성화

 

 

[ AboardcadoApplication ]

package com.rim.aboardcado;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing 
@SpringBootApplication
public class AboardcadoApplication {

	public static void main(String[] args) {
		SpringApplication.run(AboardcadoApplication.class, args);
	}
}

 

 


3. Repository

 

Data를 DB에 조작하고 조회한다

 

 

JpaRepository를 상속받으면 JPA 제공 메서드 save()가 create를 손쉽게 해준다

 

  1. repository 패키지를 생성하고
  2. 그 안에 BoardRepository 인터페이스 생성

 

 

 

 

 

BoardRepository 내용은 아래와 같다

[ BoardRepository ]

package com.rim.aboardcado.domain.repository;

import com.rim.aboardcado.domain.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {

}

 

interface 로 생성하며, JpaRepository 를 상속받고

매핑할 < Entity, Id type > Board 엔티티와 매핑하고 id의 타입인 Long 적어주기

 

 

JPA를 상속하면 CRUD 관련 쿼리메소드가 지원

findById(id);  findByEmail(email);

 

 

‘ By ‘ 를 기준으로 Where절에 대응하여

메소드명을 해석해 처리하기 때문에 상속하면 끝 🙌

 

 

나중에 검색, 페이징 등 기능 추가 작성도 가능

@Query 쿼리 작성도 가능

 

 

 


4. DTO

 

Data Access Object

Controller와 Service 사이에서 data를 주고받는 역할

 

DB와 주고받는 객체인 Entity와 유사한데

그 이유는 Entity는 직접 사용하면 안되기 때문에

Entity를 모두 Dto로 변환해 사용하기 때문에 비슷하다

 

  1. dto 패키지를 생성하고
  2. 그 안에 BoardDto 클래스 생성

 

 

 

BoardDto 내용은 아래와 같다

[ BoardDto ]

package com.rim.aboardcado.dto;

import com.rim.aboardcado.domain.entity.Board;
import lombok.*;

import java.time.LocalDateTime;

@Builder 
@Getter
public class BoardDto {
    private Long id;
    private String author;
    private String title;
    private String content;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;

    public Board toEntity() {
        Board build = Board.builder()
                .id(id)
                .author(author)
                .title(title)
                .content(content)
                .build();
        return build;
    }
}

  @Builder  

  • 메소드(), 생성자에 붙여주면 파라미터를 활용해 빌더 패턴을 자동 생성 (setter와 비슷)
  • class 레벨에 붙여주면 모든 요소를 받는 package-private 생성자 자동생성되며즉 class레벨도 결국 생성자 레벨로 변환되어 동작한다
  • → 이 생성자에 @Builder 를 사용한것과 동일하게 작동

 

위 코드에서   @Builder  를 사용하지 않았다면

  • class level에 @Setter 추가하거나
  • @NoArgsConstructor 기본 생성자 , @Builder 빌드패턴 추가
@Builder
    public BoardDto(Long id, String author, String title, String content, LocalDateTime createdDate, LocalDateTime modifiedDate) {
        this.id = id;
        this.author = author;
        this.title = title;
        this.content = content;
        this.createdDate = createdDate;
        this.modifiedDate = modifiedDate;
    }

 

❗ 변수가 추가되거나 <Optional>한 속성들을 받아야할 때 ❗

 

 

생성자, 수정자패턴은 수정을 거쳐야하지만

빌드패턴유연하게 객체의 값을 설정해주기 때문에 기존코드에 영향을 주지 않는다

 

 

  toEntity()  

  • BoardDto 에서 빌더 패턴사용해 Board Entity에 data 주입

 

 


5. Service

controller는 “/” 로 매핑됨에 따라 Service의 비즈니스 로직을 호출하는데

ServiceRepository를 통해 data를 조작, 조회하여 요청 처리 로직을 만든다

 

  1. service 패키지를 생성하고
  2. 그 안에 BoardService 클래스 생성

 

 

 

 

BoardService 내용은 아래와 같다

 

[ BoardService ]

package com.rim.aboardcado.service;

import com.rim.aboardcado.domain.repository.BoardRepository;
import com.rim.aboardcado.dto.BoardDto;
import lombok.RequiredArgsConstructor;
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();
    }
}

 

  @Service  

  • Service 비즈니스 로직임을 선언
  • Controller에서 로직을 호출하여 사용

 

  @AllArgsConstructor  

  • 모든 필드값을 포함한 생성자 생성

 

  @Transaction  

  • 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용
  • 실행, 종료(commit), 예외(rollback)class, method() 에 적용이 가능하고, method() (소분류)가 우선 적용
  • 작업 도중 오류가 발생했을 때 이전 모든 작업들이 성공하였더라도 작업 전 시점으로 되돌린다
  • JPA Service에서 필수

 

  savePost()  

  • save() 는 JPA에서 제공하는 메소드
  • BoardDto의 toEntity() 메소드를 이용해 Entity에 저장 후, 해당 글 Id값을 받아옴

 

 


6. Controller

클라이언트의 HTTP 요청이 진입하는 지점

 

요청에 따른 mapping으로

Service 로직을 호출하고 서버에서 처리된 결과를 view와 함께 전달해준다

 

  1. controller 패키지를 생성하고
  2. 그 안에 BoardController 클래스를 생성

 

 

 

 

 

[ BoardController ]

package com.rim.aboardcado.controller;

import com.rim.aboardcado.dto.BoardDto;
import com.rim.aboardcado.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@AllArgsConstructor
@Controller 
public class BoardController {

    private BoardService boardService;

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

    @PostMapping("/post") 
    public String write(BoardDto boardDto) {
        boardService.savePost(boardDto);
        return "redirect:/post";
    }
}

 

  @Controller  

  • Controller 임을 선언

 

  @GetMapping   @PostMapping  

  • Controller에 매핑하는 URL 중 method=GET, POST 로 요청된 값만 매핑

 

  write()  

  • boardDto값을 boardService에 정의된 savePost()에 넣어서 호출

 

  • return “board/post”; Thymeleaf에서
  • prefix는 classpath:/templates/
  • subfix는 .html
  • 이 것이 디폴트이기에 [ templates > board > post.html ]을 리턴

 

  • redirect
  • 새로고침 버튼 시 가장 최근 요청을 한번 더 실행한다
  • 이때 form이 재제출되는 것을 막기 위해 redirect로 경로를 다시 접속
  • 이렇게 하면 새로고침 시 경로 접속이 재실행 된다

 

글쓰기가 완료되면 List로 넘어가게 할거지만

📋List는 다음에 만들기에 우선 post로 리다이렉트

 

 

 


 

 

 

View 를 만들 차례!! 거의 다왔다 헥헥

Thymeleaf 템플릿으로 layout 공통을 통일시킬 건데

jsp로 치면 include 방식으로 header , footer등을 집어 넣게 하겠다

post.html을 빠르게 만들어 보자잇 💨

 

 

 

 

7. Layout, Header, Footer

 

 

 

resources 에서 리소스를 읽어들이는데 아래 사진처럼 구조를 생성

 

 

staticcss , image

변함없는 것 들을 정의를 해두고

templatesview 화면단이 들어간다

Thymeleaf 템플릿 엔진을 이용하며

layout, fragment 등 공통 page를 조립식으로 넣어 사용할 것이다

 

 

gradle 에 아래 dependencies가 꼭 추가되어있어야함 ❗

💡 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

 

 

[ layout.css ]

html {
    position: relative;
    min-height: 100%;
    margin: 0;
}
body {
    min-height: 100%;
}
.footer {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    padding: 15px 0;
    text-align: center;
}
.content{
    margin-bottom:100px;
    margin-top: 50px;
    margin-left: 200px;
    margin-right: 200px;
}

[ layout.html ]

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!-- CSS only -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link th:href="@{/css/layout.css}" rel="stylesheet">

    <!-- JS, Popper.js, and jQuery -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <th:block layout:fragment="script"></th:block>
    <th:block layout:fragment="css"></th:block>

</head>
<body>

<div th:replace="fragments/header::header"> </div>

<div layout:fragment="content" class="content"> </div>

<div th:replace="fragments/footer::footer"> </div>

</body>
</html>
 
 

✅ check ✅

  <html xmlns:th="http://www.thymeleaf.org">      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">  

  • Thymeleaf 사용 선언

 

  <div th:replace="fragments/header::header">  

  • header.html 로 치환

 

  <div layout:fragment="content">  

  • view html 들이 작성될 자리

 

  <div th:replace="fragments/footer::footer">  

  • footer.html 로 치환

 

 

 

 

[ header.html ] - 공통 fragment

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<div th:fragment="header">
    <nav class="navbar navbar-expand-sm bg-success navbar-dark">
        <img src="/image/aboardcado.png" width="25px" alt="logo" href="/">

        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/">ABoardcado</a>

        <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
            <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
                <li class="nav-item">
                    <a class="nav-link" href="/post">글쓰기</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/">리스트</a>
                </li>
            </ul>
        </div>
    </nav>
</div>
</html>

 

✅ check ✅

 

  <img src="/image/aboardcado.png" width="25px" alt="logo" href="/board">  

 

header 앞부분에 Logo 이미지를 넣었는데

[ resource>static>image ] 에 이미지를 넣고 .png 파일명을 치환해 입력해주어야한다

.jpg 를 넣었다면 파일명.jpg

 

Thymeleaf에서 img를 읽지 못하면 alt값을 뱉기도 하지만 error가 나버릴 때도 있다ㅜ

 

 

 

 

 

[ footer.html ] - 공통 fragment

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
    <footer class="page-footer font-small cyan darken-3">
        <div class="footer-copyright text-center py-3">
            2022 ABoardcado ⓒ Develothy
        </div>
    </footer>
</div>
</html>

 

 

layout 설정 완료

 

 

script, header, footer, content 등 각자 구역을 나누었기 때문에

이후 view 페이지   <div layout:fragment="content">   안에 작성

 

< nav bar, font-small 등 class name은 bootstrap을 위한 것 >

 

 

 

 


 

 

8. post.html

 

  1. templates 안에 board 디렉토리 생성
  2. 그 안에 post.html 글쓰기 페이지 생성

 

 

 

 

[ post.html ]

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container">
        <form action="/post" method="post">
            <div class="form-group row">
                <label for="inputTitle" class="col-sm-2 col-form-label"><strong>제목</strong></label>
                <div class="col-sm-10">
                    <input type="text" name="title" class="form-control" id="inputTitle" />
                </div>
            </div>
            <div class="form-group row">
                <label for="inputAuthor" class="col-sm-2 col-form-label"><strong>작성자</strong></label>
                <div class="col-sm-10">
                    <input type="text" name="author" class="form-control" id="inputAuthor" />
                </div>
            </div>
						<div class="form-group row">
                <label for="inputContent" class="col-sm-2 col-form-label"><strong>내용</strong></label>
                <div class="col-sm-10">
                    <textarea type="text" name="content" class="form-control" id="inputContent"></textarea>
                </div>
            </div>
            <div class="row">
                <div class="col-auto mr-auto"></div>
                <div class="col-auto">
                    <input class="btn btn-success" type="submit" role="button" value="글쓰기" />
										<a class="btn btn-danger" th:href="@{'/'}" role="button">취소</a>
                </div>
            </div>
        </form>
    </div>
</div>
</html>
 

 

✅ check ✅

  <html xmlns:th="http://www.thymeleaf.org

            xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"

            layout:decorate="~{layout/layout}">  

 

  • Thymeleaf 사용 선언
  • layout을 사용하기 위해 layout : decorate 추가됨

 

 

글쓰기는 서버로 값을 넘기기 때문에 form을 사용

취소는 List로 돌아가는 것 외의 작업이 없기 때문에 <a href>로 단순 이동

 

 

 

글쓰기는 아직 서버에서 값을 받고있지는 않기 때문에

표현식이 없어서 크게 일반 html vs Thymeleaf 를 느낄 건 없다

 

 

 

 

 

 

🤤 Create 글쓰기 드디어 완성!!!

 

 

#1편부터 지금까지 똑같이 진행했다면 아래와 같이 나온다 💯

 

 

 

6. header에서 🥑Logo 이미지는 제공하지 않았다

[ resource>static>image ] 에 이미지를 넣고 / header.html - ooo.png 파일명 치환해 입력

 

 

 

 

 

그런데… 🤔

submit 된거 맞아? 뚜둥❕❕

 

 

 

 

 

 

우린 아직 글들을 확인할 수 있는 로직과 view가 없기 때문에,,ㅋ

MySQL Workbench 로 들어가서 확인 해보면 table이 생성되고 Data가 확인된다 굿👏

 

 

 

 

 

 

 

 

와 정말 긴 여정이드아

다음편에서는 테이블에 있는 데이터들을 list로 볼 수 있도록

list 로직과 view를 만들어 보ㅏ야지

 

 

 

 

 

 

 

 

그럼 이만 👋