Spring/Spring Boot JPA
[Spring Boot JPA] 회원 기능 개발
메성
2022. 2. 17. 23:05
반응형
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강
www.inflearn.com
회원 기능
- 회원 등록
- 회원 조회
회원 등록
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다. ")
private String name;
private String city;
private String street;
private String zipcode;
}
//Controller
@PostMapping("/members/new")
public String create(@Valid MemberForm memberForm, BindingResult result) {
if(result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode());
Member member = new Member();
member.setName(memberForm.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
- javax.validation을 통해서 쉽게 validation check가 가능
- @NotEmpty, @Valid
- 스프링 2.3부터는 따로 라이브러리를 추가해야한다.
-
implementation 'org.springframework.boot:spring-boot-starter-validation'
- BindingResult
-
import org.springframework.validation.BindingResult;
- @Vaild와 함께 BindingResult를 파라미터로 가지고 있으면, @Valid에 체크한 오류가 BindingResult에 담겨서 Controller가 실행이 된다.
- Controller에서는 BindingResult의 hasErrors 메소드를 통해서 분기 처리가 가능하고, view 단에서 오류에 관련된 내용을 들고 가게 된다.
-
<style> .fieldError { border-color: #bd2130; } </style> <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'"> <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>
- hasErrors에 필드값 name이 있으면 해당 분기를 타게되고 input에 관련된 이벤트를 실행하게 된다. 추가로 th:errors로 인해 @NotEmpty에 정의한 메시지가 출력되게 된다.
-
-
주의해야할 점
- 화면에서 주고받을 데이터(DTO)와 Entity의 필드값이 같다고 하더라도 별개의 클래스를 만들어 놓는 것이 좋다.
- 둘의 성격이 다르기 때문에!
회원 목록
Entity와 DTO
- API를 만들 때 데이터를 넘기는 경우 Entity를 넘기면(외부로 반환) 절대 안 되고 DTO를 넘겨야한다.
- 만약, Member Entity에 새로운 필드를 추가하게 되면, API 스펙이 변경이 되어 큰 이슈가 발생될 수 있다.
- Entity는 순수하게 유지하는 것이 좋다.
- 화면에서 사용하는 데이터는 DTO를 따로 만들어 사용하는 것이 좋다.
회원 Repository 개발
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void save(Member member) {
em.persist(member);
}
public Member findOne(Long id) {
return em.find(Member.class, id);
}
//jpql 사용
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
//jpql 사용
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
- @Repository
- Spring Bean으로 등록
- @Component가 내장되어 있어 ComponentScan 시 스프링 빈으로 등록이 됨
- @PersistenceContext
- 해당 어노테이션이 있으면 스프링이 EntityManager를 주입해준다.
- em.persist(member);
- EntityManager에 의해 member Entity를 영속성 컨텍스트에 넣어주고 트랜잭션이 커밋되는 시점에 member를 저장하게 한다.(insert)
- em.find(Member.class, id);
- 단건 조회로서, 첫번째는 타입이고 두번째는 key값을 넣어주면 된다.
- em.createQuery("select m from Member m", Member.class) .getResultList();
- jpql을 사용한 것으로 쿼리문과 거의 비슷하나 from 절에 Member는 테이블이 아닌 Entity이다.
- 첫번째 파라미터는 쿼리, 두번째 파라미터는 from 절에서 사용하는 Entity를 넣어준다.
회원 서비스 개발
@Service
@Transactional(readOnly = true)
public class MemberService {
@Autowired
private MemberRepository memberRepository;
/**
* 회원 가입
*/
@Transactional
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId(); //@GeneratedValue에 의해 id를 저장하지 않아도 값이 들어감
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
//EXCEPTION
if(!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
/**
* 회원 전체 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
/**
* 회원 조회
*/
public Member findOne(Long id) {
return memberRepository.findOne(id);
}
}
- @Transactional(readOnly = true)
- 메소드 단계에서 readOnly 조건을 주게 되면, DB에게 해당 메소드에서 실행하는 로직은 단순히 읽기만 하니 많은 리소스를 사용하지 말라라는 의미를 줄 수 있다.
- Class 단계에서 Transactional을 사용하면 클래스 내에 있는 메소드에 모두 적용이 된다. 만약 트랜잭션의 조건을 변경하고 싶으면 join 메소드처럼 @Transactional을 걸어놓으면 된다.
- @Transactional
- 데이터 변경 시 어떤 이슈에 의해 중간에 오류가 나면 모두 rollback 처리할 수 있도록 해주는 어노테이션이다.
- 데이터 변경 작업이 있으면 무조건 @Transactional을 줘야한다.
- validateDuplicateMember()
- 해당 메소드에서 중복체크를 하더라도 멀티 스레드에 의해 회원가입을 동시에 하는 경우가 발생할 수 있다.
- 이런 일을 대비하기 위해서 Member의 Name을 DB에서 유니크한 제약 조건을 걸어두는 것이 좋다.
- @Autowired
- 스프링에 의해서 인스턴스의 주입을 받을 수 있다.
- 해당 소스에서는 필드 인젝션 처리한 예이다.
- Setter Injection
- 필드 인젝션 말고 세터 인젝션 방법도 있는데, 해당 방법을 사용하게 되면 테스트를 짤 때 Mock 객체를 넣어줌으로 원활하게 테스트가 가능하다.
-
public class MemberService { private MemberRepository memberRepository; @Autowired public void setMemberRepository(MemberRepository memberRepository) { this.memberRepository = memberRepository; } }
- 하지만 Setter를 사용하게 되면 MemberService가 뜨고 중간에 Setter에 의해서 Repository가 변경되는 경우가 발생할 수도 있다. 그러므로 가장 좋은 방법은 MemberSerivce의 생성자에 인젝션 처리를 하는 것이다.
- 생성자 Injection
-
public class MemberService { private final MemberRepository memberRepository; //final을 사용하면 생성자 시점에 repository를 정의해주지 않으면 오류가 발생하여 컴파일 시점 체크가 가능하다. @Autowired //없어도 됨. public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } }
- MemberService가 생성되는 동시에 Repository가 주입을 받으므로 중간에 Repository를 변경할 수가 없다.
- 그리고 테스트 케이스 작성할 때 MemberSerivce를 생성해야하는데 이 때, MemberService가 어떤 Repository를 의존하고 있는지 명확하게 알 수 있다.
- 추가로 요즘 스프링에서는 생성자가 하나만 있는 경우 @Autowired가 없어도 스프링이 인젝션 처리를 해준다.
- 또한, MemberRepository에 final을 사용하면 생성자 시점에 repository를 정의해주지 않으면 오류가 발생하여 컴파일 시점 체크가 가능하다.
- 롬복 사용 Injection
-
@RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; }
- @RequiredArgsConstructor를 사용하게 되면 final 필드를 생성자의 파라미터로 갖게 된다.
- 위 방법이 가장 많이 사용되는 인젝션 방법이고 생성자 Injection과 동일하다.
- 또한, 스프링 부트(Spring Data JPA)를 사용하면 Repository의 @PersistenceContext를 @Autowired로 줄 수 있어 Repository의 EntityManager를 아래와 같이 인젝션 처리가 가능하다.
-
public class MemberRepository { @AutoWired private EntityManager em; public MemberRepository(EntityManager em) { this.em = em; } } //아래로 변경 가능 @RequiredArgsConstructor public class MemberRepository { private final EntityManager em; }
회원 기능 테스트
- 회원 가입을 성공해야한다.
- 회원 가입 시 같은 이름이면 예외를 발생시킨다.
- @Transactional이 테스트 케이스에 존재하게 되면 기본적으로 Rollback 처리를 한다.
- 이로 인해서 Repository.save하는 경우 console에 insert는 안보이게 되는데, 만약 insert 하는 것을 보고 싶다면 EntityManager를 테스트 케이스에서 주입 받은 후 flush 처리를 하면 된다.
-
@Transactional //rollback이 가능 public class MemberServiceTest { @Autowired MemberService memberService; @Autowired MemberRepository memberRepository; @Autowired EntityManager em; @Test public void 회원가입() throws Exception { //given Member member = new Member(); member.setName("Lim"); //when Long savedId = memberService.join(member); //then em.flush(); //member가 들어간 영속성 컨텍스트가 쿼리로 DB에 반영(강제로 DB에 반영) assertEquals(member, memberRepository.findOne(savedId)); } }
- 쿼리에 반영 후 Transactional에 의해서 마지막에 롤백을 하게 된다.
- 만약 DB에 넣고 싶다면 해당 메소드에 Rollback(false)를 입력해주면 된다.
-
@Test @Rollback(false) public void 회원가입() throws Exception { }
-
- 이로 인해서 Repository.save하는 경우 console에 insert는 안보이게 되는데, 만약 insert 하는 것을 보고 싶다면 EntityManager를 테스트 케이스에서 주입 받은 후 flush 처리를 하면 된다.
- Exception 처리 테스트
-
@Test public void 중복_회원_예외() throws Exception { //given Member member1 = new Member(); member1.setName("Lim1"); Member member2 = new Member(); member2.setName("Lim1"); //when memberService.join(member1); try { memberService.join(member2); //예외가 발생해야 한다. } catch (IllegalStateException e) { return; } //then fail("에외가 발생해야 한다."); //해당 라인을 타게되면 테스트 실패 }
- MemberService.join에서 IllegalStateException 예외가 발생하면 catch를 하고 return을 함으로써 fail() 메소드를 안 타게 되어 테스트가 성공하게 된다.
- 하지만 코드의 복잡성을 줄이기 위해 아래와 같이 작성할 수 있다.
- @Test(expected = Exception 종류)를 함으로써 코드를 줄일 수 있다.
-
@Test(expected = IllegalStateException.class) public void 중복_회원_예외() throws Exception { //given Member member1 = new Member(); member1.setName("Lim1"); Member member2 = new Member(); member2.setName("Lim1"); //when memberService.join(member1); memberService.join(member2); //예외가 발생해야 한다. //then fail("에외가 발생해야 한다."); //해당 라인을 타게되면 테스트 실패 }
-
- 전체 소스 확인
-
@RunWith(SpringRunner.class) @SpringBootTest @Transactional //rollback이 가능 public class MemberServiceTest { @Autowired MemberService memberService; @Autowired MemberRepository memberRepository; @Test public void 회원가입() throws Exception { //given Member member = new Member(); member.setName("Lim"); //when Long savedId = memberService.join(member); //then assertEquals(member, memberRepository.findOne(savedId)); } @Test(expected = IllegalStateException.class) public void 중복_회원_예외() throws Exception { //given Member member1 = new Member(); member1.setName("Lim1"); Member member2 = new Member(); member2.setName("Lim1"); //when memberService.join(member1); memberService.join(member2); //예외가 발생해야 한다. //then fail("에외가 발생해야 한다."); //해당 라인을 타게되면 테스트 실패 } }
- @RunWith(SpringRunner.class)
- Junit 실행 시 스프링이랑 같이 엮어서 실행하겠다.
- @SpringBootTest
- 스프링 부트를 띄운 상태에서 테스트하겠다.
- 해당 어노테이션이 없으면 @Autowired가 모두 실패 한다.
- 스프링 컨테이너 안에서 테스트를 돌리는 것으로 보면 된다.
- @Transactional
- 테스트가 끝나면 모두 Rollback 처리를 한다.
-
- 메모리 DB 사용
- 실제 사용하는 DB가 아닌 테스트용으로 잠깐 사용할 DB를 메모리를 통해 사용할 수 있다.
- test 패키지 밑에 resources의 패키지를 추가하여 test 할 시에는 메모리 DB를 탈 수 있도록 할 수 있다.
- 운영 소스인 java 및 resources와 테스트 소스인 test 및 resources가 있을 때 test의 경우 test 밑에 있는 resources의 yml을 바라보게 된다.
- test용 resources/application.yml
-
spring: datasource: url: jdbc:h2:mem:test username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: # show_sql: true format_sql: true logging: level: org.hibernate.SQL: debug org.hibernate.type: trace server: port: 8083
- h2의 경우 h2에서 제공해주는 메모리 DB url을 설정함으로써, 운영 DB 접속 여부에 상관없이 메모리 DB를 활용하여 테스트를 진행할 수 있다.
-
- 하지만 스프링 부트의 경우 아래 application.yml처럼 모두 주석처리 해줘도 된다. 스프링 부트가 기본적으로 메모리 DB를 제공해준다.
-
#spring: # datasource: # url: jdbc:h2:mem:test # username: sa # password: # driver-class-name: org.h2.Driver # # jpa: # hibernate: # ddl-auto: create # properties: # hibernate: ## show_sql: true # format_sql: true logging: level: org.hibernate.SQL: debug org.hibernate.type: trace server: port: 8083
- 메모리 DB를 사용하게 되면 마지막에 모두 drop처리를 한다.
-
반응형