도메일(Domain), 서비스(Service), 리포지토리(Repository)?:
- 도메인(Domain): 엔티티가 모여 있는 계층
- 서비스(Service): 비즈니스 로직, 트랜잭션 처리가 모여 있는 계층
- 리포지토(Repsoitory): JPA를 직접 사용하면서 DB와 값을 주고 받는 계층
- 개발 순서: 도메인->서비스&리포지토리->테스트 케이스로 검증->배포
@Repository
public class MemberRepository {
private final EntityManager em;
public MemberRepository(EntityManager em) {
this.em = em;
}
public Long save(Member member) {
em.persist(member);
return member.getId();
}
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.username = :userName", Member.class)
.setParameter("userName", name)
.getResultList();
}
}
1. @Repository 애노테이션: 스프링 빈 등록
2. EntityManager 생성자 관계 주입: @Autowired, @PersistenceContext 애노테이션 등을 통해 EntityManager 관계 주입이 가능하지만, 생성자를 이용한 관계 주입이 가장 선호되는 방식(참고: EntityManagerFactory는 생성자 외에 @PersistenceUnit 애노테이션으로 관계 주입 가능)
(관계 주입에 대한 내용은 이전에 Spring 학습할때 배웠으므로 해당 내용 참고)
@Service
@Transactional(readOnly = true) //DB에서의 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 flush() 하지 않으므로 약간의 성능 향상
public class MemberService {
private final MemberRepository memberRepository; //생성 이후에 변경할 일 X => final로 설정
public MemberService(MemberRepository memberRepository) {
//관계 주입
this.memberRepository = memberRepository;
}
/**
* 회원 가입
*/
@Transactional //DB에서의 데이터를 변경 => 그러므로, readOnly = false (DEFAULT)
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
return memberRepository.save(member);
}
/**
* 회원 전체 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
/**
* 회원 하나만 존회
*/
public Member findOne(Long id) {
return memberRepository.findOne(id);
}
private void validateDuplicateMember(Member member) {
//중복 회원이 존재하면 EXCEPTION
if (!memberRepository.findByName(member.getUsername()).isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다!");
}
}
}
1. @Service 애노테이션: 스프링 빈 등록
2-1. @Transactional: DB의 상태를 변경해주도록 하는 애노테이션. being(), commit()을 수행해줍니다.
2-2. @Transactional(readOnly=true): DB에서의 데이터의 변경이 없는 읽기 전용 메서드에 사용하며, 영속성 컨텍스트를 flush() 하지 않으므로 약간의 성능 향상이 있습니다
(참고: 클래스 전체에 @Transactional(readOnly=true)를 애노테이션을 추가 했으므로 해당 클래스의 모든 메서드에서 DB 접근은 읽기만 가능하지만, public Long join(Memeber member)에는 @Transactional(readOnly=false)가 있으며, readOnly=true가 오버라이딩 되기 때문에 DB의 데이터 값 변경이 가능합니다.)
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
@Transactional //자동으로 DB를 rollback 시켜줌
@Rollback(false) //애너테이션을 추가하면 rollback 하지 않고 DB에 값을 그대로 남겨둠
void testMember() {
//given
Member member = new Member();
member.setUsername("MemberA");
//when
Long savedId = memberRepository.save(member);
Member findMember = memberRepository.findOne(savedId);
//then
Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
/**
* member랑 findMember는 영속성 컨텍스트에 존재
* 둘다 식별자(id)가 같으므로, 영속성 컨텍스트에서는 같은 엔티티라고 처리
* 그렇기 떄문에, Member에 equals()를 따로 설정해주지 않아도 Assertions.assertThat(findMember).isEqualTo(member)는 Pass
* && findMember == member는 true입니다.
*/
Assertions.assertThat(findMember).isEqualTo(member);
Assertions.assertThat(findMember == member).isTrue();
}
}
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
void 회원가입() {
//given
Member member = new Member();
member.setUsername("Kim");
//when
Long savedId = memberService.join(member);
//then
Assertions.assertEquals(member, memberService.findOne(savedId));
}
@Test
void 중복회원() {
//given
Member memberA = new Member();
memberA.setUsername("Kim");
memberService.join(memberA);
//when
Member memberB = new Member();
memberB.setUsername("Kim");
//then
Assertions.assertThrows(IllegalStateException.class, () -> {
memberService.join(memberB);
});
}
}```