[Spring] 아웃소싱 프로젝트 <게시물 CRUD>

Jiwoo·2024년 6월 20일
0

Spring

목록 보기
15/19

Post

package com.twelve.challengeapp.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "post") // 이 Entity가 매핑되는 DB 테이블의 이름 지정
@Entity // 이 클래스가 JPA Entity임을 나타냄
@Getter // Lombok 사용해 모든 필드에 대해 getter 메서드 자동 생성
@NoArgsConstructor // Lombok 사용해 매개변수 없는 기본 생성자 자동 생성
public class Post extends Timestamped {

	@Id // Entity의 기본 키임을 나타냄
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키의 값을 DB가 자동으로 생성하도록 지정
    private Long id; // 게시물 고유 ID
    
	private String title;
	private String content;

	@ManyToOne(fetch = FetchType.LAZY) // 다대일 관계, 지연 로딩 
	@JoinColumn(name = "user_id") // 외래 키 컬럼 이름 user_id
	private User user; // 게시글 작성한 사용자 나타내는 필드, User Entity와 연관 관계

	@Builder // 생성자 통해 빌더 패턴 사용
	public Post(String title, String content) { // title, content 필드 초기화
		this.title = title;
		this.content = content;
	}

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

	void setUser(User user) { // 게시글 작성자 설정 메서드 (같은 패키지 내에서만 접근 가능)
		this.user = user;
	}
}

PostRequestDto

클라이언트에서 서버로 요청 데이터 전송 (게시물 생성, 업데이트에 사용)

package com.twelve.challengeapp.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

    @Builder
    public PostRequestDto(String title, String content) { // 매개변수 필드 초기화 (객체가 유효한 상태로 생성됨을 보장)
        this.title = title; // 매개변수로 받은 title을 클래스의 title 필드에 할당
        this.content = content;
    }
}

// userID, createdAt, updatedAt 은 서버에서 관리되는 필드이기 때문에 클라이언트가 직접 설정 X

PostResponseDto

서버에서 클라이언트로 응답 데이터 전송 (게시물 조회 시 상세 정보 반환에 사용)

package com.twelve.challengeapp.dto;

import com.twelve.challengeapp.entity.Post;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor // 기본 생성자 자동 생성 (아무런 인자도 받지 않는다)
public class PostResponseDto {
    private Long id;
    private Long username;
    private String title;
    private String content;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

	// Post Entity 객체 받아서, 해당 Entity의 데이터를 DTO 필드에 복사
    public PostResponseDto(Post post) {
        this.id = post.getId(); // Post Entity의 id 필드를 PostResponseDto의 id 필드에 복사
        this.username = post.getUser().getUsername();
        this.title = post.getTitle();
        this.content = post.getContent();
        this.createdAt = post.getCreatedAt();
        this.updatedAt = post.getUpdatedAt();
    }
}

PostRepository

JPA 사용해 Post Entity에 대한 DB 작업 수행

package com.twelve.challengeapp.repository;

import com.twelve.challengeapp.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> {
}
/*JpaRepository 인터페이스 상속받음으로써 Post Entity에 대해 기본적인 CRUD 작업 수행*/

PostController

클라이언트 요청 처리, 요청에 따라 서비스 계층 호출해 비즈니스 로직 수행 후, 응답 생성해 반환

package com.twelve.challengeapp.controller;

import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import com.twelve.challengeapp.jwt.UserDetailsImpl;
import com.twelve.challengeapp.service.PostService;
import com.twelve.challengeapp.util.SuccessResponseFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController // JSON 또는 XML 형식으로 응답 반환
@RequestMapping("/api/posts") // 클래스의 모든 메서드가 이 url 경로에 매핑
@RequiredArgsConstructor // final이 붙은 필드들을 매개변수로 갖는 생성자 자동 생성 (의존성 주입 처리 간편)
public class PostController {
    private final PostService postService; // 필드 주입 위한 생성자 생성

    // 게시글 등록
    @PostMapping
    public ResponseEntity<?> createPost(@RequestBody PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Long userId = userDetails.getUserId(); // 인증된 사용자 ID 가져온다
        PostResponseDto responseDto = postService.createPost(requestDto, userId);
        return SuccessResponseFactory.ok(responseDto); // 200 ok 상태와 함께 반환
    }
    
	/*
	@RequestBody PostRequestDto requestDto: 클라이언트가 보낸 JSON 데이터를 postRequestDto 객체로 변환해 매개변수로 받는다
	@AuthenticationPrincipal UserDetailsImpl userDetails: Spring Security 통해 인증된 사용자 정보 매개변수로 받는다
	*/

    // 선택 게시글 조회
    @GetMapping("/{postId}")
    public ResponseEntity<?> getPost(@PathVariable Long postId) { // 경로에서 추출한 postId 파라미터로 받는다
        PostResponseDto responseDto = postService.getPost(postId); // getPost 호출해 postId에 해당하는 게시글 조회 후 PostResponseDto로 반환
        return SuccessResponseFactory.ok(responseDto); // 생성된 게시물 정보 포함한 성공 응답 반환
    }

    // 전체 게시글 조회
    @GetMapping
    // 기본 페이지 크기 5, createdAt 기준 내림차순 정렬
    public ResponseEntity<?> getPosts(@PageableDefault(size = 5, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
        Page<PostResponseDto> posts = postService.getPosts(pageable); // getPosts 메서드 호출해 Page<PostResponseDto> 반환
        return SuccessResponseFactory.ok(posts); // 게시글 목록 posts 반환
    }

    // 선택 게시글 수정
    @PutMapping("/{postId}")
    public ResponseEntity<?> updatePost(@PathVariable Long postId, @RequestBody PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Long userId = userDetails.getUserId();
        PostResponseDto responseDto = postService.updatePost(postId, requestDto, userId); // postId 에 해당하는 게시글 업데이트하고, 업데이트 된 PostResponseDto 반환
        return SuccessResponseFactory.ok(responseDto);
    }

    // 선택 게시글 삭제
    @DeleteMapping("/{postId}")
    public ResponseEntity<?> deletePost(@PathVariable Long postId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Long userId = userDetails.getUserId();
        postService.deletePost(postId, userId);
        return SuccessResponseFactory.noContent(); // 삭제 작업 성공적
    }
}

PostService

CRUD 핵심 기능 정의, 각 메서드는 DTO를 통해 데이터 주고받는다

package com.twelve.challengeapp.service;

import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface PostService {
    PostResponseDto createPost(PostRequestDto requestDto, Long userId);
    Page<PostResponseDto> getPosts(Pageable pageable); // 페이징 정보 담은 Pageable 객체
    PostResponseDto getPost(Long id);
    PostResponseDto updatePost(Long id, PostRequestDto requestDto, Long userId);
    void deletePost(Long id, Long userId);
}

PostServiceImpl

PostService 인터페이스 구현해 CRUD 기능 제공

package com.twelve.challengeapp.service;

import com.twelve.challengeapp.dto.PostRequestDto;
import com.twelve.challengeapp.dto.PostResponseDto;
import com.twelve.challengeapp.entity.Post;
import com.twelve.challengeapp.entity.User;
import com.twelve.challengeapp.exception.PostNotFoundException;
import com.twelve.challengeapp.exception.UserNotFoundException;
import com.twelve.challengeapp.repository.PostRepository;
import com.twelve.challengeapp.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service // Spring이 이 클래스를 서비스 빈으로 인식하고 관리
@RequiredArgsConstructor // final 필드를 매개변수로 받는 생성자 자동 생성
public class PostServiceImpl implements PostService {

	private final PostRepository postRepository; // 게시글 관련 DB 작업
	private final UserRepository userRepository; // 사용자 관련 DB 작업

	@Override
	@Transactional // 메서드 내에서 수행되는 작업들이 하나의 트랜잭션으로 처리됨을 보장
	public PostResponseDto createPost(PostRequestDto requestDto, Long userId) {

		User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("Not found user.")); // userId로 사용자 조회

		Post post = Post.builder().title(requestDto.getTitle()).content(requestDto.getContent()).build(); // 게시글 Entity 생성

		user.addPost(post); // 사용자의 게시글 리스트에 새 게시글 추가
		Post savedPost = postRepository.save(post); // 게시글 DB에 저장

		return new PostResponseDto(savedPost); // 저장된 게시글 정보 반환
	}

	@Override
	public Page<PostResponseDto> getPosts(Pageable pageable) { // 페이징 정보 기반으로 모든 게시글 조회
		return postRepository.findAll(pageable).map(PostResponseDto::new); // 매핑해 반환
	}

	@Override
	public PostResponseDto getPost(Long id) {
		Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found"));
		return new PostResponseDto(post);
	}

	@Override
	@Transactional
	public PostResponseDto updatePost(Long id, PostRequestDto requestDto, Long userId) {
		Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found"));

		if (!(post.getUser().getId() == userId)) {
			throw new SecurityException("You are not authorized to update this post");
		} // 게시글 작성자와 요청 사용자가 동일한지 확인, 다르면 예외처리

		post.update(requestDto.getTitle(), requestDto.getContent());
		return new PostResponseDto(post); // 수정된 게시글 정보 반환
	}

	@Override
	@Transactional
	public void deletePost(Long id, Long userId) {
		Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException("Post not found")); // 주어진 id로 게시글 조회

		if (!(post.getUser().getId() == userId)) {
			throw new SecurityException("You are not authorized to delete this post");
		}

		postRepository.delete(post);
	}
}

0개의 댓글