스프링/0. 입문, 전체방향

19강. JPA

sdafdq 2023. 7. 10. 23:27

코드도 줄고, 이제 쿼리도 알아서 날림.

객체 중심으로 설계, 생각 가능.

 

build.gradle에 

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

얘 추가.

 

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

이것까지 포함이라 이거는 주석처리 해도 됨.

 

 

그 다음 application.properties

spring.jpa.show-sql=true

이렇게 하면 jpa가 날리는 sql 볼 수 있음.

 

spring.jpa.hibernate.ddl-auto=none

jpa는 객체를 보고 테이블을 알아서 만들어 주는데, 우리는 이미 만들어 줬으니 저거 none 해두면 안만듦.

 

 

ORM Object Relational Mapping

오브젝트와 관계형 데이터를 맵핑

 

@Entity     //이 객체는 JPA로 관리를 하겠다.
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)    //Id이고, 생성되어지는 값, DB가 직접 생산해주는 IDENTITY 값임.
    private Long id;

    // @Column(name="username") 만약 db의 이름과 다르다면 앞처럼 하면 됨.
    private String  name;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

 

 

public class JpaMemberRepository implements MemberRepository{


    private final EntityManager em;     //jpa는 이걸로 모든 걸 동작.
    public JpaMemberRepository(EntityManager em) {  //build.gradle에 implementation해두면 spring이 자동으로 이걸 생성 해줌. 그래서 이렇게 생성자로 주입 받아야 함. 스프링에서 자동으로 해줄듯?
        this.em = em;
    }


    @Override
    public Member save(Member member) {
        em.persist(member);     //저장 끝. db에다 insert 쿼리 날리고 setID까지 (아마 Member 클래스에 jpa 설정해 뒀으니 알아서 setId 찾아서 해주는 듯?) 다 해줌.
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);  //Member 클래스를 조회하고 뭘로 찾을 건지 (id) 알려주면 됨. id같은 경우는 필수값이라 이렇게 조회 가능.
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)   //쿼리를 만드는 데, :name 이거는 플레이스 홀더임.
                .setParameter("name",name)      //저 플레이서 홀더에("name" 왜냐하면 쿼리문도 ""니까), String name을 넣음. 바인딩 해주는 거임.
                .getResultList();      //이때가 조회하는 타이밍임. createQuery는 말 그대로 쿼리만 만듦. 이 때가 쿼리 보내서 실행하고 값 받는 순간.
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member as m", Member.class)   //일반적인 쿼리와는 조금 다른, 객체지향 쿼리. 객체지향으로 쿼리를 날림. sql로 번역 됨.
                .getResultList();                                   //Member Entity 상대로 조회해, 그걸 as m, m이라 별칭하고, select m, 그 m을 가져왔네.
    }                                                               //객체지향적 쿼리라 id, name 둘다 찾을 필요 없이 이미 맵핑이 다 되어있음.
}

 

여기서 저 findByName 쿼리문에 빨간 줄 떠서 뭔가 싶었는데 걍 됨. " " 안이라서 그런지 얘가 잘못 잡았나..?

 

 

참고로 DB 쓸때는 꼭 DB상태를 변화시키는 곳에는

@Transactional

해줘야 함.

@Transactional
public class MemberService {    //Ctrl + Shift + T 테스트 바로 만들기.
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    //회원가입
    public Long join(Member member){ //id는 임시로 반환하게 한 듯.
        //같은 이름일 시 중복회원 X
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName()) //여기까지 Optional로 반환
                        .ifPresent(member1 -> { // ifPresent는 값이 있으면, 그러니까 참이라면 아래 코드를 실행
                            throw new IllegalStateException("이미 존재하는 회원입니다.");
                        });
    }

    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

기존 MemberService에

@Transactional

만 붙임. 

트랜잭션은 DB 상태를 변화 시키기 위해서 수행하는 작업이란 뜻 이므로, save엔 씀.

 

@Configuration
public class SpringConfig {

    //private DataSource dataSource;
    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }


    @Bean
    public MemberRepository memberRepository(){
        return new JpaMemberRepository(em);
    }
}

아까도 말했듯, jpa 라이브러리 설치한다고 build.gradle에 명시하면 스프링이 알아서 EntityManager 만들어 줌.

그래서 @Autowired 하면 알아서 들어감.

 

@Transactional      //이거는 @Test 함수 끝낸 후 날린 DB 쿼리를 롤백
class MemberServiceIntegrationTest {

    @Autowired
    MemberService memberService;        //내가 테스트 하는 것 이므로 테스트는 편한 방법으로.

    @Autowired
    MemberRepository memberRepository;


    @Test
    @Commit
    void 회원가입() {  //테스트때는 한국인 기업이면 한글로도 쓴다고 함.
        //given             주어진 상황
        Member member = new Member();
        member.setName("hello2");
        
        //when              실행했을 때, 검증
        Long saveId = memberService.join(member);     //동일 이름이 있을 시 예외 던짐. 아니면 제대로 memberRepository에 save 되고 id 반환

        //then              결과, 검증          보통 거의 다 이렇게 됨. 모두 해당되는 건 아님. GWT
        Member findedMember = memberService.findOne(saveId).get();    //.get()은 보통 테스트때에만 씀.
        Assertions.assertThat(member.getName()).isEqualTo(findedMember.getName());
    }

    @Test
    public void 중복_회원_예외(){
        //given
        Member member1 = new Member();
        member1.setName("spring1");

        Member member2 = new Member();
        member2.setName("spring1");


        //when
        memberService.join(member1);

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));   //assertThrows는 junit 함수. 예외를 반환.
                                                                                        //뒤의 람다식을 실행 시, 앞쪽의 예외를 throws해야 성공. 아니면 멈춤.
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

//        try{
//            memberService.join(member2);
//            fail(); //Junit의 에러 발생시키는 함수. 같은 이름이므로 예외 없이 정상적으로 작동하면 실패임.
//        }catch (IllegalStateException e){
//            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }


        //then
    }
}

테스트. 저번이랑 거진 똑같은 데 하나, @Commit을 붙였는데 이렇게 해 두면 원래 이거 테스트 하면 DB에 저장시키고, 롤백을 하기 위해 @Transactional 썼었는데,Commit하면 상관없이 롤백 안되고 DB에 저장됨.

'스프링 > 0. 입문, 전체방향' 카테고리의 다른 글

21강. AOP  (0) 2023.07.12
20강. 스프링 JPA  (0) 2023.07.12
18강. Jdbc 템플릿  (0) 2023.07.09
17강. spring 종합 테스트  (0) 2023.07.09
16강. 순수 JDBC  (0) 2023.07.09