가장 먼저 간단한 게시판 기능을 구현하기로 하였다.
카테고리를 선택하고 글의 제목과 내용을 입력하여 게시물을 등록하고 조회, 수정, 삭제까지 할 수 있도록 하였다.
그 외의 이미지, 동영상 업로드나 수정 시 권한 확인 등의 문제는 차차 추가할 예정!
2021.03.22 - [study/spring boot & jpa] - RESTful API 만들기
[WEB] RESTful API 만들기
VIEW를 담당하는 컨트롤러를 제외한 모든 컨트롤러를 @RestController로 지정하여 RESTful API로 만들었다. @RestController는 별도의 View를 제공하지 않기 때문에 문제가 발생하는 상황에서 상태코드와 응
juran-devblog.tistory.com
Board
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Board extends DateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long boardNo;
@JsonManagedReference
@ManyToOne
@JoinColumn(name = "member_no")
private Member member; //작성자 정보
@JsonManagedReference
@ManyToOne
@JoinColumn(name = "category_no")
private Category category; //카테고리
@JsonBackReference
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>(); //댓글
@Column(length = 500, nullable = false)
private String boardTitle; //제목
@Column(length = 1000, nullable = false)
private String boardContent; //내용
@Column(insertable = false, columnDefinition = "int default 0", nullable = false)
private Integer boardRcmdCnt; //추천수
@Column(insertable = false, columnDefinition = "int default 0", nullable = false)
private Integer boardViewCnt; //조회수
}
- 작성자 Member와는 다대일 관계 → Board (N) : Member (1)
- 카테고리 Category와도 다대일 관계 → Board (N) : Category (1)
- 댓글 Comment과는 일대다 관계 → Board (1) : Comment (N)
Category
@Entity
@Setter @Getter
@NoArgsConstructor
@AllArgsConstructor
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long categoryNo;
@Column(length = 50, nullable = false)
private String categoryName;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
private List<Board> boards = new ArrayList<>();
}
spring data jpa인 JpaRepository 인터페이스를 구현한 각 엔티티의 레퍼지토리로 DB에 접근
서비스 객체에서 레퍼지토리를 이용하여 서비스 로직 작성 (트랜잭션 스크립트 패턴)
- 트랜잭션 스크립트 패턴 : 서비스 계층에서 대부분의 비지니스 로직 처리
- 도메인 모델 패턴 : 엔티티가 비지니스 로직을 지니고 서비스 계층은 단순히 엔티티에 필요한 요청을 위임
Service객체 어노테이션 : @Service / @RequiredArgsConstructor
Controller객체 어노테이션 : @RestController / @Controller / @RequiredArgsConstructor
@RequiredArgsConstructor
초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자 자동 생성
주로 의존성 주입(Dependency Injection) 편의성을 위해 사용
BoardRepository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
BoardRequest
@Getter @Setter
@AllArgsConstructor
@NoArgsConstructor
public class BoardRequest {
@NotNull
private String boardTitle;
@NotNull
private String boardContent;
@NotNull
private Long categoryNo;
}
글 제목과 내용, 카테고리 정보가 담긴 DTO를 따로 구성하여 글 작성과 수정 시 request body 에 담아 사용하였다.
* DTO 객체의 경우 getter와 기본 생성자는 필수
추가설명)
- VIEW 템플릿은 JSP를 이용하였으며 대부분 ajax를 통해 데이터를 호출하였다.
- json형태로 주고 받기 위해 뷰를 담당하는 컨트롤러가 아닌 API 컨트롤러들은 모두 @RestController로 선언
- 성공 시 응답 객체로 ApiResponse에 상태와 데이터를 담아 반환하였다.
ResponseEntity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApiResponse {
private boolean success; // 성공여부
private String dateTime; // 시간
private String errorCode; // 에러코드
private String detail; // 에러 메세지
private Map<String, Object> data = new HashMap<>(); // 반환할 데이터
public ApiResponse(boolean success) {
this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.success = success;
}
public ApiResponse(boolean success, String detail) {
this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.success = success;
this.detail = detail;
}
public ApiResponse(boolean success, String errorCode, String detail) {
this.dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.success = success;
this.errorCode = errorCode;
this.detail = detail;
}
public void putData(String key, Object value) {
this.data.put(key, value);
}
}
BoardService
@Transactional
public void registerBoard(BoardRequest boardRequest, Member member, Category category) {
Board board = new Board();
LocalDateTime now = LocalDateTime.now();
board.setBoardTitle(boardRequest.getBoardTitle());
board.setBoardContent(boardRequest.getBoardContent());
board.setCategory(category);
board.setRegDate(now);
board.setUpdateDate(now);
board.setMember(member);
boardRepository.save(board);
}
새로운 Board 객체를 생성하여 BoardRequest로 받아온 값을 설정해준 뒤 저장
선언적 트랜잭션 처리 @Transactional
- 하나의 트랜젹션으로 묶어서 처리
- 도중 에러 발생 시 자동으로 롤백
- 클래스에 @Transactional 선언 시 모든 메서드에 적용되나 각 메서드에 붙은 트랜잭션 어노테이션에 대한 속성을 우선으로 따름
BoardApiController
@PostMapping("register")
public ResponseEntity<?> boardRegister(@Validated @RequestBody BoardRequest boardRequest,
Authentication authentication) throws Exception {
Member member = memberService.loadUserByUsername(authentication.getPrincipal().toString());
Category category = categoryService.findByCategoryNo(boardRequest.getCategoryNo()).get();
boardService.registerBoard(boardRequest, member, category);
ApiResponse apiResponse = new ApiResponse(true, "게시글 등록");
return ResponseEntity.ok(apiResponse);
}
스프링 시큐리티를 적용하였기 때문에 현재 작성하고 있는 사용자에 대한 정보는 Authentication 객체로 받아옴
→ 해당 객체의 Principal(아이디)을 통해 member객체를 찾음
스프링 시큐리티
Authentication : 사용자 정보가 담긴 객체
- Principal : 사용자 이름(아이디)
- Credential: 비밀번호
loadUserByUsername(String username) : MemberService가 상속받은 UserDetailsService의 메서드로 오버라이딩하여 로직 구현
BoardService
@Transactional(readOnly = true)
public List<Board> findAllBoard() {
return boardRepository.findAll();
}
@Transactional(readOnly = true)
public Board findBoardByNo(Long boardNo) {
return boardRepository.findById(boardNo).orElseThrow(() -> new NoSuchElementException());
}
findAllBoard() : 게시글 목록 전체 조회
findBoardByNo(Long boardNo) : boardNo으로 해당 게시글 상세 내용 조회
(원래 지연로딩 때문에 getOne을 주로 사용하였지만 Exception핸들링에 대해서도 알아야 할 거 같아서 한번 써 보았다. getOne 사용과 에러 핸들링에 대해서는 공부가 더 필요한 것 같다..ㅜㅜ)
* api를 만들어 응답 결과를 DTO를 사용하지 않고 엔티티를 바로 받아오는 것은 매우 안좋다고 한다 (후에 수정 필요!)
- 불필요한 정보를 모두 받아옴
- 보안상의 이유
- api 스펙 변화 등등
BoardAipController
@GetMapping("list")
public ResponseEntity<?> boardListAll() throws Exception {
ApiResponse apiResponse = new ApiResponse(true, "게시글 전체 조회");
apiResponse.putData("boardList", boardService.findAllBoard());
return ResponseEntity.ok(apiResponse);
}
@GetMapping("info/{boardNo}")
public ResponseEntity<?> boardDetailInfo(@PathVariable(name = "boardNo") Long boardNo) throws Exception{
ApiResponse apiResponse = new ApiResponse(true, "게시글 상세정보 조회");
apiResponse.putData("boardInfo", boardService.findBoardByNo(boardNo));
return ResponseEntity.ok(apiResponse);
}
전체조회와 url로 넘긴 boardNo을 통해 해당 게시글의 상세 정보를 조회
Jpa 실전활용 강의를 보던 중 setter를 남발하는 것은 안좋다는 것을 보고😥 수정 기능은 엔티티에 구현하였다.
Board
public void updateBoard (String title, String content, Category category) {
this.boardTitle = title;
this.boardContent = content;
this.category = category;
}
Board 엔티티에 작성한 게시글 수정 비지니스 로직
BoardService
@Transactional
public void updateBoard(BoardRequest boardRequest, Long boardNo) {
Board board = boardRepository.getOne(boardNo);
board.updateBoard(boardRequest.getBoardTitle(),boardRequest.getBoardContent(), categoryRepository.getOne(boardRequest.getCategoryNo()));
}
boardNo으로 수정할 board를 찾고 엔티티에 작성된 메서드를 통해 내용 수정
BoardApiController
@PostMapping("edit/{boardNo}")
public ResponseEntity<?> boardEdit(@Validated @PathVariable(name = "boardNo") Long boardNo,
@RequestBody BoardRequest boardRequest) throws Exception {
boardService.updateBoard(boardRequest, boardNo);
ApiResponse apiResponse = new ApiResponse(true, "게시글 수정");
return ResponseEntity.ok(apiResponse);
}
boardNo는 url에 담아 받았고 수정할 내용은 등록과 동일하게 BoardRequest 객체로 받았다.
삭제는 jpa 덕분에 매우 간단했다.
BoardService
@Transactional
public void deleteByNo(Long boardNo) {
boardRepository.deleteById(boardNo);
}
BoardApiController
@GetMapping("delete/{boardNo}")
public ResponseEntity<?> boardDelete(@PathVariable(name = "boardNo") Long boardNo) throws Exception {
boardService.deleteByNo(boardNo);
ApiResponse apiResponse = new ApiResponse(true, "게시글 삭제");
return ResponseEntity.ok(apiResponse);
}
url에 담긴 boardNo으로 해당 게시글을 조회하고 삭제
[ERROR] 순환참조 문제 (0) | 2021.04.14 |
---|---|
[WEB] RESTful API 만들기 (0) | 2021.03.22 |
[SPRING SECURITY] 스프링 부트 환경설정 (0) | 2021.03.14 |
[SPRING SECURITY] 스프링 시큐리티란 (0) | 2021.03.14 |
[SPRING BOOT] 디렉터리 구조 (0) | 2021.03.13 |
댓글 영역