-
Notifications
You must be signed in to change notification settings - Fork 13
Feat/week 1 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 강준이
Are you sure you want to change the base?
Feat/week 1 #2
Changes from all commits
098a8c2
5200ce0
3d54a1f
2ded9e9
2592017
a869462
9346fa9
e227126
96b956f
845bb2d
eb68811
8000aaf
7437ff5
bbc2f3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| # backend-study-sns | ||
|
|
||
| 강준이 |
| 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"; // 삭제 후 돌아올 주소 | ||
| } | ||
| } | ||
| 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 만들 때 사용 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| 계층간 데이터 교환에 사용 | ||
| - Entity 클래스를 보호 | ||
| - 필요한 데이터만 선택적으로 담을 수 있음 | ||
|
|
||
| 분리해서 사용하는 이유 | ||
| - Entity 객체의 변경을 피하기 위함 | ||
| - 클라이언트와 통신하는 ResponseDTO나 RequestDTO는 요구사항에 따라 자주 변경 | ||
| - 어떤 요청에서는 특정 값이 추가되거나 없을 수 있어서 분리해서 관리 | ||
|
Comment on lines
+4
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 내용 정리 좋습니다! entity랑 dto를 분리하는 이유를 한 가지 더 설명드리면, entity는 DB의 table을 정의하는 일종의 스키마 역할을 하게 됩니다. 따라서 table 정의 및 매핑 관련된 설정은 entity에 두고, 외부 입력 값 검증이나 변환은 DTO에서 책임지도록 하는 것이 책임 분리 관점에서 더 적절합니다. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.example.devSns.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @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; | ||
|
Comment on lines
+18
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. entity에서 DB 스키마 관련 제약조건을 추가해서 더 안전하게 구성할 수 있습니다! null 값이 들어가면 안 되는 속성에 // e.g.
@Column(nullable = false, length = 255)
private String title; |
||
|
|
||
| @PrePersist | ||
| protected void onCreate() { | ||
| this.createdAt = LocalDateTime.now(); | ||
| this.updatedAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| @PreUpdate | ||
| protected void onUpdated() { | ||
| this.updatedAt = LocalDateTime.now(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 데이터베이스와 직접적으로 맞닿는 핵심적인 클래스 | ||
| - entity를 기준으로 테이블 생성 | ||
| - Builder 패턴을 사용해서 필요한 값만 넣음 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Builder 패턴의 경우 호불호가 갈리는 편입니다. 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.PostEntity; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface PostRepository extends JpaRepository<PostEntity, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| 데이터베이스 접근 (JPA 인터페이스) | ||
|
|
||
| PostRepository<T, ID> | ||
| 상속만 하면 기본 CRUD가 다 생김 | ||
| - save, findAll, findById, deleteById |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.example.devSns.service; | ||
|
|
||
| import com.example.devSns.entity.PostEntity; | ||
| import com.example.devSns.repository.PostRepository; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @Transactional(readOnly = true) | ||
| public class PostService { | ||
| private final PostRepository postRepository; | ||
|
|
||
| // 생성자 | ||
| public PostService(PostRepository postRepository) { | ||
| this.postRepository = postRepository; | ||
| } | ||
|
|
||
| public List<PostEntity> getAllPosts() { | ||
| return postRepository.findAll(); | ||
| } | ||
|
|
||
| public PostEntity getPost(Long id) { | ||
| return postRepository.findById(id).orElseThrow(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public PostEntity createPost(PostEntity postEntity) { | ||
| return postRepository.save(postEntity); | ||
| } | ||
|
Comment on lines
+11
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| @Transactional | ||
| public PostEntity updatePost(Long id, PostEntity updated) { | ||
| PostEntity postEntity = postRepository.findById(id).orElseThrow(); | ||
| postEntity.setTitle(updated.getTitle()); | ||
| postEntity.setContent(updated.getContent()); | ||
| return postRepository.save(postEntity); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deletePost(Long id) { | ||
| postRepository.deleteById(id); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 비지니스 로직 담당 | ||
|
|
||
| PostService | ||
|
|
||
| @Service | ||
| 비지니스 로직이 모이는 곳 | ||
| - 트랜잭션을 붙여야 할 때도 주로 서비스에서 | ||
| - 의존성 주입 (생성자 주입) | ||
|
|
||
| @Transactional(readOnly = true) | ||
| 읽기 성능 최적화 | ||
| - 원자성 보장 | ||
| - 안전빵 | ||
|
|
||
| private final PostRepository postRepository; | ||
| - 의존성으로 받는 레포지토리 | ||
| - 생성자 주입 | ||
| - JpaRepository<PostEntity, Long> 상속 | ||
|
|
||
| findAll() | ||
| - 모든 게시글 조회 | ||
|
|
||
| findById() | ||
| - getPost 함수에서 PK(id)로 단건 조회 | ||
| - updatePost 함수에서 수정 대상 로드, 필드 갱신, 저장 | ||
|
|
||
| save | ||
| - 새 엔터티 저장 | ||
| - id가 없으면 persist, 있으면 merge | ||
|
Comment on lines
+27
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오.. 혹시 영속성 계층, 1차 캐시 등 JPA 관련해서 따로 공부하신 적이 있으신가요?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 과제 하면서 사용한 레퍼런스를 통해 해당 기능을 알게 됐습니다. 스프링부트에 대한 지식이 아직 부족해서 더 공부해보겠습니다! |
||
|
|
||
| deleteById | ||
| - PK(id)로 삭제 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,23 @@ | ||
| spring.application.name=devSns | ||
|
|
||
| # ?? ?? | ||
| server.port=8080 | ||
|
|
||
| # H2 ???? DB | ||
| spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_UPPER=false | ||
| spring.datasource.driver-class-name=org.h2.Driver | ||
| spring.datasource.username=sa | ||
| spring.datasource.password= | ||
|
|
||
| # JPA | ||
| spring.jpa.hibernate.ddl-auto=update | ||
| spring.jpa.show-sql=true | ||
| spring.jpa.properties.hibernate.format_sql=true | ||
|
|
||
| # H2 ?? | ||
| spring.h2.console.enabled=true | ||
| spring.h2.console.path=/h2-console | ||
|
|
||
| # Thymeleaf(??? ??? ?? ?) | ||
| spring.thymeleaf.cache=false | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <!DOCTYPE html> | ||
| <html xmlns:th="http://www.thymeleaf.org"> | ||
| <body> | ||
| <h2 th:text="${post.title}">제목</h2> | ||
| <h2 th:text="${post.content}">내용</h2> | ||
| <p> | ||
| <b>작성자:</b> | ||
| <span th:text="${post.author}"></span> | ||
| </p> | ||
|
|
||
| <a th:href="@{|/posts/${post.id}/edit|}"> | ||
| <button type="button">수정</button> | ||
| </a> | ||
| <form th:action="@{|/posts/${post.id}/delete|}" method="post"> | ||
| <button type="submit">삭제</button> | ||
| </form> | ||
| <a th:href="@{|/posts|}"> | ||
| <button type="button">게시글 보기</button> | ||
| </a> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <!DOCTYPE html> | ||
| <html xmlns:th="http://www.thymeleaf.org"> | ||
| <body> | ||
| <h2>게시글 수정</h2> | ||
|
|
||
| <form th:action="@{|/posts/${post.id}/update|}" method="post" th:object="${post}"> | ||
| <label>제목:</label><br/> | ||
| <input type="text" th:field="*{title}" /><br/><br/> | ||
|
|
||
| <label>내용:</label><br/> | ||
| <textarea th:field="*{content}" rows="6" cols="40"></textarea><br/><br/> | ||
|
|
||
| <button type="submit">수정 완료</button> | ||
| </form> | ||
|
|
||
| <br/> | ||
| <a th:href="@{|/posts/${post.id}|}">취소</a> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!DOCTYPE html> | ||
| <html xmlns:th="http://www.thymeleaf.org"> | ||
| <body> | ||
| <h2>새 글 작성</h2> | ||
| <form action="/posts" method="post" th:object="${post}"> | ||
| <input type="text" th:field="*{title}" placeholder="제목" /> | ||
| <textarea th:field="*{content}" placeholder="내용"></textarea> | ||
| <input type="text" th:field="*{author}" placeholder="작성자" /> | ||
| <button type="submit">저장</button> | ||
| </form> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <!DOCTYPE html> | ||
| <html xmlns:th="http://www.thymeleaf.org"> | ||
| <body> | ||
| <h2>게시판</h2> | ||
| <a href="/posts/new">새 글 쓰기</a> | ||
| <ul> | ||
| <li th:each="post : ${posts}"> | ||
| <a th:href="@{|/posts/${post.id}|}" th:text="${post.title}"></a> | ||
| </br> | ||
| <small> | ||
| 작성일: | ||
| <span th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></span> | ||
| / | ||
| 수정일: | ||
| <span th:text="${#temporals.format(post.updatedAt, 'yyyy-MM-dd HH:mm')}"></span> | ||
| </small> | ||
| </li> | ||
| </ul> | ||
| </body> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSR 방식을 사용하셨네요! 저도 서버 단에서 view까지 다룰 수 있다는 점에서 SSR을 선호합니다. CSR방식과 SSR방식의 장단점이 무엇일까요?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSR 방식
사용자가 웹페이지를 요청했을 때,
서버에 필요한 데이터와 HTML을 모두 합쳐서 완성된 페이지를 만들어 서용자게에 제공
pros
완성된 형태의 페이지를 빠르게 받기 가능
검색 엔진이 HTML 쉽게 분석하고 파악 가능
cons
서버가 일일이 페이지를 민들어야 함
페이지 이동 때마다 전체 페이지를 서버로부터 다시 받아오기 때문에 화면이 깜빡일 수 있음
CSR 방식
사용자가 웹페이지를 요청했을 때,
서버는 아주 기본적인 HTML 뼈대와 javascript 파일만 보내줌.
사용자의 웹 브라우저는 그 javascript를 실행해서 데이터를 서버에 다시 요청하고 데이터를 받아 동적으로 페이지를 완성
pros
한 번 페이지 받아오면 부분적 업데이트
서버는 데이터 제공하는 역할에만 집중, 화면을 그리는 일은 브라우저가 담당
cons
처음에 javascript 파일과 데이터를 모두 다운로드
초기에 내용이 거의 없는 HTML을 받아 검색 엔진이 분석하고 파악하기에 어려움
Code Review SSR
postService.getAllPosts()서버에서 미리 모든 게시물 데이터를 가져와
model.addAttribute(...)이 데이터를 list라는 이름의 View(HTML)에 전달
완성된 list 페이지를 사용자에게 반환