JPA를 사용한 게시판 구현(2) - 등록/수정/조회 API 만들기

2023. 2. 28. 01:38·혼자하는 프로젝트/스프링 부트로 구현한 웹

https://product.kyobobook.co.kr/detail/S000001019679

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱 - 교보문고

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현합니다

product.kyobobook.co.kr

클래스 이해

API를 만들기 위해 총 3개의 클래스가 필요하다.

  • Request 데이터를 받을 Dto
  • API 요청을 받을 Controller
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service

 

Spring 웹 계층

Web Layer

  • 흔히 사용하는 컨트롤러(@Controller)와 JSP/Freemarker 등의 뷰 템플릿 영역이다.
  • 필터(@Filter), 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 야기한다.

Service Layer

  • @Service에 사용되는 서비스 영역이다.
  • 일반적으로 Controller와 Dao의 중간 영역에서 사용된다.
  • @Transcational이 사용되어야 하는 영역이기도 하다.

Repository Later

  • Database와 같이 데이터 저장소에 접근하는 영역이다.
  • 제일 처음 Spring을 배울 때, JDBC를 이용했다. 그때 DAO로 이 부분을 구현했었다.

Dtos

  • Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 얘기한다.
  • 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기한다.

Domail Model

  • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 한다.
  • @Entity가 사용된 영역 역시 도메인 모델이라고 이해한다.
  • VO처럼 값 객체(ValueObject는 값으로만 이루어진 객체이다.)들도 이 영역에 해당된다.

 

직접 API 구현해 보기

저장 API

  • PostsSaveRequestDto
@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();
    }
}

지금까지는 직접 생성자를 만들어서 사용했었다. 이 책에서는 Builder를 이용해 저 매개변수를 필요한 생성자를 Builder에 올린다. toEntity()를 통해 dto를 setting 하고 있다.


다른 수업에서도 배웠지만 View Layer와 DB Layer는 철저하게 분리해야 한다.
Entity를 직접적으로 노출하는 것은 매우 안 좋은 방법이다. 보안상의 문제가 생길 수 있고 join을 할 때 필요한 것이 매번 달라지므로 Dto를 사용하는 것이 제일 좋은 방법이다!!
  • PostsApiController
@RequiredArgsConstructor
@RestController
public class PostsApiController {
    private final PostsService postsService;
    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }
}
  • PostsService
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;
    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();    }
}
  • PostsApiControllerTest
@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의 흐름을 살펴보면 requestDto를 만든다 그 후 random으로 만든 port에서 스프링부트를 실행한다.

저 url로 요청을 보냈으니 requestDto가 필요할 것이다. 그러므로 requestDto를 보낸다. 그러면 requestDto의 내용이 저장되어야 한다. 위에 assertThat 2개는 상태와 body가 잘 갔다 왔는지 확인한다.

아래 assertThat은 직접적으로 title과 content가 같은지 검사한다.

test를 성공했고 insert문도 잘 실행된 것을 알 수 있다.

 

수정 및 조회 API

  • PostsUpdateRequestDto
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;
    @Builder
    public PostsUpdateRequestDto(String title, String content){
        this.title = title;
        this.content = content;
    }
}

update를 할 때 쓸 Dto이다.

  • PostsResponseDto
@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();
    }
}

조회를 할때 사용하는 것이므로 필요한 내용만 찾는다.

  • PostsApiController
@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);
}

각각 update와 id를 찾는 api이다.

  • PostsService
@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);
}

update 부분은 Posts에 추가해줘야 한다.

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

setter가 없으므로 update라는 메서드를 생성해서 사용한다!!


여기서 의문점은 update할 때 직접적인 쿼리를 날리는 부분이없다. 즉, repository를 부르는 부분이 id를 찾는 부분만 있다. 그런데 어떻게 update를 할 수 있을까?
JPA는 영속성 컨텍스트를 사용한다. 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이다. 만약 id를 찾으면 이건 영속성 컨텍스트에 저장되어 있다. 트랜잭션 안에서 데이터베이스에 데이터를 가지고 왔으므로 영속성 컨텍스트에 저장이 된 것이다. 이제 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영하게 된다. 이때 사용하는 것이 update 메소드이다. 즉, UPDATE 쿼리를 날릴 필요가 없다. 이것을 더티 체킹이라고 한다.
  • PostsApiControllerTest
@Test
public void Posts_수정된다() {
    //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);
}

savedPosts로 처음요소를 만든다. 그 후 변경할 expectedTtitle과 expectedContent를 지정해준다. 그 후 requestDto를 설정해준다. requestDto를 통해 HttpEnttiy를 만들어준다. 그 후 url로 put 신호를 보낸다. 

테스트와 update 쿼리가 모두 잘들어 간 것을 확인할 수 있다.

 

H2Database를 사용해 API 확인하기

인프런에서 JPA 수업을 들을 때도 H2Database를 이용해 TEST를 진행했다. 솔직히 간단하게 혼자 연습할때는 H2가 좋은것 같다. 간편하게 접근할 수 있고 테스트도 수월하게 할 수 있다.

이 책에서는 jdbc:h2:mem:testdb에 접근한다. localhost로 접근할 때 자주 사용한다.

하지만 바로접근하면 h2-console이 열리지 않는다. 그러므로

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb

이걸 추가해줘야 한다!! 그러면 에러없이 열릴 것이다.

insert into posts (author, content, title) values ('author', 'content', 'title'); 이 내용을 치고 

http://localhost:8080/api/v1/posts/1 이것을 검색해보면 

잘 들어간 모습을 볼 수 있다!!

 

JPA  Auditing으로 생성시간/수정시간 자동화하기

이번 프로젝트를 할 때도 생성시간과 수정시간은 Entity에 꼭 추가했다. 그리고 유용하게 사용한 부분도 많았다.

하지만 모든 Entity에 추가할려고 하니 코드가 더러워지고 길어졌다. 그럴때는 JPA Auditing을 사용하면 된다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdDate;
    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

이것을 만들어서 사용하면 된다.

 

@MappedSuperclass

  • JPA Entity 클래스들이 BaseTimeEntity을 상속할 경우 필드들도 칼럼으로 인식하도록 한다.

@EntityListeners(AuditingEntityListener.class)

  • BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.

@CreatedDate

  • Entity가 생성되어 저장될 때 시간이 자동 저장된다

@LastModifiedDate

  • 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.

 

JPA  Auditing 테스트 해보기

테스트를 하기전에 꼭 해야하는 것은 @EnableJpaAuditing을 추가해 JPA Auditing을 활성화 시켜야 한다. 그래야 값을 가져올 수 있다.

@Test
public void BaseTimeEntity_등록() {
    //given
    LocalDateTime now = LocalDateTime.of(2023,2,28,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);
}

정확하게 현재시간을 가져온 것을 알 수 있다.

now도 훨씬 전으로 해놨으므로 Test도 통과한 모습이다.

 

이번에는 JPA를 통해 저장, 수정, 조회 API를 구현해보았다.

이제 다음에는 머스테치로 화면을 구성해볼 차례이다. 

'혼자하는 프로젝트 > 스프링 부트로 구현한 웹' 카테고리의 다른 글

머스테치로 화면 구성하기(2) 등록, 조회  (0) 2023.03.03
머스테치로 화면 구성하기(1)  (0) 2023.03.03
JPA를 사용한 게시판 구현(1)  (1) 2023.02.27
스프링 부트 JPA를 사용해야하는 이유  (0) 2023.02.27
스프링 부트에서 테스트 코스 작성하기  (0) 2023.02.26
'혼자하는 프로젝트/스프링 부트로 구현한 웹' 카테고리의 다른 글
  • 머스테치로 화면 구성하기(2) 등록, 조회
  • 머스테치로 화면 구성하기(1)
  • JPA를 사용한 게시판 구현(1)
  • 스프링 부트 JPA를 사용해야하는 이유
인프라 감자
인프라 감자
  • 인프라 감자
    삶은 인프라
    인프라 감자
  • 전체
    오늘
    어제
    • 분류 전체보기 (243)
      • 클라우드&인프라 (28)
        • 인프라 공부 (4)
        • AWS 구조와 서비스 (18)
        • 클라우드 공부 (4)
        • Terraform (2)
      • AWS Cloud School (13)
        • project (5)
        • Linux, Network (6)
        • Docker (2)
      • BackEnd (162)
        • JAVA 공부 (15)
        • 알고리즘 공부 (71)
        • MySQL 문제 풀기 (8)
        • 스프링 핵심 원리 - 기본편 (18)
        • 스프링 MVC 1편 (4)
        • 자바 ORM 표준 JPA 프로그래밍 (21)
        • 실전! 스프링 부트와 JPA 활용1 (8)
        • 실전! 스프링 부트와 JPA 활용2 (5)
        • 스프링 데이터 JPA (8)
        • Querydsl (4)
      • 혼자하는 프로젝트 (32)
        • 배달의 민족 클론코딩 (7)
        • 나만의 프로젝트 (10)
        • 스프링 부트로 구현한 웹 (15)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Email
    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    쿼드 압축
    프로그래머스
    조합
    VPN
    네트워크 기본 용어
    자바
    linux
    정렬
    상속
    스프링 핵심 원리-기본편
    완전탐색
    이것이 자바다
    다이나믹 프로그래밍
    dp
    querydsl
    유니온 파인드
    백트래킹
    중첩 선언
    자동 배포
    디팬스 게임
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
인프라 감자
JPA를 사용한 게시판 구현(2) - 등록/수정/조회 API 만들기
상단으로

티스토리툴바