쿠릉쿠릉 쾅쾅 2022. 4. 4. 21:41
728x90

 

 

애플리케이션 아키텍처

 

1. 계층형 구조 사용

  • controller, web : 웹 계층
  • service : 비즈니스 로직, 트랜잭션 처리
  • repository : JPA를 직접 사용하는 계층, 엔티티 매니저 사용
  • domain : 엔티티가 모여있는 계층, 모든 계층에서 사용

 


 

 

회원 리포지토리 개발

package bookbook.shopshop.repository;

import bookbook.shopshop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

//    @PersistenceContext  // 스프링 부틍 사용시 생략 가능
    private final EntityManager em;


    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public List<Member> findByName(String name) {
        return em.createQuery("select m from Member m where m.name =:name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }

}
  • 스프링 데이터 JPA를 사용하면 @Autowired가 @PersistenceContext를 생략해도 엔티티 매니저를 자동 주입 해준다.
    • @Repository : 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
    • @PersistenceContext : 엔티티 매니저(EntityManager) 주입
    • @PersistenceUnit : 엔티티 매니저 팩토리(EntityManagerFactory) 주입

 


 

회원 서비스 개발

package bookbook.shopshop.service;

import bookbook.shopshop.domain.Member;
import bookbook.shopshop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    /**
     * 회원 가입
     */
    @Transactional
    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    /**
     * 중복 검사
     */
    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getName());

        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    /**
     * 회원 전체 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    /**
     * 회원 단 건 조회
     */
    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }


}
  • @Service : 스프링 빈 등록
  • @Transactional : 트랜잭션, 영속성 컨텍스트
    • readOnly = true : 데이터의 변경이 없는 읽기 전용 메서드에 사용
      • 영속성 컨텍스트를 flush 하지 않으므로 약간의 성능이 향상된다.
      • 데이터베이스 드라이버가 지원하면 DB 성능을 향상 시킬 수 있다.
  • 실무에서 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회뭔명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다. 
    • 예) 회원 중복 검사

참고로 insert 메서드(등록)는 엔티티의 pk 값을 반환하고, update는 아무것도 반환하지 않고, 조회는 내부의 변경이 없는 메서드로 설계하면 좋다.
메서드를 호출했을 때 내부에서 변경(사이드 이펙트)가 일어나는 메서드인지, 아니면 내부에서 변경이 전혀 일어나지 않는 메서드인지 명확하게 분리해야 한다.
그러기 위해선 커맨드 객체를 반환하지 않는 것이 좋다.

 


 

회원 기능 테스트

package bookbook.shopshop.service;

import bookbook.shopshop.domain.Member;
import bookbook.shopshop.repository.MemberRepository;
import org.assertj.core.api.Assertions;
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 javax.persistence.EntityManager;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    EntityManager em;

    @Test
    @Rollback(false)
    void 회원가입() {

        // given
        Member member = new Member();
        member.setName("kim");

        // when
        Long savedId = memberService.join(member);

        // then
        em.flush();
        Assertions.assertThat(member).isEqualTo(memberRepository.findOne(savedId));
    }

    @Test
    void 중복_회원_검사() {

        // given
        Member member1 = new Member();
        member1.setName("kim");

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

        // when
        memberService.join(member1);

        // then
        assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));

    }

}

 

 

1. 메모리 DB에서 테스트 하는 방법

테스트를 할 때 was를 띄울 떄 메모리 db 도 같이 띄어서 테스트를 진행하는 방법이 좋은 방법이다.

테스트 폴더에 application.yml 처럼 설정 파일을 만들면 해당 파일이 메인 폴더의 설정 폴더보다 우선권을 가진다.

📌 테스트 서버의 application.yml

spring:
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
    driver-class-name: org.h2.Driver

  output:
    ansi:
      enabled: always

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        
# --------------------------------        

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace

'#----------' 기준으로  위의 부분은 다 생략해도 스프링 부트가 자동으로 메모리 DB로 연결해준다.

🔍 H2 메모리 db url 얻는 방법

H2 홈 페이지를 들어가서 Cheat Sheet를 누른다.

In-Memory 부분에 jdbc:h2mem:test 가 메모리 db 주소다.

 

 

 

 

 


👀 참고 자료

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/community

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 질문 & 답변 - 인프런 | 강의

수강생이 남긴 질문과 지식공유자의 답변을 확인할 수 있어요. 질문 & 답변 | 인프런...

www.inflearn.com

 

https://www.inflearn.com/questions/27795

 

CQRS - 인프런 | 질문 & 답변

안녕하세요. 정말 좋은 강의 항상 잘 듣고 있습니다. Repository save 메서드는 Member 를 반환하기보다는 id를 반환하는 식으로 구성하셨는데 이게 기본편에서 커맨드와 커리를 분리한기위함이라고

www.inflearn.com

 

728x90