#4 회원가입 기능 개발

seojin's 개발블로그·2023년 8월 2일
0

영화 사이트 제작

목록 보기
4/19

저번의 시퀀스, api명세에 이어 오늘은 회원가입 api를 개발해보았다.

📌 로직과 명세


먼저 회원가입 api의 블랙박스 시퀀스 다이어그램을 작성해주었다.

  1. 클라이언트가 회원가입 정보를 전송함
  2. dto로 랩핑하여 서비스 레이어에 전달
  3. 서비스 레이어에서 중복 검사 진행
  4. 중복이 있을시 에러 메세지 반환, 없을시 데이터베이스에 저장

위와 같은 단계로 구성했으며 입력, 출력값은 다음의 명세를 따르기로 약속했다.

해당 url로 input param을 post하면 output JSON이 출력된다.
code와 message로 성공여부를 알려주며 data에는 회원의 가입정보가 출력이된다.

entity는 위와 같이 설계를 하였다.

step1. entity 작성

우선 데이터를 담을 엔티티를 작성해주었다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "members")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id", nullable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "phone_number", nullable = false)
    private String phoneNumber;

    @Column(name = "birthday", nullable = false)
    private LocalDate birthday;

    @Column(name = "sex", nullable = false)
    @Enumerated(EnumType.STRING)
    private Sex sex;

    @Column(name = "role_type", nullable = false)
    @Enumerated(EnumType.STRING)
    private MemberRole roletype;

    @CreatedDate
    @Column(name = "signup_at")
    private LocalDateTime createAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Builder
    public Member(String email, String password, String phoneNumber, LocalDate birthday, Sex sex, MemberRole roletype) {
        this.email = email;
        this.password = password;
        this.phoneNumber = phoneNumber;
        this.birthday = birthday;
        this.sex = sex;
        this.roletype = BASIC;
    }

    public void updatePassword(String password, String phoneNumber){
        this.password = password;
    }

    public void updatePhoneNumber(String phoneNumber){
        this.phoneNumber = phoneNumber;
    }
}

@EntityListeners(AuditingEntityListener.class)로
생성일과 수정일을 자동으로 저장하게 하였으며 생성에는 빌더패턴을 적용했다
그리고 비밀번호 변경과 휴대폰번호 변경을 염두해둔 메서드도 선언해놨다.

step2. RequestDto 작성

다음으로는 요청을 받을 requestDto를 만들어줬다.

@Getter
public class SignupMemberRequestDto {

    @NotBlank(message = "이메일을 입력해주세요.")
    @Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "이메일 형식이 올바르지 않습니다.")
    private String email;

    @NotBlank(message = "비밀번호를 입력해주세요.")
    private String password;

    @NotBlank(message = "휴대폰 번호를 입력해주세요.")
    private String phoneNumber;

    @NotBlank(message = "생년월일 ex) 1990-00-00 을 입력해주세요.")
    private String birthday;

    @NotBlank(message = "성별을 선택해주세요.")
    private String sex;

    public Member toEntity() {
        LocalDate birthdayDate = LocalDate.parse(birthday);
        Sex sexEnum = Sex.valueOf(sex);

        return Member.builder()
                .email(email)
                .password(password)
                .phoneNumber(phoneNumber)
                .birthday(birthdayDate)
                .sex(sexEnum)
                .build();
    }
}

입력값의 검증을 위해 javax.validation 의존성을 추가하여 NotBlank와 pattern을 적용하였다
그리고 입력값을 저장하기 위해 toEntity 메서드를 만들고 생년월일과 성별의 경우 에러가 발생해 수정한 결과인데 json형식에서 생년월일이 string타입으로 되어있어 localDate타입으로 다시 변경해주는 과정을 넣었고 성별의 경우는 enum 타입에 맞게 변경을 해주는 과정을 넣어주었다.

step3. Controller 작성

데이터를 담을 객체를 만든뒤에는 컨트롤러를 만들어주었다.
나는 클라이언트와 가까운곳부터 개발을 하는게 편한것 같다.

@PostMapping("/api/member/signupMember")
    public ResponseEntity<ApiResponse<Map<String, Object>>> signupMember(@RequestBody @Valid SignupMemberRequestDto requestDto, Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "회원 가입 실패", null));
        }

        try {
            Member savedMember = memberService.save(requestDto);
            return ResponseEntity.status(HttpStatus.CREATED).body(new ApiResponse<>(1, "회원 가입 성공", Map.of("member", savedMember)));
        } catch (DuplicateEmailException ex) {
            Map<String, Object> errorData = new HashMap<>();
            errorData.put("errCode", "duplicate_email");
            errorData.put("errMsg",ex.getMessage());
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "중복된 이메일", errorData));
        }
    }

응답을 위해 ResponseEntity타입으로 선언을 했고
if문은 requestDto가 올바른지에 대한 검증,
아래의 try, catch는 MemberService에 requestDto를 보내고 그 리턴값에 대한
예외를 처리하는 코드이다.
처음부터 이렇게 생긴 코드는 아니었고
아래에서 적겠지만 api명세대로 메세지가 출력되게 하기위해서 꽤 많은 변경을 거쳤다.

step4. Service 작성

public Member save(SignupMemberRequestDto requestDto){
        if (memberRepository.existsByEmail(requestDto.getEmail())) {
            throw new DuplicateEmailException("이미 가입된 이메일 입니다: " + requestDto.getEmail());
        }
        return memberRepository.save(requestDto.toEntity());
    }

dto를 받아서 데이터베이스에 이메일을 조회후 예외, 또는 엔티티로 변경후 저장을 하는 코드이다.
findByEmail이 아닌 existsByEmail로 선언을 한 이유는 findBy보다 좋은 방법이 있지 않을까? 하는 단순한 생각으로 검색을 했는데 jpa 데이터 검색 참고자료
위 링크의 자료가 나와서 글을 읽어보니 중복확인과 같은 기능에서는 데이터를 노출시킬 필요가 없어 exist를 주로 사용한다는 내용과 exists와 count의 속도 조건에 대한 데이터의 유무를 검색시 count보다 더 성능이 좋다는 내용이 있어 exists를 사용하게 되었다.

step5. 테스트

1. dto검증 테스트


우선 request의 값들이 올바른지 검증하는 기능을 테스트 해보았다.
이메일 형식을 지키지 않으니 회원가입이 실패했다는 메세지가 출력된다.

물론 데이터베이스에서 조회시에도 회원정보가 추가되지 않았다.

2. 이메일 중복확인 테스트

위에 이미 가입해놓은 회원정보와 같은 이메일을 전송해보았다. 역시 실패 아래의 메세지는 api명세와 조금 다른데 응답 메세지에 대한 공부를 하다가 몇주전 팀플때 강사님께서 제시한 api명세가 기억이 나서 그때의 명세 양식대로 출력이 되게 구현을 했다. 그때는 시간이 없어서 구현을 못했는데 이제라도 구현방법을 공부하게 되어 다행이다. 물론 프론트 팀원이 있다면 절대로 명세를 지켰겠지만 백엔드 밖에 없으니 출력값 형식만 조금 바꾸었다.

3. 회원가입 테스트

마지막으로 정상적으로 입력된 중복이 없는 데이터를 입력하여 회원가입 요청을 하였고 회원가입 성공 메세지가 출력이 되었다. 데이터베이스에도 정상적으로 저장이 된 것을 확인하였다.

📌 발생했던 문제

I. 정상적인 데이터 입력시의 오류

제일 처음 로직만을 테스트하기 위해 만든 코드에서 발생한 에러이다.

포스트맨으로 데이터를 전송하니 위와 같은 500번 에러가 발생했고

스프링을 확인해보니 not-null 데이터이지만 데이터가 null값이다 라는 에러가 발생한 것을 확인했다.
이전 프로젝트에서 localDate타입의 데이터 입력시 형변환이 제대로 이루어지지 않아 같은 에러를 겪은적이 있었는데 그때는 시간이 없어서 localDate가 아닌 데이터 타입을 아예 string으로 바꾸어서 해결했지만 여러모로 잘못된 방법이라 이번에는 localDate를 이용하는 방법에 대해 공부를 하며 문제를 해결하기로 하였다.
날짜 타입 JSON 변환
위의 링크를 보고

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
    private LocalDate birthday;

이 코드와 같이 생년월일을 입력받을때 형변환을 제대로 해줄수 있게 설정을 해주어
해결을 하려 했으나 또 같은 에러가 발생했고 문제 파악을 위해 이번엔 requestDto의 값들을 모두 출력을 해보기로 했다.

대체 왜 모두 null값일까 를 생각해봤다.

  1. json형식, 변수명이 잘못되었나?
    모두 체크를 해봤지만 둘다 이상이 없었다.

  2. 컨트롤러를 잘못 개발했나?
    확인을 해보니 문제가 없었다. 인줄 알았으나 여기에 문제가 있었다.

    위와 같이 @RequestBody 어노테이션을 안붙여준것이 문제였다,,,
    어노테이션을 붙여주자 바로 해결이 되었다.

📌 어려웠던 점

I. requestDto에 대한 검증

입력값에 대한 검증을 어떻게 해야하는지가 고민이었다.
이 부분에 대해 들은 얘기들이 다양해서 생긴 고민이었는데
프론트에서 검증을 마친 데이터만 전송해주어야 한다.
백엔드가 전부다 해야한다. 와 같은 얘기를 들은적이 있어서
작성을 할지말지가 고민이었지만 우리는 프론트 팀원이 없기에 데이터 검증을 모두 해주기로 하였다.
request 파라미터의 검증
여러 글들을 참고했지만 위의 글이 가장 자세한것 같아서 첨부하였다.
각 파라미터마다 오류에 대한 처리가 필요하고 그를 위해 Bean Validation을 사용하는 방법이 있다는 내용이다.
각 파라미터마다 오류에 대한 처리를 해준다는 내용이 새로웠고 회원가입시에 필요한 부분인것 같아 내 코드에도 적용을 시켜주었다.

@NotBlank(message = "생년월일 ex) 1990-00-00 을 입력해주세요.")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul")
private LocalDate birthday;

그 결과 jsonFormat과 notBlank를 같이 사용할 수가 없어 처음에 적어놓은 코드처럼 jsonformat을 지우고
toEntity()에서 string으로 받은 생년월일을 localDate로 변경을 해주게 되었다.
하지만 jsonFormat과 형변환 코드를 직접적는것중 뭐가 더 좋은 방법인지가 아직 의문이다. 더 생각을 해봐야 할 것 같다.

II. 응답 메세지

가장 마지막에 구현을 한 부분이며 회원정보 저장보다 훨씬 많은 시간이 걸렸다.
어려웠던 부분은

  1. 서비스 레이어에서 에러를 어떻게 출력해야하는지.
  2. 프레젠테이션 레이어에서는 서비스레이어의 메세지를 어떻게 이용할 지

이 두 부분이였고 예외처리에 대해 먼저 참고자료들을 찾아보았다.
okky나 다른 velog들을 돌며 개발자분들은 예외처리를 어떻게 하는지를 찾아보았는데 에러처리에 대한 클래스들을 만들어서 이용한다는 글들을 참고하여 해결해보기로 했다.

public class DuplicateEmailException extends RuntimeException {
    public DuplicateEmailException(String message) {
        super(message);
    }
}

중복된 이메일이 있을시의 예외처리 코드를 만들었다.
프로그램 실행중 발생하는 에러이므로 RuntimeException을 상속하였고
다른 처리없이 메세지만 담게하였다.

public Member save(SignupMemberRequestDto requestDto){
        if (memberRepository.existsByEmail(requestDto.getEmail())) {
            throw new DuplicateEmailException("이미 가입된 이메일 입니다: " + requestDto.getEmail());
        }
        return memberRepository.save(requestDto.toEntity());
    }

서비스 계층에서는 이렇게 사용을 하였다.

그리고 다음으로 컨트롤러에서 어떻게 구현을 하느냐가 남았었는데
응답 메세지를 다루는 문제라 ResponseEntity기능에 대해 먼저 공부를 했다.ResponseEntity의 개념과 사용법
주로 위의 링크를 참고하였다.

응답메세지의 변경은 이해를 했으나

나는 단순히 데이터만 출력이 되는것이 아니라 code, message, data: {~~~}와 같은 형식으로 출력하는것을 원했기 때문에 다른 방법을 더 찾았는데
API response 구조
위와 같은 블로그를 발견했다.

정말 딱 내가 원하던 내용이다. 솔루션은 아니지만 실제 개발자분께서 작성을 한 내용이라 내가 찾던 현업에서 어떻게 할까? 에 대한 내용이라 위 블로그의 내용을 보고 레퍼런스들을 찾아서 구현을 했다.

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public ApiResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
}

ApiResponse라는 클래스를 만들어서 이 클래스를 응답메세지로 반환을 해주기로 했다. 그리고 계속 이런 형식으로 응답메세지를 반환해줄 예정이라 제네릭을 사용하여 여러 데이터에서 이용을 하기로했다.

@PostMapping("/api/member/signupMember")
    public ResponseEntity<ApiResponse<Map<String, Object>>> signupMember(@RequestBody @Valid SignupMemberRequestDto requestDto, Errors errors){
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "회원 가입 실패", null));
        }
        try {
            Member savedMember = memberService.save(requestDto);
            return ResponseEntity.status(HttpStatus.CREATED).body(new ApiResponse<>(1, "회원 가입 성공", Map.of("member", savedMember)));
        } catch (DuplicateEmailException ex) {
            Map<String, Object> errorData = new HashMap<>();
            errorData.put("errCode", "duplicate_email");
            errorData.put("errMsg",ex.getMessage());
            return ResponseEntity.badRequest().body(new ApiResponse<>(0, "중복된 이메일", errorData));
        }
    }

그렇게 탄생한 컨트롤러
하지만 ResponseEntity<ApiResponse<Map<String, Object>>>이 부분이 너무 지저분에 보이고 catch문 안의 코드도 이것이 정석적인 방법인지는 아직 의문이다.
응답메세지에 대한건 다른 파트를 개발하면서도 다시 공부를 해봐야겠다.

📌 후기

회원가입이 이렇게 오래걸릴줄 몰랐다.
아무것도 모를때는 후다닥 만들었는데 잘 만들려고 하니 아직 내가 모르는게 너무 많아 공부를 병행하니 시간이 오래 걸리게 되었다.

더 공부해서 더 좋은 코드로 고쳐야겠다.
그리고,,, 코드 리뷰를 받고 싶다.
여러 참고자료들을 보며 개발을 했는데 이게 맞는 방법인지 더 좋은 방법이 있는지
알려줄 사람이 있으면 좋겠다,,,, 더 나은 개발방법을 알고 싶은데 이 부분이 어려운것 같다.

마지막으로 명세와 알고리즘을 제대로 정해두고 개발을 시작하니
고민을 앞에서 미리 해둔느낌? 이라 조금은 편안하게 개발을 하게되는것 같다.
툴을 골라서 만드는 단계라 어떤 레퍼런스가 필요한지도 좀 더 명확해지는것 같다.

잘 만든 프로젝트가 되었으면 좋겠다.

profile
개발 공부하는 블로그

0개의 댓글