스프링 부트로 블로그 서비스 개발하기

포스트 CRUD 기능 - 도메인과 서비스단

exena 2025. 12. 5. 05:18

디렉토리를 주제별로 나누어 주자. 토비님의 강의에서는 애그리거트 단위로 나눠주는 부분에 해당한다.

일단 기본적으로 글을 올리고, 수정하고, 삭제할 수 있어야 한다(Create, Read, Update, Delete).

포스트 CRUD 기능은 post 디렉토리를 만들어서 넣어주도록 하겠다.

@Entity
@Getter
@NoArgsConstructor(access= AccessLevel.PROTECTED)
@AllArgsConstructor(access= AccessLevel.PROTECTED)
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    private String title;
    @Setter
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    @JsonIgnore
    private Member member;

    public static Post of(PublishBlogpostFormRequest request, Member member){
        Long id = requireNonNull(request.getPostId());
        String title = requireNonNull(request.getTitle());
        String content = request.getContent();
        return new Post(id, title, content, requireNonNull(member));
    }
}

일단 포스트(블로그 글) 엔티티를 만들어 줬다.

  • @Entity를 사용해서 JPA(Java Persistence API)의 관리를 받는다. 이로써 데이터베이스에서 포스트 데이터를 가져와서 자바의 포스트 객체를 생성하는 것이 가능하다.
  • 롬복의 @Getter를 써서 모든 필드에 get 함수를 만들어줬다. 블로그 글의 id, 제목, 내용을 읽어야 하기 때문이다.
  • 롬복의 @NoArgsConstructor를 써서 기본 생성자를 만들어줬다. JPA가 블로그 포스트 객체를 만들 때 기본 생성자가 쓰이며, 필수이다. 대부분의 JPA 구현 라이브러리는 자동으로 생성해주긴 하지만 써주는것이 권장된다. 여기에 protected 옵션을 넣어서 다른곳에서 사용하는 것을 막았다.
  • 글 작성자 Member는 레이지로딩을 걸었고 혹시 글 정보를 json으로 보낼 때 순환참조되는 것을 막기 위해서 @JsonIgnore를 넣었다.

 

만약 데이터베이스가 자동생성이 아니라면 테이블을 만들어주자.

public interface PostRepository extends JpaRepository<Post, Long> {
    Page<Post> findByMember(Member member, Pageable pageable);
}

포스트 리파지토리를 만들었다. 리파지토리는 데이터베이스에서 블로그 포스트 데이터를 가져오거나 포스트를 데이터베이스에 저장할 때 사용되는 클래스이다.

  • @Repository 어노테이션은 상속받는 인터페이스에 포함되어 있다. 스프링이 서버가 구동될 때 하나의 리파지토리 객체를 만들고 관리한다.
  • 스프링 데이터 JPA를 사용하면 인터페이스만 만들어도 기본적인 리파지토리 클래스를 만들어준다.
  • 스프링에서 제공하는 Page 클래스를 사용하면 블로그 홈에서 간편하게 페이지네이션 기능을 쓸 수 있다.

 

@Service
@RequiredArgsConstructor
public class PostService implements PostFinder, PostModify, PostPermission {

    private final MemberRepository memberRepository;

    private final PostRepository postRepository;

    public Page<Post> getUsersPosts(String userId, Pageable pageable) {
        Member member = memberRepository.findByEmail(new Email(userId))
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + userId));

        return postRepository.findByMember(member, pageable);
    }


    public Post getPost(Long postId) {
        return postRepository.findById(postId)
                .orElseThrow(() -> new PostNotFoundException("Post not found"));
    }

    public void publishPost(String userId, PublishBlogpostFormRequest request){
        Member member = memberRepository.findByEmail(new Email(userId))
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + userId));
        Post post = Post.of(request, member);
        postRepository.save(post);
    }

    public void editPost(String userId, PublishBlogpostFormRequest request){
        Post post = getPost(request.getPostId());
        post.setTitle(request.getTitle());
        post.setContent(request.getContent());
        postRepository.save(post);
    }

    public void deletePost(Long postId, Authentication auth){
        checkAuthorPermission(postId, auth);
        postRepository.deleteById(postId);
    }

    public void checkAuthorPermission(Long postId, Authentication auth) {
        Post post = getPost(postId);
        if (!post.getMember().getEmail().address().equals(auth.getName())) {
            throw new SecurityException("You are not the author of this post");
        }
    }

}

서비스 단은 처음엔 안만들려고 했는데 포스팅 기능을 만들면서 DTO도 만들고 변환과정도 넣고 유저 검증로직도 넣고 하다보니까 결국 만들게 되더라.

  • 영속성 관리가 되는 객체에 save함수를 쓰는 것만으로 JPA가 데이터 삽입이 아닌 수정을 알아서 해준다.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class PostNotFoundException extends RuntimeException {
    public PostNotFoundException(String message) {
        super(message);
    }
}

그리고 그냥 예외도 하나 추가해줬다.

  • 런타임에러(언체크에러)는 함수에 throws를 쓰지 않아도 된다.
  • 컨트롤러에서 직접 404를 반환하게 하는 것 보다는 여기서 @ResponseStatus 어노테이션을 쓰거나 @RestControllerAdvice와 @ExceptionHandler를 써서 전역 처리 클래스를 만드는 쪽을 추천함. (서비스가 조회 결과로 null을 반환하면 컨트롤러에서 404로 응답하도록 하는 건 결국 서비스를 컨트롤러에 맞춰서 짜는 것이므로 의존성의 역전임)
@Data
public class PublishBlogpostFormRequest {
    private Long postId;
    @NotBlank
    @Size(min=1, max=50)
    private String title;
    private String content;
}

글 작성 DTO

  • 유저 ID를 포함하지 않는 이유는 어차피 컨트롤러에서 유저 Authentication 체크를 통해서 현재 유저가 누구인지를 알아내는데 프론트엔드에서 해당 정보를 넘겨줄 이유가 없기 때문이다.

세가지 포트를 서비스 하나로 구현한게 조금 그렇긴 한데.