블로그 간단 기능(CRUD) 구현
- 블로그 글 작성
- 블로그 글 수정
- 블로그 글 삭제
- 블로그 글 조회(단건 , 전체)
Build.gradle
SpringBoot 2.7.X
Java 17
DB - mariaDB를 했지만 H2 나 Mysql 등 사용해도 무관
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.12'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' // 선택사항
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
Entity
Article (블로그 글) 객체 생성
ID, title(제목), content(본문,내용), createAt(생성시간), updateAt(수정시간)
Builder 패턴을 통해 객체 생성
* Builder 패턴의 장점: 어느 필드에 데이터가 들어가는 지 명시적으로 확인할 수 있음.
@CreateDate, @LastModifiedDate 애노테이션 사용을 위해, MainApplication 에 @EnableJpaAuditing 추가
@EnableJpaAuditing
@SpringBootApplication
public class MyBlogApplication {
public static void main(String[] args) {
SpringApplication.run(MyBlogApplication.class, args);
}
}
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import javax.persistence.*;
import java.time.LocalDateTime;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 1씩 증가
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "content", nullable = false)
private String content;
@CreatedDate
@Column(name="created_at")
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Builder
public Article(String title, String content){
this.title = title;
this.content = content;
}
public void update(String title, String content){
this.title = title;
this.content = content;
}
}
Repository
JpaRepository 를 상속받은 레포지토리 생성
public interface BlogRepository extends JpaRepository<Article, Long> {
}
Service
블로그 글 조회, 수정, 삭제 기능을 JpaRepository 내의 save(저장), findAll(전체), delete(삭제), findById(단건 조회) 활용
update 의 경우, findById 로 존재유무 확인 후 Entity 내의 update 메서드를 통해서 수정 기능 구현
package com.myblog.service;
import com.myblog.domain.Article;
import com.myblog.dto.request.ArticleRequest;
import com.myblog.repository.BlogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
@RequiredArgsConstructor // final or @NotNull 이 붙은 필드의 생성자 추가
@Service
public class BlogService {
private final BlogRepository blogRepository;
// 블로그 글 추가
public Article save(ArticleRequest request){
return blogRepository.save(request.toEntity());
}
// 블로그 글 조회(전체)
public List<Article> findAll(){
return blogRepository.findAll();
}
// 블로그 글 삭제
public void delete(long id){
blogRepository.deleteById(id);
}
// 블로그 수정
@Transactional
public Article update(long id, ArticleRequest request){
Article article = blogRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("not Found :"+id));
article.update(request.getTitle(), request.getContent());
return article;
}
public Article findById(Long id){
return blogRepository.findById(id).orElseThrow(()-> new RuntimeException("ID를 찾을 수 없음"));
}
}
Controller
BlogViewController
Model(HTML로 값을 넘겨주는 객체) 을 통해서, 데이터 설정 및 View 로 해당 데이터를 전달해 View에서 조회하게 함.
Html Code로 반환
package com.myblog.controller;
import com.myblog.domain.Article;
import com.myblog.dto.response.ArticleListViewResponse;
import com.myblog.dto.response.ArticleViewResponse;
import com.myblog.service.BlogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class BlogViewController {
private final BlogService blogService;
@GetMapping("/articles")
public String getArticles(Model model){
List<ArticleListViewResponse> articles = blogService.findAll().stream()
.map(ArticleListViewResponse::new)
.toList();
model.addAttribute("articles", articles);
return "articleList";
}
@GetMapping("/articles/{id}")
public String getArticle(@PathVariable Long id, Model model){
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
return "article";
}
@GetMapping("/new-article")
public String newArticle(@RequestParam (required = false) Long id, Model model){
if(id==null){
model.addAttribute("article", new ArticleViewResponse());
} else {
Article article = blogService.findById(id);
model.addAttribute("article", new ArticleViewResponse(article));
}
return "newArticle";
}
}
BlogApiController
블로그 api 호출 관련 컨트롤러
package com.myblog.controller;
import com.myblog.domain.Article;
import com.myblog.dto.request.ArticleRequest;
import com.myblog.dto.response.ArticleResponse;
import com.myblog.service.BlogService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@RestController
public class BlogApiController {
private final BlogService blogService;
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody ArticleRequest request){
Article savedArticle = blogService.save(request);
return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle);
}
@GetMapping("/api/articles")
public ResponseEntity<List<ArticleResponse>> findAllArticle(){
List<ArticleResponse> articles = blogService
.findAll()
.stream().map(ArticleResponse::new)
.toList();
return ResponseEntity.status(HttpStatus.OK).body(articles);
}
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable long id){
blogService.delete(id);
return ResponseEntity.ok().build();
}
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> upateArticle(@PathVariable long id, @RequestBody ArticleRequest request){
Article updateArticle = blogService.update(id, request);
return ResponseEntity.ok().body(updateArticle);
}
}
타임리프
템플릿 엔진
: 템플릿 엔진은 스프링 서버에서 데이터를 받아 웹페이지(HTML) 상에 데이터를 넣어 보여주는 도구
: JSP, 타임리프, 프리마커 등등이 있음.
[관련 문법]
${...} 변수 값 표현
#{...} 속성 파일 값 표현
@{..} URL 표현
*{...} 선택한 변수의 표현식, th:object에서 선택한 객체에 접근
th:text 텍스트 표현할 때 사용
th:each 컬랙션 반복할 때 사용
th:if if 조건문
th:href 이동경로
th:object 선택한 객체로 지정
article.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h4 class="mb-3">My Blog</h4>
<h4 class="mb-3">블로그에 오신 걸 환영합니다</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<input type="hidden" id="article-id" th:value="${article.id}">
<header>
<h1 class="fw-bolder mb-1" th:text="${article.title}"></h1>
<div class="text-muted fst-italic mb-2" th:text="|Posted on ${#temporals.format(article.createdAt, 'yyyy-MM-dd HH:mm')}|"></div>
</header>
<section class="mb-5">
<p class="fs-5 mb-5" th:text="${article.content}"></p>
</section>
<button type="button" id="modify-btn" th:onclick="|location.href='@{/new-article?id={articleId}(articleId=${article.id})}'|" class="btn btn-primary btn-sm">수정</button>
<button type="button" id="delete-btn" class="btn btn-secondary btn-sm">삭제</button>
<button type="button" th:href="@{/articles}" class="btn btn-sm">돌아가기</button>
</article>
</div>
</div>
</div>
<script src="/js/article.js"></script>
</body>
</html>
articleList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글 목록</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h4 class="mb-3">My Blog</h4>
<h4 class="mb-3">블로그에 오신 걸 환영합니다</h4>
</div>
<div class="container">
<button type="button" id="create-btn"
th:onclick="|location.href='@{/new-article}'|"
class="btn btn-secondary btn-sm mb-3">글 등록></button>
<div class="row-6" th:each="item : ${articles}">
<div class="card">
<div class="card-header" th:text="${item.id}"></div>
<div class="card-body">
<h5 class="card-title" th:text="${item.title}"></h5>
<p class="card-text" th:text="${item.content}"></p>
<a th:href="@{/articles/{id}(id=${item.id})}" class="btn btn-primary">보러 가기</a>
</div>
<br>
</div>
</div>
</div>
<button type="button" class="btn btn-secondary" onclick="location.href='/logout'">로그아웃</button>
<script src="/js/article.js"></script>
</body>
</html>
newArticle.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>블로그 글</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
</head>
<body>
<div class="p-5 mb-5 text-center</> bg-light">
<h4 class="mb-3">My Blog</h4>
<h4 class="mb-3">블로그에 오신 걸 환영합니다</h4>
</div>
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<article>
<input type="hidden" id="article-id" th:value="${article.id}">
<header class="mb-4">
<input type="text" class="form-control" placeholder="제목" id="title" th:value="${article.title}">
</header>
<section class="mb-5">
<textarea class="form-control h-25" rows="10" placeholder="내용" id="content" th:text="${article.content}"></textarea>
</section>
<button th:if="${article.id} != null" type="button" id="modify-btn" class="btn-primary btn-sm">수정</button>
<button th:if="${article.id} == null" type="button" id="create-btn" class="btn btn-secondary btn-sm">등록</button>
</article>
</div>
</div>
</div>
<script src="/js/article.js"></script>
</body>
</html>
application.yml
테스트용을 위해서 SpringBoot 기동 시 data.sql 로 데이터 넣기
data.sql 이 실행 안되는 경우 확인해볼 것
* defer-datasource-initialization: true ( true로 셋팅 시, Hibernate 초기화 전에 쿼리가 실행됨)
* hibernate ddl-auto 는 create / create-drop 으로 초기화
스프링 버전
- v2.5.x 이상 설정: spring.sql.init.mode
- 위 버전 이하 설정 : spring.datasource.initialization-mode
spring:
# 데이터 소스 설정
datasource:
url: jdbc:mariadb://localhost:3306/myblog
driver-class-name: org.mariadb.jdbc.Driver
username: root
password: 1234
data: classpath:data.sql
initialization-mode: always
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
properties:
hibernate:
format_sql: true
defer-datasource-initialization: true
sql:
init:
mode: always
jwt:
issuer: yhmJwtToken
secret_key: 1233j23j23hjj4kbknzdfdsfkaadfjdsf
data.sql
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목1', '내용1', NOW(), NOW())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목2', '내용2', NOW(), NOW())
INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목3', '내용3', NOW(), NOW())
추후 진행
Spring Security 를 이용한 로그인/로그아웃 관련 기능 구현
JWT Token 기반 인증 구현
OAuth 2.0 로그인 기능 구현
[참고, Ref]
스프링 부트 개발자3 백엔드 개발자 되기 https://github.com/shinsunyoung/springboot-developer
'프로그래밍' 카테고리의 다른 글
[업무기록] 20230623 관련 잡다 (Tomcat, MariaDB) (0) | 2023.06.23 |
---|---|
[오류] javax.net.ssl.SSLHandshakeException : Received fatal alert : handshake_failure (0) | 2023.06.14 |
[Spring MVC] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(2) (0) | 2023.05.23 |
[Spring] 국세청_사업자등록정보 진위확인 및 상태조회 서비스 (WebClient) (0) | 2023.05.22 |
[SpringBoot] 1. Oauth 2.0 기반 소셜 로그인 구현해보기 (1) | 2023.05.19 |