본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지#003] 타임라인 도메인 개발 및 테스트

by 뚜루리 2025. 1. 20.
728x90
320x100

[사용기술]

Java, Spring Boot, Spring JPA, MySQL

 

[만들려는 것]

책을 위한 SNS.

 

[오늘 하려는 것]

타임라인(Timeline) 도메인 개발 및 테스트

 


 

 

TimelineRepository.java

package seulgi.bookbookclub.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import seulgi.bookbookclub.domain.Follow;
import seulgi.bookbookclub.domain.State;
import seulgi.bookbookclub.domain.Timeline;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Repository
public class TimelineRepository {

    @PersistenceContext
    private EntityManager em;

    //저장
    public void save(Timeline timeline){
        em.persist(timeline);
    }

    //삭제
    public void delete(Timeline timeline) {
        timeline.delete(); //상태변경
    }

    // 특정 회원의 타임라인 조회
    public List<Timeline> findTimelinesByMember(Integer memberSeq) {
        return em.createQuery("SELECT t FROM Timeline t WHERE t.member.memberSeq = :memberSeq AND t.state = :state ORDER BY t.createdDate DESC", Timeline.class)
                .setParameter("memberSeq", memberSeq)
                .setParameter("state", State.ACTIVE)
                .getResultList();
    }

    // 타임라인 ID로 조회
    public Timeline findById(Integer timelineSeq) {
        return em.createQuery("SELECT t FROM Timeline t WHERE t.timelineSeq = :timelineSeq AND t.state = :state", Timeline.class)
                .setParameter("timelineSeq", timelineSeq)
                .setParameter("state", State.ACTIVE)
                .getSingleResult();
    }

    // 팔로우한 회원들의 타임라인 조회
    public List<Timeline> findByFollowers(List<Integer> memberSeqs) {
        return em.createQuery("SELECT t FROM Timeline t WHERE t.member.memberSeq IN :memberSeqs ORDER BY t.createdDate", Timeline.class)
                .setParameter("memberSeqs", memberSeqs)
                .getResultList();
    }

}
  • 회원과 타임라인 모두 물리삭제가 아닌 논리삭제여서 삭제 메서드에서는 상태만 변경하도록 해주었음.
  • 그래서, 조회할 때도 where절에 상태 값을 반드시 넣어 조회함.

 

(+) Timeline.java 추가

    /**
     * 논리 삭제 메서드
     */
    public void delete() {
        this.state = State.INACTIVE;
    }
  • 삭제 메서드는 timeline 클래스에 따로 추가해주었음.

 

 

TimeService.java

package seulgi.bookbookclub.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import seulgi.bookbookclub.domain.Book;
import seulgi.bookbookclub.domain.Follow;
import seulgi.bookbookclub.domain.Member;
import seulgi.bookbookclub.domain.Timeline;
import seulgi.bookbookclub.repository.BookRepository;
import seulgi.bookbookclub.repository.MemberRepository;
import seulgi.bookbookclub.repository.TimelineRepository;

import java.util.List;
import java.util.stream.Collectors;

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

    private final TimelineRepository timelineRepository;
    private final MemberRepository memberRepository;
    private final BookRepository bookRepository;
    private final FollowService followService;

    //글 작성
    @Transactional
    public Integer createTimeline(Integer memberSeq, Long bookSeq, String contents) {
        Member member = memberRepository.findByMemberSeq(memberSeq);
        Book book = bookRepository.findByBookSeq(bookSeq);

        Timeline timeline = Timeline.createTimeline(member, contents, book);

        timelineRepository.save(timeline);
        return timeline.getTimelineSeq();
    }

    //글 삭제
    @Transactional
    public void deleteTimeline(Integer timelineSeq){
        Timeline timeline = timelineRepository.findById(timelineSeq);
        if (timeline == null){
            throw new IllegalArgumentException("해당 글이 존재하지 않습니다.");
        }
        timeline.delete();
    }

    // 특정 회원의 타임라인 조회
    public List<Timeline> getMemberTimelines(Integer memberSeq){
        return timelineRepository.findTimelinesByMember(memberSeq);
    }

    // 팔로우한 회원들의 타임라인 조회
    public List<Timeline> getFollowedTimelines(Integer memberSeq) {
        List<Follow> follows = followService.getFollowing(memberSeq);
        List<Integer> followingSeqs = follows.stream()
                .map(follow -> follow.getFollowing().getMemberSeq())
                .collect(Collectors.toList());
        return timelineRepository.findByFollowers(followingSeqs);
    }

}
  • 글 작성하는 서비스를 만들기 위해선 Member, Book 객체가 반드시 있어야 해서 아예 타임라인 도메인에 생성메서드를 생성해서 서비스에서 사용하는 형태로 만듦.
  • 여러 서비스나 레파지토리에서 llegalArgumentException, IllegalStateException를 자주 쓰는데 어떤 걸 어떨 때 써야 할지를 아래의 포스팅에 간단히 정리하였다.

 

(+) llegalArgumentException VS IllegalStateException

 

[Java] IllegalArgumentException VS IllegalStateException : 어떨 때 뭘 써야 하지?

개발할 때 IllegalArgumentException과 IllegalStateException를 자연스럽게 많이 쓰는 경우를 봤는데 어떨 때 어떤 걸 써야 할지 알아두고 싶어서 정리!  1. IllegalArgumentException목적 : 메서드에 전달된 인자가

ddururiiiiiii.tistory.com

 

 

(+) Timeline.java 수정

package seulgi.bookbookclub.domain;

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

import java.time.LocalDateTime;

import static jakarta.persistence.FetchType.*;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
public class Timeline {

    //...코드생략...
    
    // 생성 메서드
    public static Timeline createTimeline(Member member, String contents, Book book) {
        if (member == null) throw new IllegalArgumentException("Member는 null일 수 없습니다.");
        if (book == null) throw new IllegalArgumentException("Book은 null일 수 없습니다.");
        if (contents == null || contents.isBlank()) throw new IllegalArgumentException("Contents는 필수입니다.");
        return new Timeline(member, contents, book);
    }

    //...코드생략...
}

 

 

 

타임라인 도메인 테스트

package seulgi.bookbookclub.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import seulgi.bookbookclub.domain.Book;
import seulgi.bookbookclub.domain.Member;
import seulgi.bookbookclub.domain.Timeline;
import seulgi.bookbookclub.repository.BookRepository;
import seulgi.bookbookclub.repository.FollowRepository;
import seulgi.bookbookclub.repository.MemberRepository;
import seulgi.bookbookclub.repository.TimelineRepository;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class TimelineServiceTest {

    @Autowired TimelineService timelineService;
    @Autowired TimelineRepository timelineRepository;
    @Autowired  MemberRepository memberRepository;
    @Autowired BookRepository bookRepository;
    @Autowired FollowRepository followRepository;
    @Autowired FollowService followService;

    @Test
    void 글작성(){
        // given
        Member member = new Member("testid", "1234", "닉네임");
        memberRepository.save(member); // Member 저장
        Book book = new Book("1234567890123", "테스트 책", "테스트 저자", "테스트 출판사");
        bookRepository.save(book); // Book 저장
        String contents = "이 책 정말 재미있었습니다!";

        // when
        Integer timelineId = timelineService.createTimeline(member.getMemberSeq(), book.getBookSeq(), contents);

        // then
        Timeline savedTimeline = timelineRepository.findById(timelineId);
        assertNotNull(savedTimeline);
        assertEquals(contents, savedTimeline.getContents());
        assertEquals(member.getMemberSeq(), savedTimeline.getMember().getMemberSeq());
        assertEquals(book.getBookSeq(), savedTimeline.getBook().getBookSeq());

    }

    @Test
    void 글삭제(){
        //given
        Member member = new Member("testid", "1234", "닉네임");
        memberRepository.save(member);
        Book book = new Book("1234567890123", "테스트 책", "테스트 저자", "테스트 출판사");
        bookRepository.save(book);
        String contents = "이 책 정말 재미있었습니다!";
        Integer timelineId = timelineService.createTimeline(member.getMemberSeq(), book.getBookSeq(), contents);

        //when
        timelineService.deleteTimeline(timelineId);

        //then
        assertThrows(Exception.class, () -> {
            timelineRepository.findById(timelineId);
        });
    }

    @Test
    void 특정회원_타임라인조회() {
        // given
        Member member = new Member("testid", "1234", "닉네임");
        memberRepository.save(member);

        Book book1 = new Book("1234567890123", "책1", "저자1", "출판사1");
        Book book2 = new Book("1234567890456", "책2", "저자2", "출판사2");
        bookRepository.save(book1);
        bookRepository.save(book2);

        timelineService.createTimeline(member.getMemberSeq(), book1.getBookSeq(), "첫 번째 글");
        timelineService.createTimeline(member.getMemberSeq(), book2.getBookSeq(), "두 번째 글");

        // when
        List<Timeline> timelines = timelineService.getMemberTimelines(member.getMemberSeq());

        // then
        assertNotNull(timelines);
        assertEquals(2, timelines.size());
        assertEquals("두 번째 글", timelines.get(0).getContents());
        assertEquals("첫 번째 글", timelines.get(1).getContents());
    }

    @Test
    void 팔로우회원_타임라인조회() {
        // given
        Member follower = new Member("followerId", "1234", "팔로워");
        Member following1 = new Member("following1Id", "5678", "팔로잉1");
        Member following2 = new Member("following2Id", "91011", "팔로잉2");

        memberRepository.save(follower);
        memberRepository.save(following1);
        memberRepository.save(following2);

        followService.follow(follower, following1);
        followService.follow(follower, following2);

        Book book1 = new Book("1234567890123", "책1", "저자1", "출판사1");
        Book book2 = new Book("1234567890456", "책2", "저자2", "출판사2");
        bookRepository.save(book1);
        bookRepository.save(book2);

        timelineService.createTimeline(following1.getMemberSeq(), book1.getBookSeq(), "팔로잉1의 첫 번째 글");
        timelineService.createTimeline(following2.getMemberSeq(), book2.getBookSeq(), "팔로잉2의 첫 번째 글");

        // when
        List<Timeline> timelines = timelineService.getFollowedTimelines(follower.getMemberSeq());

        // then
        assertNotNull(timelines);
        assertEquals(2, timelines.size());
        assertEquals("팔로잉1의 첫 번째 글", timelines.get(0).getContents());
        assertEquals("팔로잉2의 첫 번째 글", timelines.get(1).getContents());
    }
}
  • 글작성()
    • 타임라인 작성에 필요한 Book, Member 그리고 Timeline 객체를 생성한 후 타임라인 저장을 수행한다. 그리고 저장된 타임라인 객체가 비어있지 않는지, 글 내용이 같은지를 확인해준다.
  • 글삭제()
    • 글작성()테스트와 같이 타임라인 작성에 필요한 book, member, timeline객체를 생성한 후 타임라인 저장을 수행한다. 그리고 삭제 메서드를 실행한 후 저장한 타임라인 객체의 상태가 변경되었는지 확인해준다.
  • 특정회원_타임라인조회()
    • 글작성()테스트와 같이 타임라인 작성에 필요한 book, member, timeline객체를 생성해주는데 여기서는 Timeline객체를 2개 생성해준다. 그리고 특정회원의 타임라인을 조회해 왔을 때, 타임라인 객체가 2개인지를 확인해준다. 

 

테스트 성공

728x90
320x100