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
(+) 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
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지#005] 좋아요 도메인 개발 및 테스트 (0) | 2025.01.20 |
---|---|
[개발일지#004] 팔로우 도메인 개발 및 테스트 (0) | 2025.01.20 |
[개발일지#002] 회원 도메인 개발 및 테스트 (0) | 2025.01.16 |
[개발일지#001] 엔터티 설계 및 개발 (0) | 2025.01.06 |
[개발일지#000] 프로젝트 생성 (요구사항 분석, 프로젝트 생성, MySQL 연결, 개발 편의 설정 등) (0) | 2025.01.03 |