요구사항
다대일 연관관계를 이용한 댓글 CRUD
순서
- 다대일 연관관계
- 댓글 처리 구현
다대일 연관관계
- PK와 FK를 이용해 연관관계를 표현하는 데이터베이스
- 연관관계의 방향성을 판단하기 어려운 JPA
JPA에서 연관관계 판단 기준
- 변화가 많은 쪽 기준
- 회원-게시물 : 게시물
- 회원-좋아요 : 좋아요
- ERD의 FK 기준
참조방식
- 데이터베이스 : PK를 FK가 참조
- JPA : A <-> B와 같은 형태로 서로 참조가 가능한 양방향도 지원
양방향과 단반향 참조
- 양방향 : 구현은 가능하지만, 관리가 어렵고 에러 발생 가능성이 높아서 웬만하면 사용하지 않도록 한다.
- 단방향 : 조인 처리를 통해 다른 엔티티를 사용하므로, 불편함이 존재하지만 에러 발생 확률이 적아서 권장
댓글 처리 구현
다대일 연관관계를 이용한 댓글처리
요구사항
- Board에 종속적인 Reply 구현 (BoardService)
- 특정 게시물에 댓글 작성
- 게시물 목록에 게시물당 댓글 수
- Board에 독립적인 Reply 구현 (ReplyService)
- 댓글 CRUD
- 특정 게시물의 댓글 목록
구현 순서
- DTO
- 엔티티
- 리포지토리
- 서비스
- 컨트롤러
- 화면 처리
Board에 종속적인 Reply 구현 (BoardService)
1.Reply 엔티티 작성
-@Entity : JPA가 관리하는 클래스라는 의미로, 객체 맵핑을 위한 어노테이션
-@Id, @GeneratedValue : 기본키 설정을 위한 어노테이션으로, strategy 속성으로 기본키 생성전략을 선택할 수 있다.
-@ManyToOne : 다대일 연관관계를 위한 어노테이션으로, Fetch타입을 선택할 수 있다.
[기본값이 즉시로딩이므로, fetch 속성을 이용해 지연로딩으로 변경한다.]
-@ToString : 참조관계에 있는 필드는 exclude 속성을 이용해 제외해야 순환참조 오류를 방지할 수 있다.
-@Getter, @Builder, @NoArgsConstructor, @AllArgsConstructor : VO 어노테이션
package org.zerock.b01.domain;
import com.fasterxml.jackson.databind.ser.Serializers;
import lombok.*;
import org.springframework.web.bind.annotation.GetMapping;
import javax.persistence.*;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
//참조객체를 제외하도록 exclude 속성 이용
@ToString(exclude = "board")
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rno;
//다대일의 경우 기본값이 즉시 로딩으로, 지연 로딩으로 변경
//:연관관계를 나타낼 때는 항상 LAZY로 지정
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
private String replyText;
private String replyer;
}
-> board_bno칼럼이 생기며, 자동으로 FK값으로 설정한다.
2. 댓글을 처리하는 리포지토리 작성
(1) ReplyRepository 작성
-Reply의 CRUD를 담당하는 리포지토리로 JpaRepository<Reply, Long>을 상속하는 인터페이스
-JpaRepository : 엔티티 타입과 @Id 타입을 지정
package org.zerock.b01.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.b01.domain.Reply;
public interface ReplyRepository extends JpaRepository<Reply, Long> {
}
(2) ReplyRepositoryTests에 댓글 작성하는 메소드 작성
-@SpringBootTest : 스프링부트에서 테스트를 위한 어노테이션
-@Autowired : 필드로 의존성 주입
package org.zerock.b01.repository;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.zerock.b01.domain.Board;
import org.zerock.b01.domain.Reply;
@SpringBootTest
@Log4j2
public class ReplyRepositoryTests {
@Autowired
private ReplyRepository replyRepository;
@Test
public void testInsert(){
//DB에 존재하는 게시물 번호
Long bno = 100L;
Board board = Board.builder()
.bno(bno)
.build();
Reply reply=Reply.builder()
.board(board)
.replyText("댓글...")
.replyer("replyer1")
.build();
replyRepository.save(reply);
}
}
3. 인덱스를 이용한 특정 게시물의 댓글 조회
-게시물 번호를 이용해 댓글 객체를 참조하는 경우가 많다. (게시물 댓글 수, 특정 게시물의 댓글 목록)
-자주 사용하는 쿼리 조건의 칼럼을 인덱스로 지정
(1) 댓글 칼럼을 인덱스로 지정한 인덱스 테이블 생성
Reply 엔티티에 @Table 어노테이션으로 인덱스 테이블 생성
-@Table : 테이블을 생성하는 어노테이션
-indexes : 인덱스 설정을 위한 속성
-@Index : 인덱스의 속성을 결정하는 어노테이션으로, name, columnList 속성을 이용해 인덱스를 지정한다.
@Table(name = "Reply", indexes = {@Index(name = "idx_reply_board_bno", columnList = "board_bno")})
(2) JPQL을 이용해 게시물의 댓글 페이징 처리
-ReplyRepository에 listOfBoard() 메서드 작성
: 특정 게시물 번호와, Pageable 객체를 파라미터로 전달해 JPQL로 작성한 쿼리를 수행해 페이징 처리된 게시물의 댓글 목록을 리턴
- 사용하는 쿼리 : JPQL
- 메서드 명 : listOfBoard()
- 파라미터 : bno, pageable
- 리턴값 : Page<Reply>
@Query("select r from Reply r where r.board.bno=:bno")
Page<Reply> listOfBoard(Long bno, Pageable pageable);
(3) 테스트 코드 작성
-JPA는 필요한 엔티티를 최소한의 자원으로 사용하므로, @ToString에서 exclude로 참조하는 객체를 제외하지 않으면 에러 발생.
-강제로 실행하기 위해서는 선언적 트랜잭션 처리를 수행하는 어노테이션인 @Transactional를 사용한다.
-FetchType.LAZY : 필요한 순간까지 데이터베이스 접근을 늦추는 속성으로 성능을 위해 가장 최소화하는 것이 좋다.
@Test
public void testBoardReplies(){
Long bno = 100L;
Pageable pageable= PageRequest.of(0, 10, Sort.by("rno").descending());
Page<Reply> result = replyRepository.listOfBoard(bno, pageable);
result.getContent().forEach(reply -> {
log.info(reply);
});
}
select reply0_.rno as rno1_1_, reply0_.moddate as moddate2_1_, reply0_.regdate as regdate3_1_, reply0_.board_bno as board_bn6_1_, reply0_.reply_text as reply_te4_1_, reply0_.replyer as replyer5_1_ from reply reply0_ where reply0_.board_bno=? order by reply0_.rno desc limit ? |
4. 게시물 목록에 댓글 적용
-게시물 당 댓글 수를 게시물 목록에 반영한다.
기존 목록 | 댓글 적용한 목록 |
Board 객체를 BoardDTO로 변환해 내용 출력 | 화면에 필요한 BoardListReplyCountDTO을 생성해 출력 |
(1) BoardListReplyCountDTO 작성
@Data
//생성자를 이용해 객체 생성을 할 필요가 없는 화면 출력만을 위한 DTO
public class BoardListReplyDTO {
private Long bno;
private String title;
private String writer;
private LocalDateTime regDate;
private Long replyCount;
}
(2) 검색으로 게시물을 찾기 때문에 Querydsl을 이용한 동적쿼리로 BoardSearch에 searchWithReplyCount() 메서드 작성
- 메소드 명 : searchWithReplyCount
- 파라미터 : String[] types, String keyword, Pageable pageable
- 리턴 타입 : Page<BoardListReplyCountDTO>
- 참조 방식 : 단방향
- 쿼리 종류 : JPQL을 이용한 left outer join / inner join
(댓글이 없는 게시물이 존재할 수 있으므로 외부조인을 사용하는것이 바람직하다)
Page<BoardListReplyDTO> searchWithReplyCount(String [] types, String keyword, Pageable pageable);
(3) BoardSearchImpl에 searchWithReplyCount() 메서드 구현
- Querydsl을 이용한 조인
- 쿼리 결과를 프로젝션을 이용해 한번에 DTO로 처리
(프로젝션 : JPQL의 결과를 즉시 DTO로 처리하는 기능) - 페이징 처리
- 테스트 코드 작성
(3-1) Querydsl을 이용한 조인 처리 구현
-게시물 기준 외부 조인 (댓글없는 게시물도 null로 출력)하고, 게시물당 처리
Querydsl 기본 문법
QMember qMember = new QMember("m");//별칭 지정
QMember qMember = QMember.member;//기본 인스턴스 사용
동일성 체크
- eq(), ne(), not() // == , != ,!= (not은 마지막에 붙여준다.)
- isNotNull() // Null이 아니면 true
원소, 범위 체크
- in(1,2,3,4), notIn(1,3,5), between(10,20) // 원소에 있는경우, 원소에 없는경우, 10 ~ 20 사이
- x.goe(y); (x >= y)
- x.gt(y); (x > y)
- x.loe(y); (x <= y)
- x.lt(y); (x < y)
그 외
- like("str%"); (like 검색)
- contains("str"); (like %str%)
- startsWith("str"); (like str%)
결과 조회
- fetch(); 리스트 조회 없으면 빈 리스트 반환
- fetchOne(); 단일 객체 반환, 없으면 null, 둘 이상이면 NonUniqueResultException
- fetchFirst(); 가장 먼저 찾는걸 반환
- fetchResults(); 페이징 정보 포함, total 쿼리 추가 실행 / 페이징이 아니라면 지양
- fetchCount(); count쿼리로 갯수 조회 (long형)
정렬
- desc(), asc()
- nullsLast(), nullsFirst() // null 데이터 순서 부여
@Override
public Page<BoardListReplyDTO> searchWithReplyCount(String[] types, String keyword, Pageable pageable) {
QBoard board=QBoard.board;
QReply reply=QReply.reply;
JPQLQuery<Board> query=from(board);
query.leftJoin(reply).on(reply.board.eq(board));
query.groupBy(board);
//from board left join reply on reply.board=board
//:게시물 기준 외부 조인 (댓글없는 게시물도 null로 출력)
//group by board
//:조인 처리 후, 게시물당 처리를 위해
return null;
}
(3-2) BoardListReplyDTO에 검색 조건 추가
//검색 조건 추가
if((types!=null&&types.length>0) && keyword!= null){
//or 조건 묶기 위한 괄호 생성
BooleanBuilder booleanBuilder = new BooleanBuilder();
for(String type : types){
switch (type) {
case "t":
booleanBuilder.or(board.title.contains(keyword));
break;
case "c":
booleanBuilder.or(board.content.contains(keyword));
break;
case "w":
booleanBuilder.or(board.writer.contains(keyword));
break;
}
}
//where (('t') or ('c') or ('w'))
query.where(booleanBuilder);
}
//bno>0
query.where(board.bno.gt(0L));
(3-3) 목록 화면에 필요한 쿼리 결과를 한번에 DTO로 처리
-프로젝션 : JPQL 결과를 즉시 DTO로 처리하는 기능
-Projections.bean() : 목록 화면에 필요한 쿼리 결과를 한번에 DTO로 처리
-select() :JPQL 쿼리를 위한 Query 인터페이스인 JPQLQuery 객체의 메서드
JPQLQuery<BoardListReplyDTO> dtoQuery = query.select(Projections.bean(BoardListReplyDTO.class,
board.bno, board.title, board.writer, board.regDate,reply.count().as("replyCount")));
(3-4) BoardListReplyDTO에 페이징 처리
Querydsl로 검색 조건과 목록 처리
Querydsl로 검색 조건을 구현하고 JPQL 생성
'제목(t), 내용(c), 작성자(w)'의 조합 + 페이징 처리
Querydsl로 검색 조건 구현
- BooleanBuilder : JPQL에 '()'를 작성하기 위한 객체
- 검색을 위한 메소드 선언과 테스트
- types [] : 여러 가지 검색 조건
- keyword : 키워드
- PageImpl를 이용한 Page<T> 반환
- PageImpl : 페이징 처리시 Page<T> 타입을 반환하는 불편함을 해소하기 위해 제공하는 클래스로
PageImpl를 이용하면 3개의 파라미터를 전달해 Page<T>를 생성한다.
- List<T> : 실제 목록 데이터
- Pageable : 페이지 관련 정보 객체
- long : 전체 개수
- PageImpl : 페이징 처리시 Page<T> 타입을 반환하는 불편함을 해소하기 위해 제공하는 클래스로
//페이징 처리
this.getQuerydsl().applyPagination(pageable, dtoQuery);
List<BoardListReplyDTO> dtoList=dtoQuery.fetch();
long count=dtoQuery.fetchCount();
//PageImpl를 이용한 Page<T> 반환
//:3개의 파라미터를 전달해 Page<T>를 생성
return new PageImpl<>(dtoList, pageable, count);
(4) 테스트 코드 작성
@Test
public void testSearchReplyCount(){
String[] types={"t", "c", "w"};
String keyword= "1";
Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
Page<BoardListReplyDTO> result = boardRepository.searchWithReplyCount(types, keyword, pageable);
log.info(result.getTotalPages());
log.info(result.getSize());
log.info(result.getNumber());
log.info(result.hasPrevious() +" :" +result.hasNext());
result.getContent().forEach(board -> log.info(board));
}
5. 서비스 구현
-BoardListReplyCountDTO : BoardRepository + BoardSearch(Impl)
-BoardDTO -> BoardListReplyCountDTO로 변경하기 위한 메서드 작성
(1) BoardService에 listWithReplyCount()메서드 작성, BoardServiceImpl에 listWithReplyCount() 메서드 구현
- 메소드 명 : listWithReplyCount()
- 파라미터 : PageRequestDTO pageRequestDTO
- 리턴 타입 : PageResponseDTO<BoardListReplyCountDTO>
-페이징처리까지 고려
PageResponseDTO<BoardListReplyDTO> listWithReplyCount(PageRequestDTO pageRequestDTO);
@Override
public PageResponseDTO<BoardListReplyDTO> listWithReplyCount(PageRequestDTO pageRequestDTO) {
//1. 리포지토리 호출
String[] types = pageRequestDTO.getTypes();
String keyword = pageRequestDTO.getKeyword();
Pageable pageable = pageRequestDTO.getPageable("bno");
Page<BoardListReplyDTO> result = boardRepository.searchWithReplyCount(types, keyword, pageable);
//2. PageResponseDTO<BoardListReplyDTO>를 생성자를 이용해 생성
//: 생성자를 호출하기위해 PageRequestDTO , dtoList, total가 필요
return PageResponseDTO.<BoardListReplyDTO>withAll()
.pageRequestDTO(pageRequestDTO)
.dtoList(result.getContent())
.total((int)result.getTotalElements())
.build();
}
6. 컨트롤러 구현
-호출하는 메서드 변경
-BoardService.list() -> BoardService.listWithReplyCount()로 변경
변경 전, 서비스에서 list() 메서드 호출
@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model){
PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
log.info(responseDTO);
model.addAttribute("responseDTO", responseDTO);
}
변경 후, 서비스에서 listWithReplyCount() 메서드 호출
@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model){
PageResponseDTO<BoardListReplyDTO> responseDTO = boardService.listWithReplyCount(pageRequestDTO);
log.info(responseDTO);
model.addAttribute("responseDTO", responseDTO);
}
7. 게시물 목록 화면 처리
변경 전, 게시물에 댓글 정보가 존재하지 않는 화면
<!--th:with을 이용해 재사용해서 게시물의 링크처리 -->
<tbody th:with="link =${pageRequestDTO.getLink()}">
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
<a th:href="|@{/board/read(bno =${dto.bno})}&${link}|"> [[${dto.title}]] </a>
</td>
<td>
[[${dto.writer}]]
</td>
<td>
<!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
</td>
</tr>
</tbody>
변경 후, 게시물에 댓글 정보가 추가된 화면
https://getbootstrap.kr/docs/5.2/components/badge/#%EC%9C%84%EC%B9%98
<table class="table">
<thead>
<tr>
<th scope="col">Bno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">RegDate</th>
</tr>
</thead>
<!--th:with을 이용해 재사용해서 게시물의 링크처리 -->
<tbody th:with="link =${pageRequestDTO.getLink()}">
<tr th:each="dto:${responseDTO.dtoList}">
<th scope="row">
[[${dto.bno}]]
</th>
<td>
<a th:href="|@{/board/read(bno =${dto.bno})}&${link}|"> [[${dto.title}]] </a>
<!-- 게시물당 댓글 정보 추가-->
<span class="badge progress-bar-success" style="background-color: #0a53be">
[[${dto.replyCount}]]
</span>
</td>
<td>
[[${dto.writer}]]
</td>
<td>
<!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
[[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
</td>
</tr>
</tbody>
Board에 독립 Reply 구현 (BoardService)
- 댓글 CRUD
- 등록
- 조회/수정/삭제/목록
1. 댓글 등록 처리
(1) ReplyService 인터페이스에 register() 선언
package org.zerock.b01.service;
import org.zerock.b01.dto.ReplyDTO;
public interface ReplyService {
Long register(ReplyDTO replyDTO);
}
(2) ReplyServiceImpl에 register() 구현
-@Service : 서비스를 담당하는 클래스를 나타내고, 빈 등록을 위한 어노테이션
-@RequiredArgsConstructor : ReplyRepository, ModelMapper 주입
-ModelMapper : DTO <-> Entity 변환을 담당하는 역할
- 메서드 명 : register()
- 파라미터 : ReplyDTO replyDTO
- 리턴 타입 : Long
- 기능 : 댓글 등록 후, 등록한 댓글 번호를 리턴한다.
- 특징 : DTO를 엔티티로 변환 후, 변환한 엔티티를 영속 컨텍스트에 저장한다.
@Override
public Long register(ReplyDTO replyDTO) {
Reply reply =modelMapper.map(replyDTO, Reply.class);
Long rno=replyRepository.save(reply).getRno();
return rno;
}
(3) 테스트 코드 작성
package org.zerock.b01.service;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.zerock.b01.dto.BoardDTO;
import org.zerock.b01.dto.ReplyDTO;
@SpringBootTest
@Log4j2
public class ReplyServiceTests {
@Autowired
private ReplyService replyService;
@Test
public void testRegister(){
ReplyDTO replyDTO = ReplyDTO.builder()
.replyText("ReplyDTO Text")
.replyer("replyer")
.bno(100L)
.build();
//댓글 번호 출력
log.info(replyService.register(replyDTO));
}
}
2. 댓글 조회/수정/삭제/목록 처리
(1) Reply 엔티티에 changeText() 메서드 추가
- replyText만 수정할 수 있도록, replyText Setter 메서드 작성
package org.zerock.b01.domain;
import com.fasterxml.jackson.databind.ser.Serializers;
import lombok.*;
import org.springframework.web.bind.annotation.GetMapping;
import javax.persistence.*;
@Entity
@Table(name = "Reply", indexes = {@Index(name = "idx_reply_board_bno", columnList = "board_bno")})
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
//참조객체를 제외하도록 exclude 속성 이용
@ToString(exclude = "board")
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rno;
//다대일의 경우 기본값이 즉시 로딩으로, 지연 로딩으로 변경
//:연관관계를 나타낼 때는 항상 LAZY로 지정
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
private String replyText;
private String replyer;
public void changeText(String replyText){
this.replyText=replyText;
}
}
(2) 조회/수정/삭제/목록 서비스 구현
메서드 명 | register() | read() | modify() | remove() | getListOfBoard() |
파라미터 | ReplyDTO replyDTO | Long rno | ReplyDTO replyDTO | Long rno | Long bno PageRequestDTO pageRequestDTO |
리턴 타입 | Long | ReplyDTO | void | void | PageResponseDTO<ReplyDTO> |
Repository | getRno() | findById() | findById() | deleteById() | listOfBoard() |
Repository 리턴타입 |
Long rno | Optional <Reply> |
Optional<Reply> | Optional <Reply> |
Page<Reply> |
페이징 처리 여부 |
X | X | X | X | O |
검색 조건 처리 여부 |
X | X | X | X | O |
ModelMapper | ReplyDTO -> Reply | Reply -> ReplyDTO |
X | X | Page<Reply>의 Reply -> List<ReplyDTO> |
해당 객체가 존재하지 않는 예외처리를 위해 Optional<T>로 리포지토리에서 받은 후, orElseThrow() 메서드 호출하는 과정을 거친다.
조회 메서드 : read()
@Override
public ReplyDTO read(Long rno){
//Reply reply = replyRepository.findById(rno);
Optional<Reply> replyOptional = replyRepository.findById(rno);
Reply reply = replyOptional.orElseThrow();
return modelMapper.map(reply, ReplyDTO.class);
}
수정 메서드 : modify()
@Override
public void modify(ReplyDTO replyDTO){
//Reply reply = replyRepository.findById(rno);
//Long rno = replyDTO.getRno();
Optional<Reply>replyOptional=replyRepository.findById(replyDTO.getRno());
Reply reply=replyOptional.orElseThrow();
reply.changeText(replyDTO.getReplyText());
replyRepository.save(reply);
}
삭제 메서드 : remove()
@Override
public void remove(Long rno){
replyRepository.deleteById(rno);
}
목록 메서드 : getListOfBoard()
-Pageable 객체 -> Page<Reply> 객체 -> List<ReplyDTO> 객체
-리턴타입 : PageResponseDTO
-PageResponseDTO를 생성자로 생성하기 위해 필요한 파라미터 : PageRequestDTO, dtoList, total
@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> result = replyRepository.listOfBoard(bno, pageable);
//페이징 처리된 Reply인 Page<Reply>의 Reply를 스트림을 이용해, Reply -> ReplyDTO로 변환
List<ReplyDTO> dtoList=result.getContent().stream().map(reply -> modelMapper.map(reply, ReplyDTO.class))
.collect(Collectors.toList());
//생성자를 이용해 PageResponseDTO 생성
//파라미터 : pageRequestDTO, dtoList, count
return PageResponseDTO.<ReplyDTO>withAll()
.pageRequestDTO(pageRequestDTO)
.dtoList(dtoList)
.total((int)result.getTotalElements())
.build();
}
(3) ReplyConroller 구현
-ReplyService 의존성 주입
//Ajax와 JSON을 이용한 비동기처리를 위한 RestController
//: 메소드의 리턴 값이 JSON/XML으로 처리된다.
@RestController
@Log4j2
//JSON -> DTO로 변환하는 어노테이션
@RequestMapping("/replies")
@RequiredArgsConstructor
public class ReplyController {
private final ReplyService replyService;
}
ReplyController의 댓글 등록/조회/수정/삭제/목록 메서드 매핑
메서드 명 | register() | read() | modify() | remove() | getListOfBoard() |
HTTP 메서드 | POST | GET | PUT | DELETE | GET |
URLPatterns | "/" | "/{rno}" | "/{rno}" | "/{rno}" | "/list/{bno}" |
consumes : JSON 처리 |
O | X | O | X | X |
파라미터 | @Valid @RequestBody replyDTO bindingResult |
rno | replyDTO | rno | bno pageRequestDTO |
예외처리 | throws BindException handleBindException |
handleSuchException | handleFKException | ||
리턴 타입 | Map<String, Long> | ReplyDTO | void | void | PageResponseDTO<ReplyDTO> |
- REST 방식 : URL이 '원하는 대상'을 의미, PUT/DELETE가 '행위나 작업'을 의미한다
- 쿼리 스트링을 이용하지 않고, 직접 주소의 일부로 사용해
하나의 자원을 하나의 주소로 유일무이 표현함으로써, 하나의 URL이 하나의 자원을 식별하는 고유값이 된다. - URI이라는 자원을 이용해 '원하는 행위/작업'을 나타내는 GET/POST/PUT/DELETE를 수행하는 방식
- 따라서, 같은 URL에 대해서 메서드에 따라 이루어지는 행위/작업이 달라진다.
- 특정한 댓글 번호에 대한 URL인 "/{rno}"에 대해서, 메서드에 따라 조회/수정/삭제 기능을 수행한다.
1. 등록 메서드 매핑
(1) register() 변경
-@RequestBody : JSON 문자열 전송하기 위한 어노테이션
-리턴타입 : ResponseEntity<Map<String,Long>> -> Map<String, Long>로 변경
-파라미터 : ReplyDTO 검증하기 위한 @Valid 어노테이션 추가, 검증 처리를 위한 BindingResult 추가
변경 전, register()
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글등록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public ResponseEntity<Map<String, Long>> register(@RequestBody ReplyDTO replyDTO){
log.info(replyDTO);
Map<String,Long> resultMap=new HashMap<>();
return ResponseEntity.ok(resultMap);
}
변경 후, register()
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies POST", notes = "POST 방식으로 댓글등록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
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> resultMap=new HashMap<>();
Long rno = replyService.register(replyDTO);
resultMap.put("rno",rno);
return resultMap;
}
(2) CustomRestAdvice에 DataIntegrityViolationException 예외 처리를 위해 handleFKException() 메서드 생성
메서드 명 | handleBindException | handleFKException | handleSuchException |
예외처리 | BindException | DataIntegrityViolationExcetpion | NoSuchElementException |
파라미터 | BindException e | Exception e | Exception e |
리턴타입 | ResponseEntity <Map<String, String>> |
ResponseEntity <Map<String, String>> |
ResponseEntity <Map<String, String>> |
특징 | JSON과 DTO 불일치 | 참조하는 FK값이 존재하지 않음 | 원하는 요소가 존재하지 않음 |
메시지 | 에러 발생 시간, 에러 메시지 | 에러 발생 시간, 에러 메시지 | 에러 발생 시간, 에러 메시지 |
리턴값 | ResponseEntity.badRequest() .body(errorMap); |
ResponseEntity.badRequest() .body(errorMap); |
ResponseEntity.badRequest() .body(errorMap); |
- DataIntegrityViolationException으로 데이터에 대한 검증오류에도 불구하고 상태코드 500 오류라고 반환
-> 상태코드 500은 서버 내부의 오류로, 서버 문제라고 Ajax는 인식한다.
-정확하게 데이터 문제라는 점을 전송하기 위해, CustomRestAdvice에 DataIntegrityViolationException 생성
-CustomRestAdvice 클래스는 @RestControllerAdvice 어노테이션을 사용한 클래스
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleFKException(Exception e) {
log.error(e);
Map<String, String> errorMap = new HashMap<>();
errorMap.put("time", ""+System.currentTimeMillis());
//DataIntegrityViolationException 발생시 메시지 출력
errorMap.put("msg", "constraint fails");
return ResponseEntity.badRequest().body(errorMap);
}
-> 400오류 리턴
2. 조회 메서드 매핑
(1) 컨트롤러 추가
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Read Reply", notes = "GET 방식으로 댓글 조회")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@GetMapping(value = "/")
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public ReplyDTO read(@PathVariable("rno") Long rno){
log.info("rno "+rno+" read");
//서비스에서 수정 메서드 호출
ReplyDTO replyDTO=replyService.read(rno);
Map<String, Long> resultMap = new HashMap<>();
return replyDTO;
//특정 댓글의 번호의 데이터가 존재하지 않는 경우
//500 에러 발생: CustomRestAdvice에서 NoSuchElementException 추가
}
(2) NoSuchElementException 예외 처리를 위해 CustomRestAdvice에 handleSuchElement 메서드 추가
//댓글 조회시 없는 댓글번호
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleSuchElement(Exception e) {
log.error(e);
Map<String, String> errorMap = new HashMap<>();
errorMap.put("time", ""+System.currentTimeMillis());
//DataIntegrityViolationException 발생시 메시지 출력
errorMap.put("msg", "No Such Element fails");
return ResponseEntity.badRequest().body(errorMap);
}
3. 수정 메서드 매핑
컨트롤러 추가
-@RequestBody : JSON 문자열 전송하기 위한 어노테이션
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies Update", notes = "PUT 방식으로 댓글 수정")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@PutMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public Map<String,Long> update(@PathVariable("rno") Long rno, @RequestBody ReplyDTO replyDTO){
log.info("rno "+rno+" update");
//파라미터로 받은 DTO에 수정할 댓글번호를 대입
replyDTO.setRno(rno);
//서비스에서 수정 메서드 호출
replyService.modify(replyDTO);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("rno",rno);
return resultMap;
}
4. 삭제 메서드 매핑
(1) 컨트롤러 추가
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies Delete", notes = "DELETE 방식으로 댓글 삭제")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@DeleteMapping(value = "/")
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public Map<String,Long> remove(@PathVariable("rno") Long rno){
log.info("rno "+rno+" remove");
//서비스에서 삭제 메서드 호출
replyService.remove(rno);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("rno",rno);
return resultMap;
}
(2) CustomRestAdvice에 NoSuchElementException 예외 처리를 위해 handleSuchException() 메서드 생성
//댓글 조회시 없는 댓글번호
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleSuchElement(Exception e) {
log.error(e);
Map<String, String> errorMap = new HashMap<>();
errorMap.put("time", ""+System.currentTimeMillis());
//DataIntegrityViolationException 발생시 메시지 출력
errorMap.put("msg", "No Such Element fails");
return ResponseEntity.badRequest().body(errorMap);
}
5. 목록 메서드 매핑
-@PathVariable : 경로에 있는 값을 사용하기 위한 어노테이션으로 나머지 경로(페이지 관련 정보)는 일반 쿼리 스트링 이용
(동적 경로는 일반 쿼리 스트링, 고정인 값은 URL로 고정)
//Swagger UI에서 출력하는 메시지
@ApiOperation(value = "Replies of Board", notes = "GET 방식으로 댓글 목록")
//consumes : 메서드를 받아서 사용하는 데이터가 어떤 데이터인지 명시하는 속성
@DeleteMapping(value = "/")
//댓글 등록 메서드로, ReplyDTO를 전달받아 키,값 객체인 ResponseEntity로 반환한다.
public PageResponseDTO<ReplyDTO> getList(@PathVariable("bno") Long bno, PageRequestDTO pageRequestDTO){
log.info("bno "+bno+" replies list");
PageResponseDTO<ReplyDTO> responseDTO = replyService.getListOfBoard(bno, pageRequestDTO);
return responseDTO;
}
-> PageResponseDTO가 JSON으로 처리된다.
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
7장-1. 다중 파일 업로드 처리 (+MultipartFile) (0) | 2022.12.09 |
---|---|
6장-3. 댓글의 자바스크립트 처리 (+Axios, @JsonFormat, @JsonIgnore) (0) | 2022.12.08 |
6장-1. Ajax와 JSON을 이용한 REST 서비스 구현 (+ swagger, BindException, BindingResult, @RestControllerAdvice) (0) | 2022.12.07 |
5장-5. 게시물 관리 프로젝트 구현 (2) CRUD (+ #temporals, #number, HistoryAPI) (0) | 2022.12.06 |
5장-4. 게시물 관리 프로젝트 구현 (1) (+ withAll, RootConfig) (0) | 2022.12.05 |