[스프링부트와 AWS로 혼자 구현하는 웹 서비스] 등록/수정/조회 API 만들기

세이라·2023년 7월 22일
0

스터디를 통해 스프링부트와 AWS로 혼자 구현하는 웹 서비스(저자 이동욱) 서적을 공부하는 중입니다.

공부/실습한 내용을 정리한 포스팅입니다.
책에 모르는 부분이 있으면 구글링하거나 챗gpt에 물어봐서 보충하였습니다.
(아직 초보라 모르는 부분이 많아 이것저것 다 적었습니다.)

참고한 사이트 출처는 포스팅 맨 하단에 적었습니다.

API 필요 클래스 종류별 분류

Request Data를 받을 DTO

web.dto 패키지에 클래스로 생성

PostsSaveRequestDto - 등록요청

package com.webservice.springboot.springboot_board.web.dto;

import com.webservice.springboot.springboot_board.domain.posts.Posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;

    @Builder
    public PostsSaveRequestDto(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }
    public Posts toEntity() {
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }
}

PostsUpdateRequestDto - 수정요청

package com.webservice.springboot.springboot_board.web.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content){
        this.title=title;
        this.content=content;
    }
}

PostResponseDto - 조회응답

package com.webservice.springboot.springboot_board.web.dto;

import com.webservice.springboot.springboot_board.domain.posts.Posts;
import lombok.Getter;

@Getter
public class PostResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostResponseDto(Posts entity){
        this.id=entity.getId();
        this.title=entity.getTitle();
        this.content=entity.getContent();
        this.author=entity.getAuthor();
    }
}
  • Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣음.

Entity클래스와 유사한 형태임에도 DTO를 사용하는 이유

  • Entity 클래스의 경우 데이터베이스와 맞닿은 핵심 클래스. Entity 클래스를 기준으로 테이블 생성되고 스키마가 변경. 그리고 수많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작.
    → 원활한 유지보수를 위해 변경이 잦지 않아야 함.
  • Entity 클래스 변경 시 여러 클래스에 영향을 끼치지만, Request, Response용 Dto는 View를 위한 Controller에서 쓸 클래스라 변경이 잦음.
  • View Layer와 DB Layer의 역할 분리를 철저하기 위해 DTO를 사용.

ResponseDto에 @NoArgsConstructor 사용하지 않는 이유

  • @RequestBody와 같은 annotation을 통해 요청이 들어올 시, Controller 메서드 매개변수 인자로 들어가는 것이 아니기 때문.
    : Controller에서 DTO를 @RequestBody를 통해 가져올 때, 바인딩을 ObjectMapper가 수행. 이때 ObjectMapper가 매핑 시, DTO를 DTO의 기본 생성자를 이용하여 생성. 기본 생성자 없을 시, 맵핑 오류 발생.
  • Spring은 Jackson 라이브러리의 ObjectMapper를 사용하여 JSON 매핑
    ※ 참고 : 직렬화(serialize), 역직렬화(deserialize) 수행. 직렬화는 Object에서 JSON으로, 역직렬화는 그 반대.

API 요청을 받을 Controller

web 패키지에 클래스로 생성

PostApiController

package com.webservice.springboot.springboot_board.web;

import com.webservice.springboot.springboot_board.service.posts.PostsService;
import com.webservice.springboot.springboot_board.web.dto.PostResponseDto;
import com.webservice.springboot.springboot_board.web.dto.PostsSaveRequestDto;
import com.webservice.springboot.springboot_board.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
public class PostApiController {
    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }

    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id,@RequestBody PostsUpdateRequestDto requestDto){
        return postsService.update(id, requestDto);
    }

    @GetMapping("/api/v1/posts/{id}")
    public PostResponseDto findById(@PathVariable Long id){
        return postsService.findById(id);
    }
}

트랜잭션, 도메인 기능 간의 순서 보장 Service

service.posts 패키지에 클래스로 생성

PostsService

package com.webservice.springboot.springboot_board.service.posts;

import com.webservice.springboot.springboot_board.domain.posts.Posts;
import com.webservice.springboot.springboot_board.domain.posts.PostsRepository;
import com.webservice.springboot.springboot_board.web.dto.PostResponseDto;
import com.webservice.springboot.springboot_board.web.dto.PostsSaveRequestDto;
import com.webservice.springboot.springboot_board.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }

    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto){
        Posts posts = postsRepository.findById(id)
                .orElseThrow(()->new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
        posts.update(requestDto.getTitle(),requestDto.getContent());
        return id;
    }

    public PostResponseDto findById(Long id){
        Posts entity = postsRepository.findById(id)
                .orElseThrow(()-> new IllegalArgumentException("해당 게시글이 없습니다. id="+id));
        return new PostResponseDto(entity);
    }
}

스프링에서 Bean을 주입받는 방식

  1. @Autowired를 사용한 필드 주입 방식 : 권장하지 않음.
public class PostsApiController {
	@Autowired
    private final PostsService postService;
}
  1. @Autowired를 사용한setter 주입 방식 : 권장하지 않음.
public class PostsApiController {
	private PostsService postsService;

	@Autowired
    public void setPostsService(PostsService postService){
    	this.postService = postService;
    }
}
  1. 생성자 : 가장 권장하는 방식. @RequiredArgsConstructor을 사용함으로써 롬복의 @RequiredArgsConstructor가 대신 생성. 순환참조를 방지할 수 있는 장점이 있음.
    (생성자로 객체 생성하는 시점에 필요한 Bean을 주입. 먼저, 생성자의 인자에 사용하는 Bean을 찾거나 Bean Factory에서 만듦.)
  • 필드 주입 방식과 setter 주입 방식이 권장되지 않는 이유
    : 런타임 시 순환참조 문제가 발생할 수 있기 때문. 즉, 간단히 말하면 생성자 주입 방식과 달리 필드 주입방식과 setter 주입 방식은 Bean 주입하는 순서가 다름.
    → Bean을 먼저 생성 후 의존하고 있는@Autowired가 붙은 필드의 Bean을 생성(setter 주입 방식의 경우, 생성자 인자에 사용하는 Bean 생성) 그리고 실제로 사용하는 시점에 주입함. 실제로 사용하는 시점이란, 순환참조를 일으킬 수 있는 메서드를 호출하는 시점이며 이때 순환참조 문제가 발생.
    ※ 순환참조 : A클래스가 B클래스의 Bean을 주입받고, B클래스가 A클래스의 Bean을 주입받는 상황처럼 서로 순환되어 참조. 맞물리는 DI 상황에서 Spring이 어느 Spring Bean을 먼저 생성할지 결정하지 못하여 생겨 에러 발생. 순환참조되는 설계를 사전에 막아야 함.

update 기능 중 DB에 쿼리를 날리는 부분이 없는 이유

: JPA의 영속성 컨텍스트 때문. 영속성 컨텍스트란 Entity를 영구 저장하는 환경. 일종의 논리적 개념이라 보면 되며, JPA의 핵심 내용은 Entity가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈림. JPA의 Entity Manager가 활성화된 상태로(Spring Data Jpa는 기본 옵션) 트랜잭션 안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태. 이 상태에서 값을 변경 시, 트랜잭션이 끝나는 시점에 해당 테이블에 변경분 반영. 즉, Entity 객체 값 변경 시 별도로 update 쿼리를 날릴 필요가 없음. dirty checking이라 불림.

※ 이해가 안되어서 더 찾아본 영속성 컨텍스트 내용
: 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할.
서비스별로 하나의 EntityManager Factory가 존재하며 Entity Manager Factory에서 DB에 접근하는 트랜잭션이 생길 때 마다 쓰레드 별로 Entity Manager를 생성하여 영속성 컨텍스트에 접근.
EntityManager에 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리.
영속성 컨텍스트는 EntityManager를 생성할 때 만들어지며 EntityManager를 통해 영속성 컨텍스트에 접근하고 관리.

dirty checking : dirty란 상태의 변화가 생김을 의미. dirty checking이란 상태 변경 검사를 의미. JPA에서 트랜잭션이 끝나는 시점에 변화가 있는 모든 Entity 객체를 DB에 자동으로 반영. 기준은 최초 조회 상태.

Posts 클래스 추가 메소드

    public void update(String title,String content){
        this.title = title;
        this.content = content;
    }
  • Entity에 비즈니스 로직을 처리하는 update 메서드 추가

출처

생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유
스프링 순환 참조(spring circular reference)
[Spring Boot] 순환참조문제
DTO에 기본 생성자가 필요한 이유
[Spring JPA] 영속성 컨텍스트(Persistence Context)
더티 체킹 (Dirty Checking)이란?

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

많은 도움이 되었습니다, 감사합니다.

답글 달기