Article 클래스(엔티티) 코드 추가 하기 - 1
package com.example.demo._domain.blog.entity;
import com.example.demo.common.errors.Exception400;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
// 반드시 기본 생성자가 있어야 한다.
@Entity(name = "tb_article")
@NoArgsConstructor
@Data
public class Article {
// 특정 생성자에만 빌더 패턴을 추가할 수 있다.
@Builder
public Article(String title, String content){
this.title = title;
this.content = content;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // DB로 위임
@Column(name = "id", updatable = false) // updatable = false 수정 막음
private Long id;
@Column(name = "title", nullable = false) // nullable = false not null
private String title;
@Column(name = "content", nullable = false) // nullable = false not null
private String content;
// 객체의 상태값 수정
public void update(String title, String content){
// 유효성 검사 반드시 진행해야 함
// 즉, 데이터가 엔티티에 저장되기 전에 반드시 검증
if(title == null || title.isEmpty()){
throw new Exception400("제목은 null 이거나 빈 문자열을 포함할 수 없습니다.");
}
if(content == null || content.isEmpty()){
throw new Exception400("내용은 null 이거나 빈 문자열을 포함할 수 없습니다.");
}
this.title = title;
this.content = content;
}
}
도메인 모델 - 현실 세계의 중요한 개념을 코드로 나타낸 것 (게시글, 사용자, 댓글, 주문, 상품)
객체 스스로 자신의 상태를 관리하도록 한다 - 자신의 데이터와 행동에 책임을 진다.
application-dev.yml
server:
servlet:
encoding:
charset: utf-8 # 요청 및 응답에 UTF-8 인코딩을 사용하여 한글 및 특수문자가 깨지지 않도록 설정
force: true # 강제로 UTF-8 인코딩을 적용, 클라이언트가 다른 인코딩을 요청하더라도 무시하고 UTF-8을 사용
port: 8080 # 서버가 8080 포트에서 실행되도록 설정
spring:
mustache:
servlet:
expose-session-attributes: true # Mustache 템플릿에서 세션 속성에 접근할 수 있도록 허용
expose-request-attributes: true # Mustache 템플릿에서 요청 속성에 접근할 수 있도록 허용
datasource:
url: jdbc:mysql://localhost:3306/jpa_demo?useSSL=false&serverTimezone=Asia/Seoul&useLegacyDatetimeCode=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: asd123
# 데이터베이스 기본 비밀번호 (비어 있음)
h2:
console:
enabled: true # H2 데이터베이스 콘솔을 활성화하여 브라우저에서 데이터베이스를 관리할 수 있도록 함
#sql:
#init:
#data-locations:
#- classpath:db/data.sql # 애플리케이션 초기화 시 실행할 데이터 삽입 SQL 파일의 경로 (data.sql)
jpa:
hibernate:
ddl-auto: update # 애플리케이션이 시작될 때 데이터베이스 테이블을 자동으로 생성
show-sql: true # Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력
properties:
hibernate:
format_sql: true # 출력되는 SQL 쿼리를 포맷팅하여 읽기 쉽게 출력
defer-datasource-initialization: true # 데이터베이스 초기화가 지연되도록 설정하여 JPA 설정 후에 데이터 초기화
output:
ansi:
enabled: always # 콘솔 출력 시 ANSI 색상을 항상 사용하도록 설정 (색상을 통해 로그를 더 쉽게 구분 가능)
logging:
level:
'[com.example.class_blog_jpa_v1]': DEBUG # 특정 패키지(com.tenco.blog_jpa_step1) 수준에서 DEBUG 레벨로 로깅을 설정
Article 클래스(엔티티) 코드 추가 하기 - 1
package com.example.demo._domain.blog.entity;
import com.example.demo.common.errors.Exception400;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
// 반드시 기본 생성자가 있어야 된다.
@Entity(name = "tb_article")
@NoArgsConstructor // 기본 생성자
@Data
public class Article {
// 특정 생성자에만 빌더 패턴을 추가할 수 있다.
@Builder
public Article(String title, String content) {
this.title = title;
this.content = content;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // db로 위임
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "title", nullable = false) // not null
private String title;
@Column(name = "content", nullable = false) // not null
private String content;
// 객체의 상태 값 수정
public void update(String title, String content) {
// 유효성 검사 반드시 진행 해야 함
// 즉, 데이터가 엔티티에 저장되기 전에 반드시 검증
if(title == null || title.trim().isEmpty()) {
throw new Exception400("제목은 null 이거나 빈 문자열일 수 없습니다.");
}
if(content == null || content.trim().isEmpty()) {
throw new Exception400("내용은 null 이거나 빈 문자열일 수 없습니다.");
}
this.title = title;
this.content = content;
}
}
BlogService 클래스에 수정 기능과 트랜잭션 처리 - 2
수정기능에 @Transactional 처리 하기 JpaRepository 메서드인 save()나 delete()를 직접사용 했었음.
이 메서드들은 이미 트랜잭션 처리되어 있다. 따라서 서비스 계층에서 추가로 트랜잭션을 선언할 필요가 없었음.
package com.example.demo._domain.blog.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo._domain.blog.dto.ArticleDTO;
import com.example.demo._domain.blog.entity.Article;
import com.example.demo._domain.blog.repository.PostRepository;
import com.example.demo.common.ApiUtil;
import com.example.demo.common.errors.Exception400;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service // IoC (빈으로 등록)
public class BlogService {
@Autowired // DI <--- 개발자들이 가독성 때문에 작성을 해 준다.
private final PostRepository postRepository;
@Transactional // 쓰기 지연 처리 까지
public Article save(ArticleDTO dto) {
// 비즈니스 로직이 필요하다면 작성 ...
return postRepository.save(dto.toEntity());
}
// 전체 게시글 조회 기능
public List<Article> findAll() {
List<Article> articles = postRepository.findAll();
return articles;
}
// 상세 보기 게시글 조회
public Article findById(Integer id) {
// Optional<T>는 Java 8에서 도입된 클래스이며,
// 값이 존재할 수도 있고 없을 수도 있는 상황을 명확하게 처리하기 위해 사용됩니다.
// Optional 타입에 대해서 직접 조사하고 숙지 하세요(테스트 코드 작성)
return postRepository.findById(id).orElseThrow( () -> new Exception400("해당 게시글이 없습니다."));
}
// 수정 비즈니스 로직에 대한 생각!
// 영속성 컨텍스트에서 또는 DB 존재하는 Article 엔티티(row)를 가지고 와서
// 상태 값을 수정하고 그 결과를 호출한 곳으로 반환 한다.
@Transactional
public Article update(Integer id, ArticleDTO dto) {
// 수정 로직
Article articleEntity = postRepository
.findById(id).orElseThrow( () -> new Exception400("not found : " + id));
// 객체 상태 값 변경
articleEntity.update(dto.getTitle(), dto.getContent());
// 영속성 컨텍스트 - 더티 체킹을 알아보자.
// 리포지토리의 save() 메서드는 수정할 때도 사용 가능 하다.
// 단, 호출하지 않는 이유는 더티 체킹(Dirty Checking) 동작 때문이다.
// 즉, 트랜잭션 커밋 시 자동으로 영속성 컨텍스트와 데이터베이스(DB)에 변경 사항이 반영된다
// blogRepository.save(articleEntity);
return articleEntity;
}
}
트랜잭션 사용에 일반적인 규칙은 서비스 메서드가 여러 데이터베이스 작업을 포함하거나, 영속성 컨텍스트를 통해 엔티티 변경 사항을 추적해야 하는 경우 @Transactional을 사용하여 해당을 수행 한다.
트랜잭션과 영속성 컨텍스트의 관계
- 트랜잭션이 시작되면 영속성 컨텍스트도 활성화된다.
- 트랜잭션 내에서 조회된 엔티티는 영속성 컨텍스트에서 관리되는 영속 상태가 된다.
더티 체킹의 메커니즘
- 엔티티의 필드 값을 변경하면 영속성 컨텍스트가 이를 감지한다.
- 변경된 엔티티는 트랜잭션 커밋 시 DB에 자동으로 반영된다.
save() 메서드의 필요성
- 영속 상태의 엔티티는 save()를 호출하지 않아도 변경 사항이 DB에 반영된다.
- 준영속 상태(detached)의 엔티티나 트랜잭션이 없는 경우에는 save()를 사용하여 변경 사항을 저장해야 한다.
- 코드의 효율성
- 불필요한 save() 호출을 줄임
주요 내용 정리
BlogApiController 코드 추가
package com.example.demo._domain.blog.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo._domain.blog.dto.ArticleDTO;
import com.example.demo._domain.blog.entity.Article;
import com.example.demo._domain.blog.service.BlogService;
import com.example.demo.common.ApiUtil;
import com.example.demo.common.errors.Exception400;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@RestController // @controller + @responsebody
public class BlogApiController {
private final BlogService blogService;
// URL , 즉, 주소 설계 - http://localhost:8080/api/article
@PostMapping("/api/articles")
public ResponseEntity<Article> addArticle(@RequestBody ArticleDTO dto) {
// 1. 인증 검사
// 2. 유효성 검사
Article savedArtilce = blogService.save(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(savedArtilce);
}
// URL , 즉, 주소 설계 - http://localhost:8080/api/articles
@GetMapping(value = "/api/articles", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiUtil<?> getAllArticles() {
List<Article> articles = blogService.findAll();
if(articles.isEmpty()) {
// return new ApiUtil<>(new Exception400("게시글이 없습니다."));
throw new Exception400("게시글이 없습니다.");
}
return new ApiUtil<>(articles);
}
// URL , 즉, 주소 설계 - http://localhost:8080/api/articles/1
@GetMapping(value = "/api/articles/{id}")
public ApiUtil<?> findArtilcle(@PathVariable(name = "id") Integer id) {
// 1. 유효성 검사 생략
Article article = blogService.findById(id);
return new ApiUtil<>(article);
}
// URL , 즉, 주소 설계 - http://localhost:8080/api/articles/1
@PutMapping(value = "/api/articles/{id}")
public ApiUtil<?> updateArticle(@PathVariable(name = "id") Integer id, @RequestBody ArticleDTO dto) {
// 1. 인증 검사
// 2. 유효성 검사
Article updateArticle = blogService.update(id, dto);
return new ApiUtil<>(updateArticle);
}
}
추가 읽어 보기
데이터 바인딩은 HTTP 요청에서 전달된 데이터를 서버 측의 자바 객체나 메서드 파라미터에 자동으로 변환하고 할당하는 과정을 말합니다. 이를 통해 개발자는 복잡한 데이터 추출 및 변환 로직을 직접 구현하지 않고도 간편하게 데이터를 사용할 수 있습니다.
참고 사항
DispatcherServlet
- Spring MVC의 프론트 컨트롤러(Front Controller) 역할을 한다.
- 모든 HTTP 요청을 받아 적절한 컨트롤러(Controller)로 전달한다.
- 요청 처리 과정의 중앙 허브로, 요청의 라우팅 및 데이터 바인딩을 조율한다.
HandlerMapping
- 요청 URL과 HTTP 메서드에 따라 적절한 컨트롤러 메서드를 매핑한다.
- 예를 들어, @PutMapping("/api/articles/{id}")와 같은 매핑 정보를 바탕으로 해당 요청을 처리할 메서드를 찾는다.
HandlerAdapter
- 매핑된 컨트롤러 메서드를 호출하고, 필요한 인자를 제공하는 역할을 한다.
- HandlerMethodArgumentResolver를 사용하여 메서드 파라미터에 데이터를 바인딩한다.
HandlerMethodArgumentResolver
- 컨트롤러 메서드의 파라미터에 데이터를 바인딩하기 위한 전략을 정의한다.
- 대표적인 구현체로는 RequestParamMethodArgumentResolver, PathVariableMethodArgumentResolver, RequestBodyMethodArgumentResolver 등이 있다.
HttpMessageConverter
- HTTP 요청의 바디에 담긴 데이터를 자바 객체로 변환하거나, 자바 객체를 HTTP 응답의 바디로 변환하는 역할을 한다.
- Jackson 라이브러리를 사용하여 JSON 데이터를 자바 객체로 변환하는 MappingJackson2HttpMessageConverter가 대표적이다.
728x90
'Spring boot > Blog 프로젝트 만들기(JPA)' 카테고리의 다른 글
Mustache 란 뭘까? (2) | 2024.10.07 |
---|---|
템플릿 엔진이란 뭘까? (2) | 2024.10.07 |
[Blog] 블로그 프로젝트 만들기 - 6 (글 상세보기 조회 API ) (1) | 2024.10.02 |
[Blog] 블로그 프로젝트 만들기 - 5 (글 목록 조회 API ) (1) | 2024.10.02 |
[Blog] 블로그 프로젝트 만들기 - 4 (서비스, 컨트롤러 만들기) (0) | 2024.10.02 |