Blog GDSC CAU 개발자와 디자이너의 작업 과정과 결과물을 공유하는 공간입니다.
어떻게 프로젝트를 시작하게 되었고, 진행하면서 느낀 개발자와 디자이너의 생생한 스토리를 직접 확인해보세요!
Designed by Sohyun Kim, Serin Seong Developed by Yeojin Kim, Jiwoo Park, Yujin Son, Yongmin Yoo, Junesung Jang
Development 3주차 Spring Study #Back-End Seojin Lim 2023. 11. 28.
3주차 Spring Study JPA소개 객체지향 프로그래밍에 맞추어 JPA가 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행함
<장점>
→SQL에 종속적인 개발을 하지 않아도 됨(테이블 마다 CRUD SQL작성할 필요없음, C:create R:read U:update, D:delete) : 패러다임 일치
→JPA ← Hiberante ← Spring Data JPA( 구현체 교체의 용이성, 저장소 교체의 용이성)
<단점>
→실무에선 높은 러닝커브로 잘 안쓰임.
요구사항 분석 게시판
0.프로젝트 Spring Data JPA 적용 →테스트 코드
1.등록 API → 테스트 코드
2.수정 API → 테스트 코드
3.조회 API → 테스트 코드
4.JPA Auditing으로 조회시간/생성시간 자동화
0. 프로젝트에 Spring Data JPA 적용하기 build.gralde에 dependencies 의존성 추가 spring-boot-starter-data-jpa : JPA 관련 라이브러리 관리
h2 : 인메모리 관계형 데이터베이스
domain 패키지→ posts 패키지 → Posts 클래스 도메인: 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제영역
실제 DB 테이블과 매칭될 클래스(entity class)
@Entity : 테이블과 링크될 클래스임을 나타냄. @GeneratedValue : PK의 생성규칙을 나타냄. @Column : 테이블의 칼럼을 나타내며 굳이 선언하지 않아도 해당 클래스의 필드는 모두 칼럼이 됨. 기본값외에 추가로 변경이 필요한 경우 사용 (ex.사이즈, 타입 변경 등 )
domain 패키지→ posts 패키지 → PostsRepository (클래스-인터페이스) Posts 클래스로 DB접근하게 해줄 JpaRepository
인터페이스로 생성하고 / 상속하면 기본적인 crud 메소드 자동 생성
Spring Data JPA 테스트 코드
test 디렉→ com.~springboot 패키지→ domain.posts 패키지 생성 → PostsRepositoryTest 클래스
@SpringBootTest 사용할 경우 H2 데이타베이스 자동 실행해준다
application.properties mysql문 버전으로 쿼리로그 확인 가능
1. 등록API 생성 PostsApiController PostsService @RequiredArgsConstructor에서 생성자 생성
PostsSaveRequestDto Controller와 Service에서 사용할 DTO클래스 생성
view를 위한 클래스로, entity 클래스(테이블, 스키마 생성)와 분리!(자주 변경)
Test WebMvcTest를 사용하지 않는 이유는 JPA 기능이 작동하지 않기 때문이다.
2,3. 수정/조회 API 생성 PostApiController
PostsResponseDto entity 필드의 일부만 사용하므로, 생성자로 entity를 받아 필드에 값을 넣는다.
PostsUpdateRequestDto
Posts
PostsService
PostApiControllerTest
확인
4. JPA Auditing으로 생성시간/조회시간 자동화 BaseTimeEntity 생성 테스트 코드
implementation ('javax.persistence:javax.persistence-api:2.2')
implementation('org.springframework.boot:spring-boot-starter-web')
implementation('org.springframework.boot:spring-boot-starter-validation')
implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
implementation('org.projectlombok:lombok')
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
implementation('com.h2database:h2')
implementation ('org.springframework.boot:spring-boot-starter-data-jpa')
testImplementation('org.springframework.boot:spring-boot-starter-test')
package com.hello.book.springboot.domain.posts;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
package com.hello.book.springboot.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long>{
}
package com.hello.book.springboot.web.domain.posts;
import com.hello.book.springboot.domain.posts.Posts;
import com.hello.book.springboot.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.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup() {
postsRepository.deleteAll();
}
@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("jojoldu@gmail.com")
.build()
);
//when
List<Posts> postsList = postsRepository.findAll();
//then
Posts posts = postsList.get(0);
assertThat(posts.getTitle()).isEqualTo(title);
assertThat(posts.getContent()).isEqualTo(content);
}
}
spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb
spring.h2.console.enabled=true
package com.hello.book.springboot.web;
import com.hello.book.springboot.service.posts.PostsService;
import com.hello.book.springboot.web.dto.PostsSaveRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto) {
return postsService.save(requestDto);
}
}
package com.hello.book.springboot.service.posts;
//import com.hello.book.domain.posts.PostsRepository;
//import com.hello.book.dto.PostsSaveRequestDto;
import com.hello.book.springboot.domain.posts.PostsRepository;
import com.hello.book.springboot.web.dto.PostsSaveRequestDto;
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();
}
}
package com.hello.book.springboot.web.dto;
import com.hello.book.springboot.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();
}
}
package com.hello.book.springboot.web;
import com.hello.book.springboot.domain.posts.Posts;
import com.hello.book.springboot.domain.posts.PostsRepository;
import com.hello.book.springboot.web.dto.PostsSaveRequestDto;
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.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
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() {
postsRepository.deleteAll();
}
@Test
public void Posts_register() throws Exception {
//given
String title = "title";
String content = "content";
String author = "author";
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> postsList = postsRepository.findAll();
assertThat(postsList.get(0).getTitle()).isEqualTo(title);
assertThat(postsList.get(0).getContent()).isEqualTo(content);
assertThat(postsList.get(0).getAuthor()).isEqualTo(author);
}
}
@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);
}
package com.hello.book.springboot.web.dto;
import com.hello.book.springboot.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();
}
}
package com.hello.book.springboot.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;
}
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
@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);
}
package com.hello.book.springboot.web;
import com.hello.book.springboot.domain.posts.Posts;
import com.hello.book.springboot.domain.posts.PostsRepository;
import com.hello.book.springboot.web.dto.PostsSaveRequestDto;
import com.hello.book.springboot.web.dto.PostsUpdateRequestDto;
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.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.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.time.LocalDateTime;
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() {
postsRepository.deleteAll();
}
@Test
public void Posts_register() throws Exception {
//given
String title = "title";
String content = "content";
String author = "author";
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> postsList = postsRepository.findAll();
assertThat(postsList.get(0).getTitle()).isEqualTo(title);
assertThat(postsList.get(0).getContent()).isEqualTo(content);
assertThat(postsList.get(0).getAuthor()).isEqualTo(author);
}
@Test
public void Posts_수정된다() throws Exception{
Posts savedPosts = postsRepository.save(Posts.builder()
.title("title")
.content("content")
.author("author")
.build());
Long updatedId = 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/"+updatedId;
HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,requestEntity,Long.class);
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);
}
}
package com.hello.book.springboot.domain;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
@Test
public void BaseTimeEntity_등록() {
LocalDateTime now = LocalDateTime.of(2023, 11, 7, 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(">>>>>>>>> createdDate = " + posts.getCreatedDate() + ", modifiedDate = " + posts.getModifiedDate());
assertThat(posts.getCreatedDate()).isAfter(now);
assertThat(posts.getModifiedDate()).isAfter(now);
}