<스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 이동욱> 책을 활용해
공부한 내용을 정리할 예정입니다.
1. API란?
API(Application Programming Interface, 응용 프로그램 프로그래밍 인터페이스)는 응용 프로그램에서 사용할 수 있도록,
운영체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻합니다.
쉽게 말해 어떠한 응용프로그램에서 데이터를 주고 받기 위한 방법을 의미합니다.
어떤 특정 사이트에서 특정 데이터를 공유할 경우 어떠한 방식으로 정보를 요청해야 하는지,
그리고 어떠한 데이터를 제공 받을 수 있을지에 대한 규격들을 API라고 하는것 입니다.
- 인터페이스(Interface)
인터페이스(interface)는 컴퓨터 시스템끼리 정보를 교한하는 공유 경계를 의미한다, 터치 스크린과 같은 일부 컴퓨터 하드웨어 장치들은 인터페이스를 통해 데이터를 송수신 할 수 있으며, 마우스나 마이크론 폰가 같은 장치들은 오직 시스템에 데이터를 전송만 하는 인터페이스를 제공한다.
2. API를 만들기 위해 필요한 클래스
- Dto: request 데이터를 받음
- Controller: api 요청을 받음
- Service: 트랜잭션, 도메인 기능 간의 순서를 보장
서비스 메소드는 트랜잭션과 도메인 간의 수서만 보장해 줍니다.
3. 생성
1) PostApiController
src>main>java>com>hyemcomi>springboot>web>PostsApiController
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
2) PostsService
src>main>java>com>hyemcomi>springboot>service>PostsService
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();
}
}
final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor가 대신 생성해 준다.
롬복 어노테이션을 사용하므로서 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결할 수 있습니다.
3) PostSaveRequestDto
src>main>java>com>hyemcomi>springboot>web>dto>PostsSaveRequestDto
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();
}
}
Entity클래스와 유사한 형태임에도 Dto 클래스를 추가로 생성했습니다.
Entity클래스를 Request/Response 클래스로 사용해서는 안됩니다.
Entity클래스 데이터베이스와 맞닿은 핵심 클래스로 사소한 변경들을 위해 Entity클래스를 변경하는 것은 너무 큰 변경입니다.
Entity클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 뷰를 위한 클래스라 자주 변경이 필요합니다.
뷰 레이어와 DB레이어의 역할 분리를 철저하게 하는게 좋습니다.
4) PostsApiControllerTest
src>test>java>com>hyemcomi>springboot>web>PostsApiControllerTest
import org.junit.After;
import org.junit.Before;
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.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@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
@WithMockUser(roles="USER")
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
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(new ObjectMapper().writeValueAsString(requestDto)))
.andExpect(status().isOk());
//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을 사용한다.
수정/조회 기능까지는 저자 이동욱님 깃을 참고해서 작성합시다ㅎ
https://github.com/jojoldu/freelec-springboot2-webservice
jojoldu/freelec-springboot2-webservice
Contribute to jojoldu/freelec-springboot2-webservice development by creating an account on GitHub.
github.com
4. 더티 체킹
JPA의 영속성 컨텍스트 때문에 update기능에서 데이터 베이스에 쿼리를 날리지 않습니다.
영속성 컨텍스트란, 엔티티를 영구 저장하는 환경입니다.
JPA의 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태입니다. 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영합니다.
즉, Entity 객체의 값만 변경하면 별도 쿼리를 날릴 필요가 없는 것 입니다.
이 개념을 더티체킹이라고 합니다.
5. 웹 콘솔로 직접 접근
로컬 환경에서 데이터베이스로 H2를 사용합니다.
웹 콘솔 옵션을 활성화해 확인할 수 있습니다.
application.properties에 아래와 같이 옵션을 추가합니다.
spring.h2.console.enabled=true
Application의 main메소드를 실행하고
http://localhost:8080/h2-console로 접속하면 아래와 같은 화면이 나옵니다.
connect 후 쿼리문을 실행해 update 하고 등록해봅니다.
insert into posts (author,content, title) values ('author', 'content', 'title');
등록된 데이터를 확인한 후 API를 요청해 봅니다.
http://localhost:8080/api/v1/posts/1을 입력해 API조회 기능을 확인해 봅니다.