코드 그라데이션

입문 과제 라이브코딩 (3) 자세한 설명 추가 본문

Spring/CRUD 연습

입문 과제 라이브코딩 (3) 자세한 설명 추가

완벽한 장면 2023. 2. 4. 22:06

Entity

Post

TimeStamped

@Entity
@Getter
@NoArgsConstructor
public class Post extends TimeStamped {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)

    private Long id;
    private String title;
    private String writer;
    private String password;
    private String content;

    // public Board() {} // 기본 생성자는 반드시 있어야 한다.

    public Post(String title, String writer, String password, String content) {
        this.title = title;
        this.writer = writer;
        this.password = password;
        this.content = content;
    } // 게시판 생성 - 제목, 글쓴이, 비밀번호, 내용 => 이렇게 네 개의 요소

    public void update(String title, String writer, String content) {
        this.title = title;
        this.writer = writer;
        this.content = content;
    } // 게시글 업데이트(수정) 로직 / 제목, 작성자명, 작성 내용을 수정

    //비밀번호 검증 로직
    public boolean isValidPassword(String inputPassword) {
        if (inputPassword.equals(this.password)) {
            return true;
        } else {
            return false;
        }
    }
}


---------------------


@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class TimeStamped {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

 

Repository

public interface PostRepository extends JpaRepository<Post, Long> {

    List<Post> findAllByOrderByCreatedAtDesc();

}

 

Service

@Service // 이게 없으면 서버가 안 뜬다.
public class PostService {
    private final PostRepository postRepository;
    // Repository를 상속받은 이 인터페이스는 DB처럼 동작한다. DB에게 시킬 때 얘한테 시키면 된다.

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    } // ??? 이거 질문하자. => 이것도@RequiredArgsConstructor 쓰면 되나?

    // 게시글 전체 조회 -> 요 코드 해석 부탁드립니당... 튜터님께서 쓰셔서 쓰긴 했는데, 무슨 의미인지 모르겠어요. 어레이리스트부터 for문의 의미...
    @Transactional
    public List<PostResponse> getPostList() {
        // 작성 날짜 기준 내림차순으로 정리하기 ==> 게시물 작성 최신순으로 정렬해 내놓으란 뜻.
        List<Post> postList = postRepository.findAllByOrderByCreatedAtDesc();
        List<PostResponse> postResponseList = new ArrayList<>();
        for (Post post : postList) {
            postResponseList.add(new PostResponse((post)));
        }
        return postResponseList;
    }

    @Transactional
    // 게시글 생성 로직
    public void createPost(CreatePostRequest createPostRequest) {
        Post post = new Post(createPostRequest.getTitle(), createPostRequest.getWriter(), createPostRequest.getPassword(), createPostRequest.getContent());
        postRepository.save(post);
    }

    @Transactional
    // 게시글 조회 로직
    public PostResponse getPost(Long postId) {
        Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
        return new PostResponse(post);
    }

    // 게시글 수정 로직
    @Transactional
    public void updatePost(Long postId, UpdatePostRequest updatePostRequest) {
        Post postSaved = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
        //    boardSaved.update(updateBoardRequest.getTitle(), updateBoardRequest.getWriter(), updateBoardRequest.getContent());
        //  추가요건 : 수정요청 시 수정 데이터와 비밀번호를 함께 보내서 서버에서 비밀번호 일치 여부 확인 후 업데이트 해라.
        // 이게 앞서 넣어놓은(Board 에다가) 비밀번호 유효성 검사.
        if (postSaved.isValidPassword(updatePostRequest.getPassword())) {
            postSaved.update(updatePostRequest.getTitle(), updatePostRequest.getWriter(), updatePostRequest.getContent());
            postRepository.save(postSaved);
        } else {
            throw new IllegalArgumentException("패스워드가 틀렸습니다!");
        }
    }

    // 게시글 삭제 로직
    @Transactional
    public void deletePost(Long postId, DeletePostRequest deletePostRequest) {
        Post postDelete = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
        String password = deletePostRequest.getPassword();
        if (postDelete.isValidPassword(password)) {
            postRepository.delete(postDelete); //delete는 JPA에서 직접 제공 => 쿼리 직접 날려준다
            System.out.println("삭제에 성공했습니다.");
        } else {
            throw new IllegalArgumentException("패스워드가 다릅니다!");
        }
    }

}

 

Controller

@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;

    // 게시물 생성
    @PostMapping("/api/posts")
    //CreateBoardRequest 의 값을 전달 받음.
    public void createPost(@RequestBody CreatePostRequest createPostRequest) {
        postService.createPost(createPostRequest);
    }

    // 전체 게시물 조회
    @GetMapping("/api/posts")
    public List<PostResponse> getPostList() {
        return postService.getPostList();
    }

    // 게시물 1개 조회
    ///api/posts/1
    @GetMapping("/api/posts/{postId}")
    public PostResponse getPost(@PathVariable Long postId) {
      return postService.getPost(postId);
    }

    // 게시물 1개 수정
    @PutMapping("/api/posts/{postId}")
    public void updatePost(@PathVariable Long postId, @RequestBody UpdatePostRequest updatePostRequest) {
        postService.updatePost(postId, updatePostRequest);
    }

    // 게시물 삭제
    @DeleteMapping("/api/posts/{postId}")
    public void deletePost(@PathVariable Long postId, @RequestBody DeletePostRequest deletePostRequest) {
        postService.deletePost(postId, deletePostRequest);
    }
}

 

 

DTO

@Getter
@NoArgsConstructor
public class PostResponse {
    private String title;
    private String writer;
    private String content;

    private LocalDateTime createdAt;

    private LocalDateTime modifiedAt;


    public PostResponse(Post post) {
        this.title = post.getTitle();
        this.writer = post.getWriter();
        this.content = post.getContent();
        this.createdAt = post.getCreatedAt();
        this.modifiedAt = post.getModifiedAt();
    }
}

-------------------------

@Getter
@NoArgsConstructor // 기본생성자 생성과 동일
public class CreatePostRequest {
    private String title;
    private String writer;
    private String password;
    private String content;

    //@Getter와 @NoArgsConstructor가 합쳐져서 @Setter 없어도 값이 title, writer, password, content에 꽂힌다.

    //public CreateBoardRequest() {} => NoArgsConstructor로 치환.

    public CreatePostRequest(String title, String writer, String password, String content) {
        this.title = title;
        this.writer = writer;
        this.password = password;
        this.content = content;
    }
}

-----------------

@Getter
@NoArgsConstructor
public class UpdatePostRequest {
    private String title;
    private String writer;
    private String content;
    private String password;

    public UpdatePostRequest(String title, String writer, String content, String password) {
        this.title = title;
        this.writer = writer;
        this.content = content;
        this.password = password;
    }
}

-------------------

@Getter
@NoArgsConstructor
public class DeletePostRequest {

    private String password;

    public DeletePostRequest(String password) {
        this.password = password;
    }
}

 

먼저 과제 요구사항의 3번, 게시글 작성부터 시작.

게시글 작성 - 제목, 작성자명, 작성 내용을 저장하고 저장된 게시글을 클라이언트로 반환하기

 

1. 먼저 Entity Post에 

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

	private String title;
    
    private String writer;
    
    private String password;
    
    private String content;

이거 적어줬음. 그런데 튜터님이 @Entity인데 처음에 빠뜨린 부분이 id값. id값은 고유키이므로, @Id를 써주어야 한다. 그리고 생성전략은 @GeneratedValue를 통해 결정되게 만듦

 

생성자까지 생성

public Post(String title, String writer, String password, String content) {
    this.title = title;
    this.writer = writer;
    this.password = password;
    this.content = content;
} // 게시판 생성 - 제목, 글쓴이, 비밀번호, 내용 => 이렇게 네 개의 요소

 

2. 다음으로, Post는 수정이라는 게 있네

제목, 작성자명, 작성 내용을 수정 / 값을 바꾸므로 () 안에 바꿀 값들을 넣어줌

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

 이제 update 함수를 만들었음. Post는 이 3가지의 값을 주면(title, writer, content) 바꿀 수 있네

 

3. 부연) 여기서 password에 final 붙여버리면, 생성자 만들 때, password가 나도 만들어 달라고 아우성을 친다. 그래서 일단은 final 빼고 가는 것.

4. 부연2) 이 @Entity를 만들 때는 기본 생성자를 만들어야지 각 요소들(을 찾을 수 있다.

각 요소란 위에서 선언해놓은 

private String title;
private String writer;
private String password;
private String content; 이러한 것들.

 

나중에 우리가

postRepository.save(post);

이렇게 저장을 할 건데, 그러면 뭔가 기준이 있어야 저장을 하지 않겠는가. 그래야 JPA가 로직을 처리하지.

이 때 일을 기본 생성자가 있어야지만 한다고 생각하면 된다.

Lombok으로 @NoArgsConstructor 넣어주면 기본 생성자 굳이 안 써줘도 된다.

 

5. 부연3) @Id 와 Entity의 관계

Entity는 고유해야해요. 어디서? 내 시스템에서. ex. 내 시스템에서 1번 아이디를 가진 게시물은 유일해야한다.
그렇기 때문에 Entity는 Id를 반드시 가지고 있어야 해요.

// 만약 Id 지워버리면? 빨간줄 쳐지고  "Persistent Entity should have primary key" 라는 메시지 뜬다.
고유하다는게 뭐냐면, 작성 후에 database에 저장을 했어요. 그런데 그 글이 그냥 두면 내 것인지 어떻게 알아.
내 게시물이지 남의 게시물 아니잖아. 구분 안 되면 어떻게 알아...
그것에 대한 유일성을 보장해야한다.

 

6. 게시글 작성(전반 40') - 먼저, Service 생성하고 역할 설명

@Service
public class PostService { }
public void createPost(CreatePostRequest createPostRequest) {

}

- 여러분들이 새로운 게시글을 작성하고 결국 DB에까지 저장을 해주어야만이 게시글이 생성이 완료가 되는데 DB에 요청을 해야 하잖아요. DB에 요청을 하는 그 곳. 그것이 Service다. 이렇게 이해하고 있자.

- 우리가 Entity에 Post라는 것을 만들어 놓아도 일단 얘를 핸들링(저장을 하든, 조회를 하든....) 해 줄 누군가가 필요하다

즉, @Service 는 DB 또는 Controller을 통해서 전달받은 데이터를 가지고, DB나 entity + Entity에 있는 행위(update 등)를 시키는 역할 => Controll-tower

=> 조각조각 나눠져 있는 기능들을 하나로 묶어서 하나의 큰 서비스로 묶어 주는 거예요.

 

7. 저장할 리포지토리 생성 - repository 패키지에 PostRepository 인터페이스 생성

public interface PostRepository extends JpaRepository<Post, Long> {

}

왜 인터페이스인지는 Jpa 쓰려면 상속받아서 쎃야한다고 생각하자.

 

8. 상속 이후 작업

- 상속받았기 때문에 PostService로 돌아가서

@Service // 이게 없으면 서버가 안 뜬다.
public class PostService {
    private final PostRepository postRepository;
    // 

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

여기까지 추가해줬다. Repository를 상속받은 이 인터페이스는 DB처럼 동작한다. DB에게 시킬 때 얘한테 시키면 된다.

그리고, 아직 완전한 구현 상태는 아니지만(미구현)

@Transactional
// 게시글 생성 로직
public void createPost(CreatePostRequest createPostRequest) {
    블라블라....
    postRepository.save(post);
}

postRepository.save(post); 이것까지 붙여주면, 게시글 저장이 되는 거야

 

9. Controller로 떠나기

- 그럼 우리는 createPost 할 때 필요한 데이터를 전달받아야 할 거잖아요. 이제 전달받은 쪽으로 떠난다.

- Controller은 UI 계층이나 다름없으니까 Data를 요청받고 Data를 요청을 주는...

ex) 게시물 1번! 그 1번 값을 받아야지만 뭔가 저장을 하든가 처리를 할 거잖아요. 이 역할을 하는 게 바로 컨트롤러다.

컨트롤러 생성

@RestController
@RequiredArgsConstructor
public class PostController {

}
/*
RestController와 Controller은 뭐가 달라?
일단 Rest의 유무, 그럼 RestAPI와 연관이 있겠지.json
Controller는 Model & View와 관련이 있어. => 웹페이지
 */

 

PostMapping이 하는 건 뭐냐면, UI(Browser) 에서 백엔드로 넘어오는 것, 연결선 역할을 한다.

간단한 그림

그럼 데이터가 들어왔을 때, " 난 뭘 받을 거예요." 이렇게 컴퓨터에게 알려주어야 한다.

 

지금 필요한 것이

  • 제목, 작성자명, 비밀번호, 작성 내용을 저장하고.... 니까.

전체 요구조건에 있으니까.(DTO에 데이터를 받아서 반환하라고)

 

10. DTO CreatePostRequest 생성

@Getter
@NoArgsConstructor // 기본생성자 생성과 동일
public class CreatePostRequest {
    private String title;
    private String writer;
    private String password;
    private String content;
}

// 얘가 만들어질 때, 무조건 네 가지 요소가 필요하므로, 생성자도 그렇게 만들어줘야지.
	
    public CreatePostRequest(String title, String writer, String password, String content) {
        this.title = title;
        this.writer = writer;
        this.password = password;
        this.content = content;
    }

 

그리고 이제 Controller는 기능 중에 전달 받는 곳이라고 했으므로, DTO에서 전달 받으면 된다.

 

11. 다시 PostController로 돌아가서  

@PostMapping으로 createPost를 구체화 시켜줘야 한다.

@RestController
@RequiredArgsConstructor
public class PostController {
 
 @PostMapping("/api/posts")
    public void createPost(@RequestBody CreatePostRequest createPostRequest) {
        postService.createPost(createPostRequest);
    }
}

1) Url 매핑을 해줘야 하므로 ("/api/posts")를 적었고,

2) CreateBoardRequest(DB)에서 @RequestBody로 값을 전달받아야 하니까(컴퓨터에게 알려주는 것) ()안에 이것들을 써주는 거야.

3) 이 값들을 가지고 작업 로직을 수행하는 계층이 service이므로, postService에서 createPost를 수행하고, 그 값을 다시 DB에 저장을 해 주어야 하기 때문에, postService.creatPost(createpostRequest);

 

여기서 조금 더 생각해 볼 게 뭐냐면(@Contstructor에 @NoArgs를 붙이는 이유)

(@RequestBody CreatePostRequest createPostRequest)에서 얘는 객체다. 

근데 우리가 Body로 보낼 값은 String이잖아. 

    private String title;
    private String writer;
    private String password;
    private String content; 니까.

 

근데 우리가 원하는 게 뭐냐면

들어갈 내용 예시
/*
	"title": "스프링최고" (1)이라 하고,
    "content": "abcabc"	  (2)	
    "writer": "aaa"       (3)
    "password": 123"      (4) 라고 하면
    
    이라고 하자.
*/

이 때 지금 우리가 원하는 건,

@Getter
@NoArgsConstructor 
public class CreatePostRequest {
    private String title;
    private String writer;
    private String password;
    private String content;
}

	
    public CreatePostRequest(String title, String writer, String password, String content) {
        this.title = title;  //이 우변의 title에 (1)이 들어가고
        this.writer = writer; //(2)
        this.password = password; //(3)
        this.content = content; //(4) 가 들어가게 만드는 것이다.
  }

그래서 우리는 스프링에게 뭐라고 했냐면,

"(@RequestBody CreatePostRequest createPostRequest) 얘 RequestBody야.

JSON이라는 형태로 들어올 건데,

CreatePostRequest 객체 안에 이 값들좀 넣어줘" 하고 시킨 거야.

 

요기서 등장하는 개념이 jackson

jackson은 스프링에 내장되어 있어서 gradle 문서 속에 보면 들어있다.

 

그냥 기본생성자를 붙여줘야, 컴퓨터가 지금 게시글 생성에 필요한 title, writer, password, content가 뭔지를 알게 되고,

@RequestBody를 붙여주면, json이라는 네트워크를 타고,  CreatePostRequest에 꽂히고, 객체를 가져와서 쓸 수 있게 만들어준다.

이게 Setter값이 없는데 값이 들어가는 신비! (@NoArgsConstructor와 @Getter의 컬레버)

 

12. Service로 가서 생성 작업 로직 구체화

@Service 
public class PostService {
    private final PostRepository postRepository;
    // Repository를 상속받은 이 인터페이스는 DB처럼 동작한다. DB에게 시킬 때 얘한테 시키면 된다.

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }
    
     @Transactional
    // 게시글 생성 로직
    public void createPost(CreatePostRequest createPostRequest) { // 이제 우리는 재료들을 전부 전달받았으므로, 새로운 Post를 만들면 돼.
        Post post = new Post(createPostRequest.getTitle(), createPostRequest.getWriter(), createPostRequest.getPassword(), createPostRequest.getContent());
        //DB에서 title, writer, password, content를 꺼내와서 만들면 되고, 마지막에 저장시켜 주면
        postRepository.save(post);
    }
}

// 비로소 게시글 생성이 끝난다.

- 원래대로는 Entity -> Repository -> Service - > Controller 순으로 작업하라고 했다.

- 일단 튜터님 설명 따라가면서 만드느라고 중구난방.

 

@GeneratedValue(stratege = GenerationType.AUTO) 때문에 아이디값을 우리가 직접 지정해주지 않아도 1, 2, 3... 자동 증가하였다.

 

13. 추가 설명. 의존성 주입 Controller 가지고 예를 들었음.

@RestController
public class PostController {
    private final PostService postService;
    
    public PostController(PostService postService) {
    	this.postService = postService;
    }
...
}

 

지금 여기서 우리가 new PostService(); 로 새롭게 객체생성을 하지 않고 넣어줬잖아.

원래대로 하면 이상해...

이걸 새롭게 객체생성 하지 않고 넣어줄 수 있는게 바로, @Service 어노테이션이다.

 

진짜 생성 끝!!

 

----------------------------

이제

조회

- 조회는 값을 "줘야 해"

 

14. PostService에서 조회 로직 구현 시작

PostService에서

@Transactional
// 게시글 조회 로직
public PostResponse getPost(Long postId) {
    Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
    return new PostResponse(post);
}

지금

Post post = postRepository.findById(postId)여기까지 하면, 레파지토리에서 Id를 찾으라는 말, postId 기준으로, 그러면 레파지토리가 post를 줄 거예요. 

그런데, 없으면, 없을 때 작업 로직을 하나 더 만들어줘야지. 그 때 사용하는 게 옵셔널

.orElseThrow(() -> new IllegalArgumentException("id 없음"));

 

만약 옵셔널을 안 쓴다면

/*
if (board == null) {

} else {

}
 */

뭐 요런 식으로 귀찮게 예외 처리 코드를 적어줘야 하는 성가심이 발생.

 

그런데 위에서 return new PostResponse(post); 로  new를 써준 이유는 new를 안 쓰면 가장 최근의 값만 저장되어 불러지기 때문에, 반환해줄 값이 없다. 새로 만들어서 반환해줄 필요가 있다. 그러서 new를 붙인다.

 

15. Controller 가서 url 매핑 하고 로직 써준다. (@GetMapping)

// 게시물 1개 조회
///api/posts/1
@GetMapping("/api/posts/{postId}")
public PostResponse getPost(@PathVariable Long postId) {
  return postService.getPost(postId);
}

보면,  post를 가지고 오라는 게 @PathVariable 얘고,

가지고 오는 것은 게시글 번호만 있으면 되니까 Long postId

그럼 마지막으로 나는 postService에게 postId를 가지고 게시물을 가져오라고 명령(.get(postId))

그럼 그 값을 리턴해주면 끝나!

 

16. 그런데, 필요한 부분들만 노출해야 하므로, DTO가 필요해 : PostResponse 생성

 

@Getter
@NoArgsConstructor
public class PostResponse {
    private String title;
    private String writer;
    private String content;

    public PostResponse(Post post) {
        this.title = post.getTitle();
        this.writer = post.getWriter();
        this.content = post.getContent();
    }
}
Post의 내용물을 가져와서 title, writer, content를 위에 생성 로직 만드는 때 처럼 작동하게 만드는 거지.
// 지금 비밀번호 빼준 거잖아. 빼고 반환하려고.

지금 이 세개만 걸러서 반환해주면 게시글 단건 조회 끝.

 

--------------------

수정

17. Service에 수정 로직 구현 시작. updatePost()

- 수정을 할 때는 당연히 postId를 알아야겠지.

왜? 수정을 하려면, 기존의 원 정보를 불러와야하니까.

@Transactional
public void updatePost(Long postId, UpdatePostRequest updatePostRequest) {
    Post postSaved = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
}

- Post에서 저장되어있는 post이기 때문에 명칭을 postSaved로 적어줬어.

- 우변은 조회 글하고 로직이 똑같은데, 리파지토리에서 먼저 Id를 가지고 찾고, 예외(글이 없는 상황에 대한) 처리를 옵셔널로

- update를 하려고 하니까, 필요한 게 총 3개네, 우리가 DTO를 만들어서 3개를 받아와야겠네!

그래서

 

18. UpdatePostRequest 생성해주고, DTO 구현.

public void updatePost() 안에 DTO를 끌어오는 것을 넣어줘야하므로(Long postId, UpdatePostRequest updatePostRequest)

@Getter
@NoArgsConstructor
public class UpdatePostRequest {
    private String title;
    private String writer;
    private String content;
    private String password;

    public UpdatePostRequest(String title, String writer, String content, String password) {
        this.title = title;
        this.writer = writer;
        this.content = content;
        this.password = password;
    }
}

 

19. (비밀번호 검증 로직 넣기 전) 다시 Service로

@Transactional
public void updatePost(Long postId, UpdatePostRequest updatePostRequest) {
    Post postSaved = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
    postSaved.update(updatePostRequest.getTitle(), updatePostRequest.getWriter(), updatePostRequest.getContent());
    postRepostory.save(postSaved);
}

 

정리하면, postId를 기반으로 저장된 정보(postSaved)를 가져오고, 저장된 정보에다가 요청받은 정보(UpdatePostRequest updatePostRequest)로 업데이트를 하고, 그다음에 다시 저장을 하면, 이 post가 변경된 정보대로 저장이 되겠지. 그럼 진짜 끝나는데....

 

여기서 빠진 게 비밀번호 일치 검증 로직이지.

 

20. entity Post 에 비밀번호 일치 여부 판단하는 로직 코드 작성

entity Post에서...
//비밀번호 검증 로직
    public boolean isValidPassword(String inputPassword) {
        if (inputPassword.equals(this.password)) {
            return true;
        } else {
            return false;
        }
    }
}

여기서 this.password는 원래 게시글이 가지고 있던 패스워드를 의미. 즉, inputPassword(사용자가 입력한 값)과 원래 존재하던 password를 일치하는지 비교한다는 말이겠지요.

 

21. 다시 Service로 돌아와서 저장된 post에게 물어보는 거야.

업데이트 할 때 비밀번호는 이거인데, 이게 유효한 비밀번호야?

비밀번호가 맞으면은, postRepository에 저장하고 아니면 오류메시지 내.

PostService에서...

if (postSaved.isValidPassword(updatePostRequest.getPassword())) {
    postSaved.update(updatePostRequest.getTitle(), updatePostRequest.getWriter(), updatePostRequest.getContent());
    postRepository.save(postSaved);
} else {
    throw new IllegalArgumentException("패스워드가 틀렸습니다!");
}

 

22. 전체적으로 합쳐보면 PostService에

// 게시글 수정 로직
@Transactional
public void updateBoard(Long boardId, UpdatePostRequest updatePostRequest) {
    Post postSaved = postRepository.findById(boardId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
    //    boardSaved.update(updateBoardRequest.getTitle(), updateBoardRequest.getWriter(), updateBoardRequest.getContent());
    //  추가요건 : 수정요청 시 수정 데이터와 비밀번호를 함께 보내서 서버에서 비밀번호 일치 여부 확인 후 업데이트 해라.
    // 이게 앞서 넣어놓은(Board 에다가) 비밀번호 유효성 검사.
    if (postSaved.isValidPassword(updatePostRequest.getPassword())) {
        postSaved.update(updatePostRequest.getTitle(), updatePostRequest.getWriter(), updatePostRequest.getContent());
        postRepository.save(postSaved);
    } else {
        throw new IllegalArgumentException("패스워드가 틀렸습니다!");
    }
}

 

cf. 여러분들은 대체로 이렇게 짤 것 같아요.

if(updateBoardRequest.getPassword().equals(boardSaved.getPassword())) {
} 

틀린 건 아니나...

이것은 Service가 일을 시키는 게 아니라 자기가 직접 일을 하는 것.

업데이트가 되고 안 되고는 컨트롤타워인 Service가 결정할 수도 있어, 그런데 비밀번호가 맞는지 안 맞는지는 Post에게 시켜야 하는 거야.

============

 

23. @Transactional 설명

@Transactional 설명
후15 : 우리가 Repository find를 하거나 save를 하면, insert into -> 해서 쿼리가 날아가잖아.
이것을 @Transactional가 달려있으면 이것이 달려있는 모든 함수가 정상적으로 구동 완료될 때까지
insert into를 날리지 않고 기다린다. 그 다음에 정상적으로 모두 완료가 되면, 쿼리를 날린다.
=> Transaction이 달리면 묶어서 계속 관찰하는 느낌. 무엇을? Board를.
** 어떻게 관찰할 수 있느냐, Board는 JPA 입장에선 내 뱃속에서 낳은 아이거든 그럼 Board를 계속 어르고 달래.
그런데 보드가 중간에 어떤 요건에 의해 바뀌었잖아. 그 변경 사실을 jpa가 알고 있는 거야. 변경된 사실을 알고,
개발자가 리포지토리 호출해서 뭘 하지 않아도, 이 묶은 게 정상적으로 다 수행되고 나면,
SQL문으로 바꾸는 과정에서, update 쿼리를 알아서 날려줘요.

덩어리로 묶는 가장 큰 이유는 만약 개별적으로 있을 때라면 쿼리가 각각 날아가야 하는데, 한 번 잘못되어버리면
일일이 복구를 해줘야하는 매우 큰 번거로움이 있지. 그런데 @Transactional이 달려있으면, 추적 관찰하다가
정상완료 딱 해서 소급적용 해버리면 "임팩트 있게 딱 필요한 것만" DB로 보낼 수 있어서 효율적.

오류가 나면 앞으로 되돌아가는게 아니라 그 부분을 처리를 안 해버리는 것.

 

24. Controller에서 매핑

 

// 게시물 1개 수정
@PutMapping("/api/posts/{boardId}")
public void updateBoard(@PathVariable Long boardId, @RequestBody UpdatePostRequest updatePostRequest) {
    boardService.updateBoard(boardId, updatePostRequest);
}

업데이트 요청을 받을 때는 

Long postId를 가지고 하고, 요청을 받으려면 @RequestBody로 넣어줘야만 스프링이 인지한다고 했고, 넣어주면 값이 전달되어 들어올 것이고,  그럼 Service에서 실질적으로 로직이 수행 되면서, 업데이트가 되는 코드입니다.

 

-------------------------

 

25. 삭제 구현 시작. Service에서

@Transactional
public void deleteBoard(Long boardId, String password) {
    Post postDelete = postRepository.findById(boardId).orElseThrow(() -> new IllegalArgumentException("id 없음"));
    if (postDelete.isValidPassword(password)) {
            postRepository.delete(postDelete); //delete는 JPA에서 직접 제공 => 쿼리 직접 날려준다
            System.out.println("삭제에 성공했습니다.");
        } else {
            throw new IllegalArgumentException("패스워드가 다릅니다!");
        }
    }

 

26. Controller로 간다. 

 

그런데 API 표준 문서를 보면, @DeleteMapping은 Body를 못 보내게 되어있다.

@RequestBody로 구현하려면, @DeleteMapping을 @PostMapping으로 바꾸고, (@PathVariable Long boardId, @RequestBody DeleteRequest deleteRequest) 이렇게 썼다.

그래서 우리가 쓰는 게 @RequestParam을 쓴다.

@RequestParam에서 String password를 직접 받아버려

 

    // 게시물 삭제
    @DeleteMapping("/api/posts/{boardId}")
    public void deleteBoard(@PathVariable Long boardId, @RequestParam String password) {
        boardService.deleteBoard(boardId, password);
    }
}

 이러면 끝!

 

----

전체 게시글 목록 조회

27. 시간 조회한 거 미리 만들었음(영빈 설명)

일단 TimeStamped 클래스 생성

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class TimeStamped {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

 

28. Aplication에 일부 내용 추가

@EnableJpaAuditing

그래서

@SpringBootApplication
@EnableJpaAuditing
public class Spring1stReApplication {

    public static void main(String[] args) {
        SpringApplication.run(Spring1stReApplication.class, args);
    }

}

이렇게 됨

 

29. 그다음에 entity Post 가서 TimeStamped 상속

@Entity
@Getter
@NoArgsConstructor
public class Post extends TimeStamped {

 

30. PostResponse에 시간 관련 변수 2개 추가

@Getter
@NoArgsConstructor
public class PostResponse {
    private String title;
    private String writer;
    private String content;

    private LocalDateTime createdAt;

    private LocalDateTime modifiedAt;


    public PostResponse(Post post) {
        this.title = post.getTitle();
        this.writer = post.getWriter();
        this.content = post.getContent();
        this.createdAt = post.getCreatedAt();
        this.modifiedAt = post.getModifiedAt();
    }
}

 

31. 문제 요구 조건에서 (조회) 작성 날짜 기준 내림차순(최신순)으로 정렬하라고 했으므로, Repository 가서 코드 추가.

public interface PostRepository extends JpaRepository<Post, Long> {

    List<Post> findAllByOrderByCreatedAtDesc();
}

 

---------

32. 튜터님은 컨트롤러 작업부터 하셨음.

전체니까 리스트로 내놓으라 하면 되고, id는 필요 없고,

PostService에서 getPost리스트 가져오고, id는 필요 없는 걸 써주면 된다. (return postService.getPostList();)

// 전체 게시물 조회
@GetMapping("/api/posts")
public List<PostResponse> getPostList() {
    return postService.getPostList();
}

 

33. Service에서 역시, 전체 Post를 내놓으라고 하면 되고, 리포지토리에서 작성 날짜 기준 내림차순 정렬 되게 가져오면 된다. => List<Post> postList = postRepository.findAllByOrderByCreatedAtDesc();

 

리턴 하려면 리스트 형태로 만들어줘야 한다. List<PostResponse> postResponseList = new ArrayList<>();

이중for문을 돌면서 이것들을 리스트에 넣어준다(배워서 안다.)

그리고 이 작업이 완료된 List값을 반환해준다.

@Transactional
public List<PostResponse> getPostList() {
    // 작성 날짜 기준 내림차순으로 정리하기 ==> 게시물 작성 최신순으로 정렬해 내놓으란 뜻.
    List<Post> postList = postRepository.findAllByOrderByCreatedAtDesc();
    List<PostResponse> postResponseList = new ArrayList<>();
    for (Post post : postList) {
        postResponseList.add(new PostResponse((post)));
    }
    return postResponseList;
}
728x90
Comments