[SpringBoot + JPA 프로젝트] (3) Member 등록 API 개발 및 테스트 코드 작성 (+ModelMapper)

반응형
728x90
반응형

신규 파일 생성

MemberController.java
package com.api.westmall.controller;

import com.api.westmall.common.CommonResponse;
import com.api.westmall.form.MemberForm;
import com.api.westmall.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
    private final CommonResponse commonResponse;
    private final MemberService memberService;

    @PostMapping("")
    public ResponseEntity<?> user(@ModelAttribute @Valid MemberForm memberForm) {
        memberService.saveUser(memberForm);

        return commonResponse.send();
    }

}

 

@Valid 어노테이션을 사용하여 validation 에러 처리도 적용하였다. 아래 포스팅을 참고하자.

https://devfunny.tistory.com/428

 

CommonException 으로 @Valid 어노테이션 BindingException, MethodArgumentNotValidException 처리하기

설정 1) build.gradle 의존성 추가 implementation 'org.springframework.boot:spring-boot-starter-validation' 2) @Valid 어노테이션 추가 /** * 회원 등록 * @param memberDto * @return */ @PostMapping("") p..

devfunny.tistory.com

 

MemberService.java
package com.api.westmall.service;

import com.api.westmall.entity.Member;
import com.api.westmall.form.MemberForm;
import com.api.westmall.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final ModelMapper modelMapper;

    /**
     * 회원 저장
     * @param memberForm
     */
    public void saveUser(MemberForm memberForm) {
        Member member = modelMapper.map(memberForm, Member.class);

        memberRepository.save(member);
    }
}

 

ModelMapper을 사용하여 memberForm -> Member 로 매핑하였고, 그 결과로 데이터를 저장한다.

 

 

MemberForm.java
package com.api.westmall.form;

import com.api.westmall.entity.Gender;
import lombok.Data;
import lombok.NonNull;
import org.hibernate.validator.constraints.Length;

import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

@Data
public class MemberForm {
    private Long idx;

    @NotBlank
    private String userId;

    @NotBlank
    @Length(min = 8, max = 50)
    private String password;

    @NotBlank
    private String userName;

    private Gender gender;

    @Email
    @NotBlank
    private String email;
}

 

Member.java
package com.api.westmall.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idx;

    @Column(unique = true)
    @NonNull
    private String userId;

    @NonNull
    private String password;

    @NonNull
    private String userName;

    /* enum type */
    @Enumerated(value = EnumType.STRING)
    private Gender gender;

    @NonNull
    private String email;

}

 

application.yml

JPA 사용을 위한 yml 설정을 추가한다.

 

spring:
  datasource:
    hikari:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/westmalldb?characeterEncoding=UTF-8&serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: westmall
      password: westmall
  jpa:
    show-sql: true # query 출력
    hibernate:
      ddl-auto: create # 항상 새로 생성하고, 종료되면 자동으로 drop 처리한다. (운영에선 none 대부분)
    properties:
      format_sql: true # query format

 

 

 

 

단위테스트 코드

MemberServiceTest.java
package com.api.westmall.service;

import com.api.westmall.entity.Gender;
import com.api.westmall.entity.Member;
import com.api.westmall.form.MemberForm;
import com.api.westmall.repository.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
class MemberServiceTest {
    @Autowired
    private MemberService memberService;

    @Autowired
    private MemberRepository memberRepository;

    @Test
    @DisplayName("회원 등록이 정상적으로 저장되는지 체크")
    @Transactional
    public void add_member_success() throws Exception {
        MemberForm memberForm = new MemberForm();

        String userId = "test001";
        memberForm.setUserId(userId);
        memberForm.setUserName("테스트");
        memberForm.setPassword("root");
        memberForm.setGender(Gender.FEMALE);
        memberForm.setEmail("test@naver.com");

        memberService.saveUser(memberForm);

        Member member = memberRepository.getById(1L);

        Assertions.assertThat(member.getUserId()).isEqualTo(userId);

    }
}

 

저장 이후 getById() 메서드를 사용하여 방금 등록한 데이터를 가져와서 userId 일치 여부를 확인하였다.

 

참고로 여기서 처음에 에러가 발생하였는데 짚어보자.

 

@Transactional 없는 버전
...
    @Test
    @DisplayName("회원 등록이 정상적으로 저장되는지 체크")
    public void add_member_success() throws Exception {
        MemberForm memberForm = new MemberForm();

        String userId = "test001";
        memberForm.setUserId(userId);
        memberForm.setUserName("테스트");
        memberForm.setPassword("root");
        memberForm.setGender(Gender.FEMALE);
        memberForm.setEmail("test@naver.com");

        memberService.saveUser(memberForm);

        Member member = memberRepository.getById(1L);

        Assertions.assertThat(member.getUserId()).isEqualTo(userId);

    }
...

 

아래 에러가 발생한다.

could not initialize proxy [com.api.westmall.entity.Member#1] - no Session

 

디버깅을 해보면, 마지막에 getById() 해온 결과 이후 member.getUserId()를 해오는 경우에  아래 에러가 발생한다.

 

org.hibernate.LazyInitializationException

 

이 에러는 지연 로딩을 하려고 할때, 이미 세션이 사라져서 지연 로딩을 못할 경우 생기는 에러다. 해결방안은 2가지가 있다.

 

1) JPA 에서 지연로딩을 하려면 영속 상태로 묶여야한다. 이를 해결하기위해 @Transactional 어노테이션을 추가한다.

2) getById() 대신 findById()를 사용하자.

Member member = memberRepository.findById(1L).get();

 

getById()는 내부적으로 EntityManager.getReference() 메소드를 호출하여 엔티티를 직접 반환하지 않고 프록시 객체를 반환한다. 따라서 프록시를 실제로 사용하기 전까진 DB에 접근하지 않는다. (지연로딩) 

 

findById()는 영속성 컨텍스트의 1차 캐시가 없으면 DB에서 데이터를 조회해온다.

 

@Transactional 있는 버전
...
    @Test
    @DisplayName("회원 등록이 정상적으로 저장되는지 체크")
    @Transactional
    public void add_member_success() throws Exception {
        MemberForm memberForm = new MemberForm();

        String userId = "test001";
        memberForm.setUserId(userId);
        memberForm.setUserName("테스트");
        memberForm.setPassword("root");
        memberForm.setGender(Gender.FEMALE);
        memberForm.setEmail("test@naver.com");

        memberService.saveUser(memberForm);

        Member member = memberRepository.getById(1L);

        Assertions.assertThat(member.getUserId()).isEqualTo(userId);

    }
...

 

 

 

 

 

Postman

테스트 코드 외에도 직접 Postman 으로 API를 호출하여 확인해볼 수 있다.

 

 

 

 

반응형

Designed by JB FACTORY