스프링 부트 게시판 API - seupeuling buteu gesipan API

본격적으로 API를 만들기 전에 PostsRepository에 id를 기준으로 정렬하는 쿼리 함수 하나를 작성하도록 하겠습니다.

public interface PostsRepository extends JpaRepository<Posts, Long> {

    @Query("select p from Posts p order by p.id desc")
    List<Posts> findAllDesc();

}

JPA에서는 함수명을 지을 때 키워드가 있으면 그 키워드에 맞게 자동으로 SQL을 짜줍니다.

예를 들면 함수명이 findByIdOrderByDateAsc(Long id)면 JPA에서 다음과 같은 쿼리를 만들어줍니다.

SELECT * FROM [JpaRepository의 엔티티] WHERE id=?1 ORDER BY date ASC
스프링 부트 게시판 API - seupeuling buteu gesipan API
JPA 키워드

하지만 이런 키워드를 사용해서 함수명을 짓다보면 가독성이 떨어집니다. 그래서 저는 @Query라는 어노테이션을 사용해서 직접 쿼리문을 작성해서 사용합니다. 

@Query

실행할 메소드 위에 정적 쿼리를 작성함. (JPQL 쿼리가 들어감)

JPQL은 SQL과 다릅니다.

  • SQL: 테이블을 대상으로 쿼리함 ex) SELECT * FROM users(테이블 명) WHERE name='홍길동'
  • JPQL: 엔티티 객체를 대상으로 쿼리함 ex) SELECT u FROM User(엔티티 객체 명) u where u.name='홍길동'

이제 API를 작성하도록 하겠습니다.

총 코드는 다음과 같습니다.

import com.lgh.springbootwebservice.web.domain.posts.Posts;
import com.lgh.springbootwebservice.web.domain.posts.PostsRepository;
import com.lgh.springbootwebservice.web.dto.PostsListResponseDto;
import com.lgh.springbootwebservice.web.dto.PostsResponseDto;
import com.lgh.springbootwebservice.web.dto.PostsSaveRequestDto;
import com.lgh.springbootwebservice.web.dto.PostsUpdateRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@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;
    }

    @Transactional
    public void delete(Long id) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));
        postsRepository.delete(posts);
    }

    @Transactional(readOnly = true)
    public List<PostsListResponseDto> findAllDesc() {
        return postsRepository.findAllDesc().stream().map(PostsListResponseDto::new).collect(Collectors.toList());
    }

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

        return new PostsResponseDto(entity);
    }

}
  • @RequiredArgsConstructor: final 혹은 @NotNull이 붙은 필드의 생성자를 자동으로 생성해줌
  • @Service: 서비스 레이어 클래스라는 것을 정의함. (정보를 가공하여 컨트롤러에게 전달)

@Transaction

  • 데이터베이스의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들을 의미함.
  • 롤백 처리, 커밋을 자동으로 수행함.
  • 원자성, 일관성, 격리성, 영속성

이제는 컨트롤러와 서비스에서 사용할 DTO 클래스를 생성하겠습니다.

  • PostsSaveRequestDto 클래스
import com.lgh.springbootwebservice.web.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 클래스
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;
    }
}
  • PostsListResponseDto 클래스
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostsListResponseDto {

    private Long id;
    private String title;
    private String author;
    private LocalDateTime modifiedDate;

    public PostsListResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.author = entity.getAuthor();
        this.modifiedDate = entity.getModifiedDate();
    }
}
  • PostsResponseDto 클래스
import lombok.Getter;

@Getter
public class PostsResponseDto {

    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }

}

코드를 보면 엔티티 클래스와 유사한 형태임을 알 수 있는데 이렇게 따로 DTO 클래스를 만드는 이유는 엔티티 클래스를 Request/Response 클래스로 사용해서는 안되기 때문입니다.

엔티티 클래스는 데이터베이스와 맞닿아 있는 핵심적인 클래스입니다. 엔티티 클래스를 기준으로 테이블이 생성되고,

스키마가 변경됩니다. 테이블과 연결된 엔티티 클래스를 변경하게 되면 여러 클래스에 영향을 끼치게 됩니다.

이제 이 API 서비스를 가져오는 컨트롤러 클래스를 생성하겠습니다.

  • PostsApiController
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
public class PostsApiController {

    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 PostsResponseDto findById(@PathVariable Long id) {
        return postsService.findById(id);
    }

    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id) {
        postsService.delete(id);
        return id;
    }

}
  • @RestController: @Controller + @RequestBody (JSON 형태로 객체 데이터를 반환할 때 사용함)

한 번 테스트해보기 위해 insert 쿼리 하나를 작성하겠습니다.

INSERT INTO posts (author, content, title) values ('ㅁㄴㅇ', 'ㅁㄴㅇ', 'ㅁㄴㅇ')

이제 애플리케이션을 실행해보도록 하겠습니다.

그리고 브라우저에 http://localhost:8080/api/v1/posts/1을 입력해 API 조회 기능을 테스트해보도록 하겠습니다.

스프링 부트 게시판 API - seupeuling buteu gesipan API

위와 같이 뜨면 성공

다음에는 화면을 구성해보도록 하겠습니다. 메인 화면, 게시물 등록 화면, 수정 화면을 만들 예정입니다.