저번에 만든 회원 CRUD 프로젝트에 새로운 기능들을 추가하고 테스트코드를 돌려보았다.
기능들을 추가하면서 현재까지 내가 배운 것들을 포스팅해보겠다.
@Builder, 왜 쓰는걸까?
Getter, Setter를 쓰는 이유가 바로 필드변수를 함부로 수정하는 것을 방지하기 위해서인데,
@Builder를 쓰는 이유 또한 그렇다. 우리는 흔히 lombok을 사용할 때, Entity 위에 @Getter를 사용하는 것은 많이 볼 수 있지만, @Setter를 사용하는 경우는 거의 보지 못했다. 나는 얼마전까지만 해도 @Setter를 사용하는 것이 테스트 코드 등 여러모로 편해서 자주 사용해왔었는데, 이 때 문제점이 발생한다.
Setter 메소드를 사용하면, (lombok에서 아무런 설정을 하지 않는 이상) 다른 데서 객체의 값을 변경할 수 있기 때문에 일관성이 보장되지 않는다.
따라서 수많은 Spring 인강 및 책에서 Entity에 @Setter를 사용하는 것을 권장하지 않는다. 작은 프로젝트나 가벼운 테스트용 연습용 코드라면 모르겠으나, 그렇지 않은 이상 @Setter 를 지양하자.
대안으로 나온 것이 @Builder이다. Setter가 없으면 값을 어떻게 지정하는지 궁금할 수 있다.
물론 당연히 생성자를 만들어 저장을 하면 된다.
그런데 생성자가 좀 많거나, 필수 파라미터 누락을 방지하고 싶을 땐 이 @Builder가 큰 도움이 된다. 아래 블로그를 참고하자.
https://zorba91.tistory.com/298
@Builder 사용방법
사실 난 아직 필수 파라미터가 없기 때문에 기본생성자와 다를 바 없이 썼다.
따라서 현 시점 포스팅 기준으론,,, 자세한 사용방법은 위 블로그를 참고하는 것을 추천한다.
Item.Class
@Entity
@Getter
@NoArgsConstructor
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Builder
public Item(String name){
this.name=name;
}
}
Test Code
@Test
public void 상품등록(){
Item item=Item.builder()
.name("computer")
.build();
}
new Item()이 아닌, Item.builder(). ~~~ .build(); 형태임에 주의하자.
JpaRepository 사용방법
이게 진짜 대박이다. 도메인 엔티티의 CRUD 기능을 자동으로 만들어주는 리포지토리가 바로 JpaRepository<type.class, type> 이다. Spring Data JPA 기능을 이용한 것이라 한다.
사용 방법은 리포지토리 인터페이스가 JpaRepository(엔티티클래스명, PK 자료형)을 extends(상속)해주면 된다.
대신, 엔티티 클래스와 같은 패키지구조를 이루어야 한다.
ItemRepository.Interface
package shop210504.pracshop.domain.item;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ItemRepository extends JpaRepository<Item, Long> {
}
인터페이스임에 주의하자.
그 외에 인터페이스엔 아무것도 작성하지 않아도 기본적인 CRUD 메소드는 모두 소유하게 된다.
Optional 문법
다만, JpaRepository 기능을 추가한다면, findById 메소드 등 일부 메소드의 자료형이 단순히 엔티티 클래스의 자료형이 아닌, Optional형 자료형으로 변경된다.
MemberServiceImpl.class (Service 클래스 코드이다.)
@Service
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
@Override
public Long join(Member member) {
Member joinMember = memberRepository.save(member);
return joinMember.getId();
}
@Override
public Optional<Member> find(Long id) {
Optional<Member> member = memberRepository.findById(id);
return member;
}
@Override
public void delete(Long id) {
memberRepository.deleteById(id);
}
@Override
public List<Member> showMembers() {
List<Member> memberList = memberRepository.findAll();
return memberList;
}
}
Optional 문법은 자바8에서부터 도입됐으며, NullPointerException을 방지하기 위해 null일 때 예외처리를 편리하게 해주기 위해 생겨난 문법이다.
코드를 보면서 아주아주 간략한 일부분의 사용법을 익혀보자.
assertThat(findMember.isPresent());
if(findMember.isPresent()){
assertThat(findMember.get().getId()).isEqualTo(member.getId());
assertThat(findMember.get()).isEqualTo(joinMember);
}
findMember은 Optional<Member> 자료형 객체이다.
Optional<T> 클래스는 Wrapper Class로, T타입의 객체를 'null값으로 발생하는 예외로부터 포장'해준다.
따라서 findMember.getId()와 같이 바로 Member의 메소드를 이용하는 것은 불가능하다.
그럼 어떻게 해야할까?
우선, findMember가 null값인지 파악하기 위해 findMember.isPresent() 메소드를 사용하여 assertThat 문법으로 검증하였다. null이 아니라면 true가 떠 assertThat문법 테스트는 통과할 것이다.
다음으로 null이 아님이 파악되면, findMember.get()을 하여 Member형 객체를 호출해준다. 즉, 멤버의 아이디를 호출하고 싶다면 findMember.getId()가 아닌, findMember.get().getId()를 사용해야 하는 것이다.
사실 위 문법은 나도 아직 제대로 썼다고 하기 애매하다. 원래 if(isPresent())로 처리하기보단, Optional.noElse 문법과 Optional.noElseThrow 문법으로 예외를 처리한다. 자세한 건 아래 블로그를 참고하자.
대체적으로 패키지 구조 (domain, repository를 합치고, repository패키지 삭제)가 많이 변했고,
테스트 코드가 많이 변경됐기에
테스트 코드는 전문을 첨부하겠다.
package shop210504.pracshop.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import shop210504.pracshop.domain.item.Item;
import shop210504.pracshop.domain.member.Member;
import shop210504.pracshop.domain.item.ItemRepository;
import shop210504.pracshop.domain.member.MemberRepository;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@Transactional
@SpringBootTest
@Rollback(false)
public class IndexControllerTest {
@Autowired
MemberRepository memberRepository;
@Autowired
ItemRepository itemRepository;
@Test
public void 회원가입(){
Member member=Member.builder()
.name("kth990303")
.tier("Platinum")
.build();
Member joinMember = memberRepository.save(member);
Optional<Member> findMember = memberRepository.findById(joinMember.getId());
assertThat(findMember.isPresent());
if(findMember.isPresent()){
assertThat(findMember.get().getId()).isEqualTo(member.getId());
assertThat(findMember.get()).isEqualTo(joinMember);
}
}
@Test
public void 상품등록(){
Item item=Item.builder()
.name("computer")
.build();
Item joinItem = itemRepository.save(item);
Optional<Item> findItem = itemRepository.findById(joinItem.getId());
assertThat(findItem.isPresent());
if(findItem.isPresent()){
assertThat(findItem.get().getId()).isEqualTo(item.getId());
assertThat(findItem.get()).isEqualTo(joinItem);
}
}
}
JpaRepository가 너무 편리해 앞으로 자주 이용할 듯 하다.
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[TDD] @Rollback을 쓰면 원래 DB의 데이터도 사라질까? (0) | 2021.08.27 |
---|---|
[Spring] 스프링이 태어난 이유_서블릿, JSP로 만든 MVC의 한계 (0) | 2021.07.06 |
[Spring] CRUD 기능 실습 중 @DeleteMapping 관련 에러 (0) | 2021.05.11 |
[Spring] MySQL+JPA+Spring+Gradle 회원조회 Read 실습 (0) | 2021.05.10 |
[Spring] Spring boot Test 코드 작성해보기 (TDD 연습) (2) | 2021.05.05 |