Spring - Lv.1 과제

김지현·2023년 4월 17일
0

항해99 과제

목록 보기
2/4

2023-04-14 ~ 2023-04-17


주의사항

  1. Entity를 그대로 반환하지 말고, Dto에 담아서 반환
  2. JSON 반환하는 API형태
  3. Postman 사용

요구사항

  1. UseCase 그려보기
  2. 전체 게시글 목록 조회 API
    • 제목, 작성자명, 작성내용, 작성날짜 조회
    • 작성날짜 기준 내림차순 정리
  3. 게시글 작성 API
    • 제목, 작성자명, 비밀번호, 작성내용 저장
  4. 선택한 게시글 조회 API
    • 선택한 게시글의 제목, 작성자명, 작성날짜, 작성내용 조회
  5. 선택한 게시글 수정 API
    • 수정할 데이터와 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부 확인
    • 제목, 작성자명, 작성내용 수정
  6. 선택한 게시글 삭제 API
    • 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부 확인

API 명세서

■ 전체 게시글 목록 조회

  • Method : GET
  • URL : /api/posts
  • Request : -
  • Response
[
    {
        "createdAt": "2023-04-17T12:22:37.542175",
        "modifiedAt": "2023-04-17T12:22:37.542175",
        "id": 3,
        "author": "11",
        "title": "11",
        "content": "11"
    },
    {
        "createdAt": "2023-04-17T12:22:36.939707",
        "modifiedAt": "2023-04-17T12:22:36.939707",
        "id": 2,
        "author": "11",
        "title": "11",
        "content": "11"
    },
    {
        "createdAt": "2023-04-17T12:22:36.187211",
        "modifiedAt": "2023-04-17T12:22:36.187211",
        "id": 1,
        "author": "11",
        "title": "11",
        "content": "11"
    }
]

■ 선택한 게시글 조회

  • Method : GET
  • URL : /api/posts/{id}
  • Request : -
  • Response
{
    "createdAt": "2023-04-17T12:22:36.187211",
    "modifiedAt": "2023-04-17T12:22:43.910917",
    "id": 1,
    "author": "11",
    "title": "11",
    "content": "11"
}

■ 게시글 작성

  • Method : POST
  • URL : /api/post
  • Request
{
    "author":"11",
    "password":"11",
    "title":"11",
    "content":"11"
}
  • Response
{
    "createdAt": "2023-04-17T12:22:37.5421747",
    "modifiedAt": "2023-04-17T12:22:37.5421747",
    "id": 1,
    "author": "11",
    "title": "11",
    "content": "11"
}

■ 선택한 게시글 수정

  • Method : PUT
  • URL : /api/post/{id}
  • Request
{
    "author":"11",
    "password":"11",
    "title":"11 수정",
    "content":"11 수정"
}
  • Response
{
    "createdAt": "2023-04-17T12:22:36.187211",
    "modifiedAt": "2023-04-17T12:22:36.187211",
    "id": 1,
    "author": "11",
    "title": "11 수정",
    "content": "11 수정"
}

■ 선택한 게시글 삭제

  • Method : DELETE
  • URL : /api/post/{id}
  • Request
{
	"password" :"11"
}
  • Response
성공적으로 삭제되었습니다.

폴더 구조


전체적인 패키지 및 파일

■ controller

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.controller;

import com.sparta.spring_post.dto.PostRequestDto;
import com.sparta.spring_post.dto.PostResponseDto;
import com.sparta.spring_post.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class PostController {

    // PostService 연결
    private final PostService postService;

    // 목록 조회
    @GetMapping("/api/posts")
    public List<PostResponseDto> getAllPosts() {
        return postService.getAllPosts();
    }

    // 상세 조회
    @GetMapping("/api/posts/{id}")
    public PostResponseDto getPost(@PathVariable Long id) {
        return postService.getPost(id);
    }

    // 추가
    @PostMapping("/api/post")
    public PostResponseDto createPost(@RequestBody PostRequestDto postRequestDto) {
        return postService.createPost(postRequestDto);
    }

    // 수정
    @PutMapping("/api/post/{id}")
    public PostResponseDto updatePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto) {
        return postService.updatePost(id, postRequestDto);
    }

    // 삭제
    @DeleteMapping("/api/post/{id}")
    public String deletePost(@PathVariable Long id, @RequestParam("password") String password) {
        return postService.deletePost(id, password);
    }

}

■ dto

▲ RequestDto

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.dto;

import com.sparta.spring_post.entity.Post;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostRequestDto {
    private String author;
    private String password;
    private String title;
    private String content;

    public Post toEntity() {
        return Post.builder()
                .author(author)
                .password(password)
                .title(title)
                .content(content)
                .build();
    }
}

▲ ResponseDto

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.dto;

import com.sparta.spring_post.entity.Post;
import com.sparta.spring_post.entity.Timestamped;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
public class PostResponseDto extends Timestamped {
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;
    private Long id;
    private String author;
    private String title;
    private String content;

    public PostResponseDto(Post post) {
        this.createdAt = post.getCreatedAt();
        this.modifiedAt = post.getModifiedAt();
        this.id = post.getId();
        this.author = post.getAuthor();
        this.title = post.getTitle();
        this.content = post.getContent();
    }
}

■ entity

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.sparta.spring_post.dto.PostRequestDto;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
public class Post extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String author;

    @Column(nullable = false)
    @JsonIgnore // 데이터를 주고받을 때, 해당 데이터 ignore. 응답값 보이지 않음
    private String password;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @Builder
    public Post(String author, String password, String title, String content) {
        this.author = author;
        this.password = password;
        this.title = title;
        this.content = content;
    }

    public void update(PostRequestDto postRequestDto) {
        this.author = postRequestDto.getAuthor();
        this.password = postRequestDto.getPassword();
        this.title = postRequestDto.getTitle();
        this.content = postRequestDto.getContent();
    }
}

▲ Timstamped

package com.sparta.spring_post.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

■ repository

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.repository;

import com.sparta.spring_post.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface PostRepository extends JpaRepository<Post, Long> {
    List<Post> findAllByOrderByModifiedAtDesc();
}

■ service

// Client <-Dto-> Controller <-Dto-> Service <-Dto-> Repository <-Entity-> DB
package com.sparta.spring_post.service;

import com.sparta.spring_post.dto.PostRequestDto;
import com.sparta.spring_post.dto.PostResponseDto;
import com.sparta.spring_post.entity.Post;
import com.sparta.spring_post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@RequiredArgsConstructor
public class PostService {

    // PostRepository 연결
    private final PostRepository postRepository;

    // 목록 조회
    @Transactional(readOnly = true)
    public List<PostResponseDto> getAllPosts() {
        List<Post> posts = postRepository.findAllByOrderByModifiedAtDesc();
        List<PostResponseDto> dtos = new ArrayList<>();
        for (Post post : posts) {
            dtos.add(new PostResponseDto(post));
        }
        return dtos;
    }

    // 상세 조회
    @Transactional(readOnly = true)
    public PostResponseDto getPost(Long id) {
        Post post = postRepository.findById(id).orElseThrow();
        PostResponseDto dtos = new PostResponseDto(post);
        dtos.getClass();
        return dtos;
    }

    // 추가
    @Transactional
    public PostResponseDto createPost(PostRequestDto postRequestDto) {
        Post post = postRepository.save(postRequestDto.toEntity());
        return new  PostResponseDto(post);
    }

    // 수정
    @Transactional
    public PostResponseDto updatePost(Long id, PostRequestDto postRequestDto) {
        Post post = postRepository.findById(id).orElseThrow();
        PostResponseDto dtos = new PostResponseDto(post);
        if (!post.getPassword().equals(postRequestDto.getPassword())) {
            return dtos;
        }
        post.update(postRequestDto);
        return new PostResponseDto(post);
    }

    // 삭제
    @Transactional
    public String deletePost(Long id, String password) {
        Post post = postRepository.findById(id).orElseThrow();
        if (!post.getPassword().equals(password)) {
            return "비밀번호가 일치하지 않습니다.";
        }
        postRepository.deleteById(id);
        return "성공적으로 삭제되었습니다.";
    }

}

역경과 고난

  1. Entity를 그대로 반환하지 말고 Dto에 담아서 반환하라는 말의 의미를 처음에는 이해하지 못해서 그 말의 의미부터 구글링을 했다.
    Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이므로 Entity를 그대로 반환했을 경우 객체의 일관성, 안전성을 보장하기 어렵기 때문에 목적 자체가 전달인 Dto에 데이터를 담아서 반환해야한다.

  2. 처음엔 Dto를 RequestDto, ResponseDto 이렇게 둘로만 나누지 않고 CRUD별로 Dto를 나눴다. 그걸 본 기술매니저님이 그럴 필요없다 오히려 비효율적이다 라는 피드백을 주셔서 요청과 응답으로만 Dto를 다시 만들고 그걸 어떻게 적용해야하나 고민과 구글링의 연속이었다.

  3. 비밀번호같은 개인정보는 보안을 위해 데이터를 Response 해줄 때 나타나지 않도록 해야했는데 Request와 Response 둘의 정확한 쓰임새나 정의, 또 언제 어떻게 값을 담아서 무엇으로 응답을 해줘야하는지 너무 헷갈렸다. 그래서 일단 비밀번호까지 응답이 오도록 코드를 만든 다음 그것에 대해 고민을 해봤다. ResponseDto에서 password값을 포함하지 않도록 하면 되는 간단한 문제였고 또 그 후에 다시 찾아보니 Entity에서 애초에 @JsonIgnore 라고 선언을 해놓으면 그 값은 응답값이 보이지 않는다는 것을 알아냈다.

0개의 댓글

관련 채용 정보