본문 바로가기
뒤끝 (Back-End)

[Spring boot] 게시판 API 만들기

728x90

개발환경

- Java 버전 : openjdk version "17.0.2"

- IntelliJ 버전 : IntelliJ IDEA Community 2023.03.06

- Springboot 버전 : 3.1.10

 

프로젝트 구성

 

 

 

main 폴더

 

domain 패키지 : 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제영역

 

Posts.java

package com.example.testspringboot.domain.posts;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.Length;

@Getter // @Getter : 클래스 내 모든 필드의 Getter 메소드 자동 생성, 롬복 어노테이션
@NoArgsConstructor // @NoArgsConstructor : 기본 생성자 자동 추가, 롬복 어노테이션
@Entity // @Entity : 테이블과 링크될 클래스, JPA 어노테이션, 주요 어노테이션은 코드 가까이 둠
public class Posts extends BaseTimeEntity {
    @Id // @Id : 해당 테이블의 PK 필드 나타냄
    @GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue() : PK의 생성 규칙을 나타냄
    // GenerationType.IDENTITY : auto_increment
    private Long id;

    @Column(length = 500, nullable = false) // @Column : 테이블의 칼럼을 나타냄
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder // @Builder : 해당 클래스의 빌더 패턴 클래스를 생성
    public Posts(String title, String content, String author)
    {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }

}

 

@Getter : 클래스 내 모든 필드의 Getter 메소드 자동 생성, 롬복 어노테이션
@NoArgsConstructor : 기본 생성자 자동 추가, 롬복 어노테이션
@Entity : 테이블과 링크될 클래스, JPA 어노테이션, 주요 어노테이션은 코드 가까이 둠
@Id : 해당 테이블의 PK 필드 나타냄
@GeneratedValue() : PK의 생성 규칙을 나타냄
@Column : 테이블의 칼럼을 나타냄
@Builder : 해당 클래스의 빌더 패턴 클래스를 생성

 

 

JpaRepository<Entity 클래스, PK 타입> 을 상속하면 기본적인 CRUD 메소드 자동으로 생성
※단, Entity 클래스와 기본 Entity Repository는 함께 위치해야 함

PostsRepository.java

// PostsRepository : Posts 클래스로 Database를 접근하게 해줌
package com.example.testspringboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts, Long> {
}

 

API 구성 요소

 

Request 데이터를 받을 Dto

API 요청을 받을 Controller

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

 

+-----------------------+      +----------------------+      +------------------------+
|       Controller      | ---> |       Service        | ---> |       Repository       |
+-----------------------+      +----------------------+      +------------------------+

 

컨트롤러(Controller)는 사용자의 요청을 받아들이고, 그에 맞는 처리를 담당합니다. 사용자의 요청을 받으면 해당 요청에 대한 서비스(Service) 메서드를 호출하고, 필요한 경우 DTO로 데이터를 전달합니다.
서비스(Service)는 비즈니스 로직을 담당하며, 컨트롤러로부터 받은 요청을 처리합니다. 서비스는 필요에 따라 데이터베이스와의 상호작용을 담당하는 리포지토리(Repository)를 호출하고,필요한 데이터를 DTO로 변환하여 반환합니다.
리포지토리(Repository)는 데이터베이스와의 상호작용을 담당하며, 데이터를 조회, 삽입, 수정, 삭제하는 역할을 수행합니다. 필요한 데이터를 DTO로 변환하여 서비스에 반환합니다.
DTO(Data Transfer Object)는 데이터 전송을 위한 객체로, 서비스와 컨트롤러 간에 데이터를 주고받을 때 사용됩니다. 주로 데이터베이스로부터 조회한 결과나 컨트롤러에서 입력한 데이터를 담아서 전달합니다.

 

 

PostsApiController.java

package com.example.testspringboot.controller;

import com.example.testspringboot.controller.dto.PostsResponseDto;
import com.example.testspringboot.controller.dto.PostsSaveRequestDto;
import com.example.testspringboot.controller.dto.PostsUpdateRequestDto;
import com.example.testspringboot.service.posts.PostsService;
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);
    }
}

 

 

PostsService.java

package com.example.testspringboot.service.posts;

import com.example.testspringboot.controller.dto.PostsResponseDto;
import com.example.testspringboot.controller.dto.PostsSaveRequestDto;
import com.example.testspringboot.controller.dto.PostsUpdateRequestDto;
import com.example.testspringboot.domain.posts.Posts;
import com.example.testspringboot.domain.posts.PostsRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@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 PostsResponseDto findById (Long id) {
        Posts entity = postsRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id = " + id));

        return new PostsResponseDto(entity);
    }
}

 

PostsSaveRequestDto.java

package com.example.testspringboot.controller.dto;

import com.example.testspringboot.domain.posts.Posts;
import com.example.testspringboot.service.posts.PostsService;
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();
    }
}

 

PostResponseDto.java

package com.example.testspringboot.controller.dto;

import com.example.testspringboot.domain.posts.Posts;
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();
    }
}

 

PostUpdateRequestDto.java

package com.example.testspringboot.controller.dto;

import com.example.testspringboot.domain.posts.Posts;
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();
    }
}

 

BaseTimeEntity.java

package com.example.testspringboot.domain.posts;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass // @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드(createdDate, modifiedDate)들도 칼럼으로 인식하도록 함
@EntityListeners(AuditingEntityListener.class) // @EntityListeners() : BaseTimeEntity 클래스에 Auditing 기능을 포함
public class BaseTimeEntity {

    @CreatedDate    // @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동 저장
    private LocalDateTime createdDate;

    @LastModifiedDate   // @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장
    private LocalDateTime modifiedDate;
}

 

Test 폴더

 

 

PostRepositoryTest.java

package com.example.testspringboot.controller.domain.posts;

import com.example.testspringboot.domain.posts.Posts;
import com.example.testspringboot.domain.posts.PostsRepository;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDateTime;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

    @After // @After : JUnit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정
    // 베포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용
    public void cleanup()
    {
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기()
    {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder() // postsRepository.save : 테이블 posts에 id 값 있으면 update, 없으면 insert 쿼리 실행
                .title(title)
                .content(content)
                .author("z_zen@naver.com")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }

    @Test
    public void BaseTimeEntity_등록()
    {
        // given
        LocalDateTime now = LocalDateTime.of(2024,4,2,0,0,0);
        postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        //then
        Posts posts = postsList.get(0);

        System.out.println(">>>>>>>>> createDate="+posts.getCreatedDate()+", modifiedDate="+posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

postsRepository.save : 테이블 posts에 id 값 있으면 update, 없으면 insert 쿼리 실행

postsRepository.findAll : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드

 

PostsApiControllerTest.java

package com.example.testspringboot.controller;

import com.example.testspringboot.controller.dto.PostsSaveRequestDto;
import com.example.testspringboot.controller.dto.PostsUpdateRequestDto;
import com.example.testspringboot.domain.posts.Posts;
import com.example.testspringboot.domain.posts.PostsRepository;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception
    {
        postsRepository.deleteAll();
    }

    @Test
    public void Posts_등록된다() throws Exception
    {
        // given
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        // when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }

    @Test
    public void Posts_수정된다() throws Exception{

        // given
        Posts savedPosts = postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updateId = savedPosts.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts/" + updateId;

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        // when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,
                requestEntity, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);
        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    }
}

 

 

실행 화면

 

h2 실행화면

 

브라우저 실행화면

728x90