반응형
공식문서를 참고하며 기록하는 SpringBoot Test 적용하기
TDD를 기반으로 프로젝트를 시작하는 예제
- 테스트 코드를 통해 Entity와 Dao 구현
- 테스트 코드를 통해 Controller 구현
- 테스트 코드를 통해 Oauth 인증 구현
TDD로 실전 프로젝트를 해본적이 없어 개인적으로 만들 서비스의 예행연습으로 보고 진행함을 먼저 얘기한다.
1. @DataJpaTest
- SpringBoot에서 JPA만 테스트할 수 있도록 제공하는 어노테이션
- 개발의 첫 단계인 Entity 설계 단계에서 불필요한 코드 작성 없이, Entity간의 관계 설정 및 기능 테스트가 가능해졌다.
- 예를 들자면 View를 만들거나, Controller를 작성하는 것 등등 Entity 설계 확인을 위한 코드 작성이 필요없어졌다.
- 사용법은 간단하다.
@RunWith(SpringRunner.class) //Junit 테스트 선언
@DataJpaTest // DataJpaTest 선언
public class DataJpaTest {
/*
Repository == Dao
본인이 테스트하려는 Dao를 선언하고 기능을 테스트 하면 된다.
아래는 여기 프로젝트에서 사용한 코드의 일부이다.
*/
@Autowired
private MemberRepository memberRepository;
@Autowired
private PostRepository postRepository;
@Autowired
private CommentRepository commentRepository;
}
- H2 + JPA + @DataJpaTest = AWESOME!!
- 단위 테스트가 끝날때 마다 자동으로 DB를 롤백시켜 준다 (우앙!)
- 여기에서 사용할 예제 Entity는 아래와 같다
- 사용자 : Member
- 글 : Post
- 댓글 : Comment
- JPA 참고 자료
- 아라한사님이 번역하신 공식 문서
- 김영한님의 자바 ORM 표준 JPA 프로그래밍
1.1 상황1
- Post와 Comment간의 관계 설정
- 하나의 글은 여러개의 댓글을 가질 수도 있고, 없을 수도 있다.
- 하나의 글을 조회하면 해당하는 댓글이 같이 와야 한다.
ManyToOne(다대일) 양방향으로 해결
- Code (자세한 코드는 생략)
// Post 클래스
@Entity
public class Post {
@OneToMany(mappedBy="post", cascade = CascadeType.ALL)
private List<Comment> comments;
}
// Comment 클래스
@Entity
public class Comment {
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(name = "fk_comment_post"))
private Post post;
}
@Test
public void test_Post와Comment관계정의() throws Exception {
Post savedPost = postRepository.save(post);
savedPost.addComment(comment); // 글에 댓글 추가
comment.setPost(savedPost);
commentRepository.save(comment); // 댓글에 글 추가
Post firstPost = postRepository.findOne(1L);
Comment firstComment = commentRepository.findOne(1L);
assertThat(savedPost.getContent(), is("content"));
assertThat(savedPost.getComments().get(0).getContent(), is(firstComment.getContent()));
}
- OneToMany(일대다) 를 왜 쓰지 않은걸까? 예를 들어 Comment를 수정해야하는일이 생길 경우
// OneToMany(일대다) 단방향
Post post = postRepository.findOne(1L);
List<Comment> comments = post.getComments();
Comment comment = comments.get(0);
comment.setXXX(); // update
// ManyToOne(다대일) 양방향
Comment comment = commentRepository.findOne(1L);
comment.setXXX(); // update
- 즉, 일대다 단방향일 경우 '다'에 속해있는 객체 하나를 수정하기 위해선 '일'을 조회하고 '일' 내부에 있는 '다'에서 원하는 객체를 다시 뽑아내야하는 과정이 필요하다.
- 반변에 다대일 양방향일 경우 수정을 원하는 하위 객체를 바로 수정할 수가 있기 때문에 다대일 양방향으로 해결하는것을 권장한다.
1.2 상황2
- Member와 Comment간의 관계 설정
- 사용자가 직접 댓글 작성 기능 구현
- 글이 올라오면, 사용자는 해당 글에 댓글을 남길수 있다.
- 한명의 사용자는 여러개의 글에 여러개의 댓글을 작성할 수 있다.
- 사용자 정보 조회시 해당 사용자가 작성한 댓글을 모두 조회할 수 있어야 한다.
상황 1.1과 동일한 ManyToOne(다대일) 양방향으로 해결
객체간 연간관계는 양방향이란게 없기 때문에, 이를 해결하기 위해 단방향 2개(Comment -> Member와 Member -> Comment)를 사용한것이라고 보면 된다.
- Code (자세한 코드는 생략)
// Member 클래스
@Entity
public class Member {
@OneToMany(mappedBy="member", cascade = CascadeType.ALL)
@OrderBy("idx DESC")
private List<Comment> comments;
}
// Comment 클래스
@Entity
public class Comment {
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(name = "fk_comment_member"))
private Member member;
}
// Test 코드
@Test
public void test_Member와Comment관계정의() throws Exception {
Post savedPost = postRepository.save(post);
Member savedMember = memberRepository.save(member);
savedPost.addComment(comment);
savedMember.addComment(comment);
comment.setPostAndMember(savedPost, savedMember);
commentRepository.save(comment);
Post afterPost = postRepository.findOne(1L);
Member afterMember = memberRepository.findOne(1L);
assertThat(afterPost.getComments().get(0).getContent(), is("댓글"));
assertThat(afterMember.getComments().get(0).getContent(), is("댓글"));
assertThat(commentRepository.findAll().size(), is(1)); // savedPost와 savedMember에 각각 addComment를 했지만 결국 comment는 1개가 들어간것을 확인
}
1.3 상황3
- Member와 Post간의 관계 설정
- 사용자별 즐겨찾기 기능 구현
- 사용자는 0개 혹은 다수의 글를 가질수 있다.
- 글은 꼭 사용자가 있어야 하는것은 아니다.
- 하나의 글은 여러 사용자에 참조 될 수도 있다.
JoinTable을 통해 해결
사용자와 글은 선택적(Optional) 참조이므로 joinColumn 으로는 해결할 수가 없다.
- Code (자세한 코드는 생략)
// Member 클래스
@Entity
public class Member {
@OneToMany()
@JoinTable(name="MEMBER_POST",
joinColumns=@JoinColumn(name="MEMBER_IDX"),
inverseJoinColumns=@JoinColumn(name="POST_IDX"))
@OrderBy("idx DESC")
private List<Post> favorites;
}
// Post 클래스는 변경 없음
// Test 코드
@Test
public void test_Post와Member관계정의() throws Exception {
Member member2 = new Member("test@gmail.com", new ArrayList<>(), new LinkedHashSet<>());
Post savedPost = postRepository.save(post);
member.addPost(savedPost);
member2.addPost(savedPost);
Member savedMember = memberRepository.save(member);
Member savedMember2 = memberRepository.save(member2);
assertThat(savedMember.getFavorites().stream().findFirst().orElse(new Post()).getContent(), is("content")); // 1번 사용자의 1번 글이 post인지 확인
assertThat(savedMember2.getFavorites().stream().findFirst().orElse(new Post()).getContent(), is("content")); // 2번 사용자의 1번 글이 post인지 확인
}
1.4 상황4
- ORM에서 컬렉션 사용법
- 사용자는 중복된 글을 가질수 없다. 여러개의 글을 가질순 없지만 고유하게 하나씩 있어야만 한다.
- 이럴 경우 Member.favorites가 List타입일 경우 중복 제거를 위한 비지니스 로직이 추가되어야 한다.
- 중복제거를 로직으로 해결하지말고 자료구조로 해결하자
Member.favorites를 List에서 Set으로 변경하여 해결
- Code (자세한 코드는 생략)
// Member 클래스
@Entity
public class Member {
@OneToMany()
@JoinTable(name="MEMBER_POST",
joinColumns=@JoinColumn(name="MEMBER_IDX"),
inverseJoinColumns=@JoinColumn(name="POST_IDX"))
@OrderBy("idx DESC")
private Set<Post> favorites;
}
// Post 클래스는 변경 없음
// Test 코드
@Test
public void test_oneToMany에서Set과List차이() throws Exception {
Post savedPost = postRepository.save(post);
member.addPost(savedPost);
member.addPost(savedPost);
Member savedMember = memberRepository.save(member);
assertThat(savedMember.getFavorites().size(), is(1)); // 2개의 Post를 넣었지만 결국 중복된게 제거되서 1개만 등록된것을 확인할수 있다.
}
1.5 상황5
- 상속관계 매핑
- Post가 Job, Tech, Essay 라는 3가지 타입으로 분류되도록 해야한다.
- 3타입 모두 가지고 있는 컬럼은 같다. (idx, content, updateDate, comments)
- 객체지향적 코드 작성을 위해 각 클래스는 분리하길 원한다.
JPA의 상속관계중 단일테이블전략을 사용한다.
조인전략의 경우 3타입이 서로 다른 컬럼을 1개이상 가지고 있으며, 차후 별도로 컬럼이 추가/삭제 될 가능성이 높은 경우에 고려해볼만 하다.
하지만 일반적으로 동일한 속성들을 가지고 있는 경우엔 단일 테이블 전략이 더 좋다
조회 속도 역시 불필요한 조인이 없어 일반적으로 더 빠르다.
- Code
// Post 클래스
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속관계(단일테이블) 선언
@DiscriminatorColumn(name="DTYPE") // 하위 엔티티들을 구분하는 컬럼명 (default가 DTYPE라서 생략가능, 정보전달을 위해 명시함)
public abstract class Post { }
// Job 클래스 (Tech, Essay 역시 동일함)
@Entity
@DiscriminatorValue("JOB") // DTYPE에 저장될 값
public class Job extends Post { }
// PostRepository 인터페이스
// 제네릭을 사용하여 하위 인터페이스 타입 보장
public interface PostRepository<T extends Post> extends JpaRepository<T, Long>{}
// JobRepository 인터페이스
public interface JobRepository extends PostRepository<Job>{}
// Test 코드
@Test
public void test_상속관계() throws Exception {
jobRepository.save(new Job("잡플래닛", LocalDateTime.now(), new ArrayList<>()));
techRepository.save(new Tech("OKKY", LocalDateTime.now(), new ArrayList<>()));
essayRepository.save(new Essay("임백준", LocalDateTime.now(), new ArrayList<>()));
Job savedJob = jobRepository.findAll().get(0);
Tech savedTech = techRepository.findAll().get(0);
Essay savedEssay = essayRepository.findAll().get(0);
assertThat(savedJob.getContent(), is("잡플래닛"));
assertThat(savedTech.getContent(), is("OKKY"));
assertThat(savedEssay.getContent(), is("임백준"));
}
1번 스탭을 통해 Repository (Dao) 의 기능테스트가 끝이났으니 Controller 구현 & 테스트를 진행해보자
반응형