https://wooncloud.tistory.com/161
Spring 기본 시작 - 세팅부터 CRUD API 까지
1. 자바 설치 (Java JDK 17 이상 설치)Java 8 (2014년 출시)여전히 많은 기업에서 레거시 시스템으로 사용 중람다 표현식, Stream API 등 중요한 기능이 도입된 버전장기적으로는 점차 마이그레이션하는 추
wooncloud.tistory.com
https://wooncloud.tistory.com/162
Spring 기본 - Service 계층
이전 내용https://wooncloud.tistory.com/161 Spring 기본 시작 - 세팅부터 CRUD API 까지1. 자바 설치 (Java JDK 17 이상 설치)Java 8 (2014년 출시)여전히 많은 기업에서 레거시 시스템으로 사용 중람다 표현식, Stream
wooncloud.tistory.com
https://wooncloud.tistory.com/163
Spring 기본 - DTO (Data Transfer Object) 패턴
https://wooncloud.tistory.com/161 Spring 기본 - Service 계층이전 내용https://wooncloud.tistory.com/161 Spring 기본 시작 - 세팅부터 CRUD API 까지1. 자바 설치 (Java JDK 17 이상 설치)Java 8 (2014년 출시)여전히 많은 기업에
wooncloud.tistory.com
가장 기본적인 게시판 기능을 만들어볼까 한다.
1. 게시판이고 글, 댓글 을 달 수 있다.
2. 스크롤 페이징
3. 회원만 글과 댓글을 달 수 있음
4. 조회는 비회원 누구나 가능
5. 회원가입과 회원은 JWT 사용.
위와 같은 기본 기능을 만들려고 하고, front를 따로 둔 순수 api 서버로서 역할을 하려고 한다.
먼저 게시판 기능을 위해, Post, Comment 기능을 만들고자 한다.
테이블 구성
- users 테이블에는 user_id와 password를 넣어 정말 계정으로서 역할을 할 수 있게 한다.
- posts 테이블과 comments 테이블을 만든다.
이런 테이블 구성은 entity로 만들고 하이버네이트로 테이블을 생성할 수 있도록 만들어 테스트 할 예정이다.
1. Post Entity
Post Entity 생성한다. entity 패키지에 Post.java 생성
package com.example.demo_api.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author; // 작성자
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments; // 댓글들
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt; // null이면 삭제되지 않음, 값이 있으면 삭제됨
// 기본 생성자
public Post() {}
// 생성자
public Post(String title, String content, User author) {
this.title = title;
this.content = content;
this.author = author;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// getter, setter 추가
}
getter, setter는 알아서 만드시구요..
여기서 이전 내용에서 나오지 않은 관계 매핑이 있다.
@ManyToOne 관계 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author;
- 다대일 관계: 여러 게시글 → 한 명의 작성자
- LAZY: 필요할 때만 User 정보를 로딩 (성능 최적화)
- user_id 외래키로 연결
@OneToMany 관계 설정
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments;
- 일대다 관계: 한 게시글 → 여러 댓글
- mappedBy = "post": Comment 엔티티의 post 필드가 주인
- cascade = ALL: 게시글 삭제시 댓글도 함께 삭제
- orphanRemoval = true: 댓글이 게시글에서 제거되면 DB에서도 삭제
그리고 Post와 Comment는 deletedAt를 넣음으로서 soft delete 정책을 가져가려고 한다.
- 실수로 삭제한 데이터를 복구할 수 있음
- 삭제된 데이터도 통계나 분석에 활용 가능
- 외래키 참조 무결성 문제 해결 (댓글이 있는 게시글을 삭제할 때)
2. Comment Entity
Comment Entity 생성한다. entity 패키지에 Comment.java 생성.
package com.example.demo_api.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User author; // 댓글 작성자
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post; // 어떤 게시글의 댓글인지
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt; // Soft Delete
// 기본 생성자
public Comment() {}
// 생성자
public Comment(String content, User author, Post post) {
this.content = content;
this.author = author;
this.post = post;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// getter, setter..
}
Comment는 아래와 같은 특징을 가진다.
- Post와 User 둘 다와 다대일 관계
- 한 댓글은 하나의 게시글에만 속함
- 한 댓글은 한 명의 작성자만 가짐
3. Repository 인터페이스 생성
여기서는 최대한 JPA의 기본 함수들을 최대한 활용하는 방향으로 Repository를 만들어보려 한다.
Spring Data JPA가 제공하는 쿼리 메소드 네이밍 규칙을 활용하면 별도의 쿼리를 작성하지 않아도 된다.
PostRepository.java
package com.example.demo_api.repository;
import com.example.demo_api.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface PostRepository extends JpaRepository<Post, Long> {
// === JPA 기본 함수들 ===
// save() - 생성/수정
// findById() - ID로 조회
// findAll() - 전체 조회
// deleteById() - 삭제 (실제로는 Soft Delete로 처리)
// existsById() - 존재 여부 확인
// === Soft Delete를 위한 최소한의 커스텀 쿼리만 ===
@Query("SELECT p FROM Post p WHERE p.deletedAt IS NULL ORDER BY p.createdAt DESC")
Page<Post> findAllNotDeleted(Pageable pageable);
@Query("SELECT p FROM Post p WHERE p.id = :id AND p.deletedAt IS NULL")
Optional<Post> findByIdAndNotDeleted(@Param("id") Long id);
// === JPA 메소드 이름 규칙 활용 ===
// 특정 사용자의 게시글 (deletedAt이 null인 것만)
List<Post> findByAuthorIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long authorId);
// 제목으로 검색
Page<Post> findByTitleContainingAndDeletedAtIsNullOrderByCreatedAtDesc(String keyword, Pageable pageable);
// 존재 여부 확인 (삭제되지 않은 것만)
boolean existsByIdAndDeletedAtIsNull(Long id);
}
Spring Data JPA 메소드 네이밍 규칙
- findBy: SELECT 쿼리
- AuthorId: author.id 필드로 조건
- And: AND 조건
- DeletedAtIsNull: deletedAt IS NULL 조건
- OrderBy: 정렬
- CreatedAtDesc: createdAt 내림차순
이렇게 메소드 이름만으로도 복잡한 쿼리를 만들 수 있다. 개 신기함..
CommentRepository.java
package com.example.demo_api.repository;
import com.example.demo_api.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface CommentRepository extends JpaRepository<Comment, Long> {
// === JPA 기본 함수들 ===
// save() - 생성/수정
// findById() - ID로 조회
// deleteById() - 삭제 (실제로는 Soft Delete로 처리)
// === JPA 메소드 이름 규칙만 사용 ===
// 특정 게시글의 댓글들
List<Comment> findByPostIdAndDeletedAtIsNullOrderByCreatedAtAsc(Long postId);
// 특정 사용자의 댓글들
List<Comment> findByAuthorIdAndDeletedAtIsNullOrderByCreatedAtDesc(Long authorId);
// ID로 조회 (삭제되지 않은 것만)
Optional<Comment> findByIdAndDeletedAtIsNull(Long id);
// 댓글 개수
long countByPostIdAndDeletedAtIsNull(Long postId);
// 존재 여부 확인
boolean existsByIdAndDeletedAtIsNull(Long id);
}
여기서 Page과 Optional이 보이는데 간단히 설명을 하자면..
Page<T> (페이징 처리)
Spring Data에서 제공하는 페이징 결과를 담는 컨테이너이다. Page<T> 안에 있는 함수들을 살펴보면 아래와 같다.
Page<Post> posts = postRepository.findAllNotDeleted(pageable);
// Page가 제공하는 정보들
posts.getContent(); // 실제 데이터 (List<Post>)
posts.getTotalElements(); // 전체 데이터 개수
posts.getTotalPages(); // 전체 페이지 수
posts.getNumber(); // 현재 페이지 번호 (0부터 시작)
posts.getSize(); // 페이지 크기
posts.hasNext(); // 다음 페이지 존재 여부
posts.hasPrevious(); // 이전 페이지 존재 여부
posts.isFirst(); // 첫 번째 페이지인지
posts.isLast(); // 마지막 페이지인지
예시
// Controller에서
@GetMapping("/posts")
public Page<PostDto> getPosts(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
return postService.getAllPosts(pageable);
}
세상 많이 좋아졌다.. 라떼는 페이징 하나 만들라면.. 하루이틀 걸렸는ㄷ..
Optional<T> (null 안전성)
이거는 Java 8에서 제공되는 null 처리 컨테이너이다.
예전에는 null 체크를 위해 if문을 남발했는데, Optional을 사용하면 훨씬 안전하고 깔끔하게 처리할 수 있다.
Optional<Post> post = postRepository.findByIdAndNotDeleted(1L);
// Optional 메소드들
post.isPresent(); // 값이 있는지 확인
post.isEmpty(); // 값이 없는지 확인
post.get(); // 값 가져오기 (위험함 - NoSuchElementException 가능)
post.orElse(null); // 값이 없으면 기본값 반환
post.orElseThrow(() -> new PostNotFoundException()); // 값이 없으면 예외 발생
예시
// Service에서
public PostDto getPost(Long id) {
Post post = postRepository.findByIdAndNotDeleted(id)
.orElse(null);
return post != null ? convertToDto(post) : null;
}
// 또는
public PostDto getPost(Long id) {
return postRepository.findByIdAndNotDeleted(id)
.map(this::convertToDto)
.orElse(null);
}
Optional을 사용하면 명시적으로 "값이 없을 수 있음"을 표현할 수 있고 NullPointerException를 방지할 수 있다.
4. DTO
PostDto.java
package com.example.demo_api.dto;
import java.time.LocalDateTime;
public class PostDto {
private Long id;
private String title;
private String content;
private String authorName;
private Long authorId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int commentCount; // 댓글 개수
// 기본 생성자
public PostDto() {}
// 생성자
public PostDto(Long id, String title, String content, String authorName,
Long authorId, LocalDateTime createdAt, LocalDateTime updatedAt,
int commentCount) {
this.id = id;
this.title = title;
this.content = content;
this.authorName = authorName;
this.authorId = authorId;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.commentCount = commentCount;
}
// getter, setter..
}
PostCreateDto.java
package com.example.demo_api.dto;
public class PostCreateDto {
private String title;
private String content;
private Long authorId; // 나중에 JWT에서 가져올 예정
// 생성자, getter, setter
}
PostUpdateDto.java
package com.example.demo_api.dto;
public class PostUpdateDto {
private String title;
private String content;
// 생성자, getter, setter
}
CommentDto.java
package com.example.demo_api.dto;
import java.time.LocalDateTime;
public class CommentDto {
private Long id;
private String content;
private String authorName;
private Long authorId;
private Long postId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 기본 생성자
public CommentDto() {}
// 생성자
public CommentDto(Long id, String content, String authorName, Long authorId,
Long postId, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.content = content;
this.authorName = authorName;
this.authorId = authorId;
this.postId = postId;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// getter, setter...
}
CommentCreateDto.java
package com.example.demo_api.dto;
public class CommentCreateDto {
private String content;
private Long postId;
private Long authorId; // 나중에 JWT에서 가져올 예정
// 생성자, getter, setter
}
CommentUpdateDto.java
package com.example.demo_api.dto;
public class CommentUpdateDto {
private String content;
// 생성자, getter, setter
}
5. Service
PostService.java
package com.example.demo_api.service;
import com.example.demo_api.dto.*;
import com.example.demo_api.entity.Post;
import com.example.demo_api.entity.User;
import com.example.demo_api.repository.PostRepository;
import com.example.demo_api.repository.UserRepository;
import com.example.demo_api.repository.CommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private CommentRepository commentRepository;
// Entity -> DTO 변환
private PostDto convertToDto(Post post) {
int commentCount = (int) commentRepository.countByPostIdAndDeletedAtIsNull(post.getId());
return new PostDto(
post.getId(),
post.getTitle(),
post.getContent(),
post.getAuthor().getName(),
post.getAuthor().getId(),
post.getCreatedAt(),
post.getUpdatedAt(),
commentCount
);
}
// 전체 게시글 조회 (페이징)
public Page<PostDto> getAllPosts(Pageable pageable) {
Page<Post> posts = postRepository.findAllNotDeleted(pageable);
return posts.map(this::convertToDto);
}
// 게시글 생성
public PostDto createPost(PostCreateDto postCreateDto) {
User author = userRepository.findById(postCreateDto.getAuthorId()).orElse(null);
if (author == null) {
return null; // 사용자가 존재하지 않음
}
Post post = new Post(postCreateDto.getTitle(), postCreateDto.getContent(), author);
Post savedPost = postRepository.save(post);
return convertToDto(savedPost);
}
// 특정 게시글 조회
public PostDto getPostById(Long id) {
Post post = postRepository.findByIdAndNotDeleted(id).orElse(null);
return post != null ? convertToDto(post) : null;
}
// 게시글 수정
public PostDto updatePost(Long id, PostUpdateDto postUpdateDto) {
Post post = postRepository.findByIdAndNotDeleted(id).orElse(null);
if (post != null) {
post.setTitle(postUpdateDto.getTitle());
post.setContent(postUpdateDto.getContent());
post.setUpdatedAt(LocalDateTime.now());
Post savedPost = postRepository.save(post);
return convertToDto(savedPost);
}
return null;
}
// 게시글 삭제 (Soft Delete)
public boolean deletePost(Long id) {
Post post = postRepository.findByIdAndNotDeleted(id).orElse(null);
if (post != null) {
post.setDeletedAt(LocalDateTime.now());
postRepository.save(post);
return true;
}
return false;
}
// 특정 사용자의 게시글들 조회
public List<PostDto> getPostsByUserId(Long userId) {
List<Post> posts = postRepository.findByAuthorIdAndDeletedAtIsNullOrderByCreatedAtDesc(userId);
return posts.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
// 제목으로 검색
public Page<PostDto> searchPostsByTitle(String keyword, Pageable pageable) {
Page<Post> posts = postRepository.findByTitleContainingAndDeletedAtIsNullOrderByCreatedAtDesc(keyword, pageable);
return posts.map(this::convertToDto);
}
}
CommentService.java
package com.example.demo_api.service;
import com.example.demo_api.dto.*;
import com.example.demo_api.entity.Comment;
import com.example.demo_api.entity.Post;
import com.example.demo_api.entity.User;
import com.example.demo_api.repository.CommentRepository;
import com.example.demo_api.repository.PostRepository;
import com.example.demo_api.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
// Entity -> DTO 변환
private CommentDto convertToDto(Comment comment) {
return new CommentDto(
comment.getId(),
comment.getContent(),
comment.getAuthor().getName(),
comment.getAuthor().getId(),
comment.getPost().getId(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}
// 특정 게시글의 댓글들 조회
public List<CommentDto> getCommentsByPostId(Long postId) {
List<Comment> comments = commentRepository.findByPostIdAndDeletedAtIsNullOrderByCreatedAtAsc(postId);
return comments.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
// 댓글 생성
public CommentDto createComment(CommentCreateDto commentCreateDto) {
User author = userRepository.findById(commentCreateDto.getAuthorId()).orElse(null);
Post post = postRepository.findByIdAndNotDeleted(commentCreateDto.getPostId()).orElse(null);
if (author == null || post == null) {
return null; // 사용자나 게시글이 존재하지 않음
}
Comment comment = new Comment(commentCreateDto.getContent(), author, post);
Comment savedComment = commentRepository.save(comment);
return convertToDto(savedComment);
}
// 댓글 조회
public CommentDto getCommentById(Long id) {
Comment comment = commentRepository.findByIdAndDeletedAtIsNull(id).orElse(null);
return comment != null ? convertToDto(comment) : null;
}
// 댓글 수정
public CommentDto updateComment(Long id, CommentUpdateDto commentUpdateDto) {
Comment comment = commentRepository.findByIdAndDeletedAtIsNull(id).orElse(null);
if (comment != null) {
comment.setContent(commentUpdateDto.getContent());
comment.setUpdatedAt(LocalDateTime.now());
Comment savedComment = commentRepository.save(comment);
return convertToDto(savedComment);
}
return null;
}
// 댓글 삭제 (Soft Delete)
public boolean deleteComment(Long id) {
Comment comment = commentRepository.findByIdAndDeletedAtIsNull(id).orElse(null);
if (comment != null) {
comment.setDeletedAt(LocalDateTime.now());
commentRepository.save(comment);
return true;
}
return false;
}
// 특정 사용자의 댓글들 조회
public List<CommentDto> getCommentsByUserId(Long userId) {
List<Comment> comments = commentRepository.findByAuthorIdAndDeletedAtIsNullOrderByCreatedAtDesc(userId);
return comments.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
}
6. Controller
RESTful API 설계
- GET /api/posts: 전체 조회
- GET /api/posts/{id}: 특정 게시글 조회
- POST /api/posts: 생성
- PUT /api/posts/{id}: 수정
- DELETE /api/posts/{id}: 삭제
- GET /api/posts/search: 검색
@RequestParam 와 @PathVariable
- @PathVariable: URL 경로의 일부 (예: /posts/{id}의 id)
- @RequestParam: 쿼리 파라미터 (예: ?page=0&size=10)
기타 참고사항
- 기본값 설정: defaultValue로 클라이언트가 값을 안 보내도 기본값 사용
- 페이징 처리: PageRequest.of()로 Pageable 객체 생성
PostController.java
package com.example.demo_api.controller;
import com.example.demo_api.dto.*;
import com.example.demo_api.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/posts")
public class PostController {
@Autowired
private PostService postService;
// 전체 게시글 조회 (페이징)
@GetMapping
public Page<PostDto> getAllPosts(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
return postService.getAllPosts(pageable);
}
// 특정 게시글 조회
@GetMapping("/{id}")
public PostDto getPostById(@PathVariable Long id) {
return postService.getPostById(id);
}
// 게시글 생성
@PostMapping
public PostDto createPost(@RequestBody PostCreateDto postCreateDto) {
return postService.createPost(postCreateDto);
}
// 게시글 수정
@PutMapping("/{id}")
public PostDto updatePost(@PathVariable Long id, @RequestBody PostUpdateDto postUpdateDto) {
return postService.updatePost(id, postUpdateDto);
}
// 게시글 삭제
@DeleteMapping("/{id}")
public String deletePost(@PathVariable Long id) {
boolean deleted = postService.deletePost(id);
return deleted ? "게시글이 삭제되었습니다." : "게시글을 찾을 수 없습니다.";
}
// 제목으로 검색
@GetMapping("/search")
public Page<PostDto> searchPosts(@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
return postService.searchPostsByTitle(keyword, pageable);
}
}
CommentController.java
- 계층적 URL 구조: /api/comments/post/{postId}로 특정 게시글의 댓글들 조회
- 사용자별 댓글 조회: /api/comments/user/{userId}로 마이페이지 기능 지원
- 일관된 응답 형태: 성공시 DTO 반환, 삭제시 메시지 반환
package com.example.demo_api.controller;
import com.example.demo_api.dto.*;
import com.example.demo_api.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/comments")
public class CommentController {
@Autowired
private CommentService commentService;
// 특정 게시글의 댓글들 조회
@GetMapping("/post/{postId}")
public List<CommentDto> getCommentsByPostId(@PathVariable Long postId) {
return commentService.getCommentsByPostId(postId);
}
// 특정 댓글 조회
@GetMapping("/{id}")
public CommentDto getCommentById(@PathVariable Long id) {
return commentService.getCommentById(id);
}
// 댓글 생성
@PostMapping
public CommentDto createComment(@RequestBody CommentCreateDto commentCreateDto) {
return commentService.createComment(commentCreateDto);
}
// 댓글 수정
@PutMapping("/{id}")
public CommentDto updateComment(@PathVariable Long id, @RequestBody CommentUpdateDto commentUpdateDto) {
return commentService.updateComment(id, commentUpdateDto);
}
// 댓글 삭제
@DeleteMapping("/{id}")
public String deleteComment(@PathVariable Long id) {
boolean deleted = commentService.deleteComment(id);
return deleted ? "댓글이 삭제되었습니다." : "댓글을 찾을 수 없습니다.";
}
// 특정 사용자의 댓글들 조회
@GetMapping("/user/{userId}")
public List<CommentDto> getCommentsByUserId(@PathVariable Long userId) {
return commentService.getCommentsByUserId(userId);
}
}
7. 테스트!!
1. 사용자 생성
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "김철수", "email": "kim@example.com"}'
2. 게시글 생성
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "첫 번째 게시글", "content": "안녕하세요!", "authorId": 1}'
3. 댓글 생성
curl -X POST http://localhost:8080/api/comments \
-H "Content-Type: application/json" \
-d '{"content": "좋은 글이네요!", "postId": 1, "authorId": 1}'
4. 게시글 목록 조회
curl -X GET http://localhost:8080/api/posts
5. 특정 게시글의 댓글 조회
curl -X GET http://localhost:8080/api/comments/post/1
잘 안된다면 에러로그 보고 뭐가 문제인지 잘 봐야한다.
휴.. 힘들다.. 다음은 회원 시스템!
'개발 아카이브 > JAVA' 카테고리의 다른 글
JAVA equals, hashCode 오버라이드시 instanceof와 getClass 차이 (0) | 2025.09.21 |
---|---|
Spring 기본 - DTO (Data Transfer Object) 패턴 (0) | 2025.09.20 |
Spring 기본 - Service 계층 (0) | 2025.09.20 |
Spring 기본 시작 - 세팅부터 CRUD API 까지 (2) | 2025.09.04 |
Java 17 다운로드 방법 (0) | 2025.09.03 |