[클론 코딩] 네이버 카페 - 네이버 회원 가입(등록)

2024. 4. 28. 19:18Project/Naver Cafe

  먼저 이전에 작성한 ERD를 바탕으로 네이버 회원 가입(등록)에 대한 구현을 해보고자 한다. 일단 기능에 중점을 두고 개발을 한 뒤에 제약조건이나 검증에 대한 부분을 추가적으로 고민하고 구현하도록 할 것 이다.


목차

  1. 기본 구성
  2. 회원 가입
  3. 제약조건 및 검증

기본 구성

Member

package CloneCoding.NaverCafe.domain.member;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDate;

@Entity
@Table(name = "MEMBER")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Column(name = "ACCOUNT_ID", unique = true)
    private String accountId;

    @Column(name = "ACCOUNT_PASSWORD")
    private String accountPassword;

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

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "BIRTHDAY")
    private LocalDate birthday;

    @Column(name = "PHONE_NUMBER", unique = true)
    private String phoneNumber;

    @Column(name = "NICKNAME", unique = true)
    private String nickname;

}

 

  멤버(회원)가 가져야 할 정보를 가진 클래스, 가져야 할 정보와 간략한 설명은 아래와 같다.

※ 추후 변경(추가/삭제) 될 수 있으며, 변경 시 동시에 해당 게시글에 반영 또는 변경 내용을 남기도록 할 것 이다.

  • id : 객체 PK(식별키)
  • accountId : 계정 ID
  • accountPassword : 계정 PW
  • email : 계정과 별개의 E-mail, 계정 분실 시 찾기 위한 용도
  • username : 사용자 이름(본명)
  • birthday : 생년월일
  • phoneNumber : 휴대전화번호
  • nickname : 별명, 카페 별명 미설정 시 해당 별명으로 자동 등록됨

  @Getter는 사용하되 @Setter는 사용하지 않았다. set 메서드가 필요하다면 직접 코드를 작성할 생각이다. 또한 Builder 패턴을 사용하기 위해 @Builder를 사용했다.


MemberRepository

package CloneCoding.NaverCafe.domain.member.repository;

import CloneCoding.NaverCafe.domain.member.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long>, QueryMemberRepository {
}

 

  MemberRepository 인터페이스는 Spring Data JPA와 QueryDSL을 사용하기 위해 JpaRepository 인터페이스와 QueryMemberRepository 인터페이스를 상속받는다. 추후 사용하는 기술이 변경될 수 있기에 유지보수에 용이하도록 인터페이스로 구현하였다.


QueryMemberRepository

package CloneCoding.NaverCafe.domain.member.repository;

import CloneCoding.NaverCafe.domain.member.Member;
import CloneCoding.NaverCafe.domain.member.dto.RequestJoinMember;

public interface QueryMemberRepository {
}

 

  QueryDSL을 사용하는 구현 클래스가 상속 받을 인터페이스이다. 구현체 변경에 유연하게 대처하기 위해 인터페이스를 만들었으며 해당 인터페이스에는 구현체 클래스가 구현할 추상 메서드들이 추가될 것이다.


QueryMemberRepositoryImpl

package CloneCoding.NaverCafe.domain.member.repository;

import CloneCoding.NaverCafe.domain.member.Member;
import CloneCoding.NaverCafe.domain.member.dto.RequestJoinMember;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class QueryMemberRepositoryImpl implements QueryMemberRepository {

    private final JPAQueryFactory query;

}

 

  QueryMemberRepository 인터페이스의 구현 클래스이다. JPAQueryFactory는 QueryDSL이 제공하는 일종의 도구로  JPQLQuery를 보다 쉽고 편리하게 작성할 수 있게 도와준다. 해당 구현 클래스는 JPAQueryFactory를 의존관계로 주입받는다.


MemberService

package CloneCoding.NaverCafe.domain.member.service;

public interface MemberService {
}

 

  구현체 변경에 유연하게 대처할 수 있도록 인터페이스를 만들었다. 구현체에서 구현할 추상 메서드들이 추가 될 것이다.


MemberServiceImpl

package CloneCoding.NaverCafe.domain.member.service;

import CloneCoding.NaverCafe.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

}

 

  MemberService 인터페이스의 구현 클래스이다. MemberRepository 인터페이스를 의존관계로 주입받는다.


MemberController

package CloneCoding.NaverCafe.domain.member.controller;

import CloneCoding.NaverCafe.domain.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {

    private final MemberService memberService;

}

 

  Member의 컨트롤러다. json 형태로 객체 데이터를 반환하기 위해서 @RestController를 사용했다. 요청 API 매핑은 /member로 시작하도록 클래스에 @RequestMapping을 적용했다. 컨트롤러는 MemberService 인터페이스를 의존관계로 주입받는다.


Config

package CloneCoding.NaverCafe.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {

    @Bean
    public JPAQueryFactory jpaQueryFactory (EntityManager em) {
        return new JPAQueryFactory(em);
    }

}

 

  글로벌 설정 클래스이다. EntityManager를 파라미터로 받아 생성한 JPAQueryFactory 객체를 스프링 빈(Spring Bean)으로 등록하고 해당 빈을 각 도메인의 Repository에서 사용하도록 했다. 이러한 방법은 의존관계 강도를 낮추기 위해 사용했다.


회원 가입

  회원 정보를 데이터베이스에 저장하는 기능을 구현한다.

QueryMemberRepository

package CloneCoding.NaverCafe.domain.member.repository;

public interface QueryMemberRepository {

    Member join(RequestJoinMember request);

}

 

  인터페이스에 join() 추상 메서드를 작성해두었다. 구현체 클래스에서 이를 구현할 것 이다.


QueryMemberRespositoryImpl

package CloneCoding.NaverCafe.domain.member.repository;

@Repository
@RequiredArgsConstructor
public class QueryMemberRepositoryImpl implements QueryMemberRepository {

    private final JPAQueryFactory query;

    @Override
    public Member join(RequestJoinMember request) {
        LocalDate birth = LocalDate.of(request.getYear(), request.getMonth(), request.getDay());

        return Member.builder()
                .accountId(request.getAccountId())
                .accountPassword(request.getAccountPassword())
                .email(request.getEmail())
                .username(request.getUsername())
                .birthday(birth)
                .phoneNumber(request.getPhoneNumber())
                .nickname(request.getNickname())
                .build();
    }

}

 

  MemberRepository 인터페이스의 join() 메서드를 구현하였다.

  • join(RequestJoinMember request) : 요청 정보(request)로 Member 객체를 생성하는 메서드

MemberService

package CloneCoding.NaverCafe.domain.member.service;

public interface MemberService {

    String joinMember(RequestJoinMember request);

}

 

  인터페이스에 joinMember() 추상 메서드를 작성해두었다. 구현체 클래스에서 이를 구현할 것 이다.


MemberServiceImpl

package CloneCoding.NaverCafe.domain.member.service;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Override
    public String joinMember(RequestJoinMember request) {

        Member joinMember = memberRepository.join(request);
        memberRepository.save(joinMember);

        return SystemMessage.JOIN_COMPLETE_NAVER.getMessage();

    }

}

 

  MemberService 인터페이스의 joinMember() 메서드를 구현하였다.

  • joinMember(RequestJoinMember request) : 요청 정보를 통해 생성한 Member 객체를 데이터베이스에 저장하고 시스템 메시지를 반환한다.

MemberController

package CloneCoding.NaverCafe.domain.member.controller;

@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {

    private final MemberService memberService;

    @PostMapping("/join")
    public String joinMember(@RequestBody RequestJoinMember request) {
        return memberService.joinMember(request);
    }

}

 

  컨트롤러에 회원 가입 요청에 따른 서비스가 수행될 수 있도록 코드를 작성하였다. 요청 정보와 함께 API 요청(/member/join, POST) 시 요청 정보를 토대로 데이터베이스에 회원 정보가 저장될 수 있다.

  • joinMember(RequestJoinMember request) : MemberService의 joinMember()를 호출하고 반환된 값을 반환

RequestJoinMember

package CloneCoding.NaverCafe.domain.member.dto;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestJoinMember {

    private String accountId;

    private String accountPassword;

    private String email;

    private String username;

    private int year;

    private int month;

    private int day;

    private String phoneNumber;

    private String nickname;

}

 

  회원 가입 요청 정보를 전달하는 용도의 DTO이다. Member 클래스와 다른 점이라면 생년월일을 한 번에 받지 않고 년, 월, 일로 나누어 전달 받는다.


SystemMessage

package CloneCoding.NaverCafe.message;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum SystemMessage {

    JOIN_COMPLETE_NAVER("정상적으로 회원가입을 완료했습니다!" +
            System.lineSeparator() +
            "로그인 페이지로 이동해 로그인 해주세요.");

    private final String message;

}

 

  사용자의 요청에 대한 응답 메시지를 설정하는 Enum 클래스이다. 전역적으로 사용할 수 있도록 했으며 설정한, 상수를 사용하면 지정한 메시지(문자열)을 사용할 수 있도록 했다. 각 메서드마다 반환 메시지를 따로 지정해도 되긴하지만 그렇게하면 추후 메시지 변경 시 각 메서드를 찾아서 반환 메시지를 하나하나 수정해 주어야 한다. 하지만 이렇게 사용하면 상수의 메시지 수정을 통해 상수를 사용하는 모든 곳의 메시지를 한 번에 관리 할 수 있어 편하다.


테스트 코드

package CloneCoding.NaverCafe.domain.member.service;

@Transactional
@SpringBootTest
class MemberServiceImplTest {

    @Autowired
    MemberRepository memberRepository;

    @DisplayName("회원가입 테스트")
    @Test
    void joinMember() {
        // given
        LocalDate birthday = LocalDate.of(2000, 2, 20);
        Member member = new Member(1L,"java", "0000",
                "testEmail@test.com", "Kim",
                birthday, "01055558888", "intellij");

        // when
        Member joinMember = memberRepository.save(member);
        Member findMember = memberRepository.findById(1L)
                .orElseThrow(()-> new NoSuchElementException("id와 일치하는 회원이 없습니다."));

        // then
        assertThat(findMember).isEqualTo(joinMember);
    }

}

 

  회원 가입 기능이 제대로 작동하는지 확인하는 테스트 코드이다. 회원 정보를 파라미터로 Member 객체를 생성하고 데이터베이스에 저장한다. save(Entity e) 메서드는 저장된 엔티티 객체를 반환한다. 반환 받은 객체와 데이터베이스에 조회한 엔티티 객체를 비교하여 두 객체가 같은지 확인한다. 같다면 회원 정보가 정상적으로 저장된 것이므로 회원가입이 정상적으로 이루어졌다고 할 수 있다.

테스트 코드 결과 - 회원 가입


API TEST

  포스트맨(postman)으로 API 테스트를 진행

API 테스트 - 회원 가입
테스트 결과 - DB

 

  테스트 결과 데이터베이스에도 정상적으로 회원 정보가 저장되었고 지정한 시스템 메시지가 정상적으로 반환되는 것도 확인 하였다.


제약조건 및 검증

  스펙 상으로 회원 가입(등록)시 Member 클래스의 모든 필드는 반드시 정상적인 값을 입력 받아야 한다. 하지만 현재 별다른 제약조건과 검증 설정을 하지 않은 상태로 필드 타입만 맞추면 어떠한 값(null 값, 빈 문자열, 형식에 맞지 않는 값 등)도 입력이 가능한 상태이다. 만약 사용자가 정상적인 데이터를 입력하지 않는다면 사용자 요청에 따른 서비스 수행을 중단하고 사용자에게 오류 메시지로 응답해 무엇이 문제인지 알려야 한다.

네이버 회원 가입 - 입력 정보 제약 조건

 

  위 이미지는 현재 네이버 회원 가입시 입력하는 정보에 대한 제약조건을 캡쳐한 것이다. 해당 내용을 참고해 제약조건을 설정했다.

RequestJoinMember

package CloneCoding.NaverCafe.domain.member.dto;

import jakarta.validation.constraints.*;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestJoinMember {

    @NotNull
    @Size(min = 5, max = 20)
    @Pattern(regexp = "^[a-z0-9_-]+")
    private String accountId;

    @NotNull
    @Size(min = 8, max = 16)
    @Pattern(regexp = "^[a-zA-Z0-9~!@#$%]+")
    private String accountPassword;

    @NotNull
    @Pattern(regexp = "^[a-zA-Z0-9_]+@[a-zA-Z]+\\.[a-zA-Z]+(\\.[a-zA-Z]+)?")
    private String email;

    @NotNull
    @Size(min = 2, max = 18)
    @Pattern(regexp = "^[a-zA-Z]+")
    private String username;

    @NotNull
    @Min(value = 1924) @Max(value = 2024)
    private int year;

    @NotNull
    @Min(value = 1) @Max(value = 12)
    private int month;

    @NotNull
    @Min(value = 1) @Max(value = 31)
    private int day;

    @NotNull
    @Size(min = 11, max = 11)
    @Pattern(regexp = "^[0-9]+")
    private String phoneNumber;

    @NotNull
    @Size(min = 2, max = 20)
    @Pattern(regexp = "^[a-zA-Z0-9]+")
    private String nickname;

}

 

 

  회원 가입시 가입 정보를 RequestJoinMember를 통해 전달하기에 해당 DTO에 제약조건을 설정하였다. 기본적으로 모든 필드는 반드시 입력되어야 하며 null 값, 빈 문자열(""), 공백 문자(" ")를 허용하지 않을 것이다. @Pattern이나 필드 타입으로 빈 문자열과 공백 문자는 걸러낼 수 있으므로, null 값을 받지 않도록 @NotNull을 기본적으로 사용하였다.

  • accountId(계정 아이디) : 5 ~ 20자의 영소문자, 숫자, 특수기호(-, _)를 사용할 수 있다.
  • accountPassword(계정 비밀번호) : 8 ~ 16자의 영소문자, 영대문자, 숫자, 특수문자(~,!,@,#,$,%)를 사용할 수 있다.
  • email(이메일) : 이메일 형식의 문자열
  • username(사용자명) : 사용자 본명
  • year(태어난 연도) : 입력 가능 범위 - 1924 ~ 2024
  • month(태어난 달) : 입력 가능 범위 - 1 ~ 12
  • day(태어난 날) : 입력 가능 범위 - 1 ~ 31
  • phoneNumber(휴대전화번호) : 11자리로 숫자로 이루어진 문자열
  • nickname(별명) : 2 ~ 20자의 영소문자, 영대문자, 숫자를 사용할 수 있다.

  최대한 제약 조건을 맞추려 했으나 모든 내용을 맞추진 못하여 아래에 따로 적어두고 추후 네이버 카페쪽 구현이 완료되면 해당 제약조건들을 적용해 볼 생각이다.

  • 한글 입력 : 사용자명(username)과 별명(nickname)은 사실 한글을 사용할 수 있어야 한다.
  • 생년월일 : 현재 DTO에는 전체적인 입력 가능 범위만 포괄적으로 설정되어있다. 이부분은 사실 실제 날짜 데이터와 입력 데이터를 비교할 필요가 있다고 생각된다.
  • 휴대전화번호 : 현재 사용 가능한, 사용되는 모든 휴대전화번호 형식을 알진 못하기에 보편적으로 많이 보고 사용되는 010-XXXX-YYYY 형식을 기준으로 제약조건을 설정하였다.
  • 별명 : 별명은 사실 네이버 카페 기본 별명으로 사용하기 위해 추가했기에 네이버 카페 기준으로 제약조건을 확인하고 설정했다.

MemberController

package CloneCoding.NaverCafe.domain.member.controller;

import jakarta.validation.Valid;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {

    private final MemberService memberService;

    @PostMapping("/join")
    public String joinMember(@RequestBody @Valid RequestJoinMember request) {
        log.info("회원 등록 요청");
        return memberService.joinMember(request);
    }

}

 

  클라이언트 요청시 입력 정보를 통해 RequestJoinMember 객체를 생성하는데, 이 때 @Valid를 통해 설정한 제약조건에 대한 검증이 이루어진다.


Member

package CloneCoding.NaverCafe.domain.member;

import jakarta.persistence.*;

@Entity
@Table(name = "MEMBER")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;

    @Column(name = "ACCOUNT_ID", unique = true)
    private String accountId;

    @Column(name = "ACCOUNT_PASSWORD")
    private String accountPassword;

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

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "BIRTHDAY")
    private LocalDate birthday;

    @Column(name = "PHONE_NUMBER", unique = true)
    private String phoneNumber;

    @Column(name = "NICKNAME", unique = true)
    private String nickname;

}

 

  엔티티로 사용되는 Member 클래스의 경우 회원 정보들 중 다른 회원과 중복되면 안되는 정보에 대한 제약조건을 설정하였다. @Column의 unique 속성을 사용해 중복방지 설정을 하였다. 해당 방식은 데이터베이스 테이블의 속성(= 필드, 컴럼)에 유니크(Unique) 제약조건을 걸어주는 방식이다.

  • id : 이미 DB에 관리를 위임해 자동적으로 증가하는 Long 타입 값을 받으므로 따로 중복방지를 설정하지 않았다.
  • accountId : 다른 계정과 구별되는 계정 이름이기에 중복되면 안된다.
  • accountPassword : 계정 비밀번호는 다른 사용자와 같더라도 문제가 되지 않는다. 중복되도 상관 없다.
  • email : 입력하는 이메일은 본인 확인용에 사용되는 이메일이다. 본인 이메일이기만 하면되므로 중복되도 상관 없다.
  • username : 이 세상에 동명인은 많다. 애초에 유일하지 않은 데이터이므로 중복되도 무방하다.
  • birthday : 같은 날에 태어난 사람은 많다. 애초에 유일하지 않은 데이터이므로 중복되도 무방하다.
  • phoneNumber : 휴대전화번호는 중복되지 않는 데이터다 사람마다 다른 번호를 가지기에 중복되면 안 된다.
  • nickname : 계정 아이디와 마찬가지로 사용자를 구별하는 데이터이므로 중복되면 안 된다.

  정리하면 데이터베이스 테이블에서 네이버 회원을 식별키(PK)를 제외하고도 계정 아이디, 이메일, 전화번호, 별명으로 구분할 수 있게 된 것이다.

 

  사실 네이버의 경우 사용자 당 3개의 계정을 가질 수가 있다. 하지만 현재 구현 중인 네이버 계정은 네이버 카페 구현을 하기 위한 초석(?)이기에 일단 사용자 당 1개의 계정을 생성할 수 있다고 가정하고 구현하였다. 이 부분에 대해서도 추후 적용해 볼 것이다.


API TEST

API 테스트 - 회원 가입 정보 검증

 

  계정 아이디를 제약조건에 맞지 않게 입력하니 응답이 Bad Request이다. 다른 필드 값들도 제약조건에 맞지 않게 입력하니 Bad Request 응답이 전달됬다(물론 제약조건에 맞게 필드 값을 입력하면 가입 완료 메시지가 출력된다). 제약조건이 잘 적용된 것을 확인 할 수 있었다. 하지만 이것만으로 사용자는 요청에서 무엇이 잘못되었는지 알 수 없다. 시스템 오류에 대한 내용을 사용자가 알 수 있도록 예외 처리를 할 필요가 있다.


수정 이력

  • 제약조건 및 검증 추가 (2024.04.30)