1. Kanboard 할일 정리
2. 어플리케션 구조
3. DTO(Data Transfer Object)에 대해
DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체를 말합니다.
Spring boot와 JPA를 사용하다 보면, Enitity 클래스의 중요성과 민감성에 대해서 잘알고 있을 것입니다. Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스이며, Entity 클래스를 기준으로 테이블이 생성되고 스키마가 변경됩니다.
그렇기 때문에 다양한 계층에서 Entity를 직접적으로 사용하게 된다면 원치 않게 Entity의 속성을 변경시킬 위험이 존재하며, Entitiy의 모든 속성이 불필요하게 외부에 노출될 가능성이 있습니다.
그렇기 때문에 우리는 DTO를 사용합니다.
Entity 클래스에서 필요한 데이터만 선택적으로 DTO에 담아서 생성해 사용함으로써, Entitiy 클래스를 감추며 보호할 수 있습니다.
3. DTO -> Entity 변환 계층 선택
- Controller layer층에서 변환
- 장점 :Service layer에서 entity를 바로 받게 함으로써, Service Layer은 Entity에만 의존하기 때문에 코드 재사용성이 높아짐
- 단점 : controller layer에서의 코드가 복잡해지고 변환과정에서 비지니스 로직이 controller에 포함되게됨
- Service layer층에서 변환
- 장점 : controller layer에서의 비지니스 로직과 같은 부분을 해결하며 jpa에서 lazy 조회시 LazyInitializationException에 노출 위험을 줄여줍니다.
- 단점 : 특정 DTO에 의존성이 높아지기때문에 여러 종류의 컨트롤러에서 해당 서비스를 사용할수 없어 코드재사용성떨어트림
LazyInitializationException 예외가 발생하는 이유
- 일반적인 백엔드 프로젝트에서 로직의 대부분은 REST API에 해당하며 실행의 흐름은 @Controller, @Service, @Repository 순서가 된다.
- 서비스 레벨에서 @Transactional이 명시된 메써드가 종료되면 Hibernate의 Session도 함께 종료된다.
- FetchType.LAZY가 설정된 필드가 포함된 엔티티 오브젝트에 대해, 컨트롤러 레벨에서 해당 필드를 조회할 때 Getter 메써드를 호출하고 실제 조회 쿼리가 실행된다. 하지만 앞서 이미 Session이 종료된 상태이기 때문에 LazyInitializationException 예외가 발생하게 되는 것이다.
3. PostAPIController
게시글 생성, 수정, 삭제 api 생성
@RequiredArgsConstructor
@RequestMapping("/posts")
@Controller
@Slf4j
public class PostApiController {
private final PostService postService;
private final PaginationService paginationService;
//게시글 form 가져오기
@GetMapping("/form")
public String postForm(ModelMap map) {
map.addAttribute("formStatus", FormStatus.CREATE);
return "posts/form";
}
//게시글 등록
@PostMapping
public ResponseEntity<Long> newPost(@AuthenticationPrincipal PostPrincipal postPrincipal, @RequestBody PostsRequest request) {
Long result = postService.savePost(request.toDto(postPrincipal.toDto()));
return ResponseEntity.status(HttpStatus.CREATED)
.body(result);
}
// 게시글 업데이트 form
@GetMapping("form/{postId}")
public String updatePostForm(@PathVariable Long postId, ModelMap map) {
PostsResponse post = PostsResponse.from(postService.getPost(postId));
map.addAttribute("post", post);
map.addAttribute("formStatus", FormStatus.UPDATE);
return "posts/form";
}
//게시글 업데이트
@PostMapping("/{postId}")
public ResponseEntity<Long> updatePost(@PathVariable Long postId, @AuthenticationPrincipal PostPrincipal postPrincipal,
PostsRequest postsRequest) {
Long result = postService.updatePost(postId, postsRequest.toDto(postPrincipal.toDto()));
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(result);
}
//게시글 삭제
@DeleteMapping("/{postId}")
public ResponseEntity<Void> deletePost(@PathVariable Long postId, @AuthenticationPrincipal PostPrincipal postPrincipal) {
postService.deletePost(postId, postPrincipal.email());
return ResponseEntity.status(HttpStatus.ACCEPTED).build();
}
//게시글 페이지 조회
// 게시글 조회
}
3. postRepository
jpa와 querydsl 사용
public interface PostRepository extends
JpaRepository<Post, Long>,
QuerydslPredicateExecutor<Post>,
QuerydslBinderCustomizer<QPost> // querydsl 제작
{
void deleteByIdAndMember_email(Long post_no, String email);
}
4. Postdto
게시판을 저장을 위에 데이터 전달과 받는 dto 생성
entity 클래스를 데이터베이스와 맞닿는 클래스여서 Entity를변경하게 된다면 여러클래스에 영향을 받음
Request와 Response용 dto는 view를 위한 클래스라 자주 변경이 필요하여 역할를 분리함
package com.portfolio.ohousev1.dto.post;
import com.portfolio.ohousev1.dto.member.MemberDto;
import com.portfolio.ohousev1.entity.Member;
import com.portfolio.ohousev1.entity.Post;
import java.time.LocalDateTime;
public record PostDto(
Long postId,
MemberDto memberDto,
String title,
String content,
String img_path,
LocalDateTime createdAt,
LocalDateTime modifiedAt
) {
public static PostDto of(MemberDto memberDto,String title,String content, String img_path){
return new PostDto(null, memberDto, title, content, img_path, null,null);
}
public static PostDto of(MemberDto memberDto,String title,String content, String img_path, LocalDateTime createdAt, LocalDateTime modifiedAt){
return new PostDto(null, memberDto, title, content, img_path, createdAt,modifiedAt);
}
public static PostDto from(Post entity){
return new PostDto(
entity.getId(),
MemberDto.from(entity.getMember()),
entity.getTitle(),
entity.getContent(),
entity.getImgPath(),
entity.getCreatedAt(),
entity.getModifiedAt()
);
}
public Post toEntity(Member member){
return Post.of(
member,
title,
content
);
}
}
public record PostsRequest(
String title,
String content,
String imgpath
) {
public static PostsRequest of(String title, String content, String imgpath) {
return new PostsRequest(title, content, imgpath);
}
public PostDto toDto(MemberDto memberDto){
return PostDto.of(
memberDto,
title,
content,
imgpath
);
}
}
public record PostsResponse(
Long postNo,
Long memberNo,
String title,
String content,
String email,
LocalDateTime createdAt,
LocalDateTime modifiedAt
) {
public static PostsResponse of(Long id, Long memberNo,String title, String content, LocalDateTime createdAt, String email, LocalDateTime modifiedAt) {
return new PostsResponse(id, memberNo,title, content, email,createdAt, modifiedAt);
}
public static PostsResponse from(PostDto dto) {
String nickname = dto.memberDto().nickname();
return new PostsResponse(
dto.postId(),
dto.memberDto().toEntity().getMemberNo(),
dto.title(),
dto.content(),
dto.img_path(),
dto.createdAt(),
dto.modifiedAt()
);
}
}
5. Service
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {
private final PostRepository postRepository;
private final MemberRepository memberRepository;
private final PaginationService paginationService;
@Transactional
public PostDto getPost(Long postId) {
return postRepository.findById(postId)
.map(PostDto::from)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다 - postId: " + postId));
}
@Transactional
public Long savePost(PostDto dto) { //게시물 생성
Member member = memberRepository.getReferenceById(dto.memberDto().email());
Post post = dto.toEntity(member);
postRepository.save(post);
return post.getId();
}
@Transactional
public long updatePost(Long postId, PostDto dto) {
Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("해당게시글은 없습니다. id" + postId));
Member member = memberRepository.getReferenceById(dto.memberDto().email());
if(post.getMember().equals(member)){
if (dto.title() != null){ post.setTitle(dto.title());}
if (dto.content() != null){post.setContent(dto.content());}
postRepository.flush();
}
postRepository.save(post);
return post.getId();
}
@Transactional
public void deletePost(Long postNo, String email) {
postRepository.getReferenceById(postNo);
postRepository.deleteByIdAndMember_email(postNo, email);
}
public long getPostCount() {
return postRepository.count();
}
6. 메인 페이지 (thymeleaf + jquery)
bootstrap과 bootdey 가져와서 header footer main으로 분리
참고:
https://meetup.nhncloud.com/posts/92
REST API 제대로 알고 사용하기 : NHN Cloud Meetup
REST API 제대로 알고 사용하기
meetup.nhncloud.com
https://devlog-wjdrbs96.tistory.com/182
[Spring Boot] ResponseEntity란 무엇인가?
먼저 REST API가 무엇인지는 아래 블로그를 먼저 잘 읽어보자. https://meetup.toast.com/posts/92 REST API 제대로 알고 사용하기 : TOAST Meetup REST API 제대로 알고 사용하기 meetup.toast.com 1. ResponseEntity란? Spring Fra
devlog-wjdrbs96.tistory.com
[Spring boot] DTO <-> Entity 간 변환, 어느 Layer에서 하는게 좋을까?
Spring Boot 개발 중 학습이 필요한 내용을 정리하고,트러블 슈팅 과정을 기록하는 포스팅입니다.
velog.io
'포트폴리오 > ohouseClone' 카테고리의 다른 글
5. SpringSecurity 설정 및 customlogin페이지 제작 (0) | 2023.09.05 |
---|---|
4. 게시판 작성 front & member api (0) | 2023.09.05 |
2. Entity 작성 및 구조도 체크 (0) | 2023.08.16 |
1. 기본 Intellj setting 및 db연결 & 칸반보드 작성 (0) | 2023.08.11 |
0.오늘의집 클론 포트폴리오 (0) | 2023.08.02 |