본문 바로가기
개발진행목록/게시판 서비스

[JPA로 게시판 만들기] 서비스, 레포지토리 개발

by o3oppp 2025. 1. 18.
Member 서비스, 레포지토리

MemberService.java

package project.post.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import project.post.domain.Member;
import project.post.repository.MemberRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

    private final MemberRepository memberRepository;

    /**
     * 회원 가입
     */
    @Transactional
    public Long join(Member member){
        validMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    /**
     * 회원 중복 조회
     */
    private void validMember(Member member){
        int result = memberRepository.findByEmail(member.getEmail());
        if(result > 0){
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    /**
     * 특정 회원 조회
     */
    public Member findOne(Long memberId){
        return memberRepository.findOne(memberId);
    }
}
  • @RequiredArgsConstructor : 초기화 되지않은 final 필드나, @NotNull이 붙은 필드에 대해 생성자를 생성해 줌
    • 새로운 필드를 추가할 때 다시 생성자를 만들어야 하는 번거로움을 없애줌(@Autowired를 사용하지 않고 의존성 주입)
  • @Transactional : 트랜잭션 처리를 위한 어노테이션
    • 클래스 단위 혹은 메서드 단위에 선언
    • 클래스에 선언 시 해당 클래스에 속하는 public 메서드에 공통적으로 적용
    • 클래스에 선언된 트랜잭션의 우선순위가 가장 높고, 인터페이스에 선언된 트랜잭션의 우선순위가 가장 낮음
    • 메서드에 선언 시 해당 메서드만 적용
    • readOnly = true 속성을 설정함으로써 클래스 전체에 읽기 전용 모드로 설정
    • 읽기 전용 모드로 설정하는 이유는 영속성 컨텍스트에 flush를 하지 않으므로 약간의 성능 최적화
    • @PostConstruct 같이 초기화 코드에 선언 시 트랜잭션 미적용

MemberRepository.java

package project.post.repository;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import project.post.domain.Member;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public void save(Member member){
        em.persist(member);
    }

    public int findByEmail(String memberEmail){
         return em.createQuery("select m from Member m"
                +" where m.email = :email", Member.class)
                .setParameter("email",memberEmail)
                .getResultList()
                .size();
    }

    public List<Member> findAll(){
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public Member findOne(Long memberId){
        return em.find(Member.class, memberId);
    }
}
  • @Repository : 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환

Post 서비스, 레포지토리

PostDto.java

package project.post.dto;

import lombok.Getter;
import lombok.Setter;
import project.post.domain.PostStatus;

import java.time.LocalDateTime;

@Getter
@Setter
public class PostDto {
    String title;
    String content;
    LocalDateTime postDate;
    
    public PostDto(String title, String content, LocalDateTime postDate) {
        this.title = title;
        this.content = content;
        this.postDate= postDate;
    }
}

PostService.java

package project.post.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import project.post.domain.Member;
import project.post.domain.Post;
import project.post.domain.PostStatus;
import project.post.dto.PostDto;
import project.post.repository.MemberRepository;
import project.post.repository.PostRepository;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {

    private final MemberRepository memberRepository;
    private final PostRepository postRepository;
    //private final PostStatus postStatus;

    /**
     * 게시글 등록
     */
    @Transactional
    public Long savePost(Long memberId, PostDto postDto){
        Member member = memberRepository.findOne(memberId);
        Post post = new Post();
        post.setTitle(postDto.getTitle());
        post.setContent(postDto.getContent());
        post.setStatus(PostStatus.CREATE);
        post.setPostDate(postDto.getPostDate());
        post.setMember(member);
        postRepository.savePost(post);
        return post.getId();
    }

    /**
     * 게시글 조회
     */
    public List<Post> findPost(){
        return postRepository.findAll();
    }

    /**
     * 제목, 내용으로 게시글 조회
     */
    public List<Post> findPostByCondition(String title, String content){
        return postRepository.findAllByCriteria(title, content);
    }

    public Post findPostById(Long postId){
        return postRepository.findOne(postId);
    }

    /**
     * 게시글 수정
     */
    @Transactional
    public void updatePost(Long postId, PostDto postDto){
        Post findPost = postRepository.findOne(postId);
        findPost.update(postDto.getTitle(), postDto.getContent(), PostStatus.MODIFY, LocalDateTime.now());
    }

    /**
     * 게시글 삭제
     */
    @Transactional
    public void deletePost(Long postId){
        Post findPost = postRepository.findOne(postId);
        findPost.update(findPost.getTitle(), findPost.getContent(), PostStatus.DELETE, LocalDateTime.now());
    }
}
  • 게시글 수정, 삭제의 경우 status의 값만 변경할 예정이므로 데이터 수정으로 진행
  • 변경 감지 기능 사용
    • Repository에서 직접 update를 진행하는 것이 아닌, 엔티티 객체를 수정함으로써 commit 시점에 초기 저장된 엔티티(스냅샷)와 비교하여 변경된 내용이 있으면 자동으로 update 쿼리를 보내도록 구현
    • 트랜잭션 내에서 조회 후 변경하면 commit 시점에 변경 감지 발생
  • merge를 사용하는 경우, 모든 속성이 변경되기 때문에 파라미터로 전달되지 않은 필드의 값은 null로 변경되어 사이드 이펙트 발생 가능
  • updatePost에서 직접 데이터를 set하지 않고 엔티티 내의 메서드에서 진행하는 이유는 추후 추적이 용이하도록 하기 위함(담당자 변경, 유지보수 등)

PostRepository.java

package project.post.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import project.post.domain.Post;
import project.post.domain.PostStatus;

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

@Repository
@AllArgsConstructor
public class PostRepository {

    private EntityManager em;

    public void savePost(Post post){
        em.persist(post);
    }

    public List<Post> findAll(){
        return em.createQuery("select p from Post p where p.status <> :postStatus", Post.class)
                .setParameter("postStatus", PostStatus.DELETE)
                .getResultList();
    }

    public Post findOne(Long postId){
        return em.find(Post.class, postId);
    }

    public List<Post> findAllByCriteria(String title, String content){
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Post> cq = cb.createQuery(Post.class);
        Root<Post> p = cq.from(Post.class);

        List<Predicate> criteria = new ArrayList<>();

        // 제목 검색
        if(StringUtils.hasText(title)){
            Predicate findTitle = cb.like(p.<String>get("title"),"%" + title + "%");
            criteria.add(findTitle);
        }

        // 내용 검색
        if(StringUtils.hasText(content)){
            Predicate findContent = cb.like(p.<String>get("content"),"%" + content + "%");
            criteria.add(findContent);
        }

        Predicate basicCondition = cb.notEqual(p.get("status"), "DELETE");
        criteria.add(basicCondition);

        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Post> query = em.createQuery(cq).setMaxResults(1000);
        return query.getResultList();
    }
}
  • @AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자를 생성해 줌
  • findAll 메서드와 findAllByCriteria 메서드의 경우 status <> "DELETE"로 설정하여 삭제된 게시글은 조회되지 않도록 함

전체 코드

 

to_post/src/main/java/project/post at main · pparkcoder/to_post

[Side Project] SpringBoot + JPA를 활용한 게시판 만들기. Contribute to pparkcoder/to_post development by creating an account on GitHub.

github.com