Spring Boot & REST
1. REST (REpresentational State Transfer)
: HTTP를 이용한 서버를 구현할 때 지켜야 하는 설계 원칙.
표현을 이용해서 상태를 전달한다.
- 서버와 클라이언트 사이의 결합성 감소에 목표를 둔다. -> 성능 향상, 확장성 확보, 사용 편의성 증대.
6원칙
1) Client - Server Architecture
: 클라이언트와 서버의 역할 분리
- 양측의 독립적 발전 추구.
- 서버는 데이터가 어떤 방식으로 표현되는지 몰라도 됨 & 클라이언트는 데이터가 어떻게 저장되는지 몰라도 됨.
2) Statelessness (상태를 저장하지 않는다.)
: 클라이언트가 보내는 개별적인 요청은 각각의 요청을 표현하기 충분해야 함. "독립적인 요청"
- 즉, 서버/클라이언트가 몇번째 보낸 요청인지, 이전에 보낸 요청에서 데이터가 어떤지? 기억할 필요가 없어야 함.
3) Cacheability
: 서버에서 보내주는 응답은 캐시 가능성(캐싱을 할 수 있는지!?)에 대해 표현해 주어야 한다.
(잘 바뀌지 않는 정적 자원들...을 클라이언트 쪽에 저장해두고 필요할 때 써먹는 것... > 캐싱한다...)
- Cache-Control Header
4) Layered System
: 클라이언트가 서버에 요청이 도달하기까지의 과정을 알 필요 없고, 영향 받지 않아야 한다.
5) Code On Demand (필수는 아님. Optional)
: 실행 가능한 코드를 전달함으로써 일시적으로 클라이언트의 기능을 확장
- JavaScript와 브라우저.
6) Uniform Interface
: "일관된 인터페이스"를 가지고 있어야 한다.
- REST에서 가장 근본적인 제약사항.
- 서버에 요청하고 있는 자원이 "요청 자체"로 식별이 되어야 한다. 클라이언트가 요구하는 자원이 무엇인지 명확히 표현.
GET /students/{studentId}
// 학생들 중 ID가 studentId인 자원에 대한 요청
- 자원에 대하여 조작할 때 그 자원의 상태나 표현으로 조작이 가능해야 한다. (JSON과 같은 형태로 데이터 표현.)
- 각 요청과 응답은 자기 자신을 해석하기 위한 충분한 정보를 포함해야 한다. (Content-Type 헤더로 명시)
- HAETOAS (Hypermedia As The Engine Of Application State) : 최초의 URI에 접근했을 때 이후 서버가 제공하는 모든 정보에 도달할 수 있는 방법을 알 수 있어야 한다.
** 실제로 모든 원칙을 다 갖춘다고 하기는 어렵다... 그러나 RESTful한 모습을 목표로 API를 제작해야 한다.
https://en.wikipedia.org/wiki/Richardson_Maturity_Model
2. Spring Boot로 RESTful 구현
* 우선 지켜볼 원칙 > Richardson Maturity Model Level 2
- URL을 구성할 때 URL이 자원을 논리적이게 잘 표현할 수 있도록
- 자원에 적용할 작업을 HTTP Method로 구분할 수 있도록
ex) Article Entity & ArticleDto
// Article Entity
@Entity
@NoArgsConstructor
@Getter
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String title;
@Setter
private String content;
@Setter
private String writer;
}
// ArticleDto
@Getter
@ToString
@NoArgsConstructor
public class ArticleDto {
private Long id;
private String title;
private String content;
private String writer;
public ArticleDto fromEntity(Article article) {
ArticleDto dto = new ArticleDto();
dto.id = article.getId();
dto.title = article.getTitle();
dto.content = article.getContent();
dto.writer = article.getWriter();
return dto;
}
}
1) 작업을 위한 URL이 자원을 식별할 수 있도록 구성해보자.
- 전체 게시글들에 대해 작업하기 위한 URL : /articles
- 어떤 특정 게시글에 대해 작업하기 위한 URL : /articles/{articleId}
2) 자원에 어떤 행동을 할지를 HTTP Method로 구분해보자.
- CREATE : 새로운 자원 생성. => POST Method
- READ : 이미 존재하는 자원 조회 => GET Method
- UPDATE : 이미 존재하는 자원 수정 => PUT Method (REST의 상태 표현의 관점에서 PUT은 객체의 새로운 상태를 놓겠다!라는 개념.... PATCH는 이미 있는 존재를 조금만 수정한다...의 의미이므로 PUT 메서드가 조금 더 REST의 목적에 맞다.)
- DELETE : 삭제하기 위한 자원 => DELETE Method
==> 종합해보면
게시글 생성 | POST /articles |
게시글 전체 조회 | GET /articles |
게시글 단일 조회 | GET /articles/{articleId} |
게시글 수정 | PUT /articles/{articleId} |
게시글 삭제 | DELETE /articles/{articleId} |
코드 구현
- Controller
// ArticleController class
@Slf4j
@RestController
@RequestMapping("/articles")
@RequiredArgsConstructor
public class ArticleController {
private final ArticleService service;
// 게시글 생성
@PostMapping
public ArticleDto create(@RequestBody ArticleDto dto) {
return service.create(dto);
}
// 게시글 전체 조회
@GetMapping
public List<ArticleDto> readAll() {
return service.readAll();
}
// 게시글 단일 조회
@GetMapping("/{articleId}")
public ArticleDto read(@PathVariable Long articleId) {
return service.read(articleId);
}
// 게시글 수정
@PutMapping("/{articleId}")
@Transactional
public ArticleDto update(@PathVariable Long articleId,
@RequestBody ArticleDto dto) {
return service.update(articleId, dto);
}
// 게시글 삭제
@DeleteMapping("/{articleId}")
@Transactional
public void delete(@PathVariable Long articleId) {
service.delete(articleId);
}
}
- Service
// ArticleService class
@Service
@RequiredArgsConstructor
@Slf4j
public class ArticleService {
private final ArticleRepository repository;
// 게시글 생성
public ArticleDto create(ArticleDto dto) {
Article article = new Article(dto.getTitle(), dto.getContent(), dto.getWriter());
return ArticleDto.fromEntity(repository.save(article));
}
// 게시글 전체 조회
public List<ArticleDto> readAll() {
List<ArticleDto> articleList = new ArrayList<>();
for (Article article : repository.findAll()) {
articleList.add(ArticleDto.fromEntity(article));
}
return articleList;
}
// 게시글 단일 조회
public ArticleDto read(Long id) {
Optional<Article> optionalArticle = repository.findById(id);
if (optionalArticle.isPresent()) { // 있으면
Article article = optionalArticle.get();
return ArticleDto.fromEntity(article);
} // 없으면 특정 예외를 발생시킨다.
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// 게시글 수정
public ArticleDto update(Long id, ArticleDto dto) {
Optional<Article> optionalArticle = repository.findById(id);
if (optionalArticle.isPresent()) {
Article article = optionalArticle.get();
article.setTitle(dto.getTitle());
article.setContent(dto.getContent());
article.setWriter(dto.getWriter());
return ArticleDto.fromEntity(repository.save(article));
} //없으면 특정 예외 발생
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// 게시글 삭제
public void delete(Long id) {
if (repository.existsById(id)) {
repository.deleteById(id);
} else throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
- ArticleRepository
// ArticleRepository interface
public interface ArticleRepository extends JpaRepository<Article, Long> {
}
+ 댓글 기능 구현 : 댓글은 게시글에 종속되므로 URL도 맞춰서 작성한다.
Comment Entity & CommentDto
// Comment Entity
@Entity
@Getter
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter
private String content;
@Setter
private String writer;
@Setter
@ManyToOne
private Article article;
public Comment(String content, String writer, Article article) {
this.content = content;
this.writer = writer;
this.article = article;
}
}
// CommentDto
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CommentDto {
private Long id;
@Setter
private String content;
@Setter
private String writer;
// static factory method
public static CommentDto fromEntity(Comment comment) {
return new CommentDto(
comment.getId(),
comment.getContent(),
comment.getWriter());
}
}
게시글에 댓글 추가 | POST /articles/{articleId}/comments |
게시글의 전체 댓글 조회 | GET /articles/{articleId}/comments |
댓글 수정 | PUT /articles/{articleId}/comments/{commentId} |
댓글 삭제 | DELETE /articles/{articleId}/comments/{commentId} |
코드 구현
- Controller
// CommentController class
@Slf4j
@RestController
@RequestMapping("articles/{articleId}/comments")
@RequiredArgsConstructor
public class CommentController {
private final CommentService service;
// 게시글에 댓글 추가
@PostMapping
public CommentDto createComment(@PathVariable Long articleId,
@RequestBody CommentDto dto) {
return service.createComment(articleId, dto);
}
// 게시글의 전체 댓글 조회
@GetMapping
public List<CommentDto> readAll(@PathVariable Long articleId) {
return service.readAll(articleId);
}
// 댓글 수정
@PutMapping("/{commentId}")
public CommentDto updateComment(@PathVariable Long articleId,
@PathVariable Long commentId,
@RequestBody CommentDto dto) {
return service.update(articleId, commentId, dto);
}
// 댓글 삭제
@DeleteMapping("/{commentId}")
public void deleteComment(@PathVariable Long articleId,
@PathVariable Long commentId) {
service.delete(articleId, commentId);
}
}
- Service
// CommentService class
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final ArticleRepository articleRepository;
// 게시글에 댓글 추가
public CommentDto createComment(Long articleId, CommentDto dto) {
Optional<Article> optionalArticle = articleRepository.findById(articleId);
if (optionalArticle.isPresent()) {
Comment newComment = new Comment(dto.getContent(), dto.getWriter(), optionalArticle.get());
return CommentDto.fromEntity(commentRepository.save(newComment));
} else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
// 댓글 전체 조회
public List<CommentDto> readAll(Long articleId) {
// 게시글 존재 여부에 따른 에러 반환.
if (!articleRepository.existsById(articleId)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
List<CommentDto> commentDtoList = new ArrayList<>();
for (Comment comment : commentRepository.findAllByArticleId(articleId)) {
commentDtoList.add(CommentDto.fromEntity(comment));
}
return commentDtoList;
}
// 댓글 수정하기
public CommentDto update(Long articleId, Long commentId, CommentDto dto) {
// 수정대상 댓글이 존재하기는 하는지 ?
Optional<Comment> optionalComment = commentRepository.findById(commentId);
// 없으면 404
if (optionalComment.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
Comment comment = optionalComment.get();
// 댓글 - 게시글 불일치
if (!articleId.equals(comment.getArticle().getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
comment.setContent(dto.getContent());
comment.setWriter(dto.getWriter());
return CommentDto.fromEntity(commentRepository.save(comment));
}
// 댓글 삭제하기
public void delete(Long articleId, Long commentId) {
Optional<Comment> optionalComment = commentRepository.findById(commentId);
if (optionalComment.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
Comment comment = optionalComment.get();
if (!articleId.equals(comment.getArticle().getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
commentRepository.deleteById(commentId);
}
}
- Repository
// CommentRepository Interface
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findAllByArticleId(Long articleId);
}
'Programming > Spring, SpringBoot' 카테고리의 다른 글
File Handling (정적파일과 Multipart/form-data) (0) | 2024.01.22 |
---|---|
Query Parameter (0) | 2024.01.22 |
연습문제(로깅) (0) | 2024.01.15 |
Profiles (0) | 2024.01.15 |
Logging (0) | 2024.01.15 |