글 수정 API 만들기 - 7

 

목차

    💡 학습 목표
        1. 트랜잭션 처리에 대한 개념을 설명할 수 있다.
        2. 더티 체킹 개념과 영속성 컨텍스트에 특징을 설명할 수 있다.

    1. 작업

    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을 사용하여 해당을 수행 한다.

     

    BlogApiController 코드 추가 - 3
    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);
    	}
    
    }

    2. 추가 내용

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

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

    더티 체킹의 메커니즘:

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

    save() 메서드의 필요성:

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

    3. 참고 사항

    데이터 바인딩은 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가 대표적이다.

    목차로 돌아가기