본문 바로가기

Spring boot/Blog 프로젝트 만들기(JPA)

[Blog] 블로그 프로젝트 만들기 - 7 (글 수정 API 만들기)

 

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