[Spring Boot] REST API 게시판 서버 만들기 #5(User쪽 적용, 내가 TDD 활용하는 방법.)

프로젝트 구조

- 유저쪽 패키지를 생성합니다.

 

- 컨트롤러와 서비스 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을 리턴하는 코드를 작성하여 실행하게 만들어주면 됩니다.

- 이런식으로 빨간색, 노란색을 줄이고 초록색 위주의 코드 커버리지를 확인하여 미처 테스트하지 못한 로직을 확인할 수 있게 됩니다.

댓글

Designed by JB FACTORY