개발환경
- 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 실행화면
브라우저 실행화면
'뒤끝 (Back-End)' 카테고리의 다른 글
[JSP] Servlet과 JSP (3) | 2024.10.09 |
---|---|
[JSP] 웹 프로그래밍 (2) | 2024.10.09 |
[Spring boot] MVC, API, JPA, 데이터 베이스 이해하기 (0) | 2024.04.06 |
[Spring boot] 스프링 부트 기본 어노테이션 (0) | 2024.04.06 |
[Spring boot] 스프링 부트에 대해 알아가기 (0) | 2024.04.05 |