이번엔 스프링부트 로그 파일 설정과 회원 가입 API 생성을 해보도록 하겠습니다. 물론 회원가입을 하기 위한 ERD도 작성을 하겠습니다.
로깅을 위해 logback-spring.xml 설정을 하겠습니다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%5level) %cyan(%logger) - %msg%n" />
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %5level %logger - %msg%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- <appender name="FILE" class="ch.qos.logback.core.FileAppender">-->
<!-- <file>./log/localFile.log</file>-->
<!-- <encoder>-->
<!-- <pattern>${FILE_LOG_PATTERN}</pattern>-->
<!-- </encoder>-->
<!-- </appender>-->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./log/%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<springProfile name="local">
<logger name="org.project.shoppingmall" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</springProfile>
<springProfile name="prod">
<root level="ERROR">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</springProfile>
</configuration>
다음과 같이 logback-spring.xml을 설정했습니다.
그리고 이제 회원 가입을 위한 Entity, Controller, Service, Test 등을 작성해보도록 하겠습니다.
회원 가입을 위해 받는 정보는
이메일, 패스워드, 우편번호, 주소, 상세주소, 전화번호가 들어오게 됩니다.
(원래는 참고사항도 있었지만 불필요하여 제거했습니다. 참고해주세요)
여기서 생각을 해야될 것이 추후에 우편번호, 주소, 상세주소는 주문에 사용을 할 것 같습니다.
그리고 한명의 사용자가 하나의 주소만 가지는 것이 아닌 여러 개의 주소를 입력하고 가지고 있을 수 있다고 생각이 듭니다.
그래서 사용자와 주소 테이블을 각각 생성하여 연관 관계 맵핑을 해주도록 하겠습니다.
한명의 사용자가 여러 개의 주소를 가지고 있다면 User와 Address는 일대다의 관계를 가지게 됩니다.
그래서 저는 다음과 같이 ERD를 작성을 했습니다.
users에 role이 들어간 이유는 판매자와 사용자의 권한을 관리 해줘야 될 것 같아서 미리 넣어줬습니다.
User.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nickname;
private String email;
private String password;
private String phoneNumber;
private Role role;
@Builder
public User(String nickname, String email, String password, String phoneNumber, Role role) {
this.nickname = nickname;
this.email = email;
this.password = password;
this.phoneNumber = phoneNumber;
this.role = Role.USER;
}
}
package org.project.shoppingmall.type;
public enum Role {
USER("ROLE_USER"),
SELLER("ROLE_SELLER");
private String authority;
Role(String authority) {
this.authority = authority;
}
}
권한을 위해 enum으로 Role을 하나 만들어줍니다.
그 후 User Entity를 만들었는데 처음 생성을 할 땐 무조건 User 권한을 가지도록 할 예정이여서
생성자에 바로 Role.USER을 넣어줬습니다.
User 테이블을 복수형으로 쓰기위해 @Table(name="users")를 넣어줬습니다.
Address.java
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "addresses")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String postCode;
private String mainAddress;
private String detailAddress;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Builder
public Address(String postCode, String mainAddress, String detailAddress) {
this.postCode = postCode;
this.mainAddress = mainAddress;
this.detailAddress = detailAddress;
}
}
주소를 입력 받기 위한 테이블과 ManyToOne으로 User와 연관관계 맵핑을 진행하여 한명의 유저에 여러 개의 주소를 가질 수 있는 구조를 만들었습니다.
생성 시간과 수정 시간을 자동으로 맵핑 해주는 코드와 다른 여러 가지 설정이 있지만 현재는 설정하지 않고 넘어가고
추후에 다시 설정 하도록 하겠습니다.
그럼 Data JPA를 이용하여 DB와 연결을 할 수 있는 Repository를 생성하겠습니다.
package org.project.shoppingmall.repository;
import org.project.shoppingmall.domain.Address;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AddressRepository extends JpaRepository<Address, Long> {
}
package org.project.shoppingmall.repository;
import org.project.shoppingmall.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
다음과 같이 레포지토리를 생성 했습니다.
이제 API를 위한 컨트롤러와 비지니스 로직을 작성하여 회원 가입을 진행 하도록 하겠습니다.
중복 체크와 다른 로직을 생성하지 않고 가장 기본적인 여러 가지 데이터를 받아 저장하는 로직을 만들도록 하겠습니다.
그러기 위해선 각각 데이터를 받아올 Dto를 생성하도록 하겠습니다.
@Getter
public class SignupRequestDto {
private final String nickname;
private final String email;
private final String password;
private final String postCode;
private final String mainAddress;
private final String detailAddress;
private final String phoneNumber;
@Builder
public SignupRequestDto(String nickname, String email, String password, String postCode, String mainAddress, String detailAddress, String phoneNumber) {
this.nickname = nickname;
this.email = email;
this.password = password;
this.postCode = postCode;
this.mainAddress = mainAddress;
this.detailAddress = detailAddress;
this.phoneNumber = phoneNumber;
}
}
다음과 같이 Dto를 작성을 했는데 인텔리제이 추천에 record로 변환을 하라고 추천을 해주네요.
record에 대해 공부를 한 후에 변경을 할지 말지 추후에 변경을 할 때 다시 말씀드리겠습니다.
일단 제가 좋아하는 방식으로 진행을 하도록 하겠습니다.
아 그리고 작성을 하다보니 생각이 났는데 핸드폰 번호에 대한 유효성 검사를 진행을 안했습니다.
추후에 핸드폰 번호 길이 제한도 넣어보도록 하겠습니다.
SignupService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class SignupService {
private final UserRepository userRepository;
private final AddressRepository addressRepository;
// TODO : 이메일 인증, 인증 번호 확인, 닉네임 중복 확인
@Transactional
public Long signup(SignupRequestDto signupRequestDto) {
User user = User.createUser(signupRequestDto.getNickname(), signupRequestDto.getEmail(),
signupRequestDto.getPassword(), signupRequestDto.getPhoneNumber());
User userSave = userRepository.save(user);
Address address = Address.createAddress(signupRequestDto.getPostCode(), signupRequestDto.getMainAddress(),
signupRequestDto.getDetailAddress(), user);
addressRepository.save(address);
log.debug("회원가입 성공");
return userSave.getId();
}
}
회원가입 서비스를 먼저 작성을 했습니다.
서비스에서 사용할 createUser와 createAddress도 생성해주도록 하겠습니다.
public static User createUser(String nickname, String email, String password, String phoneNumber) {
return User.builder()
.nickname(nickname)
.email(email)
.password(password)
.phoneNumber(phoneNumber)
.role(Role.USER)
.build();
}
}
public static Address createAddress(String postCode, String mainAddress, String detailAddress, User user) {
return Address.builder()
.postCode(postCode)
.mainAddress(mainAddress)
.detailAddress(detailAddress)
.user(user)
.build();
}
그리고 서비스가 제대로 동작하는지 단위 테스트를 작성하도록 하겠습니다.
SignupServiceTest.java
@ExtendWith(MockitoExtension.class)
class SignupServiceTest {
@Mock
UserRepository userRepository;
@Mock
AddressRepository addressRepository;
@InjectMocks
SignupService signupService;
private SignupRequestDto signupRequestDto;
@BeforeEach
void setUp() {
signupRequestDto = SignupRequestDto.builder()
.nickname("min")
.email("abcd@mail.com")
.password("12345a@@")
.phoneNumber("01011111111")
.postCode("12345")
.mainAddress("서울시")
.detailAddress("집")
.build();
}
@Test
@DisplayName("[Service] 회원 가입 성공")
void userSave_and_addressSave() {
// given
User user = User.createUser(
signupRequestDto.getNickname(),
signupRequestDto.getEmail(),
signupRequestDto.getPassword(),
signupRequestDto.getPhoneNumber());
given(userRepository.save(any()))
.willReturn(user);
ReflectionTestUtils.setField(user, "id", 1L);
Address address = Address.createAddress(
signupRequestDto.getPostCode(),
signupRequestDto.getMainAddress(),
signupRequestDto.getDetailAddress(),
user);
given(addressRepository.save(any()))
.willReturn(address);
ReflectionTestUtils.setField(address, "id", 1L);
// when
Long userId = signupService.signup(signupRequestDto);
// then
assertThat(user.getId()).isEqualTo(userId);
}
}
Mockito와 assertj를 이용하여 테스트를 작성해줬습니다.
@Slf4j
@RestController
@RequiredArgsConstructor
public class SignupController {
private final SignupService signupService;
private final CommonService commonService;
@PostMapping("/signup")
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<Object> signup(@RequestBody SignupRequestDto signupRequestDto) {
log.debug("회원가입 API 호출");
Long userId = signupService.signup(signupRequestDto);
CommonResponseDto<Object> commonResponse =
commonService.successResponse(SuccessMessage.SIGNUP_SUCCESS.getDescription(), userId);
return ResponseEntity.status(HttpStatus.OK).body(commonResponse);
}
}
컨트롤러를 작성했습니다. 반환을 위해 ResponseEntity를 사용했습니다.
그리고 공통된 반환을 위해 CommonResponseDto와 CommonService를 작성했습니다.
CommonReponseDto.java
@Getter
public class CommonResponseDto<Data> {
private final String status;
private final String message;
private final Data data;
@Builder
public CommonResponseDto(String status, String message, Data data) {
this.status = status;
this.message = message;
this.data = data;
}
}
CommonSerivce.java
@Service
public class CommonService {
public CommonResponseDto<Object> successResponse(String message, Object data) {
return CommonResponseDto.builder()
.status(ResponseStatus.SUCCESS.getDescription())
.message(message)
.data(data)
.build();
}
}
Status와 Message는 Role과 동일하게 Enum으로 작성했습니다.
SignupController.java
@Slf4j
@RestController
@RequiredArgsConstructor
public class SignupController {
private final SignupService signupService;
private final CommonService commonService;
@PostMapping("/signup")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Object> signup(@RequestBody SignupRequestDto signupRequestDto) {
log.debug("회원가입 API 호출");
Long userId = signupService.signup(signupRequestDto);
CommonResponseDto<Object> commonResponse =
commonService.successResponse(SuccessMessage.SIGNUP_SUCCESS.getDescription(), userId);
return ResponseEntity.status(HttpStatus.CREATED).body(commonResponse);
}
}
회원 가입 API를 위해 컨트롤러를 작성하고 이후 테스트 코드도 작성했습니다.
SignupControllerTest.java
@WebMvcTest(SignupController.class)
class SignupControllerTest {
@MockBean
private SignupService signupService;
@MockBean
private CommonService commonService;
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
private SignupRequestDto signupRequestDto() {
return SignupRequestDto.builder()
.nickname("min")
.email("abcd@mail.com")
.password("12345a@@")
.phoneNumber("01011111111")
.postCode("12345")
.mainAddress("서울시")
.detailAddress("집")
.build();
}
@Test
@DisplayName("[API] 회원 가입 컨트롤러 - 성공")
void signupController() throws Exception {
//given
SignupRequestDto signupRequestDto = signupRequestDto();
String body = objectMapper.writeValueAsString(signupRequestDto);
Long userId = 1L;
CommonResponseDto<Object> commonResponseDto =
CommonResponseDto.builder()
.status(ResponseStatus.SUCCESS.getDescription())
.message(SuccessMessage.SIGNUP_SUCCESS.getDescription())
.data(userId)
.build();
given(signupService.signup(any())).willReturn(userId);
given(commonService.successResponse(
SuccessMessage.SIGNUP_SUCCESS.getDescription(),
userId
)).willReturn(commonResponseDto);
//when
//then
mvc.perform(post("/signup")
.contentType(MediaType.APPLICATION_JSON).content(body))
.andExpect(status().isCreated())
.andExpect(jsonPath("status")
.value(ResponseStatus.SUCCESS.getDescription()))
.andExpect(jsonPath("message")
.value(SuccessMessage.SIGNUP_SUCCESS.getDescription()))
.andExpect(jsonPath("data")
.value(userId))
.andDo(print());
}
}
글이 너무 길어진 것 같아 다음 글에서 작성한 API 연동 및 전화번호 길이 제한 등 진행을 해보려고 합니다.
Chats 프로젝트 (2) - Signup
이번에는 회원가입 페이지와 회원가입 API를 만들어보도록 하겠습니다. 먼저 회원가입 페이지를 만든 후 스프링 시큐리티와 로그 설정을 한 후에 API를 만들어보겠습니다. signup.html 회원가입 중
classruntime.tistory.com
'Project' 카테고리의 다른 글
Chats 프로젝트 (1) (1) | 2024.01.08 |
---|