JAVA/JAVA | Spring 학습기록

[Spring] lombok의 @Builder와 JpaRepository를 도입해보았다

kth990303 2021. 5. 17. 19:53
반응형

저번에 만든 회원 CRUD 프로젝트에 새로운 기능들을 추가하고 테스트코드를 돌려보았다.

기능들을 추가하면서 현재까지 내가 배운 것들을 포스팅해보겠다.


@Builder, 왜 쓰는걸까?

Getter, Setter를 쓰는 이유가 바로 필드변수를 함부로 수정하는 것을 방지하기 위해서인데,

@Builder를 쓰는 이유 또한 그렇다. 우리는 흔히 lombok을 사용할 때, Entity 위에 @Getter를 사용하는 것은 많이 볼 수 있지만, @Setter를 사용하는 경우는 거의 보지 못했다. 나는 얼마전까지만 해도 @Setter를 사용하는 것이 테스트 코드 등 여러모로 편해서 자주 사용해왔었는데, 이 때 문제점이 발생한다.

 

Setter 메소드를 사용하면, (lombok에서 아무런 설정을 하지 않는 이상) 다른 데서 객체의 값을 변경할 수 있기 때문에 일관성이 보장되지 않는다. 

따라서 수많은 Spring 인강 및 책에서 Entity에 @Setter를 사용하는 것을 권장하지 않는다. 작은 프로젝트나 가벼운 테스트용 연습용 코드라면 모르겠으나, 그렇지 않은 이상 @Setter 를 지양하자.

 

대안으로 나온 것이 @Builder이다. Setter가 없으면 값을 어떻게 지정하는지 궁금할 수 있다.

물론 당연히 생성자를 만들어 저장을 하면 된다.

그런데 생성자가 좀 많거나, 필수 파라미터 누락을 방지하고 싶을 땐 이 @Builder가 큰 도움이 된다. 아래 블로그를 참고하자.

https://zorba91.tistory.com/298 

 

[Spring] Lombok을 이용해 Builder 패턴을 만들어보자.

Builder 패턴이란? Effective Java 규칙 2 - 조슈아 블로크 생성자에 인자가 많을 때는 빌더 패턴을 고려하라 빌더 패턴(Builder pattern) 이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성

zorba91.tistory.com


@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(상속)해주면 된다.

대신, 엔티티 클래스와 같은 패키지구조를 이루어야 한다.

도메인의 Item, Member 클래스와 각각의 리포지토리가 함께 있다.

 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 문법으로 예외를 처리한다. 자세한 건 아래 블로그를 참고하자.

http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/

 

Java Optional 바르게 쓰기

Java Optional 바르게 쓰기Brian Goetz는 스택오버플로우에서 Optional을 만든 의도에 대해 다음과 같이 말했다. … it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention w

homoefficio.github.io


대체적으로 패키지 구조 (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가 너무 편리해 앞으로 자주 이용할 듯 하다.

반응형