티스토리 뷰

반응형

 

 

실전! 스프링 부트와 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 {
              
        }
  • 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처리를 한다.
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함