Skip to content

Latest commit

 

History

History
608 lines (367 loc) · 22.2 KB

File metadata and controls

608 lines (367 loc) · 22.2 KB

Part 4. REST 방식과 Ajax를 이용하는 댓글 처리

16. REST 방식으로 전환

  • 모바일 시대가 되면서 서버의 역활이 변화됨

    • 기존 브라우저 대상을 위해 HTML형태로 전달을 해왔다면, 이제는 순수한 데이터를 전달하는 형태로 변화됨 (API 서버)
  • URI (Uniform Resource Identifier)

    • 이전에는 페이지를 이동하더라도 브라우저의 주소는 변화하지 않는 방식을 선호했음
    • 현재의 대부분은 페이지를 이동하면 브라우저의 주소도 같이 이동하는 방식을 사용함
  • URL와 URI(자원의 식별자)의 상징적인 의미

    • URL : 이 곳에 가면 당신이 원하는 것을 찾을 수 있습니다.
    • URI: 당신이 원하는 곳의 주소는 여기입니다.
      • URI의 I는 DB의 PK와 같은 의미로 생각할 수 있음.
REST는...
  • URI는 하나의 고유한 리소스(Resource)를 대표하도록 설계된다는 개념에 전송방식을 결합해서 원하는 작업을 지정함.

  • REST 방식의 구성

    • URI + GET/POST/PUT/DELETE/...
Spring에서 제공하는 REST 관련 어노테이션
어노테이션 기능
@RestController Controller가 REST 방식을 처리위한 것임을 명시합니다.
@ResponseBody 일반적인 JSP와 같은 뷰로 전달된는 것이 아니라 데이터 자체를 전달하기 위한 용도
@PathVariable URL 경로에 있는 값을 파라미터로 추출하려고 할 때 사용
@CrossOrigin Ajax의 크로스 도메인 문제를 해결해주는 어노테이션
@RequestBody JSON 데이터를 원하는 타입으로 바인딩 처리

16.1 @RestController

  • 서버에서 존송하는 것이 순수한 데이터이므로, 모든 메서드의 리턴타입을 기존과 다르게 처리함을 명시해야 함.
    • 스프링 4부터는 @RestController를 붙일 수 있음.
    • 이전 버전에서는 클래스 또는 메서드 위에 @ResponseBody를 붙여줬었음.
16.1.1 예제 프로젝트 준비
  • ex03 프로젝트
    • jackson 을 사용한다.
    • 테스트 환경에서 gson을 사용하는 부분이 있다고 하여, 추가했다.
  • jex03 프로젝트
    • gson을 사용해보자!

16.2 @RestController의 반환타입

  • SampleController 클래스 확인 주소
    • http://localhost:8080/sample/getText
16.2.1 단순문자열 반환
  • @GetMapping에 사용된 produces 속성은 해당 메서드가 생산하는 MIME 타입을 의미함.
16.2.2 객체의 반환
  • MediaType.APPLICATION_JSON_UTF8_VALUE 를 사용할 때 Deprecated 경고

    • 현시점에서 크롬과 같은 주요 브라우저들이 charset=UTF-8 파라미터 없이 UTF-8 특수 문자들을 올바르게 해석하므로, 스프링 5.2부터 APPLICATION_JSON_VALUE를 선호한다고 함.

      A String equivalent of APPLICATION_JSON_UTF8.
      Deprecated
      as of 5.2 in favor of APPLICATION_JSON_VALUE since major browsers like Chrome now comply with the specification  and interpret correctly UTF-8 special characters without requiring a charset=UTF-8 parameter.
      
      • 괜히 Deprecated 경고 나오니, APPLICATION_JSON_VALUE 으로 사용하자!
  • getSample 테스트주소

    • 브라우저에서 다음과 요청할 떄 http://localhost:8080/sample/getSample

      • XML로 결과를 받음
      • http://localhost:8080/sample/getSample.xml 도 동일하게 xml로 결과를 받음
    • 그런데 끝에 json 확장자를 붙이면 json 결과를 받음

      • http://localhost:8080/sample/getSample.json
16.2.3 컬렉션 타입의 객체 반환
  • List

    • SampleController의 getList() 메서드 참조
  • Map

    • SampleController의 getMap() 메서드 참조

      • json

        {"First":{"mno":111,"firstName":"그루트","lastName":"주니어"}}
      • xml

        <Map>
          <First>
            <mno>111</mno>
            <firstName>그루트</firstName>
            <lastName>주니어</lastName>
          </First>
        </Map>
16.2.4 ResponseEntity 타입
  • 데이터를 요청한 쪽에서 서버로 부터 받은 메시지가 정상적인 데이터인지 알 수 있는 수단이 필요함

  • ResponseEntity는 데이터와 함께 HTTP 헤더의 상태 메시지등을 같이 전달하는 용도로 사용함.

  • SampleController 의 check() 메서드 참조

  • 기타 내용

    • String.valueOf() 입력으로 null을 주면 결과는 "null" 문자열을 반환함. NPE가 발생되지 않음
    • @GetMapping의 params와 관련된 내용
      • height, height 파라미터 전달이 모두 포함되지 않으면 400 상태코드로 에러 반환

16.3 @RestController에서 파라미터

  • @PathVariable: 일반 컨트롤러에서도 사용이 가능하지만, REST 방식에서 자주사용됨, URL 경로 일부를 파라미터로 사용할 때 이용
  • @RequestBody: JSON 데이터를 원하는 타입의 객체로 변환해야 하는 경우에 주로 사용.
16.3.1 @PathVariable
  • REST 방식에서는 URL 자체에 데이터를 식별할 수 있는 정보들을 펴현하는 경우가 많아 다양한 방식으로 @PathVariable이 사용됨.

  • SampleController의 getPath() 메서드 참조

  • xml 결과

    • http://localhost:8080/sample/product/bags/1234
    • http://localhost:8080/sample/product/bags/1234.xml
  • json 결과

    • http://localhost:8080/sample/product/bags/1234.json
16.3.2 @RequestBody
  • @RequestBody는 전달된 요청(request)의 내용(body)를 이용해서 해당 파라미터의 타입으로 변환을 요구함.

  • 대부분의 경우 JSON 데이터를 서버에 보내서 원하는 타입의 객체로 변환하는 용도로 사용됨.

  • SampleController의 ticket() 메서드 참조

  • IntelliJ 에서 직접 POST 요청을 보내 테스트 해볼 수 있다.

    ### p368 /sample/ticket 테스트
    POST http://localhost:8080/sample/ticket.json
    Content-Type: application/json
    
    {"tno": "1", "owner": "apache", "grade": "100"}

    intellij-http-request-test

16.4 REST 방식의 테스트

16.4.1 JUnit 기반의 테스트
  • 책에서 사용한 Gson 사용처를 보니 단순하게 객체를 String으로 변환하는 것 뿐이여서, Gson의 디펜던시를 제거하고 Jackson의 ObjectMapper의 writeValueAsString() 메서드를 사용하는 방식으로 변경했다.

  • org.hamcrest.Matcher not found 메시지가 떠서, hamcrest 라이브러리도 디펜던시에 추가했다.

  • JSON Path

16.4.2 기타 도구
  • curl, 크롬 확장 프로그램중 REST Client 등등...

16.5 다양한 전송방식

  • HTTP 전송방식

    작업 전송방식
    Create POST
    Read GET
    Update PUT
    Delete DELETE
  • 회원이라는 자원을 대상으로 결합한다면 아래 예를 생각해볼 수 있다.

    작업 전송방식 URI
    등록 POST /members/new
    조회 GET /members/{id}
    수정 PUT /members/{id} + body {json 데이터 등}
    삭제 DELETE /member/{id}

17. Ajax 댓글 처리

17.1 프로젝트의 구성

REST 예제를 붙여넣었던 ex03에 그대로 진행을 먼저하고, jex03에도 mybatis-dynamic-sql 에 맞게변경해서 진행하면 되겠다.

17.2 댓글 처리를 위한 영속 영역

  • 댓글 처리를 위한 테이블 생성과 처리

    CREATE TABLE tbl_reply (
        rno         NUMBER(10,0),
        bno         NUMBER(10,0)        NOT NULL,
        reply       VARCHAR2(1000)      NOT NULL,
        replyer     VARCHAR2(50)        NOT NULL,
        replyDate   DATE                DEFAULT SYSDATE,
        updateDate  DATE                DEFAULT SYSDATE
    );
    
    CREATE SEQUENCE seq_reply;
    
    ALTER TABLE tbl_reply ADD CONSTRAINT pk_reply PRIMARY KEY (rno);
    
    ALTER TABLE tbl_reply ADD CONSTRAINT fk_reply_board
      FOREIGN KEY (bno) REFERENCES tbl_board (bno);
  • 최신 게시글 몇개를 보고 싶을 때

    SELECT * FROM tbl_board WHERE rownum < 10 ORDER BY bno DESC;

    책에 위와 같이 나왔는데, 잘못 적으신 것 같다. ORDER BY 보다 rownum < 10 이 먼저 실행되어 원하는 내용이 안나온다.

    SELECT  /*+ INDEX_DESC(tbl_board pk_board) */ * FROM tbl_board WHERE rownum < 10;
    
    -- 위 또는 아래 처럼 쿼리 해야한다.
    
    SELECT * FROM (
       SELECT * 
         FROM tbl_board
        ORDER BY bno DESC
    ) WHERE rownum < 10;
    BNO TITLE CONTENT WRITER REGDATE UPDATEDATE
    10000487 1새로 작성하는 글 새로 작성하는 내용 newbie 2021-12-30 00:52:26 2021-12-30 00:52:26
    10000486 새로 작성하는 글 select key 새로 작성하는 내용 select key newbie 2021-12-30 00:52:23 2021-12-30 00:52:23
    10000485 테스트 새글 제목 테스트 새글 내용 useer00 2021-12-30 00:52:23 2021-12-30 00:52:23
    10000484 새로 작성하는 글 새로 작성하는 내용 newbie 2021-12-30 00:51:49 2021-12-30 00:51:49
    10000483 새로 작성하는 글 새로 작성하는 내용 newbie 2021-12-30 00:51:48 2021-12-30 00:51:48
    10000482 새로 작성하는 글 select key 새로 작성하는 내용 select key newbie 2021-12-30 00:51:45 2021-12-30 00:51:45
    10000481 테스트 새글 제목 테스트 새글 내용 useer00 2021-12-30 00:51:45 2021-12-30 00:51:45
    10000470 새로 작성하는 글 새로 작성하는 내용 newbie 2021-12-27 15:02:27 2021-12-27 15:02:27
    10000469 새로 작성하는 글 새로 작성하는 내용 newbie 2021-12-27 15:02:26 2021-12-27 15:02:26
  • IntelliJ가 MyBatis로서 처리되는 Mapper 빈을 인식을 못하는 것 같다.

    // 테스트 코드의 필드로 mapper를 정의하고 @Autowired로 받을 때, 아래 오류 노출.
    // Could not autowire. No beans of 'ReplyMapper' type found. 
    @Autowired private ReplyMapper mapper;
    // 테스트 실행에는 문제가 없다.

17.3 서비스 영역과 Controller 처리

  • 리플의 insert의 리턴을 int로 받는 부분이 있어서 이후 코드를 보았을 때, 업데이트 카운트로 처리하고 있었다.

    그러면 XML매퍼에서 <update>로 해야했던 것 같은데.. 진행하면서 확인해보자!

17.3.1 ReplyController의 설계
작업 URL HTTP 전송방식
등록 /replies/new POST
조회 /replies/${rno} GET
삭제 /replies/${rno} DELETE
수정 /replies/${rno} PUT or PATCH
페이지 /replies/pages/${bno}/${page} GET
17.3.2 등록 작업과 테스트
  • 등록/수정 등의 테스트는 @SpringJUnitWebConfig, MockMvc로 테스트를 하자!

  • LocalDateTime을 JSON 스트링으로 변환할 일이 있을 때, jackson을 쓴다면 아래 라이브러리가 디펜던시되야한다.

    <!--
      Java 8 date/time 처리를 위해서는 아래 모듈 추가 후 등록해야한다.
      JacksonJSONWriter 클래스 참조
    -->
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>${jackson.version}</version>
    </dependency>
  • content-type으로 인코딩을 설정하는 것들이 Deprecated 되고 있어서, MVC 테스트를 할 때, 강제 UTF-8 필터를 미리 설정해줘야 한글이 안깨진다.

      @BeforeEach
      void setUp() {
        this.mockMvc =
            MockMvcBuilders.standaloneSetup(new ReplyController(service))
                .addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true))
                .build();
      }
  • JSON String으로 LocalDateTime 값을 서버로 부터 받을 때 날짜형식이 좀 특이한데, 다음 챕터 할 때 고려해야겠다.

    {"rno":6,"bno":10000487,"reply":"댓글 테스트 6","replyer":"replayer6","replyDate":[2021,12,31,1,46,27],"updateDate":[2021,12,31,1,46,27]}

    그냥 Date를 썼으면 타임스템프 숫자가 반환되긴할 텐데...

      @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
      private LocalDateTime replyDate;

    ReplyVO 도메인에 날짜 필드에 위와 같이 붙이고 이 기준으로 다음 챕터에서 적용해보자!

17.4 JavaScript 준비

  • 댓글을 다루는 부분을 reply.js로 따로 모듈화하여 다루는데, 내용이 좋은 것 같다. 😃

17.5 이벤트 처리와 HTML 처리

17.5.1 댓글 목록 처리

지금 부트스트랩 환경과 책과 안맞아서 모양을 맞춰서 바꾸었다.

  • chat 클래스 > list-group list-group-flush

  • font awesome 의 css나 이미지 등은 프로젝트에 포함하고 있지 않아서, cdnjs.cloudflare.com에서 호스팅하고 있는 것을 불러왔다.

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
시간에 대한 처리

내가 진행한 상태는 LocalDateTime의 결과를 yyyy-MM-dd HH:mm:ss 모양으로 JSON에서 리턴하고 있어서 책에서 요구하는 함수를 추가할 필요는 없어보이는데. 추가 기능(24시간이 지난 댓글은 yyyy/MM/dd 그 이내 댓글은 시간만 표시)이 있어 따라해봐야겠다.

Javascript의 Date의 함수를 보니 아래 처럼 할 수 있어서, 배열로 [년,월,일,시,분,초] 를 받아오는 내용을 그대로 활용할 수 있겠다.

var replyDate = new Date();
replyDate.setFullYear(timeValue[0],timeValue[1], timeValue[2]);
replyDate.setHours(timeValue[3],timeValue[4],timeValue[5],0);
  • 책은 24시기간 이내 댓글은 시:분:초를 표시하는 것으로 되어있었는데, 나는 오늘 0시 부터 댓글만 그렇게 표시하는 것으로 했다. 그 이전이라면 년/월/일 표시

  • 이번장 내용도 좋았다. 나라면 스크립트 처리 부분이 거지같았을 텐데, module화 되고 차근차근 진행하니문제 없이 댓글에 대한 추가/수장/목록보기/수정이 원할하게 진행되었다.. 😄

17.6 댓글의 페이징 처리

17.6.1 데이터베이스의 인덱스 설계
  • 게시물을 기준으로 댓글을 조회하므로 게시물 번호 기준 내림 차순 댓글 번호 기준 오름차순 인덱스를 생성한다.

    CREATE INDEX idx_reply ON tbl_reply (bno DESC , rno ASC);
17.6.2 인덱스를 이용한 페이징 쿼리
  • 댓글 페이지 사이즈가 10이라고하고 2페이지를 가져올 때

    SELECT rno, bno, reply, replyer, replydate, updatedate
      FROM ( SELECT ROWNUM AS rn, rno, bno, reply, replyer, replydate, updatedate
               FROM tbl_reply
              WHERE bno = 10000521 /* 댓글이 여러개 달린 게시물 번호 */
                AND rno > 0
                AND ROWNUM <= 20)
     WHERE rn > 10;
  • ReplyMapper.xml 에 적용

      <select id="getListWithPaging" resultType="org.fp024.domain.ReplyVO">
        <![CDATA[
        SELECT rno, bno, reply, replyer, replydate, updatedate
          FROM ( SELECT ROWNUM AS rn, rno, bno, reply, replyer, replydate, updatedate
                   FROM tbl_reply
                  WHERE bno = #{bno}
                    AND rno > 0
                    AND rownum <= #{cri.pageNum} * #{cri.amount})
         WHERE rn > (#{cri.pageNum} - 1) * #{cri.amount}
         ]]>
      </select>
17.6.3 댓글의 숫자 파악
  • 카운트 쿼리 (resultType을 long으로 할 필요는 없다. 어떤 게시물의 댓글이 Int max만큼 들어갈일은 없으므로...)

      <select id="getCountByBno" resultType="int">
        SELECT COUNT(*)
          FROM tbl_reply
         WHERE bno = #{bno}
      </select>
17.6.4 ReplyServiceImpl에서 댓글과 댓글 수 처리
17.6.5 ReplyController 수정

17.7 댓글 페이지의 화면 처리

17.7.1 댓글 페이지 계산과 출력
  • pageSize 와 pageNavigationSize는 서버에서 넘겨주는 값으로 처리했다.
17.7.2 댓글의 수정과 삭제
  • 코드가 처음엔 복잡하다고 느끼더라도 일단 요구하는 기능을 잘 만들어내는 게 중요한 것 같다. 🤨

jex03 프로젝트 진행

ReplyVO에 대한 도메인, Mapper 자동 생성이 필요

mvnw mybatis-generator:generate
  • 자동생성 코드부분을 왠지 모듈로 뻬야할 것 같다. 예전에도 생각한 내용이지만, 자동생성 코드는 수정 필요시 generatorConfig.xml 수정 후 mybatis-generator 로 자동생성하도록 하고 수정하지 말아야한다.

Gson은 XML을 따로 처리하지 않으므로 컨트롤러에서 JSON만 생성하도록 하자!

produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}
  • MediaType.APPLICATION_XML_VALUE 는 빼도록 하자.

Jackson과 일관성을 위해 LocalDateTime을 배열 형식으로 내보낼 필요가 있었는데...GsonHttpMessageConverter 가 쉽게 등록이 안된다.

@Slf4j
@EnableWebMvc
@ComponentScan(basePackages = {"org.fp024.controller"})
public class ServletConfig implements WebMvcConfigurer {
  //...
  @Override
  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.removeIf(
        httpMessageConverter -> httpMessageConverter.getClass() == GsonHttpMessageConverter.class);
    converters.add(GsonHelper.gsonHttpMessageConverter());
  }
  //...  
}

자동으로 GsonHttpMessageConverter가 등록이 된 상태여서, 지워주고 LocalDateTime 직렬화 규칙을 정의한 GsonHttpMessageConverter 를 추가해줘야 제대로 인식한다.

  • gsonHttpMessageConverter 란 이름으로 빈으로 등록해도안되었다. 😥

MVC 테스트시에 ServletConfig에 사용자 정의한 내용이 적용이 안되서, 별로로 설정을 해줘야한다.

 @BeforeEach
  void setUp() {
    this.mockMvc =
        MockMvcBuilders.standaloneSetup(new ReplyController(service))
            // 설정을 임의로 해주면, 기본 목록에 더해서 추가하는 것이 아니여서, 몇가지 사용하는 것을 써줘야한다.
            // 그런데 기본목록이 ServletConfig 에 사용자 정의한 내용이 추가 되는게 아님.
            .setMessageConverters(
                new StringHttpMessageConverter(), GsonHelper.gsonHttpMessageConverter())
            .addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true))
            .build();
  }
  • 그런데 이부분은 WebApplicationContext 로 mockMvc를 만들지 않아서 그럴 수도 있을 것 같긴하다.

Jackson이라면 Getter를 정의해서 직렬화가 가능한데, Gson은 필드기반 직렬화만 가능하다.

/** Gson의 경우 필드기반 직렬화를 하여, getPageNavigationSize()를 직렬화하지 않는다. */
@RequiredArgsConstructor
@Getter
@ToString
public class ReplyPageDTO {
  /** 댓글 페이지 네비게이터에 표시할 페이지 인덱스 수 */
  public final int pageNavigationSize = 3;

  private final int pageSize;
  private final int replyCount;
  private final List<ReplyVO> list;
}
  • Jackson 사용하면서, Getter 메서드 재정의해서 직렬화되도록 자주 사용하곤 했는데, Gson이 이런줄은 처음알았다.🤔

URL 접미사로 (확장자) 수신할 응답 유형을 선택하는 것이 Spring 5.3.x부터 비활성화가 기본 값이다.

예제가 .json을 붙여서 URL을 호출하면 JSON을 받는 식으로 되어있는데, Spring 5.3 부터는 이런 방식을 기본으로 사용할 수 가 없다.

확장자를 없이 요청자의 Accept Header로 판별하는 것 같다. .json으로 끝나는 URL은 전부 정리하고 js에서 호출시에서는 $.getJSON() 등으로 변경해주었다.

Mock 테스트 요청 코드에는 .accept(MediaType.APPLICATION_JSON) 를 붙여주었다.

의견

  • BoardController를 다룰 때, MVC 컨트롤러 테스트 방법을 이미 나왔으므로, ReplyController를 다룰 때도 MVC 컨트롤러 테스트로 작성하고 로그를 확인하는 방식으로 하는 것이 좋을 것 같습니다.

  • p440에서 페이지네비게이션 사이즈와 페이지 사이즈를 그냥 상수로 10.0이나 10으로 적는 것 보단 변수로 이름을 지정해서 나타내는 것이 나을 것 같습니다.

정오표

  • p421. get.jsp 내의 모달창 코드를 보면 Close 버튼이 두개가 중복으로 정의되어있다. 하나는 id가 modalRegisterBtn인 버튼이 되야할 것 같다.