categorySpring boot
2024. 10. 4. 11:50
by haejujung
학습 목표
1. 트랜잭션 처리에 대한 개념을 설명할 수 있다. 2. 더티 체킹 개념과 영속성 컨텍스트에 특징을 설명할 수 있다.
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;
}
}
도메인 모델 - 현실 세계의 중요한 개념을 코드로 나타낸 것 (게시글, 사용자, 댓글, 주문, 상품)
객체 스스로 자신의 상태를 관리하도록 한다 - 자신의 데이터와 행동에 책임을 진다.
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가 대표적입니다.