Spring boot

블로그 7 - 글 수정 API 만들기

ryeonng 2024. 10. 2. 17:42
Article 클래스(엔티티) 코드 추가하기 - 1
package com.tenco.demo._domain.blog.entity;

import com.tenco.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 클래스 설계 시 주의사항 - 반드시 기본 생성자가 있어야 한다. (@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) // 연결되어 있는 pk를 db로 위임한다.
	@Column(name = "id", updatable = false) // 컬럼명 지정, 수정 못하게 제약 설정
	private Long id;
	
	@Column(name = "title", nullable = false) // not null로 설정
	private String title;
	
	@Column(name = "content", nullable = false)
	private String content;
	
	// 객체의 상태 값 수정
	public void update(String title, String content) {
		// 유효성 검사 반드시 해야 한다.
		// 즉, 데이터가 엔티티에 저장되기 전에 반드시 검증하기
		if(title == null || title.trim().isEmpty()) {
			throw new Exception400("제목은 공백일 수 없습니다.");
		}
		if(content == null || content.trim().isEmpty()) {
			throw new Exception400("내용은 공백일 수 없습니다.");
		}
		this.title = title;
		this.content = content;
	}
	
}

도메인 모델 - 현실 세계의 중요한 개념을 코드로 나타낸 것(게시글, 사용자, 댓글, 주문, 상품)

객체 스스로 자신의 상태를 관리하도록 한다 - 자신의 데이터와 행동에 책임을 진다.

 

bulid.gradle 변경

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 레벨로 로깅을 설정

 

BlogService 클래스에 수정 기능과 트랜잭션 처리 - 2

수정기능에 @Transactional 처리하기

JPARepositoty 메서드인 save()나 delete()를 직접 사용했음. 이 메서드들은 이미 트랜잭션 처리가 되어 있다. 따라서 서비스 계층에서 추가로 트랜잭션을 선언할 필요 없다.

package com.tenco.demo._domain.blog.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.tenco.demo._domain.blog.dto.ArticleDTO;
import com.tenco.demo._domain.blog.entity.Article;
import com.tenco.demo._domain.blog.repository.PostRepository;
import com.tenco.demo.common.ApiUtil;
import com.tenco.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>는 Java8 에서 도입된 클래스 이며,
		// 값이 존재할 수도 있고, 없을 수도 있는 상황을 명확히 처리하기 위해 사용된다.
		// 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() 메서드는 수정시에도 사용이 가능하다.
		// 단, 호출하지 않는 이유는 더티 체킹 동작 때문이다.
		// 즉, 트랜잭션 커밋 시, 자동으로 영속성 컨텍스트와 DB에 변경사항이 반영된다.
		// 
		// blogRepository.save(articleEntity);
		
		// 변경된 값 DB에 save(저장) 처리
		//postRepository.save(articleEntity);
		
		return articleEntity; // 수정된 상태 값 반환
	}

	
	
}

트랜잭션 사용에 일반적인 규칙은 서비스가 메서드가 여러 데이터베이스 작업을 포함하거나, 영속성 컨텍스트를 통해 엔티티 변경 사항을 추적해야 하는 경우 @Transactional을 사용하여 해당을 수행한다.

 

트랜잭션과 영속성 컨텍스트의 관계

  • 트랜잭션이 시작되면 영속성 컨텍스트도 활성화된다.
  • 트랜잭션 내에서 조회된 엔티티는 영속성 컨텍스트에서 관리되는 영속 상태가 된다.

 

더티체킹의 메커니즘

  • 엔티티의 필드 값을 변경하면 영속성 컨텍스트가 이를 감지한다.
  • 변경된 엔티티는 트랜잭션 커밋 시 DB에 자동으로 반영된다.

 

save() 메서드의 필요성

  • 영속 상태의 엔티티는 save()를 호출하지 않아도 변경 사항이 DB에 반영된다.
  • 준영속 상태(detached)의 엔티티나 트랜잭션이 없는 경우에는 save()를 사용하여 변경 사항을 저장해야 한다.

 

코드의 효율성

  • 불필요한 save() 호출을 줄인다.

 

주요 내용 정리

 

BlogApiController 코드 추가
package com.tenco.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.tenco.demo._domain.blog.dto.ArticleDTO;
import com.tenco.demo._domain.blog.entity.Article;
import com.tenco.demo._domain.blog.service.BlogService;
import com.tenco.demo.common.ApiUtil;
import com.tenco.demo.common.errors.Exception400;

import lombok.RequiredArgsConstructor;


@RequiredArgsConstructor
@RestController // @Controller + @ResponseBody
public class BlogApiController {
	
	private final BlogService blogService;
	
	// URL 매핑 : 주소설계 - http://localhost:8080/api/articles
	@PostMapping("/api/articles")
	public ResponseEntity<Article> addArticle(@RequestBody ArticleDTO dto) {
		// 1. 인증 검사
		// 2. 유효성 검사
		Article savedArticle = blogService.save(dto);
		return ResponseEntity.status(HttpStatus.CREATED).body(savedArticle);
	}
	
	// 주소설계 - 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);
	}
	
	// 주소설계 - http://localhost:8080/api/articles/1
	@GetMapping(value = "/api/articles/{id}")
	public ApiUtil<?> findArticle(@PathVariable(name = "id") Integer id) {
		// 1. 유효성 검사 생략
		Article article = blogService.findById(id);
		return new ApiUtil<>(article);
	}
	
	// 주소설계 - 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가 대표적이다.

'Spring boot' 카테고리의 다른 글

Mustache  (0) 2024.10.04
템플릿 엔진  (0) 2024.10.04
블로그 6 - 글 상세보기(조회) API 구현  (0) 2024.10.02
블로그 5 - 글 목록 조회 API 만들기  (0) 2024.10.02
블로그4 - 서비스, 컨트롤러 만들기  (0) 2024.10.02