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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# backend-study-sns

강준이
12 changes: 10 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ description = 'Demo project for Spring Boot'

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

Expand All @@ -19,8 +19,16 @@ repositories {
}

dependencies {
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'com.mysql:mysql-connector-j:8.2.0' // MySQL
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/example/devSns/DevSnsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ public static void main(String[] args) {
SpringApplication.run(DevSnsApplication.class, args);
}

}
}
31 changes: 31 additions & 0 deletions src/main/java/com/example/devSns/controller/CommentController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.devSns.controller;

import com.example.devSns.dto.CommentRequest;
import com.example.devSns.entity.CommentEntity;
import com.example.devSns.service.CommentService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/comments")
public class CommentController {
private final CommentService commentService;

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

// 댓글 생성
@PostMapping("/{postId}")
public String createComment(@PathVariable Long postId, @ModelAttribute CommentRequest request) {
commentService.createComment(postId, request);
return "redirect:/posts/" + postId;
}

// 댓글 삭제
@PostMapping("/{commentId}/delete")
public String deleteComment(@PathVariable Long commentId, @RequestParam Long postId) {
commentService.deleteComment(commentId);
return "redirect:/posts/" + postId;
}
}
69 changes: 69 additions & 0 deletions src/main/java/com/example/devSns/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.devSns.controller;

import com.example.devSns.entity.PostEntity;
import com.example.devSns.service.PostService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

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

private final PostService postService;

// 생성자
public PostController(PostService postService) {
this.postService = postService;
}

// HTTP GET /posts
@GetMapping
public String list(Model model) {
model.addAttribute("posts", postService.getAllPosts());
return "list";
}

// HTTP GET /posts/new
@GetMapping("/new")
public String form(Model model) {
model.addAttribute("post", new PostEntity());
return "form";
}

// HTTP POST /posts
@PostMapping
public String create(@ModelAttribute PostEntity postEntity) {
postService.createPost(postEntity);
return "redirect:/posts"; // 글 생성 후 돌아올 주소
}

// HTTP GET /posts/{id}
@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("post", postService.getPost(id));
return "detail";
}

// HTTP GET /posts/{id}/edit
@GetMapping("/{id}/edit")
public String editForm(@PathVariable Long id, Model model) {
PostEntity postEntity = postService.getPost(id);
model.addAttribute("post", postEntity);
return "edit";
}

// HTTP POST /posts/{id}/update
@PostMapping("/{id}/update")
public String update(@PathVariable Long id, @ModelAttribute PostEntity updatedPost) {
postService.updatePost(id, updatedPost);
return "redirect:/posts/" + id; // 수정 후 돌아올 주소
}

// HTTP POST /posts/{id}/delete
@PostMapping("/{id}/delete")
public String delete(@PathVariable Long id) {
postService.deletePost(id);
return "redirect:/posts"; // 삭제 후 돌아올 주소
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/example/devSns/controller/explanation
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
요청을 받는 곳

PostController
Spring MVC 패턴에서 Controller 계층 역할
- 사용자의 요청을 받고 Service 계층을 호출해서 처리한 후, 결과를 View에 전달

@Controller
이 클래스는 웹 요청을 처리하는 컨트롤러임을 알려줌
- Model에 데이터를 담아 템플릿에 전달 (templates/list.html)

@RequestMapping
클래스 전체의 기본 URL 경로를 지정
- 이 컨트롤러 안의 모든 요청은 /posts로 시작
- /posts (게시글 목록), /posts/new (새 글 작성 폼)

@GetMapping, @PostMapping
HTTP 메서드 별 라우팅

@PathVariable, @ModelAttribute
URL 경로 변수 바인딩 / 폼 데이터를 객체에 바인딩

@RestController
문자열 반환이 뷰 이름이 아닌 JSON
- REST API 만들 때 사용
13 changes: 13 additions & 0 deletions src/main/java/com/example/devSns/dto/CommentRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.devSns.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentRequest {
private String username;
private String content;
}

// Controller는 CommentEntity를 직접 받지 않고 CommentRequest만 받게 됨
20 changes: 20 additions & 0 deletions src/main/java/com/example/devSns/dto/CommentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.devSns.dto;

import lombok.Getter;

@Getter
public class CommentResponse {
private final Long id;
private final String username;
private final String content;
private final String createdAt;

public CommentResponse(Long id, String username, String content, String createdAt) {
Comment on lines +5 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DTO 적용 좋습니다! 현재 @Getter를 통해 값을 조회하네요! 그런데 이런 경우에 class 대신 record를 쓴다면 코드를 아주 간결하게 작성할 수 있습니다. 불변객체라서 안정적이라는 장점도 있습니다. 저는 개인적으로 DTO는 항상 record로 작성하는 편입니다

참고 : https://wikidocs.net/287476

this.id = id;
this.username = username;
this.content = content;
this.createdAt = createdAt;
}
}

// Entity를 외부로 그대로 내보내지 않고 DTO로 가공해서 반환
8 changes: 8 additions & 0 deletions src/main/java/com/example/devSns/dto/explanation
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
계층간 데이터 교환에 사용
- Entity 클래스를 보호
- 필요한 데이터만 선택적으로 담을 수 있음

분리해서 사용하는 이유
- Entity 객체의 변경을 피하기 위함
- 클라이언트와 통신하는 ResponseDTO나 RequestDTO는 요구사항에 따라 자주 변경
- 어떤 요청에서는 특정 값이 추가되거나 없을 수 있어서 분리해서 관리
53 changes: 53 additions & 0 deletions src/main/java/com/example/devSns/entity/CommentEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.devSns.entity;

import jakarta.persistence.*;
import lombok.Getter;

import java.time.LocalDateTime;

// DTO를 사용하고 Setter를 삭제 (안전)
@Entity
@Getter
public class CommentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String content;
private String username;

private LocalDateTime createdAt;
private LocalDateTime updatedAt;

// Post와 N:1 관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private PostEntity postEntity;
Comment on lines +23 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재 로직상 Comment는 Post가 존재할 때만 만들어 질 수 있으니 @ManyToOne, @JoinColumn에 제약조건을 더 걸어도 될 것 같습니다!

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id", nullable = false)


// JPA 기본 생성자
protected CommentEntity() {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

잘 작성해주셨네요! JPA의 지연로딩 때문에 빈 프록시 객체를 만들기 위한 protected 레벨의 기본 생성자가 필요합니다! 이 부분을 다음과 같은 어노테이션 추가해서 생략할 수도 있습니다

@NoArgsConstructor(access = AccessLevel.PROTECTED)


// 정적 생성 메서드 > 엔터티 변경은 오직 메서드로
public static CommentEntity create(PostEntity postEntity, String username, String content) {
CommentEntity comment = new CommentEntity();
comment.postEntity = postEntity;
comment.username = username;
comment.content = content;
return comment;
}

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

@PrePersist
public void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}

@PreUpdate
public void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/example/devSns/entity/PostEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.devSns.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
public class PostEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;
private String content;
private String author;

private LocalDateTime createdAt;
private LocalDateTime updatedAt;

// post를 삭제하면 댓글도 같이 삭제
@OneToMany(mappedBy = "postEntity", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CommentEntity> comments = new ArrayList<>();
Comment on lines +28 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@OneToMany까지 적용해서 양방향으로 구현해주셨네요! 그런데 사실 단방향으로 @ManyToOne만 사용하더라도 DB 테이블에서 FK가 추가됩니다. 그래서 저는 양방향 연관관계가 꼭 필요한 게 아니면 보통 단방향만 사용하는 걸 추천드립니다. 양방향 연관관계 지정 시 객체 생성 시 서로 참조하는 형태라 순환참조 발생할 위험도 있고 cascade가 설정돼 있으면 삭제 시 의도치 않은 요소가 삭제될 수 있어서 관리하기가 어렵더라고요


@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}

@PreUpdate
protected void onUpdated() {
this.updatedAt = LocalDateTime.now();
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/example/devSns/entity/explanation
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
데이터베이스와 직접적으로 맞닿는 핵심적인 클래스
- entity를 기준으로 테이블 생성
- Builder 패턴을 사용해서 필요한 값만 넣음

PostEntity 객체가 곧 DB의 한 행(row)
-> 기본 키는 id이고, 나머지는 속성임을 명시하는 클래스

@Entity
데이터베이스 테이블과 매핑
- JPA가 이 클래스를 테이블로 인식
- 데이터베이스 테이블로 매핑할 수 있게 함

@Id
해당 필드가 기본 키임을 나타냄
- JPA에서 이 필드는 각 행을 고유하게 식별하는 기준

@GeneratedValue
기본 키의 값을 자동 생성할 때 어떤 전략을 쓸지 지정
GenerationType.IDENTITY는 자동 증가

@Getter, @Setter (Lombok)
보일러플레이트 제거
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.devSns.repository;

import com.example.devSns.entity.CommentEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<CommentEntity, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.devSns.repository;

import com.example.devSns.entity.PostEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<PostEntity, Long> {
}
5 changes: 5 additions & 0 deletions src/main/java/com/example/devSns/repository/explanation
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
데이터베이스 접근 (JPA 인터페이스)

PostRepository<T, ID>
상속만 하면 기본 CRUD가 다 생김
- save, findAll, findById, deleteById
Loading