[Spring Boot] RESTful

익선·2024년 7월 17일
0

스프링부트

목록 보기
2/8

1. RESTful이란

RESTful(REpresentational State Transfer) 이란 HTTP를 이용한 서버를 구현할 때 지켜야하는 설계 원칙을 의미하며 서버와 클라이언트 사이의 결합성 감소에 목표를 둔다. RESTful 에는 다음 6가지 원칙 이 있다.

(1) Server-Client Architecture

서버와 클라이언트의 역할 분리(양측의 독립적인 발전 추구)
즉, 서버는 데이터가 어떤 방식으로 표현(인터페이스)되는지 몰라도 되고, 클라이언트도 데이터가 어떻게 저장되는지 몰라도 된다.

(2) Stateless

서버는 클라이언트가 보낸 요청이 몇번쨰 요청인지, 이전에 보낸 요청에 데이터가 어떤지 기억할 필요가 없어야 한다.
즉, 클라이언트 측의 여러 요청에 대하여 각각 다르게 반응하도록 만든다는것이 상태를 저장하지 않는다는 제약사항이다.

(3) Cacheability

서버가 보내주는 응답은 클라이언트에게 명시적 또는 암시적으로 그 응답이 캐싱 가능한 응답인지 표현해주어야 한다.

  • 클라이언트 측에서 굳이 서버한테 데이터(ex. 잘 변하지 않는 네이버 로고)를 요구하지 않고, 클라이언트측에서 서버에서 보내준 응답을 캐시에 저장해놓고 나중에 필요할 떄 사용하는 방식을 채택한다.

(4) Layered System

일반적으로 사용자가 많은 서비스의 경우 실제 서버 인스턴스를 하나 이상 구동하는 경우가 있는데, 이를 유연하게 작동시키기 위해 Gateway, Proxy, Load Balancer 등의 부수적인 서버를 구축하는 경우가 많다. 이 제약사항은 클라이언트가 서버에 요청을 보내기 위해 이런 구조에 대해서 알 필요가 없어야 한다는 제약사항이다.

(5) Code on Demand (Optional)

실행 가능한 코드를 전달함으로서 일시적으로 클라이언트의 기능을 확장

(6) Uniform Interface

서버가 요청을 받는 형식에 있어서 일관된 인터페이스를 가지고 있어야 한다.

1) 서버에 요청하고 있는 자원이 요청 자체로 식별이 되어야 한다.

// 학생들 중 ID가 studentId인 자원에 대한 요청
GET /students/{studentId}

2) 자원에 대하여 조작을 할 떄, 그 자원의 상태나 표현(ex. JSON)으로 조작이 가능하여야 한다.

{
	"name" : "John",
	"age" : 30,
	"isStudent" : true,
	"address" :  {
		"street" : "123 Main st",
		"city" : "New York",
	},
	"hobbies" : ["reading" , "gaming", "traveling"]
}

3) 각 요청과 응답은 자기 자신을 해석하기 위한 충분한 정보를 포함하여야 한다.

// JSON 형태로  데이터를 보냈다면, 해당 내용이 요청에 명시되어야 한다.
// ContentType 헤더를 주로 사용
Content-Type: application/json

4) HAETOAS, Hypermedia AS The Engine Of Application State

잘 구현된 REST는 최초의 URL에 접근했을 때, 이후 서버가 제공하는 모든 정보에 도달 할 수 있는 방법을 알 수 있어야 한다.

// 책에 대한 정보 
{
  "id": 123,
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "links": [
    {
      "rel": "self",
      "href": "/books/123"
    },
    {
			// 작가에 대한 정보 API
      "rel": "author",
      "href": "/authors/456" 
    },
    {
			// 리뷰에 대한 정보 API
      "rel": "reviews",
      "href": "/books/123/reviews"
    }
  ]
}

2. RESTful 구현

(1) Entity, DTO

- ArticleEntity

Service 에서 게시글을 생성할 때 사용자로부터 받은 데이터를 일일히 Setter 메서드로 설정하는 대신 Article 엔티티 클래스에 매개변수 3개를 받는 생성자를 추가하여 생성자를 통해 데이터를 효율적으로 초기화 한다.

@ManyToOne(관계의 주) 측 Comment 엔티티의 관계 필드(Article article)@OneToManyArticle 엔티티를 참조할 수 있도록 관계 필드(List <CommentDto> comments)mappedBy = "article" 으로 지정해준다.

/**
 * CREATE TABLE article(
 *     id INTEGER PRIMARY KEY AUTOINCREMENT,
 *     title TEXT,
 *     content TEXT,
 *     writer TEXT
 * )
 */

@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Setter
    private String title;
    @Setter
    @Lob // Large Object
    private String content;
    @Setter
    private String writer;
    
    @OneToMany(mappedBy = "article")
    private final List<Comment> comments = new ArrayList<>();

    public Article(String title, String content, String writer) {
        this.title = title;
        this.content = content;
        this.writer = writer;
    }
}

- CommentEntity

@Getter
@Entity
@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;
}

- ArticleDto

@AllArgsConstructor 애너테이션을 추가해 객체 생성 시 필드를 효과적으로 초기화

@Getter
// 엔티티에 ToStrin 을 붙이지 않고 DTO에만 붙인다.
// -> 엔티티간 관계 형성 시 순한 참조 문제가 발생하기 때문
@NoArgsConstructor // JPA 연동 시 기본 생성자 필요 
@AllArgsConstructor // 모든 필드를 인자로 받는 생성자 자동 생성
public class ArticleDto {
    private Long id;
    @Setter
    private String title;
    @Setter
    private String content;
    @Setter
    private String writer;
    
    private List<CommentDto> comments = new ArrayList<>();
    
    public ArticleDto(Long id, String title, String content, String writer) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.writer = writer;
    }

    // Static Factory Method
    public static ArticleDto fromEntity(Article entity) {
        return new ArticleDto(
                entity.getId(),
                entity.getTitle(),
                entity.getContent(),
                entity.getWriter()
        );
    }
    
}

- CommentDto

@Getter
@NoArgsConstructor
public class CommentDto {
    @Id
    private Long id;
    @Setter
    private String content;
    @Setter
    private String writer;

    public CommentDto(Long id, String content, String writer) {
        this.id = id;
        this.content = content;
        this.writer = writer;
    }

    private static CommentDto fromEntity(Comment entity) {
        return new CommentDto(
                entity.getId(),
                entity.getContent(),
                entity.getWriter()
        );
    }

}

(2) URL

// CRUD를 위한 URL과 Method 틀잡기
POST /articles  		// 게시글 생성
GET /articles			// 게시글 전체 조회
GET /articles/{id}		// 게시글 단일 조회
PUT /articles/{id}		// 게시글 수정
DELETE /articles/{id}	// 게시글 삭제

POST /articles/{articleId}/comments 				// 게시글에 댓글 추가
GET /articles/{articleId}/comments 					// 게시글 댓글 전체 조회
PUT /articles/{articleId}/comments/{commentId}		// 게시글 댓글 수정
DELETE /articles/{articleId}/comments/{commentId}	// 게시글 댓글 삭제

(3) Repo

public interface ArticleRepo extends JpaRepository<Article, Long> {
}

public interface CommentRepo extends JpaRepository<Comment, Long> {
}

(4) Controller, Service

- CREATE

Article

// 뷰가 아니라 모든 메서드의 반환형에 ResponseBody 애너테이션 (데이터 그 자체)
@RestController 
@RequiredArgsConstructor
@RequestMapping("/articles")// 앞쪽 부분 url 매핑
public class ArticleController {
    private final ArticleService articleService;

    @PostMapping
    public ArticleDto create(
            @RequestBody
            ArticleDto dto
    ) {
        return articleService.create(dto);
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepo articleRepo;

    // CREATE
    public ArticleDto create(ArticleDto dto) {
        Article newArticle = new Article (
                dto.getTitle(),
                dto.getContent(),
                dto.getWriter()
        );
        // article 엔티티 객체를 저장한 후 dto로 만들어 반환
        return ArticleDto.fromEntity(articleRepo.save(newArticle));
    }
}

Comment

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles/{articleId}/comments")
public class CommentController {
    private final CommentService commentService;
	// ...
    @PostMapping
    public CommentDto craate(
            @PathVariable("articleId")
            Long articleId,
            @RequestBody
            CommentDto dto
    ) {
        return commentService.create(articleId, dto);
    }
	// ...
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
    private final ArticleRepository articleRepository;
    private final CommentRepository commentRepository;
	// ...
	public CommentDto createComment(Long articleId, CommentDto dto) {
        Optional<Article> optionalArticle
                = articleRepository.findById(articleId);
        if (optionalArticle.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);

        Comment newComment = new Comment();
        newComment.setWriter(dto.getWriter());
        newComment.setContent(dto.getContent());
        newComment.setArticle(optionalArticle.get());
        newComment = commentRepository.save(newComment);
        return CommentDto.fromEntity(newComment);
    }
	// ...    
}

- READ ALL

Article

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles")
public class ArticleController {
    private final ArticleService articleService;
    ... 
    
    @GetMapping
    public List<ArticleDto> readAll() {
        return articleService.readAll();
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepo articleRepo;
    ...
    
    public List<ArticleDto> readAll() {
        List<ArticleDto> articleDtoList = new ArrayList<>();
        List<Article> articles = articleRepo.findAll();
        for (Article entity : articles) {
            articleDtoList.add(ArticleDto.fromEntity(entity));
        }
        return articleDtoList;
    }
}

Comment

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles/{articleId}/comments")
public class CommentController {
    private final CommentService commentService;
	// ...
    @GetMapping
    public List<CommentDto> readAll(
            @PathVariable("articleId") Long articleId
    ) {
        return service.readCommentAll(articleId);
    }

	// ...
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
    private final ArticleRepository articleRepository;
    private final CommentRepository commentRepository;
	// ...
	public List<CommentDto> readAll(Long articleId) {
        if (!articleRepository.existsById(articleId))
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        List<CommentDto> commentList = new ArrayList<>();
        // Query Method
        for (Comment entity: commentRepository
                .findAllByArticleId(articleId)) {
            commentList.add(CommentDto.fromEntity(entity));
        }
        return commentList;
    }
	// ...    
}

- READ ONE

  • @PathVariable URL 경로의 매개변수를 변수로 처리하기 위해 사용
  • 특정 id 에 해당하는 게시글이 DB에 없다면, @ResponseStatueException 예외를 발생 시켜서 404 Not Found 상태 코드를 반환한다.

Article

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles")
public class ArticleController {
    private final ArticleService articleService;
    ... 
    
    @GetMapping("/{id}")
    public ArticleDto read(
            @PathVariable("id")
            Long id
    ) {
        return articleService.readArticle(id);
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepo articleRepo;
    ...
    
    public ArticleDto readArticle(Long id) {
        Optional<Article> optionalArticle = articleRepo.findById(id);
        if (optionalArticle.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return ArticleDto.fromEntity(optionalArticle.get());
    }
}

Comment

생략

- UPDATE

Article

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles")
public class ArticleController {
    private final ArticleService articleService;
    ... 
    
    @PutMapping("/{id}")
    public ArticleDto update(
            @PathVariable("id")
            Long id,
            @RequestBody
            ArticleDto dto

    ) {
        return articleService.updateArticle(id, dto);
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepo articleRepo;
    ...
    
    public ArticleDto updateArticle(Long id, ArticleDto dto) {
        Optional<Article> optionalArticle = articleRepo.findById(id);
        if (optionalArticle.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        Article targetArticle = optionalArticle.get();
        targetArticle.setTitle(dto.getTitle());
        targetArticle.setContent(dto.getContent());
        targetArticle.setWriter(dto.getWriter());
        return ArticleDto.fromEntity(articleRepo.save(targetArticle));
    }
}

Comment

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles/{articleId}/comments")
public class CommentController {
    private final CommentService commentService;
	// ...
    @PutMapping("/{commentId}")
    public CommentDto update(
            @PathVariable("articleId") Long articleId,
            @PathVariable("commentId") Long commentId,
            @RequestBody CommentDto dto
    ) {
        return service.updateComment(articleId, commentId, dto);
    }
	// ...
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
    private final ArticleRepository articleRepository;
    private final CommentRepository commentRepository;
	// ...
	public CommentDto updateComment(
            Long articleId,
            Long commentId,
            CommentDto dto
    ) {
        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);

        comment.setContent(dto.getContent());
        comment.setWriter(dto.getWriter());
        return CommentDto.fromEntity(commentRepository.save(comment));
    }
	// ...    
}

- DELETE

Article

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles")
public class ArticleController {
    private final ArticleService articleService;
    ... 
    
    @DeleteMapping("/{id}")
    public void delete(
            @PathVariable("id")
            Long id
    ) {
        articleService.deleteArticle(id);
    }
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepo articleRepo;
    ...
    
    public void deleteArticle(Long id) {
        if (!articleRepo.existsById(id)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND)
        }
        articleRepo.deleteById(id);
    }
}

Comment

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles/{articleId}/comments")
public class CommentController {
    private final CommentService commentService;
	// ...
    @DeleteMapping("/{commentId}")
    public void delete(
            @PathVariable("articleId") Long articleId,
            @PathVariable("commentId") Long commentId
    ) {
        service.deleteComment(articleId, commentId);
    }
	// ...
}
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
    private final ArticleRepository articleRepository;
    private final CommentRepository commentRepository;
	// ...
	public void deleteComment(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);
    }
	// ...    
}
profile
꾸준히 기록하는 사람

0개의 댓글