1.  컨트롤러 계층 구현

컨트롤러 영역에서는 Swagger UI를 이용해서 테스트와 함께 필요한 기능을 개발
ReplyController는 ReplyService를 주입 받도록 설계

@RestController
@RequestMapping("/api/replies")
@Log4j2
@RequiredArgsConstructor
public class ReplyController {
    private final ReplyService replyService;

 


1)  등록 기능 확인

  👩🏻‍🚀  ReplyController의 등록 기능은 이미 개발된 코드에 JSON 처리를 위해서 추가 코드가 필요

@Operation(summary = "Replies Post", description = "POST 방식으로 댓글 등록")
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> register(@Valid @RequestBody ReplyDTO replyDTO,
    BindingResult bindingResult) throws BindException {
    
    log.info(replyDTO);

    if (bindingResult.hasErrors()) {
        throw new BindException(bindingResult);
    }

    Map<String, Long> map = new HashMap<>();
    Long rno = replyService.register(replyDTO);
    map.put("rno", rno);

    return map;
}

 

  ✓  프로젝트를 실행하고 Swagger UI를 통해서 테스트를 진행. 등록 작업에서 주의할 점은 bno가 존재하는 게시물 번호여야 함

  ✓  정상적으로 동작하면 {"rno": 6}과 같은 결과가 전송되는 것을 확인

 

에러에 대한 처리

 

  📍  @Valid는 이미 처리를 했지만 연관 관계를 가진 엔티티를 처리할 때마다 항상 문제가 되는 것은 연관된 객체의 안전성을 확보하는 것

        ➡️  예를 들어 앞선 테스트에서 bno 값을 사용할 수 없는 번호로 작성하면  다음과 같은 문제가 발생

[(conn=1337) Cannot add or update a child row: a foreign key constraint fails (`boot_ex_app_01_2405`.`reply`, CONSTRAINT `FKr1bmblqir7dalmh47ngwo7mcs` FOREIGN KEY (`board_bno`) REFERENCES `board` (`bno`))]

 

  ⚡️  서버에 기록된 로그를 보면 SQL Exception이긴 하지만, org.springframework.dao.DataIntegrityViolationException 예외가 발생. 예외가 발생한다는 것은 분명 정상적인 결과이지만 서버의 상태 코드는 500으로 '서버 내부의 오류'로 처리

  ⚡️  외부에서 Ajax로 댓글 등록 기능을 호출했을 때 500 에러가 발생한다면 호출한 측에서는 현재 서버의 문제라고 생각할 것

         ➡️  클라이언트에 서버의 문제가 아니라 데이터의 문제가 있다고 전송하기 위해서는 @RestControllerAdvice를 이용하는 CustomRestAdvice에 DataIntegrityViolationException를 만들어서 사용자에게 예외 메시지를 전송하도록 구성

 

@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleFKException(Exception ex) {
    log.error(ex);

    Map<String, String> errorsMap = new HashMap<>();
    errorsMap.put("time", "" + System.currentTimeMillis());
    errorsMap.put("msg", "constraint fails");

    return ResponseEntity.badRequest().body(errorsMap);
}

 

  ✓  추가한 handlerFKException()는 DataIntegrityViolationException이 발생하면 "constraint fails" 메시지를 클라이언트로 전송

 


2)  특정 게시물의 댓글 목록

  👩🏻‍🚀  특정한 게시물의 댓글 목록 처리는 '/api/replies/list/{bno}' 경로를 이용하도록 구성. 이때 bno는 게시물의 번호를 의미.
  👩🏻‍🚀  스프링에서는 @PathVariable이라는 어노테이션을 이용해서 호출하는 경로의 값을 직접 파라미터의 변수로 처리할 수 있는 방법을 제공

ReplyController에 메서드 추가
@Operation(summary = "Replies of Board", description = "GET 방식으로 특정 게시물의 댓글 목록")
@GetMapping(value = "/list/{bno}")
public PageResponseDTO<ReplyDTO> getList(@PathVariable("bno") Long bno, PageRequestDTO pageRequestDTO) {
    PageResponseDTO<ReplyDTO> responseDTO = replyService.getListOfBoard(bno, pageRequestDTO);
    return responseDTO;
}

 

   ✓  getList()에서 bno 값은 경로에 있는 값을 취해서 사용할 것이므로 @PathVariable을 이용하고, 페이지와 관련된 정보는 일반 쿼리 스트링을 이용
  ✓  Swagger UI로 ReplyController를 호출해 보면 PageResponseDTO가 JSON으로 처리된 결과를 볼 수 있음

 


3)  특정 댓글 조회

  👩🏻‍🚀  특정한 댓글을 조회할 때는 Reply의 rno를 경로로 이용해서 GET 방식으로 처리

ReplyController에 getReplyDTO() 메서드를 추가
@Operation(summary = "Read Reply", description = "GET 방식으로 특정 댓글 조회")
@GetMapping(value = "/{rno}")
public ReplyDTO getReplyDTO(@PathVariable("rno") Long rno) {
    ReplyDTO replyDTO = replyService.read(rno);
    return replyDTO;
}

 

  ✓ 정상적인 rno 값이 전달되면 ReplyDTO가 JSON으로 처리

 

데이터가 존재하지 않는 경우의 처리

 

  📍  getReplyDTO()와 같이 특정한 번호를 이용해서 조회할 때 문제가 되는 부분은 해당 데이터가 존재하지 않는 경우
  📍  'NoSuchElementException' 예외 전송을 위해 CustomRestAdvice에 기능을 추가

@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleNoSuchElementException(Exception ex) {
    log.error(ex);

    Map<String, String> errorsMap = new HashMap<>();
    errorsMap.put("time", "" + System.currentTimeMillis());
    errorsMap.put("msg", "No Such Element Exception");

    return ResponseEntity.badRequest().body(errorsMap);
}


4)  특정 댓글 삭제

  👩🏻‍🚀  일반적으로 REST 방식에서 삭제 작업은 GET / POST 가 아닌 DELETE 방식을 이용해서 처리

ReplyController에 remove() 메서드를 추가
@Operation(summary = "Delete Reply", description = "DELETE 방식으로 특정 댓글 삭제")
@DeleteMapping(value = "/{rno}")
public Map<String, Long> remove(@PathVariable("rno") Long rno) {
    replyService.remove(rno);
    Map<String, Long> map = new HashMap<>();
    map.put("rno", rno);
    return map;
}

 

존재하지 않는 번호의 삭제 예외
@ExceptionHandler({NoSuchElementException.class, EmptyResultDataAccessException.class})
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handlerNoSuchElementException(Exception e) {

    log.error(e);

    Map<String, String> errorMap = new HashMap<>();

    errorMap.put("time", "" + System.currentTimeMillis());
    errorMap.put("msg", "No Such Element Exception");

    return ResponseEntity.badRequest().body(errorMap);
}

 


5)  특정 댓글 수정

  👩🏻‍🚀  댓글 수정은 PUT방식으로 처리

ReplyController에 modify() 메서드를 추가

 

  📍  주의할 점은 수정할 때도 등록과 마찬가지로 JSON 문자열이 전송되므로 이를 처리하도록 @RequestBody를 적용한다는 점

@Operation(summary = "Modify Reply", description = "PUT 방식으로 특정 댓글 수정")
@PutMapping(value = "/{rno}", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> modify(@PathVariable("rno") Long rno, @RequestBody ReplyDTO replyDTO) {

    replyDTO.setRno(rno);
    replyService.modify(replyDTO);
    
    Map<String, Long> map = new HashMap<>();
    map.put("rno", rno);
    
    return map;
}

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  댓글 서비스 계층의 구현

댓글의 엔티티 처리가 끝났다면 서비스 계층을 구현
Service 패키지에 ReplyService 인터페이스와 ReplyServiceImpl 클래스를 추가

 

1)  댓글 등록 처리

  👩🏻‍💻  댓글 등록은 게시물과 비슷하게 구현. ReplyService 인터페이스에 메서드를 정의

public interface ReplyService {
    Long register(ReplyDTO replyDTO);
}

 

  👩🏻‍💻  ReplyServiceImpl은 ReplyRepositoy와 ModelMapper를 주입받아서 구현

@Service
@RequiredArgsConstructor
@Log4j2
public class ReplyServiceImpl implements ReplyService {
    private final ReplyRepository replyRepository;
    private final ModelMapper modelMapper;

    @Override
    public Long register(ReplyDTO replyDTO) {
        Reply reply = modelMapper.map(replyDTO, Reply.class);
        Long rno = replyRepository.save(reply).getRno();
        return rno;
    }
}

 

 

댓글 등록 테스트


  📍  test 폴더의 service 패키지에 ReplyServiceTests 클래스를 추가해서 ReplyService 기능들의 동작에 문제가 없는지 확인

@SpringBootTest
@Log4j2
class ReplyServiceImplTest {

    @Autowired
    private ReplyService replyService;

    @Test
    public void registerReply() {
        log.info(replyService.getClass().getName());

        ReplyDTO replyDTO = ReplyDTO.builder()
                .bno(90L)
                .replyText("test...")
                .replyWriter("test...")
                .build();

       replyService.register(replyDTO);
    }
}

 

  ✓  실행 결과에 정상적으로 SQL이 동작하고 새로운 댓글이 insert되는지 확인. 실행 로그에는 새로운 댓글의 rno 값이 출력

 

 


2)  댓글 조회 / 수정 / 삭제 / 목록

  👩🏻‍💻  댓글을 수정하는 경우에는 Reply 객체에서 replyText만을 수정할 수 있으므로 Reply를 수정

public class Reply extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;

    private String replyText;
    private String replyWriter;

    public void changeText(String text) {
        this.replyText = text;
    }
}

 

  👩🏻‍💻  ReplyService 인터페이스에 CRUD 기능들을 선언

public interface ReplyService {

    Long register(ReplyDTO replyDTO);

    ReplyDTO read(Long rno);

    void modify(ReplyDTO replyDTO);

    void remove(Long rno);
}

 

  👩🏻‍💻  ReplyServiceImpl에서 register()를 제외한 기능들은 다음과 같이 구현

    @Override
    public ReplyDTO read(Long rno) {
        Optional<Reply> replyOptional = replyRepository.findById(rno);
        Reply reply = replyOptional.orElseThrow();
        return modelMapper.map(reply, ReplyDTO.class);
    }

    @Override
    public void modify(ReplyDTO replyDTO) {
        Optional<Reply> replyOptional = replyRepository.findById(replyDTO.getRno());
        Reply reply = replyOptional.orElseThrow();
        reply.changeText(replyDTO.getReplyText()); // 댓글의 내용만 수정 가능
        replyRepository.save(reply);
    }

    @Override
    public void remove(Long rno) {
        replyRepository.deleteById(rno);
    }

 


3)  특정 게시물의 댓글 목록 처리

  👩🏻‍💻  댓글 서비스의 가장 중요한 기능은 특정한 게시물의 댓글 목록을 페이징 처리한 것

ReplyService에 getListOfBoard()를 추가
PageResponseDTO<ReplyDTO> getListOfBoard(Long bno, PageRequestDTO pageRequestDTO);

 

  👩🏻‍💻  ReplyServiceImpl에서는 PageRequestDTO를 이용해서 페이지 관련 정보를 처리하고 ReplyRepository를 통해서 특정 게시물에 속하는 Page<Reply>를 구함. 실제 반환되어야 하는 타입은 Reply가 아니라 ReplyDTO 타입이므로 ReplyServiceImpl에서는 이를 변환하는 작업이 필요

@Override
public PageResponseDTO<ReplyDTO> getListOfBoard(Long bno, PageRequestDTO pageRequestDTO) {
    Pageable pageable = PageRequest.of(pageRequestDTO.getPage() <= 0 ? 0 : pageRequestDTO.getPage() - 1,
        pageRequestDTO.getSize(), Sort.by("rno").ascending());
    Page<Reply> replies = replyRepository.listOfBoard(bno, pageable);
    List<ReplyDTO> replyDTOList = replies.getContent().stream().map(reply ->
        modelMapper.map(reply, ReplyDTO.class)).collect(Collectors.toList());
        return PageResponseDTO.<ReplyDTO>withAll()
            .pageRequestDTO(pageRequestDTO)
            .dtoList(replyDTOList)
            .total((int)replies.getTotalElements())
            .build();
}

 

 

 

 

[ 내용 참고 : IT 학원 강의 ] 


1.  REST 방식의 댓글 처리 준비

📌  REST 방식의 댓글 처리는 다음과 같은 단계로 진행

  • URL 설계와 데이터 포맷 결정
  • 컨트롤러의 JSON / XML 처리
  • 동작 확인
  • 자바스크립트를 통한 화면 처리


1)  URL 설계와 DTO 설계


  ⚡️  REST 방식은 주로 XML이나 JSON 형태의 문자열을 전송하고 이를 컨트롤러에서 처리하는 방식을 이용
  ⚡️  JSON을 이용해서 DTO에 맞는 데이터를 전송하고 스프링을 이용해서 이를 DTO 처리하도록 구성

 

댓글의 URL 설계
Method URL 설명 반환 데이터
POST /replies 특정한 게시물의 댓글 추가 {'rno':11} - 생성된 댓글의 번호
GET /replies/list/:bno 특정 게시물(bno)의 댓글 목록 '?'뒤에 페이지 번호를 추가해서 댓글 페이징 처리 PageResponseDTO를 JSON으로 처리
PUT /replies/:rno 특정한 번호의 댓글 수정 {'rno':11} - 수정된 댓글의 번호
DELETE /replies/:rno 특정한 번호의 댓글 삭제 {'rno':11} - 삭제된 댓글의 번호
GET /replies/:rno 특정한 번호의 댓글 조회 댓글 객체를 JSON으로 변환한 문자열

 

프로젝트 dto 패키지에 ReplyDTO를 추가
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
    private Long rno; // 리플 고유번호
    private Long bno; // 게시판의 글 고유번호
    private String replyText; // 리플 내용
    private String replyWriter; // 리플 작성자
    private LocalDateTime regDate, modDate;
}

 

  ✓  ReplyDTO에는 고유한 rno외에도 특정한 게시물 번호 bno를 선언
  ✓  이를 통해서 현재 댓글이 특정한 게시물의 댓글임을 알 수 있음

 


 

2)  ReplyController 준비

 

  ⚡️  ReplyController는 기존과 달리 @RestController라는 어노테이션을 활용
  ⚡️ @RestController를 이용하게 되면 메서드의 모든 리턴 값은 JSP나 Thymeleaf로 전송되는 것이 아니라 바로 JSON이나 XML 등으로 처리
  ⚡️  본격적인 개발 전에 약간의 테스트를 진행 할 수 있도록 ReplyController에 POST 방식을 처리하는 메서드를 추가

@RestController
@RequestMapping("/api/replies")
@Log4j2
public class ReplyController {
    @Operation(summary = "Replies Post", description = "POST 방식으로 댓글 등록")
    @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, Long> register(@RequestBody ReplyDTO replyDTO) {
        log.info(replyDTO);
        Map<String, Long> map = new HashMap<>();
        map.put("rno", 111L);

        return map;
    }
}

 

  📍 register() 메서드의 파라미터에는 ReplyDTO를 이용해서 파라미터를 수집한다고 선언되어 있지만 앞에 @RequestBody라는 어노테이션이 존재. @RequestBody는 JSON 문자열을 ReplyDTO로 변환하기 위해 표시

  📍  @PostMapping에는 consumes 라는 속성을 사용. consumes는 해당 메서드를 받아서 소비(Consume)하는 데이터가 어떤 종류인지 명시할 수 있음. 이 경우 JSON 타입의 데이터를 처리하는 메서드임을 명시

  📍  Swagger UI에서는 필요한 JSON 데이터를 미리 구성해서 사용할 수 있도록 작성할 수 있음

  • Try it out 클릭 후 필요한 데이터만 입력해서 전송
  • register() 가 실행되고 리턴 값이 JSON으로 처리됨. @RestController는 리턴 값 자체가 JSON으로 처리
  • 서버에는 정상적으로 필요한 파라미터가 수집된 것을 확인

 

postman / Swagger 비교
  • boot 프로젝트의 경우 postman에 비해 Swagger가 작성한 애플리케이션의 특성에 맞게 테스트 할 수 있고, API 문서를 자동화 할 수 있는 장점이 존재
  • 프로젝트안에 Swagger가 들어가는 것을 기피하는 경우도 있음. 예를 들어 boot3 버전 부터는 Swagger 설정이 달라져서 프로젝트를 수정해야 함. 프로젝트와 독립된 방식으로 사용할 수 있는 postman을 선호하기도 함

 

3) @Valid와 @RestControllerAdvice

 

  ⚡️  REST 방식의 컨트롤러는 대부분 Ajax와 같이 눈에 보이지 않는 방식으로 서버를 호출하고 결과를 전송하므로 에러가 발생하면 어디에서 어떤 에러가 발생했는지 알아보기 힘든 경우가 많음 * Ajax 개발의 힘든 점 중 하나가 디버깅
  ⚡️ 이런 이유로 @Vaild 과정에서 문제가 발생하면 처리할 수 있도록 @RestControllerAdvice를 작성

 

controller 패키지에 advice 패키지를 추가하고 CustomRestAdvice 클래스를 추가
@Log4j2
@RestControllerAdvice
public class CustomRestAdvice {
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
    public ResponseEntity<Map<String, String>> handleBindException(BindException e) {
        log.error(ex);

        Map<String, String> errorsMap = new HashMap<>();

        if (e.hasErrors()) {
            BindingResult bindingResult = e.getBindingResult();

            bindingResult.getFieldErrors().forEach(fieldError -> {
                errorsMap.put(fieldError.getField(), fieldError.getCode());
            });
        }
        return ResponseEntity.badRequest().body(errorsMap);
    }
}

 

  ✓  @RestControllerAdvice을 이용하면 컨트롤러에서 발생하는 예외에 대해 JSON과 같은 응답 메시지를 생성해서 보낼 수 있음
  ✓  작성한 handleBindException() 메서드는 컨트롤러에서 BindException이 던져지는 경우 이를 이용해서 JSON 메시지와 400에러 (Bad Request)를 전송

 


4)  댓글 등록 @Valid


  ⚡️  @RestControllerAdvice를 이용해서 ReplyDTO를 검증해서 register()를 처리하도록 변경

 

ReplyDTO에 검증과 관련된 어노테이션을 추가
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ReplyDTO {
    private Long rno; // 리플 고유번호

    @NotNull
    private Long bno; // 게시판의 글 고유번호
    
    @NotEmpty
    private String replyText; // 리플 내용

    @NotEmpty
    private String replyWriter; // 리플 작성자
    private LocalDateTime regDate, modDate;
}

 

실제 동작 여부를 확인하기 위해 ReplyController의 register()를 수정
public class ReplyController {

    @Operation(summary = "Replies Post", description = "POST 방식으로 댓글 등록")
    @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Map<String, Long> register(@Valid @RequestBody ReplyDTO replyDTO,
            BindingResult bindingResult) throws BindException {

            log.info(replyDTO);

            if (bindingResult.hasErrors()) {
                throw new BindException(bindingResult);
            }

            Map<String, Long> map = new HashMap<>();
            map.put("rno", 111L);

            return map;
    }
}

 

  📍 register()는 다음과 같은 점들이 수정

  • ReplyDTO를 수집할 때 @Valid를 적용
  • BindingResult를 파라미터로 추가하고 문제가 있을 때는 BindException을 throw하도록 수정
  • 메서드 선언부에 BindException을 throws하도록 수정
  • 메서드 리턴값에 문제가 있다면 @RestControllerAdvice가 처리할 것이므로 정상적인 결과만 리턴

 

프로젝트를 실행하고 ReplyDTO가 체크할 내용이 없는 데이터를 Swagger UI로 전송 테스트

 

  ⚡️  ReplyDTO는 bno나 replyText 등의 값이 반드시 있어야하므로 실행 결과는 다음과 같이 문제가 있는 부분만 에러 메시지를 전송받을 수 있음

 

 

✓  결과 메시지를 보면 @Valid 과정에 문제가 있는 필드들과 메시지를 JSON 문자열로 전송
✓Ajax를 이용하는 개발에는 에러의 발생 소지가 서버인 경우도 있고, 브라우저나 자바 스크립트일 때도 있기 때문에 서버에서 먼저 확실하게 문제가 없는 것을 확인하고 화면을 개발하는 것이 좋음

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  AJax와 JASON : 댓글 기능 구현 

기존 작업한 게시판에 댓글 기능을 구현
클라이언트는 Ajax(Axios 라이브러리) · 서버는 RESTful · JPA는 다대일 방식으로 구현


(1)  Ajax와 REST 방식의 이해

 

1)  Ajax Asynchronous JavaScript And XML


  💫  Ajax 방식은 브라우저에서 서버를 호출하지만 모든 작업이 브라우저 내부에서 이루어지기 때문에 현재 브라우저의 브라우저 화면 변화 없이 서버와 통신할 수 있음

 

    ✓  실제 구현은 자바 스크립트를 이용해서 XML을 주고 받는 방식을 이용
    ✓  최근에는 JSON을 이용하는 방식을 더 선호. 스프링 부트는 Springweb을 추가하면 자동으로 관련 라이브러리를 받음

  💫  클라이언트 중심의 개발

    ✓  Ajax가 가져온 변화는 모바일에서도 Ajax 방식으로 데이터를 교환할 수 있기 때문에 활용이 커짐
    ✓  모바일에서도 일반 웹과 마찬가지로 서버의 데이터가 필요한데 이때 화면과 관련된 부분은 필요하지 않기 때문에 서버에서 순수한 데이터만 전송하는 방식이라면 클라이언트의 구현이 웹 / 앱에 관계없이 데이터를 재사용할 수 있음

 

 

2)  JSON 문자열

 

    📍 "서버에서 순수한 데이터를 보내고 클라이언트가 적극적으로 이를 처리한다"라는 개발 방식에서 핵심은 '문자열'

  • '문자열'은 어떠한 프로그래밍 언어나 기술에 종속되지 X
  • 문자열을 이용하면 데이터를 주고 받는 것에 신경 써야 하는 일은 없지만, 문자열로 복잡한 구조의 데이터를 표현하는데 문제가 발생
  • 문자열로 복잡한 데이터를 표현하기 위해서 고려되는 것이 XML과 JSON 이라는 형태.
  • JSON은 자바스크립트 문법에 맞는 문자열로 데이터를 표현하기 때문에 클라이언트에서 어떤 기술을 이용하든 공통적으로 인식할 수 있음
  • 스프링 부트 역시 JSON 관련 라이브러리 jackson-databind가 이미 포함되어 있으므로 별도의 설정 없이 바로 JSON 데이터를 만들어 낼 수 있음

 

3)  REST 방식

 

  💫  html이 아니라 json or xml 형식이 response에 사용

    ✓  REST 방식은 클라이언트 프로그램인 브라우저나 앱이 서버와 데이터를 어떻게주고 받는 것이 좋을지에 대한 가이드
    ✓  예전의 웹 개발 방식에서는 특정한 URL이 원하는 '행위나 작업'을 의미하고, GET / POST 등은 데이터를 전송하는 위치를 의미
      Ajax를 이용하면 브라우저의 주소가 이동할 필요 없이 서버와 데이터를 교환할수 있기 때문에 URL은 '행위나 작업'이 아닌 '원하는 대상' 그 자체를 의미하고, GET / POST 방식과 PUT / DELETE 등의 추가적인 전송방식을 활용해서 '행위나 작업'을 의미하게 됨

 

이전 표현 REST 방식 표현
/board/modify  ▶️  게시물의 수정 (행위 / 목적)
<form>  ▶️  데이터의 묶음
 /board/123  ▶️  게시물 지원 자체
PUT 방식  ▶️  행위나 목적



4)  REST 방식의 URL 설계

Method URI 의미 추가 데이터
GET /board/123 123번 게시물 조회  
POST /board/ 새로운 게시물 등록 신규 데이터 게시물
PUT /board/123 123번 게시물 수정 수정에 필요한 데이터
DELETE /board/123 123번 게시물 삭제  

 

 

5) springdoc-openapi 준비

 

  ⚡️  REST 방식의 테스트는 특별한 화면을 구성하는 것이 아니라 데이터를 전송하고 결과를 확인하는 방법
          ➡️  예를 들어 브라우저는 GET 방식의 데이터를 확인할 때는 유용하지만 POST 방식으로 데이터를 처리할 때는 상당히 불편하고, 사용자도 측정한 경로를 어떻게 호출하는지 알 수 없으므로 상세한 정보를 전달하기 어려운 단점이 있음
  ⚡️  REST 방식을 이용할 때는 전문적으로 API를 테스트 할 수 있는 Postman이나 springdoc-openapi 등을 이용

 

Swagger UI
  • 부트 2까지는 Swagger UI를 사용. 부트 3부터는 Swagger UI사용이 안되고 springdoc-openapi를 지원
  • openapi는 개발할 때 어노테이션 설정으로 API 문서와 테스트 할 수 있는 화면을 생성할 수 있으므로 개발자는 한번에 테스트 환경까지 구성할 수 있다는 장점이 있음

(2)  프로젝트의 준비

1)  modelMapper 설정 변경

  • RootConfig의 modelMapper 설정을 변경. STRICT ▶️ LOOSE로 변경
@Configuration
public class RootConfig {
    @Bean
    public ModelMapper getMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
            .setFieldMatchingEnabled(true)
            .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
            .setMatchingStrategy(MatchingStrategies.LOOSE);
       return modelMapper;
    }
}

 

 

2) bulid.gradle 설정 추가

  • 프로젝트의 bulid.gradle 파일에 'OpenAPI Starter WebMVC UI'으로 검색한 라이브러리를 추가
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

 

  • 서버 실행을 하고 아래 주소로 접근
    http://localhost:8080/swagger-ui/index.html

 

3) Config 클래스 추가

  • 프로젝트의 config 패키지에 SpringdocOpenapiConfig 클래스를 추가
  • 일반 컨트롤러와 REST 컨트롤러를 분리해서 설정
  • 경로에 /api가 포함된 컨트롤러의 경우에는 REST API로 인식
  • 경로에 /api가 포함안된 컨트롤러의 경우에는 COMMON API로 인식
@Configuration
public class SpringdocOpenapiConfig {

    @Bean
    public GroupedOpenApi restApi() {
        return GroupedOpenApi.builder()
                .pathsToMatch("/api/**")
                .group("REST API")
                .build();
    }

    @Bean
    public GroupedOpenApi commonApi() {
        return GroupedOpenApi.builder()
                .pathsToMatch("/**/*")
                .pathsToExclude("/api/**/*")
                .group("COMMON API")
                .build();
    }
}

 

SampleJasonController 경로 수정
@Log4j2
@RestController
public class SampleJSONController {

    @GetMapping("/api/helloArr")
    public String[] helloArr() {
        log.info("HelloArr...");

        return new String[] {"AAA", "BBB", "CCC"};
    }
}

SampleController에 @Operation 추가
@Operation(summary = "hello")
@GetMapping("/hello")
public void hello(Model model) {
    log.info("hello...");
    model.addAttribute("msg", "Hello World");
}

@Operation(summary = "ex/ex1")
@GetMapping("/ex/ex1")
public void ex1(Model model) {
    List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

    model.addAttribute("list", list);
}


 

  ⚡️  현재 설정은 기본적으로 가장 많이 사용하는 패키지를 기반으로 패키지의 모든 클래스에 대해서 API 테스트 환경을 만들어 냄

 

 

 

 ✓  화면의 board-controller를 선택하면 다음과 같이 메서드를 선택할 수 있는 화면이 나옴

✓  특정한 메서드를 선택하고 'Try it out' 버튼을 클릭하면 파라미터를 입력할 수 있음

✓  파라미터를 입력하고 'Execute'를 클릭하면 아래에서 결과 화면을 볼수 있음

 

 

 

 

 

 

 

정적 파일 경로 문제


  ✓  Swagger UI 설정이 끝나면 기존에 동작하던 /board/list/ 에는 문제가 생길 수 있음

  ✓  이 문제를 해결하려면 Spring Web 관련 설정을 추가해 주어야만 함
  ✓  config 폴더에 CustomServletConfig 클래스를 추가

 

  • 클래스에 @EnableWebMvc 어노테이션을 추가하는 것이 가장 중요
  • Swagger UI 가 적용되면서 정적 파일의 경로가 달라졌기 때문인데 이를 CustomServletConfig로 WebMvcConfigurer 인터페이스를 구현하도록하고 addResourceHandlers를 재정의해 수정
@Configuration
@EnableWebMvc
public class CustomServletConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
        registry.addResourceHandler("/fonts/**").addResourceLocations("classpath:/static/fonts/");
        registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
        registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/static/assets/");
    }
}

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts