jpa로 회원가입을 구현해보았다.
Request, Response, Entity, Repository, Service, Controller 구현
global/config/error 패키지, global/config/BaseEntity 클래스를 사용 중이다. domain/web/seller 도메인을 사용 중이다.package com.umc.jaetteoli.domain.web.seller.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostSignUpSellerReq {
private String uid; // 유저아이디
private String name; // 유저 이름
private String birthday; // 유저 생일 yyyy.MM.dd
private String phone; // 유저 전화번호 010xxxxxxxx
private String password; // 유저 비밀번호
private String email; //이메일
private int serviceCheck; // 서비스 이용동의
private int personalCheck; // 개인정보 이용동의
private int smsCheck; // sms 이용동의
private int emailCheck; // 이메일 이용동의
private int callCheck; // 전하 수신 동의
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PostSignUpSellerRes {
private String uid;
private String name;
private String birthday;
private String phone;
private String email;
private String completeDate; // 처리날짜
private int smsCheck;
private int emailCheck;
private int callCheck;
}
@Data Lombok 라이브러리의 어노테이션이다. @Data 어노테이션이 선언된 클래스에 대해 getter와 setter, equals(), hashCode(), toString() 메서드들을 자동으로 생성해준다.@Data
public class Example {
private int id;
private String name;
}
위와 같이 클래스를 선언하면, 아래와 같이 getter와 setter를 사용할 수 있다.
Example example = new Example();
example.setId(1); // setter
example.setName("jskim2x"); // setter
System.out.println(example.getId()); // getter
System.out.println(example.getName()); // getter
@AllArgsConstructor 모든 필드 값을 파라미터로 받는 생성자를 생성한다.@AllArgsConstructor
public class Example {
private int id;
private String name;
}
위의 클래스는 아래와 같이 사용할 수 있다.
Example example = new Example(1, "jskim2x");
@NoArgsConstructor: 파라미터가 없는 기본 생성자를 생성한다.@NoArgsConstructor
public class Example {
private int id;
private String name;
}
위의 클래스는 아래와 같이 사용할 수 있다.
Example example = new Example();
@Builder: 빌더 패턴 클래스를 생성하며, 생성자 상단에 선언하면 생성자에 포함된 필드만 빌더에 포함시킨다.@Builder
public class Example {
private int id;
private String name;
}
위의 클래스는 아래와 같이 사용할 수 있다.
Example example = Example.builder()
.id(1)
.name("jskim2x")
.build();
package com.umc.jaetteoli.global.config;
import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.time.LocalDateTime;
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreationTimestamp
@Column(updatable = false)
protected LocalDateTime createdAt;
@UpdateTimestamp
protected LocalDateTime updatedAt;
@Enumerated(value = EnumType.STRING)
protected Status status= Status.valueOf(Status.ACTIVE.toString());
public enum Status {
ACTIVE,
DELETE
}
public void updateStatus(Status status){
this.status=status;
}
}
createdAt(생성일), updatedAt(수정일), status(상태값)가 들어간다.package com.umc.jaetteoli.domain.web.seller.entity;
import lombok.*;
import com.umc.jaetteoli.global.config.BaseEntity;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = "merchandisers")
public class Seller extends BaseEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long sellerIdx;
@Column(name = "name", nullable = false, length = 75)
private String name;
@Column(name = "birthday", nullable = false, length = 45)
private String birthday;
@Column(name = "phone", nullable = false, length = 45)
private String phone;
@Column(name = "uid", nullable = false, length = 45)
private String uid;
@Column(name = "password", nullable = false, length = 100)
private String password;
@Column(name = "email", nullable = false, length = 100)
private String email;
@Column(name = "first_login", nullable = false)
@ColumnDefault("1")
private int firstLogin;
@Column(name = "menu_register", nullable = false)
@ColumnDefault("0")
private int menuRegister;
@Column(name = "service_check", nullable = false)
@ColumnDefault("0")
private int serviceCheck;
@Column(name = "personal_check", nullable = false)
@ColumnDefault("0")
private int personalCheck;
@Column(name = "sms_check", nullable = false)
@ColumnDefault("0")
private int smsCheck;
@Column(name = "email_check", nullable = false)
@ColumnDefault("0")
private int emailCheck;
@Column(name = "call_check", nullable = false)
@ColumnDefault("0")
private int callCheck;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
package com.umc.jaetteoli.domain.web.seller.repository;
import com.umc.jaetteoli.domain.web.seller.entity.Seller;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface SellerRepository extends JpaRepository<Seller,Long> {
Optional<Seller> findByUid(String uid);
}
package com.umc.jaetteoli.domain.web.seller.service;
import com.umc.jaetteoli.domain.web.seller.dto.PostSignUpSellerReq;
import com.umc.jaetteoli.domain.web.seller.dto.PostSignUpSellerRes;
import com.umc.jaetteoli.domain.web.seller.entity.Seller;
import com.umc.jaetteoli.domain.web.seller.repository.SellerRepository;
import com.umc.jaetteoli.global.config.error.exception.BaseException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static com.umc.jaetteoli.global.config.error.ErrorCode.*;
import static com.umc.jaetteoli.global.util.Regex.*;
@Service
@RequiredArgsConstructor
public class SellerService {
private final SellerRepository sellerRepository;
private final PasswordEncoder passwordEncoder;
@Transactional(rollbackFor = BaseException.class)
public PostSignUpSellerRes signUp(PostSignUpSellerReq postSignUpSellerReq) throws BaseException {
// 1. Request값 검사 (빈값여부,정규식 일치 검사)
if(checkIsEmptySignUpBySeller(postSignUpSellerReq)){ // 빈값여부 check
throw new BaseException(BAD_REQUEST);
}
if(!isRegexUid(postSignUpSellerReq.getUid())){ // 아이디 정규 표현식 예외
throw new BaseException(INVALID_UID_FORMAT);
}
if(!isRegexPassword(postSignUpSellerReq.getPassword())){ // 비밀번호 정규 표현식 예외
throw new BaseException(INVALID_PW_FORMAT);
}
if(!isRegexBirth(postSignUpSellerReq.getBirthday())){ // 생년월일 정규 표현식 예외
throw new BaseException(INVALID_BIRTH_FORMAT);
}
if(!isRegexPhone(postSignUpSellerReq.getPhone())){ // 핸드폰번호 정규 표현식 예외
throw new BaseException(INVALID_PHONE_NUM_FORMAT);
}
// 2. 중복 아이디 검사 및 비밀번호 암호화
if(sellerRepository.findByUid(postSignUpSellerReq.getUid()).isPresent()){ // 중복 아이디 검사
throw new BaseException(ID_ALREADY_EXISTS);
}
try{ // 비밀번호 암호화 -> 사용자 요청 값 중 비밀번호 최신화
String encryptPassword = passwordEncoder.encode(postSignUpSellerReq.getPassword());
postSignUpSellerReq.setPassword(encryptPassword);
}catch (Exception e){
throw new BaseException(PASSWORD_ENCRYPTION_FAILURE); // 비밀번호 암호화 실패 시
}
try{
Seller newSeller = Seller.builder()
.name(postSignUpSellerReq.getName())
.birthday(postSignUpSellerReq.getBirthday())
.phone(postSignUpSellerReq.getPhone())
.uid(postSignUpSellerReq.getUid())
.password(postSignUpSellerReq.getPassword())
.email(postSignUpSellerReq.getEmail())
.firstLogin(1)
.serviceCheck(postSignUpSellerReq.getServiceCheck())
.personalCheck(postSignUpSellerReq.getPersonalCheck())
.smsCheck(postSignUpSellerReq.getSmsCheck())
.emailCheck(postSignUpSellerReq.getEmailCheck())
.callCheck(postSignUpSellerReq.getCallCheck())
.build();
// 3. 유저 insert
newSeller = sellerRepository.save(newSeller);
// 4. 방금 insert한 유저 반환
PostSignUpSellerRes checkSeller = PostSignUpSellerRes.builder()
.uid(newSeller.getUid())
.name(newSeller.getName())
.birthday(newSeller.getBirthday())
.phone(newSeller.getPhone())
.email(newSeller.getEmail())
.completeDate(convertTimestampToString(newSeller.getCreatedAt()))
.smsCheck(newSeller.getSmsCheck())
.emailCheck(newSeller.getEmailCheck())
.callCheck(newSeller.getCallCheck())
.build();
return checkSeller;
}catch (Exception e){
System.out.println(e);
throw new BaseException(DATABASE_ERROR);
}
}
/**
* 입력값 빈값 여부 판별 메소드
* 이름, 생년월일, 휴대폰 번호, 아이디, 비밀번호 빈값 체크
*/
public boolean checkIsEmptySignUpBySeller(PostSignUpSellerReq postSignUpSellerReq){
return postSignUpSellerReq.getUid().length()==0 || postSignUpSellerReq.getPassword().length()==0 || postSignUpSellerReq.getName().length()==0
|| postSignUpSellerReq.getBirthday().length()==0 || postSignUpSellerReq.getEmail().length()==0 || postSignUpSellerReq.getPhone().length()==0;
}
public String convertTimestampToString(LocalDateTime localDateTime) {
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy.MM.dd");
return dateFormat.format(localDateTime);
}
}
첫번째로, 들어온 요청 값들에 대한 Validation 검사를 수행하는 부분인데, 이를 통해 내가 커스텀해서 만든 BaseException의 형태로 예외를 throw하게 된다.
GlobalExceptionHandler 이 클래스에서 처리하게 된다.두번째로, 중복된 아이디를 검사하게 되는데, 여기서 SellerRepository의 findByUid함수가 쓰인다.
비밀번호 암호화로는 PasswordEncoder를 사용했는데, 원래는 BCrypt, SHA256과 같은 해시함수들을 이용해 따로 커스텀하려고 했지만, 이미 스프링부트에서 너무 좋은 클래스를 지원해줘서 이걸 쓰기로 했다.
세번째로, Database에 저장하는 부분인데, JpaRepository가 제공해주는 save 함수로 간단히 구현했고, 이와 동시에 newSeller 변수에 해당 행에 들어간 데이터들을 모두 저장해준다.
네번째로, newSeller 변수의 값들을 PostSignUpSellerRes에 넣어서 반환해준다.
checkIsEmptySignUpBySeller 이 함수는 요청 값들 중에서 Empty값이 존재하는지를 검사해주는 메소드다.
convertTimestampToString 이 함수는 createdAt의 날짜 형식을 yyyy.MM.dd 형식으로 변환해주는 메소드다.
package com.umc.jaetteoli.domain.web.seller;
import com.umc.jaetteoli.domain.web.seller.dto.PostSignUpSellerReq;
import com.umc.jaetteoli.domain.web.seller.dto.PostSignUpSellerRes;
import com.umc.jaetteoli.domain.web.seller.service.SellerService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.umc.jaetteoli.global.config.error.exception.BaseException;
import com.umc.jaetteoli.global.config.error.BaseResponse;
import static com.umc.jaetteoli.global.config.error.BaseResponseStatus.BAD_REQUEST;
import static com.umc.jaetteoli.global.config.error.BaseResponseStatus.SUCCESS;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/web")
public class SellerController {
// 생성자 주입
private final SellerService sellerService;
@GetMapping("/test")
public ResponseEntity<BaseResponse> userTest(){
try{
return ResponseEntity.ok(new BaseResponse<>(SUCCESS));
} catch(BaseException exception){
return ResponseEntity.status(HttpStatus.CONFLICT).body(new BaseResponse<>(BAD_REQUEST));
}
}
@PostMapping("/jat/sellers")
public ResponseEntity<BaseResponse<PostSignUpSellerRes>> signUp(@RequestBody PostSignUpSellerReq postSignUpSellerReq) {
PostSignUpSellerRes postSignUpSellerRes = sellerService.signUp(postSignUpSellerReq);
return ResponseEntity.ok(new BaseResponse<>(postSignUpSellerRes));
}
}
package com.umc.jaetteoli.global.config.error;
import com.umc.jaetteoli.global.config.error.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import java.net.BindException;
import static com.umc.jaetteoli.global.config.error.ErrorCode.*;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
...
...
...
// Application Exception
@ExceptionHandler(BaseException.class)
public ResponseEntity<BaseResponse<?>> handleBaseException(BaseException e) {
// 예외에서 메시지와 코드만 추출
String errorMessage = e.getMessage();
int errorCode = e.getCode();
// BaseResponse 생성
BaseResponse<?> response = new BaseResponse<>(errorMessage, errorCode);
return ResponseEntity
.status(e.getCode())
.body(response);
}
}
package com.umc.jaetteoli.global.config.error;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
import static com.umc.jaetteoli.global.config.error.BaseResponseStatus.SUCCESS;
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class BaseResponse<T> {//BaseResponse 객체를 사용할때 성공, 실패 경우
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String message;
private final int code;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
// 성공
public BaseResponse(T result) {
this.isSuccess = SUCCESS.isSuccess();
this.message = SUCCESS.getMessage();
this.code = SUCCESS.getCode();
this.result = result;
}
public BaseResponse(ErrorCode errorCode) {
this.isSuccess = false;
this.message = errorCode.getErrorMessage();
this.code= errorCode.getCode();
}
public BaseResponse(String message, int code) {
this.isSuccess = false;
this.message = message;
this.code = code;
}
}
package com.umc.jaetteoli.global.config.error;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED;
@Getter
public enum ErrorCode {
SUCCESS(HttpStatus.OK,200, "요청에 성공하였습니다."),
BAD_REQUEST( HttpStatus.BAD_REQUEST,400, "입력값을 확인해주세요."),
FORBIDDEN(HttpStatus.FORBIDDEN, 403,"권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, 404,"대상을 찾을 수 없습니다."),
// [Seller] 회원가입
INVALID_UID_FORMAT(HttpStatus.BAD_REQUEST,400,"아이디 정규 표현식 예외입니다."),
INVALID_PW_FORMAT(HttpStatus.BAD_REQUEST,400,"비밀번호 정규 표현식 예외입니다."),
INVALID_BIRTH_FORMAT(HttpStatus.BAD_REQUEST,400,"생년월일 정규 표현식 예외입니다."),
INVALID_PHONE_NUM_FORMAT(HttpStatus.BAD_REQUEST,400,"핸드폰번호 정규 표현식 예외입니다."),
ID_ALREADY_EXISTS(HttpStatus.CONFLICT,409,"중복된 아이디 입니다."),
PASSWORD_ENCRYPTION_FAILURE(INTERNAL_SERVER_ERROR,500,"비밀번호 암호화에 실패했습니다."),
// Database 예외
DATABASE_ERROR(INTERNAL_SERVER_ERROR,500,"데이터베이스 오류입니다.");
private final HttpStatus httpStatus;
private final int code;
private final String errorMessage;
ErrorCode( HttpStatus httpStatus, int code, String errorMessage) {
this.httpStatus = httpStatus;
this.code = code;
this.errorMessage = errorMessage;
}
}
// 1. Request값 검사 (빈값여부,정규식 일치 검사)
if(checkIsEmptySignUpBySeller(postSignUpSellerReq)){ // 빈값여부 check
throw new BaseException(BAD_REQUEST);
}
if(!isRegexUid(postSignUpSellerReq.getUid())){ // 아이디 정규 표현식 예외
throw new BaseException(INVALID_UID_FORMAT);
}
if(!isRegexPassword(postSignUpSellerReq.getPassword())){ // 비밀번호 정규 표현식 예외
throw new BaseException(INVALID_PW_FORMAT);
}
if(!isRegexBirth(postSignUpSellerReq.getBirthday())){ // 생년월일 정규 표현식 예외
throw new BaseException(INVALID_BIRTH_FORMAT);
}
if(!isRegexPhone(postSignUpSellerReq.getPhone())){ // 핸드폰번호 정규 표현식 예외
throw new BaseException(INVALID_PHONE_NUM_FORMAT);
}
BAD_REQUEST 예외를 발생시킨다.INVALID_UID_FORMAT, INVALID_PW_FORMAT, INVALID_BIRTH_FORMAT, INVALID_PHONE_NUM_FORMAT).ID_ALREADY_EXISTS 예외를 발생시킵니다.PASSWORD_ENCRYPTION_FAILURE 예외를 발생시킵니다.@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
...
...
...
// Application Exception
@ExceptionHandler(BaseException.class)
public ResponseEntity<BaseResponse<?>> handleBaseException(BaseException e) {
// 예외에서 메시지와 코드만 추출
String errorMessage = e.getMessage();
int errorCode = e.getCode();
// BaseResponse 생성
BaseResponse<?> response = new BaseResponse<>(errorMessage, errorCode);
return ResponseEntity
.status(e.getCode())
.body(response);
}
}
예외 처리기를 사용한 예외 처리: GlobalExceptionHandler 클래스는 @RestControllerAdvice 어노테이션이 붙어 있어서 컨트롤러에서 발생하는 예외를 처리한다.
handleBaseException 메서드에서 BaseException을 처리하며, 해당 예외의 메시지와 코드를 추출하여 BaseResponse에 넣어 응답하게 된다.
이런 방식으로 예외를 처리한 이유는 API 호출에 대한 응답이 일관되게 유지되게 하고, 가독성과 유지 보수성을 향상시키기 위함이다.


yyyy.mm.dd 이 형식으로 요청을 넣어줘야 하기 때문에 해당 예외를 뱉어냈다.
010xxxxxxxx 이렇게 다 붙여서 요청을 보내줘야 하기 때문에 해당 예외를 뱉어냈다.
잘 읽었습니다. 좋은 정보 감사드립니다.