스프링부트에서 JPA로 데이터베이스 다뤄보기 (2)

박의진·2023년 6월 21일
0

스프링부트

목록 보기
5/7

API를 만들기 위해서는 총 3개의 클래스가 필요

  • Request 데이터를 받을 DTO 클래스
  • Controller 클래스
  • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service 클래스

서비스에서는 비지니스 로직을 처리하는 것이 아니라 트랜잭션, 도메인 간 순서 보장의 역할만 담당한다.

  • Web Layer
    • Controller와 JSP/Freemaker 등의 뷰 템플릿 영역
    • 필터, 인터셉터, 컨트롤러 어드바이스 등 외부요청과 응답에 대한 전반적인 영역을 이야기 함
  • Service Layer
    • @Service Layer에 사용되는 서비스 영역
    • 일반적으로 Controller와 Dao의 중간 영역에서 사용
    • @Transactional이 사용되어야 하는 영역이기도 함
  • Repository Layer
    • DB와 같이 데이터 저장소에 접근하는 영역
    • DAO 영역
  • Dtos
    • Dto는 계층 간에 데이터 교환을 위한 객체를 의미하며 Dtos는 이들의 영역을 의미
    • 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체등이 이들을 의미
  • Domain Model
    • 도메인이라 불리는 개발 대상을 모드 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 함
    • @Entity가 사용된 영역 역시 도메인 모델에 속함
    • 무조건 데이터베이스 테이블과의 관계가 있어야 하는 것은 아님
    • VO 처럼 값 객체들도 이 영역에 해당함

비즈니스를 처리를 담당해야할 곳은 바로 Domain 이다.

등록 API 만들기

1) Service 및 DTO/컨트롤러 클래스 생성

  • 서비스 클래스 생성
  • DTO 및 컨트롤러 클래스 생성

2) PostsApiController

3) PostsService

2)번과 3)번에서 @RequiredArgsConstructor 에서 생성자를 해결하며, final이 선언된 모든 필드를 인자 값으로 하는 생성자를 @RequiredArgsConstructor가 대신 생성

생성자를 안 쓰고 롬복 어노테이션을 쓰는 이유?
해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함

스프링에서 빈을 주입받는 방식
1) @Autowired
2) setter
3) 생성자

가장 권장하는 방식은 생성자로 Bean 객체를 주입받는 방식

4) PostsSaveRequestDto

  • Entity 클래스와 거의 유사한 형태이지만 Dto 클래스를 추가로 생성해줌
    => Entity 클래스를 Request/ Response 클래스로 사용해서는 안됨

  • Entity 클래스는 DB와 맞닿은 핵심 클래스로, 이 클래스를 기준으로 테이블이 생성되고, 스키마가 변경된다. 화면 변경은 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경

  • Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 View를 위한 클래스라 정말 자주 변경이 필요함

  • View Layer와 Db Layer의 역할 분리를 철저하게 하는 것이 좋음

    • 컨트롤러에서 결과값으로 여러 테이블을 조인해서 줘야할 경우가 빈번하므로 Entity 클래스만으로 표현하기가 어려운 경우가 많음
    • 따라서 Entity클래스와 Controller에서 사용할 DTO 는 분리해서 사용해야 함

5) 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);
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);
        

        //then
        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
  • @WebMvcTest를 사용하지 않음
    • 해당 기능의 경우 JPA 기능이 작동하지 않기 때문이다.
    • Controller와 ControllerAdvice 등 외부 연동과 관련된부분만 활성화 되니 지금과 같이 JPA 기능까지 한번에 테스트 할 때는 @SpringBootTest와 TestRestTemplate를 사용 한다.

6) 테스트 코드 실행 결과 및 쿼리 로그

수정/조회 API 만들기

1) PostsApiController api 만들기

2) PostsResponseDto

  • PostsResponseDto는 Entity의 필드 중 일부만 사용하므로 생성자로 Entity를 받아 필드에 값을 넣음

3) PostsUpdateRequestDto

4) Posts

5) PostsService

  • Update 기능에서 쿼리를 날리는 부분이 없음 => JPA의 영속성 컨텍스트 때문

  • 영속성 컨텍스트

    • 엔티티를 영구히 저장하는 환경
  • JPA 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태가 된다.
    => 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
    => 즉, 엔티티 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다. 이를 더티체킹 이라고 한다.

6) PostsApiControllerTest

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

7) 테스트 코드 실행 후 결과

웹 콘솔에서 H2로 확인

1) application.properties 설정

spring.h2.console.enabled=true

2) Application의 Main 메소드 실행

3) h2 콘솔 설정

http://localhost:8080/h2-console

로그인 화면

Connect 후 화면

4) 전체 데이터 확인

5) insert

6) select

profile
주니어 개발자의 개발일지

0개의 댓글