[Spring Boot] REST API 게시판 서버 만들기 #5(User쪽 적용, 내가 TDD 활용하는 방법.)
- 웹 개발/Spring Boot
- 2020. 2. 18. 23:18
프로젝트 구조
- 유저쪽 패키지를 생성합니다.
- 컨트롤러와 서비스 DAO단 클래스들을 생성합니다.
- 테스트코드에서 User api를 테스트할 클래스를 생성합니다.
파일별 설명
모든 소스는 github에서 관리하고 있습니다.
따라하시다가 미처 제가 신경쓰지 못한 부분이 있다면 이곳을 확인해주세요^^
UserController.java
package co.worker.board.user.controller;
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
private UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
@PostMapping("/add")
public Object add(@RequestBody @Valid UserParam param){
userService.add(param);
return null;
}
@PutMapping("/edit/{seq}")
public Object edit(@RequestBody @Valid UserParam param, @PathVariable("seq") @Min(1) Long seq){
param.setSeq(seq);
userService.edit(param);
return null;
}
@GetMapping("/{seq}")
public Object getUserOne(@PathVariable("seq") @Min(1) Long seq){
return userService.get(seq);
}
@GetMapping("/all")
public Object getUserAll(){
return userService.getAll();
}
@DeleteMapping("/{seq}")
public Object delete(@PathVariable("seq") @Min(1) Long seq){
userService.delete(seq);
return null;
}
}
- 최대한 RestFul한 방식으로 api를 설계합니다.
- BoardController와 상당히 유사한 방식으로 설계하였습니다.
BoardService.java
package co.worker.board.user.service;
@Service
public class UserService {
private ModelMapper modelMapper;
private UserRepository userRepository;
public UserService(UserRepository userRepository, ModelMapper modelMapper){
this.userRepository = userRepository;
this.modelMapper = modelMapper;
}
@Transactional
public Object add(UserParam param) {
return userRepository.save(sourceToDestinationTypeCasting(param, new UserEntity()));
}
@Transactional
public Object edit(UserParam param) {
return userRepository.save(sourceToDestinationTypeCasting(param, new UserEntity()));
}
public Object get(Long seq) {
Optional<UserEntity> user = userRepository.findById(seq);
return user.isPresent() ? user.get() : null;
}
public Object getAll() {
List<UserEntity> users = userRepository.findAll();
return users;
}
@Transactional
public void delete(Long seq) {
userRepository.deleteById(seq);
}
private <R, T> T sourceToDestinationTypeCasting(R source, T destination){
modelMapper.map(source, destination);
return destination;
}
}
- 지난번 modelmapper를 활용한 부분을 따로 제네릭 메서드를 만들어서 처리하였습니다. -> 이부분은 static 메소드를 따로 작성하여 Util 클래스로 관리해도 무방합니다.
- @Transcational 어노테이션 같은 경우는 DML(insert, delete, update not select)이 발생될 법한 서비스 로직에다만 적용하였습니다.
- 유저 서비스 로직 또한 마찬가지로 Aspect가 제대로 정의되었다면 옆에 @표시 같은 것으로 확인할 수 있습니다.
UserEntity UserParam UserResult
@Getter
@Setter
@ToString
@EqualsAndHashCode(of = "seq")
@Entity
@Table(name = "UserEntity")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity {
@Id @GeneratedValue
private Long seq;
private String id;
private String name;
private String password;
}
@Getter
@Setter
@ToString
@Builder
public class UserParam {
private Long seq;
@NotEmpty
private String id;
@NotEmpty
private String password;
@NotEmpty
private String name;
}
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserResult {
private Long seq;
private String id;
private String password;
private String name;
}
테스트코드
- UserControllerTests.java
package co.worker.board.user;
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
@AutoConfigureMockMvc
public class UserControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
UserRepository userRepository;
@Autowired
ObjectMapper objectMapper;
@Before
public void insert(){
for(int i =1; i<=10; i++){
UserEntity entity = UserEntity.builder().password("비밀번호"+i)
.name("이름"+i)
.id("아이디"+i).build();
userRepository.save(entity);
}
}
@Test
public void add() throws Exception {
UserParam param = UserParam.builder().id("아이디추가").name("이름추가")
.password("비밀번호추가").build();
mockMvc.perform(post("/api/users/add")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void edit() throws Exception{
UserParam param = UserParam.builder().id("아이디수정")
.name("이름수정")
.password("비밀번호수정").build();
mockMvc.perform(put("/api/users/edit/3")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void getUserOne() throws Exception {
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void getUserAll() throws Exception{
mockMvc.perform(get("/api/users/all")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void deleteUser() throws Exception{
mockMvc.perform(delete("/api/users/4")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
this.getUserAll();
}
}
- UserControllerTest도 마찬가지로 Board때와 비슷하게 작성해줍니다.
- 예외의 상황이 Board때와 매우 유사하여 Bad_Request 테스트는 진행하지 않았습니다. Board를 참조하여 스스로 해보시면 좋을 듯 합니다.
- @AutoConfigureMockMvc 어노테이션을 활용한다면 mockmvc객체를 직접 설정하지 않고 스프링부트에서 자동설정을 통해 @Autowired 어노테이션으로 주입이 가능하게 테스트가 됩니다. 쉽게쉽게 코딩합시다.
유저를 적용한 Board
BoardEntity.java
@Getter
@Setter
@ToString
@Entity
@Table(name = "BoardEntity")
@Builder
@NoArgsConstructor
public class BoardEntity {
@Id
@GeneratedValue
Long seq;
String content;
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "UserEntity_seq")
UserEntity userEntity;
String title;
LocalDateTime savedTime;
public BoardEntity(Long seq, String content, UserEntity userEntity, String title, LocalDateTime savedTime){
this.seq = seq;
this.content = content;
this.userEntity = userEntity;
this.title = title;
this.savedTime = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
}
}
- 유저를 보드에 적용하기 위해 다음과 같이 UserEntity를 BoardEntity의 멤버변수로 선언해줍니다.
- UserEntity도 마찬가지로 JPA에서 관리하는 Entity 객체이기 때문에 Board와의 관계가 존재하게됩니다. 이를 BoardEntity에서 관리해야하므로 @ManyToOne 어노테이션과 @JoinColumn 어노테이션으로 설정해줍니다.
* ManyToOne : 다대일 관계로 엔티티 관계를 설정하며 하나의 유저가 여러 게시판 글을 쓸 수 있다.
* JoinColumn : 조인할 컬럼으로 userEntity의 seq 컬럼을 꼽은 것이다. 흔히 시퀀스로 외래키를 설정하기 때문에 그와 유사하다고 생각하면 될 것 같습니다.
- LocalDateTime으로 등록된 시간(savedTime)을 추가하였습니다. 글등록시간 정도는 필요할 것 같았습니다. default는 현재 서버의 시간을 받아오지만 딱 정해주기 위해서 타임존을 대한민국 서울 표준시(KST, "Asia/Seoul"로 설정하였습니다.
게시판 테스트 코드
- BoardControllerTest.java
package co.worker.board.board;
@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class BoardControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
BoardRepository boardRepository;
@Autowired
ObjectMapper objectMapper;
@Before
public void insertBoard(){
for(int i =1; i<=10; i++){
UserEntity user = UserEntity.builder().id("doqndnf"+i).name("유저"+i).password("tjdghks"+i+"!").build();
BoardEntity boardEntity = BoardEntity.builder().content("내용"+i).title("제목"+i).userEntity(user).build();
boardRepository.save(boardEntity);
}
}
@Test
public void addBoard() throws Exception {
UserEntity user = UserEntity.builder().id("addId").name("유저추가").password("비밀번호").build();
BoardParam param = BoardParam.builder()
.content("추가내용")
.title("추가제목")
.user(user)
.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{
UserEntity user = UserEntity.builder().id("addId").name("유저추가").password("비밀번호").build();
BoardParam param = BoardParam.builder()
.content("수정내용")
.title("수정제목")
.user(user)
.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();
}
//Bad_Request 테스트
@Test //add
public void board_BadRequest_add() throws Exception{
//BoardParam param = BoardParam.builder().content("test").title("test").build(); //username not null
UserEntity user = UserEntity.builder().id("addId").name("유저추가").password("비밀번호").build();
BoardParam param = BoardParam.builder().title("title").content("").user(user).build(); // content not empty
mockMvc.perform(post("/api/boards/add")
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isBadRequest());
}
@Test
public void board_BadRequest_getOne() throws Exception{ // 문제 수정해야됨.
// 400이 나와야하는데 200이 나옴. -> ok메소드를 실행해서 그럼 -> 오류 -> 400이 나오도록 수정.
mockMvc.perform(get("/api/boards/-1")
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isBadRequest());
}
@Test
public void board_BadRequest_edit() throws Exception {
BoardParam param = BoardParam.builder().content("test").title("test").build(); // username null
//BoardParam param = BoardParam.builder().content("test").title("test").user(user).build(); // seq min 0
//UserEntity user = UserEntity.builder().id("addId").name("유저추가").password("비밀번호").build();
mockMvc.perform(put("/api/boards/0")
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(param)))
.andDo(print())
.andExpect(status().isBadRequest());
}
}
- 테스트 코드에서 변경되는 부분이 있다면 username을 String으로 직접 하드코딩하는 부분에서 UserEntity 객체를 Build해서 BoardEntity에다가 build하는 방식으로 수정하였습니다.
테스트 빌드 결과 : mvn clean verify
- 모든 @Test가 통과된 경우에만 빌드가 허용됩니다.
Jacoco 라이브러리를 이용한 프로젝트 코드 커버리지 확인.
- target/site/jacoco 에 존재하는 index.html 파일을 브라우저로 실행해줍니다.
- 코드 커버리지를 확인하여 실행하지 않는 불필요한 로직을 제거하고 불필요하게 작성된 코드들을 정리할 수 있습니다.
- 패키지를 누르면 클래스들이 나오고 클래스를 클릭하게 되면 다음과같이 소스가 나타납니다.
- 노란부위는 실행은 했으나 null 부분이 한번도 결정된 적이 없기 때문에 나타나는 것.
- 이를 확인하고자 한다면 Test코드에 null을 리턴하는 코드를 작성하여 실행하게 만들어주면 됩니다.
- 이런식으로 빨간색, 노란색을 줄이고 초록색 위주의 코드 커버리지를 확인하여 미처 테스트하지 못한 로직을 확인할 수 있게 됩니다.