ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SPRINGBOOT] 기본 CRUD API 만들기①(등록)
    SPRINGBOOT 2022. 4. 17. 16:26

    참조 : 스프링부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 저)

     

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

    - Request 데이터를 받은 DTO

    - API 요청을 받을 Controller

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

     

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

    비지니스를 처리하는 곳은 바로 Domain이다.

     

    Spring Web 계층

    - Web Layer

    컨트롤러(@Controller)와 JSP 등의 뷰 템플릿 영역

    필터(@Filter) , 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역

     

    - Service Layer

    @Service에 사용되는 서비스 영역

    Controller와 DAO 중간 영역

    @Transaction이 사용되어야 하는 영역

     

    -Repository Layer

    DB와 같이 데이터 저장소에 접근하는 영역

    DAO의 영역

     

    - Dtos

    계층 간에 데이터 교환을 위한 객체

     

    - Domain Model

    도메인이라고 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것

    @Entity가 사용된 영역도 Domain Model

    VO같은 값 객체들도 Domain Model 영역

     

    1. API 만들기(등록)

     

    디렉토리 참고

     

     먼저 Controller와 Service에서 사용할 Dto 클래스를 생성하여 준다.

    package com.study.crystal.test.web.dto;
    
    import com.study.crystal.test.domain.posts.Posts;
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    @Getter
    @NoArgsConstructor //파라미터가 없는 기본 생성자 생성
    public class PostSaveRequestDto {
        private String title;
        private String content;
        private String author;
    
    	//빌더 패턴을 통한 객체주입생성자 생성
        @Builder
        public PostSaveRequestDto(String title, String content, String author){
            this.title= title;
            this.content = content;
            this.author = author;
        }
    	
        //Entity 클래스인 Posts에 객체를 주입하여 Entity클래스 객체를 반환하는 메소드
        public Posts toEntity(){
            return Posts.builder()
                    .title(title)
                    .content(content)
                    .author(author)
                    .build();
        }
    }

    Entity클래스와 거의 유사한 형태의 Dto를 만들었다. 

    Entity 클래스는 데이터베이스와 맞닿은 핵심 클래스이므로 Request/Response 클래스로 사용하지 않고 

    Dto를 사용하는 것이다.

     

    ② 컨트롤러를 생성한다.

    컨크롤러에서 방금 만든 Dto를 요청 객체로 해서 서비스의 save메소드를 호출한다.

    package com.study.crystal.test.web;
    
    import com.study.crystal.test.service.posts.PostsService;
    import com.study.crystal.test.web.dto.PostSaveRequestDto;
    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 //final 필드 생성자 주입
    @RestController //Controller와 ResponseBody의 결합으로 Controller에서 json data를 반환하도록 한다.
    public class PostApiController {
    
        private final PostsService postsService;
    	
        //post방식 경로
        @PostMapping("/api/v1/posts")
        public Long save(@RequestBody PostSaveRequestDto requestDto){
            return postsService.save(requestDto);
        }
    }

     

    ③ 서비스 클래스를 생성한다.

    서비스 클래스에서는 JAVA에서 DAO 역할을 하는 Repository 객체를 생성 주입한다.

    그리고 save 메소드를 통해 Repository의 save()를 실행하고 그 결과를 반환받는다.

    package com.study.crystal.test.service.posts;
    
    import com.study.crystal.test.domain.posts.PostsRepository;
    import com.study.crystal.test.web.dto.PostSaveRequestDto;
    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(PostSaveRequestDto requestDto){
            return postsRepository.save(requestDto.toEntity()).getId();
        }
    }

     

    생성한 Service에서는 Dto의 toEntity()메소드를 통해 Posts라는 엔티티 클래스에 생성자를 주입하고 그 Entity 객체를 반환받았다. 그리고 그 객체를 파라미터로 Repository의 save()메소드를 실행하였고 다시 그 엔티티를 반환받았다.

    그리고 마지막 .getId()를 통해 해당 엔티티의 PK 키를 반환받은 것이다.

     

    ④ 테스트를 통해 검증

    @WebMvcTest를 사용하지 않는데 그 이유는 JPA가 작동하지 않기 때문이다.

    해당 어노테이션은 외부연동과 관련된 부부만 활성화된다.

     

    JPA기능까지 한번에 테스트를 할 때는 @SpringBootTest와 TestRestTemplate를 사용한다.

    package com.study.crystal.test.web;
    
    import com.study.crystal.test.domain.posts.Posts;
    import com.study.crystal.test.domain.posts.PostsRepository;
    import com.study.crystal.test.web.dto.PostSaveRequestDto;
    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.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() throws Exception{
            postsRepository.deleteAll();
        }
    
        @Test
        public void Posts_등록된다() throws Exception {
            //given
            String title = "title";
            String content = "content";
            PostSaveRequestDto requestDto = PostSaveRequestDto.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);
        }
    }

    결과를 보면 WebEnvironment.RANDOM_PORT로 인해서 랜덤포트로 실행된 것을 확인할 수 있다.

    그리고 INSERT 쿼리와 after처리로 인한 DELETE 쿼리까지 실행되었다.

     

     

    2. save()

     

    대체 Repository의 save는 어떤 역할을 하는지 궁금해진다.

    postRepository는 앞서서 JpaRepository를 상속받았고 이를 통해 기본적인 CRUD 쿼리를 자동 수행한다.

    상속받은 JpaRepository는 agingAndSortingRepository, agingAndSortingRepository는 CrudRepository를 상속받았다.

    이 CrudRepository안을 보면 save메소드를 확인할 수 있다.

    save()의 설명을 보면 파라미터로 받은 Entity를 저장하고 그 Entity를 반환하고 있다.

    실제 save메소드를 확인해 보면 다음과 같다.

    /*
    	 * (non-Javadoc)
    	 * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object)
    	 */
    	@Transactional
    	public <S extends T> S save(S entity) {
    
    		if (entityInformation.isNew(entity)) {
    			em.persist(entity);
    			return entity;
    		} else {
    			return em.merge(entity);
    		}
    	}

    즉, 넘겨받은 entity가 새로운 것이면 persist, 이미 존재하는 것이면 merge하는 것이다.

    아직 JPA를 공부하지 않아 인터넷에서 찾은 간단한 설명으로 보자면,

    해당 객체가 영속성에 존재하지 않는다면 persist, INSERT만을 진행하고

    영속성에 존재한다면 merge를 진행하는데, 해당 엔티티의 ID를 찾아 ID가 존재하면 INSERT 없으면 UPDATE를 실행한다.

     

     

    3. Postman을 활용한 테스트

     

    ① 어플리케이션을 실행시킨다.

     

    ② postman을 실행시켜서 Controller에서 설정한 경로를 넣어주고 POST방식으로 설정해준다.

    ③ Headers에서 Content-Type에 Json형태로 받을 것임을 명시해준다.(나머지는 기본 설정)

    ④ RequestBody로 JSON형태로 데이터를 주고 받을 것이기 때문에 Body에서 다음과 같이 필요한 데이터를 세팅해준다.

    {
        "title" : "test",
        "content" : "this is test content",
        "author" : "crystal"
    }

    ⑤ 이후 Send를 누르면 정상적으로 INSERT쿼리가 실행되었음을 알 수 있다.

    'SPRINGBOOT' 카테고리의 다른 글

    [SPRINGBOOT] JPA Auditing  (0) 2022.04.24
    [SPRINGBOOT] 기본 CRUD API 만들기②(수정, 조회)  (0) 2022.04.24
    [SPRINGBOOT] JPA 기본 기능과 설정  (0) 2022.04.07
    [SPRINGBOOT] 롬복  (0) 2022.04.05
    [SPRINGBOOT]테스트코드 작성  (0) 2022.04.04
Designed by Tistory.