Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java { sourceCompatibility = '21' }

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
}
repositories { mavenCentral() }

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}
tasks.named('test') { useJUnitPlatform() }
Empty file modified gradlew
100644 → 100755
Empty file.
35 changes: 35 additions & 0 deletions src/main/java/com/example/devSns/domain/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Column(nullable = false)
private String content;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Post post;

@Builder
private Comment(String content, Post post) {
this.content = content;
this.post = post;
}
Comment on lines +21 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setter가 아닌 Builder를 사용하는 방식 아주 좋습니다!


public void update(String content) {
this.content = content;
}

/** Post 편의 메서드에서만 설정하도록 제한 */
void setPostInternal(Post post) {
this.post = post;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/devSns/domain/CommentRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.devSns.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;

public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPostIdOrderByIdAsc(Long postId);
Optional<Comment> findByIdAndPostId(Long id, Long postId);
}
47 changes: 47 additions & 0 deletions src/main/java/com/example/devSns/domain/Post.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.devSns.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
@Column(nullable = false)
private String title;

@Lob
private String content;

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<Comment> comments = new ArrayList<>();

@Builder
private Post(String title, String content) {
this.title = title;
this.content = content;
}

public void update(String title, String content) {
this.title = title;
this.content = content;
}

/** 양방향 편의 메서드 */
void addComment(Comment c) {
comments.add(c);
c.setPostInternal(this);
}
void removeComment(Comment c) {
comments.remove(c);
c.setPostInternal(null);
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/example/devSns/domain/PostRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.devSns.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> { }
56 changes: 56 additions & 0 deletions src/main/java/com/example/devSns/service/CommentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.devSns.service;

import com.example.devSns.domain.Comment;
import com.example.devSns.domain.CommentRepository;
import com.example.devSns.domain.Post;
import com.example.devSns.domain.PostRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@Service
public class CommentService {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB를 건드리는 코드이니 @transactional을 통하여 트랜잭션을 설정해두면 좋을 것 같아요!!

private final CommentRepository commentRepo;
private final PostRepository postRepo;

public CommentService(CommentRepository commentRepo, PostRepository postRepo) {
this.commentRepo = commentRepo;
this.postRepo = postRepo;
}

public List<Comment> list(Long postId) {
ensurePostExists(postId);
return commentRepo.findByPostIdOrderByIdAsc(postId);
}

public Comment get(Long postId, Long commentId) {
return commentRepo.findByIdAndPostId(commentId, postId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Comment not found"));
}

public Comment create(Long postId, String content) {
Post post = postRepo.findById(postId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 코드는 아래에 작성한 ensurePostExists와 어떤 차이점이 존재할까요?

Comment comment = Comment.builder().content(content).post(post).build();
return commentRepo.save(comment);
}

public Comment update(Long postId, Long commentId, String content) {
Comment comment = get(postId, commentId);
comment.update(content);
return commentRepo.save(comment);
}

public void delete(Long postId, Long commentId) {
Comment comment = get(postId, commentId);
commentRepo.delete(comment);
}

private void ensurePostExists(Long postId) {
if (!postRepo.existsById(postId)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found");
}
}
}
48 changes: 48 additions & 0 deletions src/main/java/com/example/devSns/service/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.devSns.service;

import com.example.devSns.domain.Post;
import com.example.devSns.domain.PostRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@Service
public class PostService {

private final PostRepository repo;

public PostService(PostRepository repo) {
this.repo = repo;
}

public List<Post> findAll() {
return repo.findAll();
}

public Post findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found"));
}

public Post create(String title, String content) {
Post post = Post.builder()
.title(title)
.content(content)
.build();
return repo.save(post);
}

public Post update(Long id, String title, String content) {
Post post = findById(id);
post.update(title, content);
return repo.save(post);
}

public void delete(Long id) {
Post post = repo.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found"));
Comment on lines +44 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commentService에서도 같은 방식이 존재하는데, 이러한 코드 중복은 어떻게 처리하는 것이 좋을까요?

repo.delete(post); // 연관 댓글까지 안전하게 제거
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/example/devSns/web/CommentController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.devSns.web;

import com.example.devSns.service.CommentService;
import com.example.devSns.web.dto.CommentCreateRequest;
import com.example.devSns.web.dto.CommentUpdateRequest;
import com.example.devSns.web.dto.CommentResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/posts/{postId}/comments")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment를 설정하는 모든 코드에서 postId가 필수로 존재해야할까요? 그렇다면 왜 그렇게해야할까요? 아니라면 어떤식으로 코드를 수정할 수 있을까요?

public class CommentController {
private final CommentService svc;

public CommentController(CommentService svc) {
this.svc = svc;
}

@GetMapping
public List<CommentResponse> list(@PathVariable Long postId) {
return svc.list(postId).stream().map(CommentResponse::from).toList();
}

@GetMapping("/{commentId}")
public CommentResponse get(@PathVariable Long postId, @PathVariable Long commentId) {
return CommentResponse.from(svc.get(postId, commentId));
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public CommentResponse create(@PathVariable Long postId,
@Valid @RequestBody CommentCreateRequest req) {
return CommentResponse.from(svc.create(postId, req.content()));
}

@PutMapping("/{commentId}")
public CommentResponse update(@PathVariable Long postId, @PathVariable Long commentId,
@Valid @RequestBody CommentUpdateRequest req) {
return CommentResponse.from(svc.update(postId, commentId, req.content()));
}

@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{commentId}")
public void delete(@PathVariable Long postId, @PathVariable Long commentId) {
svc.delete(postId, commentId);
}
}
53 changes: 53 additions & 0 deletions src/main/java/com/example/devSns/web/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.devSns.web;

import com.example.devSns.domain.Post;
import com.example.devSns.service.PostService;
import com.example.devSns.web.dto.PostCreateRequest;
import com.example.devSns.web.dto.PostUpdateRequest;
import com.example.devSns.web.dto.PostResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/posts")
public class PostController {

private final PostService svc;

public PostController(PostService svc) {
this.svc = svc;
}

@GetMapping
public List<PostResponse> getAll() {
return svc.findAll().stream().map(PostResponse::from).toList();
}

@GetMapping("/{id}")
public PostResponse getOne(@PathVariable Long id) {
Post p = svc.findById(id);
return PostResponse.from(p);
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public PostResponse create(@Valid @RequestBody PostCreateRequest req) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valid를 사용하여 dto를 검증하는 방식 좋습니당

Post saved = svc.create(req.title(), req.content());
return PostResponse.from(saved);
}

@PutMapping("/{id}")
public PostResponse update(@PathVariable Long id, @Valid @RequestBody PostUpdateRequest req) {
Post updated = svc.update(id, req.title(), req.content());
return PostResponse.from(updated);
}

@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
svc.delete(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.devSns.web.dto;

import jakarta.validation.constraints.NotBlank;

public record CommentCreateRequest(
@NotBlank String content
) {}
12 changes: 12 additions & 0 deletions src/main/java/com/example/devSns/web/dto/CommentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.devSns.web.dto;

import com.example.devSns.domain.Comment;

public record CommentResponse(
Long id,
String content
) {
public static CommentResponse from(Comment c) {
return new CommentResponse(c.getId(), c.getContent());
}
}
Comment on lines +5 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레코드를 아주 잘 쓰고 계시군요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.devSns.web.dto;

import jakarta.validation.constraints.NotBlank;

public record CommentUpdateRequest(
@NotBlank String content
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.devSns.web.dto;

import jakarta.validation.constraints.NotBlank;

public record PostCreateRequest(
@NotBlank String title,
String content
) {}
Loading