본문 바로가기
개발입문/SPRING 게시판 만들기

[SPRING] 댓글 기능 구현

by 양히◡̈ 2022. 10. 13.

웹페이지의 댓글 처리는 REST(Representational State Transfer)방식ajax를 이용할 것이다.

화면 전환 없이 댓글 등록 완료시 처리할 명령까지 콜백으로 전달한다.

Reference ▶ 자바스크립트 비동기 처리와 콜백 함수 • 캡틴판교 (joshua1988.github.io)

 

 

비동기처리를 위해 Talend 를 chrome에 추가할 것이다.

google 웹스토어에 "talend"를 검색한 후 추가하면 된다.

 

Maven Repository 에서 "jackson-databind", "jackson-dataforamt-xml", "gson"을 각각 검색하여 복사한 후,

pom.xml 에 의존성을 추가하고 저장한다.

https://mvnrepository.com/

 

(간혹 의존성을 잘못 가져왔을 때는 project창 > clean & 프로젝트 우클릭 > Maven > project update 하면 된다.)

 

 

 

 

댓글 테이블과 클래스 만들기

댓글을 추가하기 위해서 댓글 구조에 맞는 테이블을 설계한다.

sql developer - admin 계정에서 쿼리문을 작성한다.

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;
-- rno 시퀀스 처리 예정
alter table tbl_reply add constraint pk_reply primary key(rno);
-- 테이블 생성 후에 제약조건 추가, pk는 rno
alter table tbl_reply add constraint fk_reply_board
foreign key (bno) references tbl_board(bno);
-- 외래키로 tbl_board(bno) 사용

commit;

tbl_reply 테이블은 bno라는 칼럼을 이용해서 해당 댓글이 어떤 게시글의 댓글인지를 명시하도록 한다.

댓글 자체는 단독으로 CRUD가 가능하므로, 별도의 PK를 부여하도록 하고,

외래키(FK) 설정을 통해서 tbl_board 테이블을 참조하도록 설정한다.

이제 tbl_reply를 참고해서 ReplyVO를 만들 것이다.

다시 STS로 돌아온다.

 

kr.icia.domain > new Class 생성 > name: ReplyVO

package kr.icia.domain;

import java.util.Date;

import lombok.Data;

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

kr.icia.mapper > new Interface 생성 > name: ReplyMapper

package kr.icia.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Param;

import kr.icia.domain.Criteria;
import kr.icia.domain.ReplyVO;

public interface ReplyMapper {

	public int insert(ReplyVO vo);
	
	public ReplyVO read(Long rno);
	
	public int delete(Long rno);
	
	public int update(ReplyVO reply);
	
	public List<ReplyVO> getListWithPaging(
			@Param("cri") Criteria cri,
			@Param("bno") Long bno);
			//페이지 정보과 게시물 번호를 전달
}

댓글 부분도 역시 페이징처리가 필요할 수 있기 때문에 getListWithPaging을 이용해 Criteria와 bno값을 포함한다.

댓글의 페이징처리는 기존의 페이징처리와 유사하지만, 특정 게시물의 번호도 필요로 한다.

 

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

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

 

여러 방식 중에 가장 간단하게 사용할 수 있는 방식이 Param을 이용하는 방식이다.

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

 

 

 

댓글의 SQL을 처리할 매퍼 xml을 만든다.

src/main/resources > kr > icia > mapper > new XML file 생성 > name: 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="kr.icia.mapper.ReplyMapper">
	<insert id="insert">
		insert into tbl_reply (rno,bno,reply,replyer)
		values
		(seq_reply.nextval, #{bno},#{reply},#{replyer})
	</insert>

	<!-- 게시물당 조회가 아니라 댓글 1개 조회 -->
	<select id="read" resultType="kr.icia.domain.ReplyVO">
		select * from tbl_reply where
		rno=#{rno}
	</select>

	<delete id="delete">
		delete from tbl_reply where rno=#{rno}
	</delete>

	<update id="update">
		update tbl_reply set reply=#{reply},
		updatedate=sysdate where rno=#{rno}
	</update>

	<select id="getListWithPaging"
		resultType="kr.icia.domain.ReplyVO">
		select rno, bno, reply, replyer, replydate, updatedate
		from
		tbl_reply
		where bno=#{bno}
		order by rno asc
	</select>


</mapper>

댓글의 페이징처리는 이후 추가하도록 하고, 우선은 특정 게시물의 댓글을 가져올 수 있도록만 처리한다.

여기서 #{bno} 값은 BoardMapper 인터페이스의 @Param("bno")와 매칭된다.

 

 

 

 

테스트용 댓글 삽입

src/test/java > kr.icia.mapper > new Class 생성 > name: ReplyMapperTests

package kr.icia.mapper;

import java.util.stream.IntStream;

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 kr.icia.domain.ReplyVO;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class ReplyMapperTests {
	private Long[] bnoArr = {471L, 472L, 473L, 474L, 475L };
	//실제 게시물 번호 5개를 할당해야함
	
	@Setter(onMethod_ = @Autowired)
	private ReplyMapper mapper;
	
	@Test
	public void testCreate() {
		//게시물 1개당 2개의 덧글 자동 등록
		//정수형을 stream 형태로 전환. 범위 1~10
		IntStream.rangeClosed(1, 10).forEach(i -> {
			ReplyVO vo = new ReplyVO();
			
			vo.setBno(bnoArr[i % 5]); //0~4
			vo.setReply("댓글 테스트" + i);
			vo.setReplyer("replyer" + i);
			
			mapper.insert(vo);
		});
	}
}

 

tbl_reply는 tbl_board키의 bno로 FK(외래키)가 설정되어 있기 때문에,

tbl_board의 bno값과 정확하게 일치하는 값(실제로 존재하는 값)으로 5개의 bno값을 참조해야 한다.

따라서, tbl_board에 실제로 존재하는 471~475번 게시물을 입력하도록 했다.

 

JUnit Test를 구동해보면 sql developer에 댓글 테이블인 tbl_reply 가 생성되어 데이터가 게시글당 2개씩 들어가있는 것을 볼 수 있다.

 

 

 

서비스 클래스와 컨트롤러 생성

src/main/java > kr.icia.service > new Interface 생성 > name: ReplyService

package kr.icia.service;

import java.util.List;

import org.apache.ibatis.annotations.Param;

import kr.icia.domain.Criteria;
import kr.icia.domain.ReplyVO;

public interface ReplyService {

	public int register(ReplyVO vo);
	
	public ReplyVO get(Long rno);
	
	public int remove(Long rno);
	
	public int modify(ReplyVO reply);
	
	public List<ReplyVO> getList(@Param("cri") Criteria cri, @Param("bno") Long bno);
}

 

src/main/java > kr.icia.service > new Class 생성 > name: ReplyServiceImp

생성시 Interface를 지정하여 생성하거나, 그러지 못했을 경우 "implements ReplyService"를 명시한 후

빨간줄이 나타나면 Override를 생성하면 된다.

package kr.icia.service;

import java.util.List;

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

import kr.icia.domain.Criteria;
import kr.icia.domain.ReplyVO;
import kr.icia.mapper.ReplyMapper;
import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Service
@Log4j
public class ReplyServiceImp implements ReplyService {
	@Setter(onMethod_ = @Autowired)
	private ReplyMapper mapper;

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

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

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

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

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

 

 

 

replyController는 다음과 같이 설계할 것이다.

작업
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만으로 조회, 수정, 삭제가 가능하기 때문이다.

다만 댓글의 목록은 PK를 사용할 수 없기 때문에, 파라미터로 필요한 게시물의 번호(bno)와 페이지 번호(page) 정보들을 URL에서 표현하는 방식을 사용한다.

 

 

 

src/main/java > kr.icia.controller > new Class 생성 > name: ReplyController

package kr.icia.controller;

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 kr.icia.domain.ReplyVO;
import kr.icia.service.ReplyService;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;

@RequestMapping("/replies/")
@RestController // json이나 xml로 데이터 변환
@Log4j
@AllArgsConstructor
public class ReplyController {

	private ReplyService service;

	// 요청이 /replies/new로 오면 정보를 조회해서 return하는데,
	// 정보형태는 json이고, 전달 결과물은 평범한 문자열 형태
	@PostMapping(value = "/new", consumes = "application/json", produces = { MediaType.TEXT_PLAIN_VALUE })
	public ResponseEntity<String> create(@RequestBody ReplyVO vo) {
		// @RequestBody는 json형태로 받은 값을 객체로 변환
		log.info("ReplyVO: " + vo);
		int insertCount = service.register(vo);
		log.info("Reply insert count: " + insertCount);

		return insertCount == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
				: new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		// ResponseEntity: 웹페이지 생성(상태코드, 헤더, 응답, 데이터)

		// 3항 연산자 이용.
		// HttpStatus 페이지 상태를 전달
		// return 코드를 풀이: 정상 처리되면 정상처리의 status를 전달하고, 아니면 오류 status 전달
	}
}

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

 

create() 의 파라미터에 @RequestBody를 적용하여 JSON형태의 데이터를 ReplyVO로 변환한다.

 

create( )는 내부적으로 ReplyServiceImpl을 호출해서 register()를 호출하고, 댓글이 추가된 숫자를 확인해서 브라우저에서 '200 OK' 혹은 '500 Internal Server Error'를 반환하도록 한다.

 

 

 

 

댓글 생성 테스트하기

chrome 창을 띄워 확장 프로그램을 클릭하고 이전에 확장 추가했던 talend를 클릭한다.

talend 페이지에서 아래 세 부분을 입력한다.

 

METHOD는 POST,

URL은 http://localhost:9091/replies/new

BODY부분은 {"bno":475,"reply":"hi","replyer":"user"}

BODY부분에 작성한 내용이 json이다.

이 json으로 입력하여, 값을 TEXT_PLAIN_VALUE로 출력받는 것이다.

작성 후 send 버튼을 누르면 결과가 아래창에 출력된다.

sql에서도 정상적으로 반영되었다.

 

 

 

 

특정 게시물의 댓글 목록 확인하기

ReplyController에 메소드를 추가한다.

//댓글 목록 가져오기
@GetMapping(value = "/pages/{bno}/{page}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ReplyVO>> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
    // @PathVariable: url로 넘겨받은 값을 이용
    log.info("getList......");
    Criteria cri = new Criteria(page, 10);
    log.info(cri);

    return new ResponseEntity<>(service.getList(cri, bno), HttpStatus.OK);
    // T<List<ReplyVO>> t = new T<>();
    // 댓글 목록을 출력하고, 정상처리 상태를 return함
}

getList()는 Criteria를 이용해서 파라미터를 수집하는데, /{bno/ 1page)'의 'page' 값은 Criteria를 생성해서 직접 처리해야 한다.

게시물의 번호는 @PathVariable을 이용해서 파라미터로 처리한다.

서버 구동 후, 다시 talend로 와서 아래와 같이 GET방식으로 바꾸고, URL을 pages/{게시물번호}/{페이지번호}로 변경하여 입력한다.

 

 

send를 클릭하면 아래 결과창이 나온다.

475번 게시물에 총 3개의 댓글이 있다는 것을 확인할 수 있다.

크롬의 주소창에서도 확인이 가능하다.

똑같이 http://localhost:9091/replies/pages/475/1 를 입력하면 된다.

[ {"rno":4,"bno":475,"reply":"댓글 테스트4","replyer":"replyer4", "replyDate":1633068109000,"updateDate":1633068109000},
{"rno":9,"bno":475,"reply":"댓글 테스트9","replyer":"replyer9", "replyDate":1633068109000,"updateDate":1633068109000},
{"rno":21,"bno":475,"reply":"hi","replyer":"user", "replyDate":1633072191000,"updateDate":1633072191000} ]

json은 이처럼 배열같은 형태로 나타나게 된다.

* 예전에는 URL마지막에 ".json"을 붙이면 json형태 처리가 가능했는데, 스프링 버전이 올라가면서 더 이상 지원하지 않게 되었다.

 

 

 

 

댓글 1개 읽기

ReplyController.java 에 코드를 추가한다.

//댓글 1개 읽기
@GetMapping(value = "/{rno}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReplyVO> get(@PathVariable("rno") Long rno) {
    log.info("get : " + rno);
    return new ResponseEntity<>(service.get(rno), HttpStatus.OK);
}

코드를 보면 service.get 부분에서 ReplyServiceImp로 넘어가 ReplyVO get()부분이 구동되며 mapper.read를 return하게 된다.

그러면 여기서 return한 값으로 ReplyMapper.xml의 read 부분을 거쳐 데이터를 조회하는 원리이다.

서버를 구동하고 조회가 잘 되는지를 확인해볼 것이다. sql developer의 tbl_reply에서 rno가 존재하는 행을 찾아 그 rno 번호를 URL에 입력한다.

웹페이지에서 바로 조회
talend에서 조회

 

 

 

 

댓글 삭제 처리

마찬가지로 ReplyController.java 에 코드를 추가한다.

//댓글 삭제
@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<>("success", HttpStatus.OK)
            : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}

remove()가 동작했을 때는 삭제된 행의 성공한 개수를 리턴하게 되는데, 이 말은 성공했을 때는 1, 실패했을 때는 0이 return될 것이다.

따라서, return부분의 코드를 보면 성공했을 때는 "success"와 HttpStatus.OK를 출력하고, 실패했을 때는 에러메세지를 출력하라는 의미가 된다.

이번에는 결과를 Talend에서만 확인이 가능하므로 서버 구동 후 Talend를 띄운다.

METHOD를 DELETE로 변경하고 Send 해보면, 아래에 "success"라는 메세지와 함께 리플이 삭제되었다는 화면이 보여진다.

sql developer 에서 tbl_reply 를 새로고침해보면 21번 리플이 정상적으로 삭제되었다.

 

 

 

댓글 수정 처리

마찬가지로 ReplyController.java 에 코드를 추가한다.

// 댓글 수정
@RequestMapping(method = { RequestMethod.PUT, RequestMethod.PATCH }, value = "/{rno}", 
        consumes = "application/json", produces = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<String> modify(@RequestBody ReplyVO vo, @PathVariable("rno") Long rno) {
    // 생성되는 정보의 형태는 json의 일반적인 문자열을 이용한다
    // @RequestBody: json으로 생성된 정보를 객체화한다
    vo.setRno(rno);
    log.info("rno : " + rno);
    log.info("modify : " + vo);
    return service.modify(vo) == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
            : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}

여기서 사용한 PUT과 PATCH method는 둘 다 수정 처리를 가리킨다.
차이점은 put은 리소스를 전체수정할 때, patch는 부분수정할 때 더 적합하다는 점이다.

@RequestMapping 은 URL을 컨트롤러의 메서드와 매핑할 때 사용하는 스프링 프레임워크의 어노테이션이다.

이제 서버 구동 후 Talend에서 테스트해볼 것이다.

METHOD를 PUT방식으로 변경한 후 BODY에서 어떤 부분을 수정할 것인지를 지정한 후, send를 누른다.

"success"가 잘 출력되었다면, sql developer에서 조회해보자.

방금 수정한 10번 리플이 잘 수정되었다.

이번에는 동일한 방법으로 METHOD만 PATCH로 변경하여 rno=9를 수정해보자.

URL부분에서도 replies/9 로 변경한 후 send를 눌러 "success"를 출력한 후, sql developer를 조회한다.

마찬가지로 patch 방식으로도 수정이 잘 처리되었다.

 

더보기

ResponseEntity?

HttpSatus.OK?

 

ReplyController에서 계속 등장하는 ResponseEntity가 뭘까? 그리고, HttpStatus.OK의 출력물은 뭘까?라는 의문점이 생긴다.

일반적인 API는 리소스에 Value만 존재하는 것이 아니다.

상태코드가 있을 수도 있고, 응답 메세지 등이 포함될 수도 있다.

그래서 사용하는 것이 'ResponseEntity'이다.

[Spring Boot] ResponseEntity란 무엇인가? :: Gyun's 개발일지 (tistory.com)

ResponseEntity를 Ctrl키를 누른 상태에서 클릭해보자.

public class ResponseEntity<T> extends HttpEntity<T> {

	private final Object status;

 

이걸 보면 알 수 있는 정보로는, ResponseEntity는 HttpEntity 클래스를 상속받는다는 것이다.

그리고 아래로 좀 더 내려가보면, body, header, status를 가질 수도 있다는 것을 보여준다.

 

 
public ResponseEntity(HttpStatus status) {
    this(null, null, status);
}

public ResponseEntity(@Nullable T body, HttpStatus status) {
    this(body, null, status);
}

public ResponseEntity(MultiValueMap<String, String> headers, HttpStatus status) {
    this(null, headers, status);
}

 

그래서 우리는 이걸 이용해 status를 출력할 수 있도록 HttpStatus를 출력받도록 하고 있던 것이다.

계속 사용하고 있던 HttpStatus.OK 의 반환값은 주로 "200"이 출력됐었다.

이 "200"이 바로 '상태코드'다.

 

return service.modify(vo) == 1 ? new ResponseEntity<>("success", HttpStatus.OK)
        : new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);

 

즉, 내가 작성한 코드에서 살펴보면, vo를 수정하는데에 성공했다면, 새로운 ResponseEntity를 만들고 그 안에 body는 "success", status는 "200"을 할당한다는 의미가 된다.

상태코드는 아래 사이트에서 참조해볼 수 있다.

HTTP 상태 코드 - HTTP | MDN (mozilla.org)

 

댓글