프로젝트 구조 설명
- controller, model, service, repository로 패키지 구조화
- model 패키지의 경우 DTO, VO 같은 데이터 클래스가 정의되어있는 패키지 인데, 이것을
* 컨트롤러에서 파라미터로 받는 Param,
* DB에서 select문의 결과로 반환될 Entity,
* ResponseEntity Json형태로 반환될 Result 객체로 구분
- service 클래스는 인터페이스를 클래스가 구현하는 형태가 아닌 직접 클래스가 역할을 하는 쪽으로 함. -> 인터페이스를 이용하여 다형성을 구현하는 형태가 아니라 일종의 루틴처럼 사용되는 경향이 있어보여서 과감히 탈피.
- 이전 챕터에서 진행했던 home 클래스는 삭제해도 됨.
- API 테스트를 진행할 BoardControllerTest 클래스 파일 생성.
파일 설명
POM.xml
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>co.worker</groupId>
<artifactId>board</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>board</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jacoco/org.jacoco.agent -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<version>0.8.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- jacoco 코드 커버리지 플러그인 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- <dependency> 에 JPA, H2 를 추가해줍니다.
application.properties
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
# query print
spring.jpa.show-sql=true
- 한글의 값을 http 파라미터로 넘겼을 경우 발생하는 인코딩문제를 해결하기 위해 http encoding을 UTF-8로 설정해둡니다.
- 또한 로그에 sql 쿼리형식으로 로그가 찍히도록 설정해줍니다.
BoardController.java
@RestController
@RequestMapping("/api/boards")
public class BoardController {
@Autowired
private BoardService boardService;
@GetMapping(
value = "/all",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity get(){
return ResponseEntity.ok(boardService.getBoard());
}
@GetMapping(
value = "/{seq}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity get(@PathVariable("seq") Long seq){
return ResponseEntity.ok(boardService.getBoard(seq));
}
@PostMapping(value = "/add", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity add(@RequestBody @Valid BoardParam param){
boardService.add(param);
return ResponseEntity.ok(null);
}
@PutMapping(value = "/{seq}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity edit(@RequestBody @Valid BoardParam param,
@PathVariable("seq") Long seq){
param.setSeq(seq);
boardService.edit(param);
return ResponseEntity.ok(null);
}
@DeleteMapping(value = "/{seq}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity delete(@PathVariable("seq") Long seq){
boardService.delete(seq);
return ResponseEntity.ok(null);
}
}
- @RequestBody : request객체의 Body에 JSON 형태로 넘어온 데이터를 BoardParam 객체에 매핑하는 것. Get 방식은 request Body가 없기 때문에 안됨.
- @Valid : 유효성체크해주는 어노테이션, BoardParam에 존재하는 @NotEmpty, @NotNull 따위의 어노테이션을 체크하기 위해서 사용.
- @PathVariable : 예를 들어, delete 메소드에서 /api/boards/3 이라는 URI값으로 DELETE 방식으로 데이터가 넘어온다면 3이라는 값이 {seq}에 매핑되고 Long seq에 대입된다.
BoardService.java
@Service
public class BoardService {
@Autowired
BoardRepository boardRepository;
@Transactional
public List<BoardResult> getBoard(){
List<BoardEntity> entityList = boardRepository.findAll();
List<BoardResult> results = entityList.stream().map(boardEntity -> {
BoardResult boardResult = new BoardResult();
boardResult.setContent(boardEntity.getContent());
boardResult.setUsername(boardEntity.getUsername());
boardResult.setTitle(boardEntity.getTitle());
boardResult.setSeq(boardEntity.getSeq());
return boardResult;
}).collect(Collectors.toList());
return results;
}
@Transactional
public Object getBoard(Long seq){
return boardRepository.findById(seq).map(boardEntity -> {
BoardResult boardResult = new BoardResult();
boardResult.setContent(boardEntity.getContent());
boardResult.setUsername(boardEntity.getUsername());
boardResult.setTitle(boardEntity.getTitle());
boardResult.setSeq(boardEntity.getSeq());
return boardResult;
}).get();
}
@Transactional
public void edit(BoardParam param) {
Optional<BoardEntity> getEntity = boardRepository.findById(param.getSeq());
getEntity.ifPresent(entity -> {
entity.setTitle(param.getTitle());
entity.setContent(param.getContent());
entity.setUsername(param.getUsername());
boardRepository.save(entity);
});
}
@Transactional
public void add(BoardParam param) {
BoardEntity entity = new BoardEntity();
entity.setUsername(param.getUsername());
entity.setContent(param.getContent());
entity.setTitle(param.getTitle());
boardRepository.save(entity);
}
@Transactional
public void delete(Long seq) {
boardRepository.deleteById(seq);
}
}
- 주로 간단한 객체 매핑작업이 수작업으로 이루어지는 비즈니스 로직입니다. 반복되는 코드는 추후 라이브러리를 이용해 숨기도록 해보겠습니다.
- 간단한 CRUD 로직을 구현하는 Service 로직입니다.
BoardRepository.java
@Repository
public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
}
- JpaRepository<반환테이블객체, @ID 타입> 을 상속받는 인터페이스는 JPA 레퍼지토리로써 데이터 영속층에 해당합니다.
- 자동적으로 PK를 이용한 객체 Select나 전체 Select, 삭제, 수정을 지원하는 메소드가 생깁니다.
BoardParam.java
@Getter
@Setter
@ToString
@Builder
public class BoardParam {
@Min(0)
Long seq;
@NotEmpty
String content;
@NotEmpty
String username;
@NotEmpty
String title;
}
- Controller에서 파라미터로 받는 객체
- @Min(0) : 최소값을 0으로 설정한다는 것.
- @NotEmpty : null과 "" 빈문자열 체크를 하고 있습니다.
BoardEntity.java
@Entity
@Getter
@Setter
@ToString
@Table(name = "BoardEntity")
public class BoardEntity {
@Id
@GeneratedValue
Long seq;
String content;
String username;
String title;
}
- @Table : 객체를 DB 테이블처럼 인식시키는 JPA 어노테이션
- @Id, @GeneratedValue : DB에서 PK라고 생각하면 된다.
BoardResult.java
@Getter
@Setter
@ToString
public class BoardResult {
Long seq;
String content;
String username;
String title;
}
- 반환타입 클래스. ResponseEntity.ok(여기); 에 들어갈 반환타입.
- 만일 정교한 매핑이 필요하다면 모두 정제하여 반환될 값만 딱 들어있을 객체
테스트
BoardControllerTest.java
@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class BoardControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
BoardRepository boardRepository;
@Autowired
ObjectMapper objectMapper;
@Before
public void insertBoard(){
for(int i =0; i<10; i++){
BoardEntity board = new BoardEntity();
board.setContent("내용"+i);
board.setTitle("제목"+i);
board.setUsername("코딩하는흑구");
boardRepository.save(board);
}
}
@Test
public void addBoard() throws Exception {
BoardParam param = BoardParam.builder()
.content("추가내용")
.title("추가제목")
.username("추가유저")
.build();
mockMvc.perform(post("/api/boards/add")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isOk())
.andReturn();
this.getBoard();
}
@Test
public void editBoard() throws Exception{
BoardParam param = BoardParam.builder()
.content("수정내용")
.title("수정제목")
.username("수정유저")
.build();
mockMvc.perform(put("/api/boards/3")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isOk());
this.getBoard();
}
@Test
public void getBoard() throws Exception {
mockMvc.perform(get("/api/boards/all")
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void getBoardOne() throws Exception {
mockMvc.perform(get("/api/boards/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void deleteBoardOne() throws Exception{
mockMvc.perform(delete("/api/boards/3")
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
this.getBoard();
}
}
- @Before : @Test 어노테이션이 붙은 메소드가 실행되기 전에 미리 실행하여 몇몇개의 데이터를 Insert 해놓고 테스트하기 위해서 작성.
- ObjectMapper 클래스를 이용해서 build된 param 객체를 JsonString 타입으로 변형시킵니다.
* 예를 들어, {"seq":null,"content":"추가내용","username":"추가유저","title":"추가제목"} 이런식으로 값을 넘깁니다.
- 메소드 옵션(get,post,put,delete)과 uri를 이용해서 api를 호출해봅니다.
[add 메소드 실행]
[add board] MockHttpServletRequest: [all board] -> 확인용 MockHttpServletRequest: |
Jacoco 코드 커버리지 확인
그리고 프로젝트 루트 파일로가서 빼먹지 않고 명령어를 입력해줍니다.
(윈도우 : cmd / 인텔리제이 유저 : terminal 탭 / mac 유저 : terminal)
" mvn clean verify "
모든 자바파일들이 수정되었으니 이를 메이븐으로 재빌드하고 code coverage를 확인해줍니다.
그리고 target > site > jacoco 폴더의 index.html 파일을 열어서 확인해줍니다.
- 대략 70%로 코드 커버리지를 확인할 수 있습니다.
- 아직은 복잡한 비즈니스 로직이 없기 때문에 (예를들어, switch 문이나 if 문) 비교적 높은 퍼센트를 갖게됨.
- 이를 통해 사용하지 않는 메소드나 새로 개발한 api를 고립되어 테스트할 수 있음(Test Driven Development 정석)
'웹 개발 > Spring Boot' 카테고리의 다른 글
[Spring Boot] REST API 게시판 서버 만들기 #5(User쪽 적용, 내가 TDD 활용하는 방법.) (6) | 2020.02.18 |
---|---|
[Spring Boot] REST API 게시판 서버 만들기 #4(ModelMapper를 이용한 객체 매핑) (2) | 2020.02.10 |
[Spring Boot] REST API 게시판 서버 만들기 #3(Response 커스텀 객체, Exception 처리, ExceptionHandler) (0) | 2020.02.09 |
[Spring Boot] REST API 게시판 서버 만들기 #1(프로젝트 생성 및 기본 예제) (2) | 2020.01.16 |
[Spring Boot] 스프링 @Value 어노테이션으로 properties 값 읽어오기 (2) | 2020.01.07 |