스프링부트와 AWS로 혼자 구현하는 웹서비스-등록/수정/조회 API 만들기

qq·2023년 9월 13일

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

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

Service만 비즈니스 로직을 처리해야 한다는 것이 아니라 Service는 트랜잭션,도메인 간 순서 보장의 역할만 한다

Spring 웹 계층

  • Web Layer
    - 흔히 사용하는 컨트롤러와 JSP/Freemaker등의 뷰 템플릿 영역
    • 필터, 인터셉터, 컨트롤러 어드바이스 등 외부 요청과 응답에 대한 전반적인 영역을 이야기한다.
  • Service Layer
    - @Service에 사용되는 서비스 영역이다
    • 일반적으로 Controller와 Dao의 중간 영역에서 사용된다
    • @Transactional이 사용되어야 하는 영역이기도 한다
  • Repository Layer
    - Database와 같이 데이터 저장소에 접근하는 영역이다
  • Dtos
    - Dto는 계층 간에 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 얘기한다
  • Domain Model
    - 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 것을 도메인 모델이라고 한다
    • 이를테면 택시 앱이라고 하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있다.
    • @Entity를 사용해보신 분들은 @Entity가 사용된 영역 역시 도메인 모델이라고 이해해주시면 된다.
    • 다만 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아니다
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}
@RequiredArgsConstructor
@RestController
public class PostsApiController {
    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }
}

생성자로 Bean객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있다. @RequiredArgsConstructor에서 final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해준다.

@NoArgsConstructor은 해당 클래스에 매개변수 없는 기본 생성자를 생성합니다.

@AllArgsConstructor은 이 생성자는 객체를 생성할 때 모든 필드 값을 인자로 받아서 객체를 초기화합니다. 이를 통해 객체를 생성할 때 모든 필드 값을 설정할 수 있습니다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @AfterEach
    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
        org.assertj.core.api.Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        org.assertj.core.api.Assertions.assertThat(responseEntity.getBody()).isGreaterThan(0L);


        List<Posts> all = postsRepository.findAll();
        org.assertj.core.api.Assertions.assertThat(all.get(0).getTitle()).isEqualTo(title);
        org.assertj.core.api.Assertions.assertThat(all.get(0).getContent()).isEqualTo(content);
        
    }
}

이 책에 있는 Dto를 따라 작성하면서 항상 Response에 대한 Dto를 제대로 안짜고 있었다는 것을 알게되었다

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

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

    @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 Service에는 데이터베이스에 쿼리를 날리는 부분이 없다. 이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이다.

결과:

profile
백엔드 개발자

0개의 댓글