⚙️ Backend/스프링(Spring) Framework

스프링 - Ajax 댓글 처리 구현

코너(Corner) 2021. 5. 17.
반응형

스프링 - Ajax 댓글 처리 구현

1. 프로젝트의 구성

REST 처리를 위해 pom.xml에서 수정된 내용이 대부분이므로, ex03에서 사용하던 소스 코드를 복사해서 사용한다.

1.2 pom.xml 에서 json 라이브러리 추가

<!-- JSON -->
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.9.6</version>
      </dependency>

      <dependency>
         <groupId>com.fasterxml.jackson.dataformat</groupId>
         <artifactId>jackson-dataformat-xml</artifactId>
         <version>2.9.6</version>
      </dependency>

      <dependency>
         <groupId>com.google.code.gson</groupId>
         <artifactId>gson</artifactId>
         <version>2.8.2</version>
      </dependency>

2. 댓글 처리를 위한 영속 영역

---------------------------
-- 댓글 테이블 추가
CREATE SEQUENCE SEQ_REPLY;
CREATE TABLE TBL_REPLY(
    RNO NUMBER(10),
    BNO NUMBER(10) NOT NULL,
    REPLY VARCHAR2(1000) NOT NULL, -- 댓글 
    REPLYER VARCHAR2(100) NOT NULL, -- 작성자
    REPLYDATE DATE DEFAULT SYSDATE, -- 작성날짜 
    UPDATEDATE DATE DEFAULT SYSDATE -- 수정날짜
);

ALTER TABLE TBL_REPLY ADD CONSTRAINT PK_REPLY PRIMARY KEY(RNO);
-- ALTER TABLE TBL_REPLY DROP CONSTRAINT FK_REPLY;

ALTER TABLE TBL_REPLY ADD CONSTRAINT FK_REPLY FOREIGN KEY(BNO)
REFERENCES TBL_BOARD(BNO) ON DELETE CASCADE;

SELECT * FROM TBL_REPLY;

2.1 ReplyVO 클래스의 추가 (com.koreait.domain)

package com.koreait.domain;

import lombok.Data;

@Data
public class ReplyVO {
    private Long rno;
    private Long bno;
    private String reply;
    private String replyer;
    private String replyDate;
    private String updateDate;
}

2.2 ReplyMapper 클래스와 XML 처리

com.koreait.mapper 패키지에 ReplyMapper 인터페이스를 처리하고 XML 파일도 생성해준다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.koreait.mapper.ReplyMapper">
</mapper>

ReplyMapper 테스트

src/test/javacom/koreait/mapperReplyMapperTests.java추가

package com.koreait.mapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

// SpringRunner는 SpringJUnit4ClassRunner의 자식이며,
// 4.3 버전 이상부터 사용 가능한 확장판이다.
@RunWith(SpringRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class ReplyMapperTests {
    @Setter(onMethod_ = @Autowired)
    private ReplyMapper mapper;

    @Test
    public void testMapper( ) {
        log.info(mapper);
    }
}

testMapper()로 ReplyMapper 타입 객체가 정상 사용 가능한지 확인


2.3 CRUD 작업

등록 create

우선은 외래키를 사용하는 등록 작업 진행

com.koreait.mapper에 있는 ReplyMapper 메소드 추가

public int insert(ReplyVO vo);

xml

<mapper namespace="com.koreait.mapper.ReplyMapper">

  <insert id="insert">
    insert into tbl_reply (rno, bno, reply, replyer)
    values (seq_reply.nextval, #{bno}, #{reply}, #{replyer})
  </insert>

</mapper>
    @Setter(onMethod_ = @Autowired)
    private BoardMapper board;

    private Long[] bnoArr = {1376256L, 1376255L, 1376254L, 1376253L, 1376252L}; // 방법2

    @Test
    public void testCreate() {
        java.util.List<BoardVO> boards = board.getListWithPaging(new Criteria(1, 5));
        IntStream.rangeClosed(1, 10).forEach(i -> {
            ReplyVO reply = new ReplyVO();
            reply.setReply("댓글 테스트 " + i);
            reply.setReplyer("테드한 " + i);
            // 0~n :  n+1로 나눈 나머지
            reply.setBno(boards.get(i % 5 ).getBno());
//            reply.setBno(bnoArr[i%5]); // 이렇게 해도 되는 방법2

            mapper.insert(reply);
            log.info(reply);
        });
    }

조회(read) 테스트

xml

    <select id="read" resultType="com.koreait.domain.ReplyVO">
        SELECT RNO, BNO, REPLY, REPLYER, REPLYDATE, UPDATEDATE FROM TBL_REPLY WHERE RNO = #{rno}
    </select>

mapper

    public ReplyVO read(Long bno);

ReplyMapperTests

    @Test
    public void testRead() {
        Long targetRno = 5L;
        ReplyVO vo = mapper.read(targetRno);
        log.info(vo);
    }

삭제(delete)

Mapper 인터페이스와 xml에 삭제 처리 추가

mapper

    public int delete(Long rno);

xml

    <delete id="delete">
    DELETE FROM TBL_REPLY WHERE RNO = #{rno}
    </delete>

ReplyMapperTests

    @Test
    public void testDelete() {
        Long rno = 2L;
        log.info(mapper.delete(rno));
    }

수정(update)

Mapper 인터페이스와 xml에 수정 처리 추가

public int update(ReplyVO reply);
    <update id="update">
        UPDATE TBL_REPLY 
        SET REPLY = #{reply}, UPDATEDATE = SYSDATE 
        WHERE RNO = #{rno}
    </update>

테스트

    @Test
    public void testUpdate() {
        Long rno = 3L;
        ReplyVO reply = mapper.read(rno);

        reply.setReply("수정 댓글");
        log.info("UPDATE COUNT: " + mapper.update(reply));
    }

2.4. @Param 어노테이션과 댓글 목록

MyBatis는 두개 이상이 데이터를 파라미터로 전달하기 위해선

1) 별도의 객체로 구성하거나 2)Map 이용 방식 3) Param을 이용해 이름을 사용하는 방식이 있다.

@Param 의 속성값은 MyBatis에서 SQL을 이용할 떄 #{}의 이름으로 사용 가능하다.

ReplyMapper Interface

package com.koreait.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Param;

import com.koreait.domain.Criteria;
import com.koreait.domain.ReplyVO;

public interface ReplyMapper {
    public int insert(ReplyVO reply);
    public ReplyVO read(Long bno);
    public int delete(Long rno);
    public int update(ReplyVO reply);

//    해당 게시글에 있는 전체 댓글
    /*
     * MyBatis는 두 개 이상의 데이터를 파라미터로 전달하기 위해선 
     * 1) 별도의 객체로 구성하거나 2) Map 이용 방식 3) Param을 이용해 이름을 사용하는 방식이 있다.
     * @Param 의 속성값은 MyBatis에서 SQL을 이용할 때#{}의 이름으로 사용 가능하다.
     */
    public List<ReplyVO> getListWithPaging(
            @Param("cri") Criteria cri,
            @Param("bno") Long bno
            );    
}

ReplyMapper.xml

    <select id="getListWithPaging" resultType="com.koreait.domain.ReplyVO">
        SELECT RNO, BNO, REPLY, REPLYER, REPLYDATE, UPDATEDATE FROM TBL_REPLY 
        WHERE BNO = #{bno}    
    </select>

테스트

    @Test
    public void testGetListWithPaging() {
        Criteria cri = new Criteria();
        List<ReplyVO> replies = mapper.getListWithPaging(cri, bnoArr[0]);
        replies.forEach(reply->log.info(reply));
//        mapper.getListWithPaging(bnoArr[0]).forEach(reply -> log.info(reply));
    }

3. 서비스 영역과 Controller 처리

ReplyService Interface

package com.koreait.service;

import java.util.List;

import com.koreait.domain.Criteria;
import com.koreait.domain.ReplyVO;

public interface ReplyService {
    public int register(ReplyVO reply); // 등록하기
    public ReplyVO get(Long rno); // 특정 댓글 가져오기
    public int modify(ReplyVO reply); // 수정하기
    public int remove(Long rno); // 삭제하기
    // 전체 댓글 가져오기
    public List<ReplyVO> getList(Criteria cri, Long bno);
}

ReplyServiceImple

package com.koreait.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.koreait.domain.Criteria;
import com.koreait.domain.ReplyVO;
import com.koreait.mapper.ReplyMapper;

import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Service
@Log4j
@AllArgsConstructor
public class ReplyServiceImple implements ReplyService {

//    @Setter(onMethod_ = @Autowired)
    private ReplyMapper mapper;

    @Override
    public int register(ReplyVO reply) {
        log.info("register............" + reply);
        return mapper.insert(reply);
    }

    @Override
    public ReplyVO get(Long rno) {
        log.info("get.........."+rno);
        return mapper.read(rno);
    }

    @Override
    public int modify(ReplyVO reply) {
        log.info("modify........."+reply);
        return mapper.update(reply);
    }

    @Override
    public int remove(Long rno) {
        log.info("remove............."+rno);
        return mapper.delete(rno);
    }

    @Override
    public List<ReplyVO> getList(Criteria cri, Long bno) {
        log.info("get Reply List of a Board " + bno);
        return mapper.getListWithPaging(cri, bno);
    }

}

3.1. ReplyController의 설계

URL : 사용자의 요청과 그에 맞는 응답을 주소로 나타낸 부분. 항상 페이지로 나타내진다.

URI : 사용자의 요청을 대표하는 데이터 혹은 응답에 대한 데이터를 나타낸 부분. 대부분 데이터로 나타내진다.

REST(Representational State Transfer) : "하나의 URI는 하나의 고유한 리소스를 대표하도록 설계된다."

예) /board/123 : 게시글 중 123번

REST로 설계하는 이유

  1. 데이터 통신에 제약이 없다.
  2. 데이터 소켓 경량화
  3. 다른 서버끼리도 데이터를 주고 받을 수 있다.

@RestController 어노테이션으로 다음 같은 URL을 기준으로 동작하도록 작성

작업 URL HTTP 전송 방식
등록 /replies/new POST
조회 /replies/:rno GET
삭제 /replies/:rno DELETE
수정 /replies/:rno PUT or PATCH
페이지 /replies/pages/:bno/:page GET

REST 방식으로 동작하는 URL을 설계할 땐 PK를 기준으로 작성하는 게 좋다.

다만 댓글의 목록은 PK를 사용할 수 없으므로 bno와 page 정보들을 URL에서 표현하는 방식 사용.

com.koreait.controller 패키지 밑에 ReplyController.java 생성

package com.koreait.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.koreait.domain.ReplyVO;
import com.koreait.service.ReplyService;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RequestMapping("/replies/")
@RestController // 각 메소드의 리턴은 자동으로 view resolver로 가지 않는다.
@Log4j
//@AllArgsConstructor
public class ReplyController {

    @Setter(onMethod_ = @Autowired)
    private ReplyService service;

}

브라우저에서 JSON 타입으로 데이터를 전송하고 서버에서는 댓글의 처리 결과에 따라 문자열로 결과를 리턴한다.

consumes : Ajax를 통해 전달받은 데이터의 타입 ( 외부에서 전달한 타입 )

produces : Ajax의 success: function(result)에 있는 result로 전달할 데이터의 타입 ( 외부로 전달할 타입 )

json 방식 데이터만 처리하도록 하고 문자열을 반환하도록 설계,
ResponseEntity : 서버의 상태 코드, 응답 메시지등을 담을 수 있는 타입.
create의 파라미터 @RequestBody를 적용해 JSON 데이터를 ReplyVO 타입으로 반환하도록 지정.

@RequestBody : 외부에서 전달받은 데이터를 parse (파싱) 해주는 속성이다.

create는 내부적으로 ReplyServiceImple을 호출해 register를 부르고 댓글이 추가된 숫자를 확인해서 브라우저에
200 = OK 혹은 500 = ERROR를 반환하도록 한다.


3.2 댓글 등록 작업과 테스트

크롬 확장프로그램 설치
Talend API Tester 설치

REST 방식 처리에서 주의점은 브라우저나 외부에서 서버를 호출할 때 데이터 포맷과 서버에서 보내주는 데이터의 타입을 명확히 설계해야 하는 것.

예를 들어 댓글 등록은 브라우저에선 JSON 타입으로 된 댓글 데이터를 전송하고 서버에선 댓글의 처리 결과가 정상적으로 되었는지 문자열로 결과를 알려 주도록 한다.

    // 댓글 등록
    @PostMapping(value = "/new", consumes = "application/json", produces = {MediaType.TEXT_PLAIN_VALUE})
    public ResponseEntity<String> create(@RequestBody ReplyVO reply) {
        log.info("ReplyVO: " + reply);
        int insertCount = service.register(reply);

        log.info("Reply INSERT COUNT : " + insertCount);
        return insertCount == 1 ? new ResponseEntity<> ("success", HttpStatus.OK) 
                : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);

    }

img

)

img

데이터가 잘 들어갔는지 확인해본다.

img

)

img


3.3 특정 게시물의 댓글 조회

ReplyController

    @GetMapping(value="/pages/{bno}/{page}", produces = {MediaType.APPLICATION_XML_VALUE,
            MediaType.APPLICATION_JSON_UTF8_VALUE
    })
    public ResponseEntity<List<ReplyVO>> getList(
            @PathVariable("page") int page,
            @PathVariable("bno") Long bno) {
        log.info("getList..........");
        Criteria cri = new Criteria(page, 10);
        log.info(cri);
        return new ResponseEntity<>(service.getList(cri, bno), HttpStatus.OK);
    }

uri에다 {} 중괄호가 들어가면 변수 선언이다. value="/page/{bno}/{page}

uri로 넘겨 매핑하기 위해 어노테이션 @PathVariable을 사용한다.

URL 주소로 localhost:8080/replies/pages/게시글번호/페이지번호 로 확인한다.

img

)

img

이게 백 엔드 개발자 이다. 서버의 통신으로 내가 원하는 데이터를 주고 받을 수 있는 REST 방식을 사용해야 한다.

JSON으로 확인하는법은 url뒤에 끝에 .json 확장자를 붙인다.

URL : corner-mini.com/replies/pages/1376256/1.json

img

크롬 json Formatter 확장프로그램 사용 시

img

@RestController는 잘 사용하지 않기도 한다.

@Controller 어노테이션을 사용할 때 반환(return) 타입을 .jsp로도 보낼 수 있도록 하고 싶을 때

@ResponseBody 라는 어노테이션을 사용하여 @Controller에서 Body를 응답하기 위해서는 (viewResolver로 가지않게 하기 위해서) 사용된다.


3.4 특정 댓글 조회 및 삭제

ReplyController

    // 댓글 조회 
    @GetMapping(value="/{rno}", produces = {
            MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE
    })
    public ResponseEntity<ReplyVO> get(@PathVariable("rno") Long rno) {
        log.info("get....... : "+rno);
        return new ResponseEntity<>(service.get(rno), HttpStatus.OK);
    }

img

댓글 삭제

 

	@DeleteMapping(value="/{rno}", produces = {MediaType.TEXT_PLAIN_VALUE})
	public ResponseEntity<String> remove(@PathVariable("rno") Long rno) {
		log.info("remove ... : " +rno);
		return service.remove(rno) == 1 
					? new ResponseEntity<String>("success", HttpStatus.OK)
							: new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
	}

 

 

3.5 댓글 수정

PUT :자원 전체 수정, 자원 내 모든 필드를 전달해야 함, 일부만 전달할 경우 전달되지 않은 필드는 모두 초기화 처리가 된다.

PATCH : 자원 일부 수정, 수정할 필드만 전송

PUT 방식보단 PATCH 방식을 많이 사용한다.

    // 댓글 수정 
    // PUT :자원 전체 수정, 자원 내 모든 필드를 전달해야 함, 일부만 전달할 경우 전달되지 않은 필드는 모두 초기화 처리가 된다.
    // PATCH : 자원 일부 수정, 수정할 필드만 전송
    // @RequestMapping을 사용하는 이유는 여러개의 메소드를 받기 위해.
    @RequestMapping(method = {RequestMethod.PUT, RequestMethod.PATCH},
            value = "/{rno}",
            consumes = "application/json",
            produces = {MediaType.TEXT_PLAIN_VALUE})
    public ResponseEntity<String> modify(@RequestBody ReplyVO reply, @PathVariable("rno") Long rno){
        reply.setRno(rno);
        log.info("rno : " + rno);
        log.info("modify : " + reply);
        return service.modify(reply) == 1 
                    ? new ResponseEntity<String> ("success", HttpStatus.OK)
                            : new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
    }

 

전체 수정 (put)

 

일부 수정 

 

 


 

 

 

 

 

 

반응형

댓글