2024. 4. 28. 19:18ㆍProject/Naver Cafe
먼저 이전에 작성한 ERD를 바탕으로 네이버 회원 가입(등록)에 대한 구현을 해보고자 한다. 일단 기능에 중점을 두고 개발을 한 뒤에 제약조건이나 검증에 대한 부분을 추가적으로 고민하고 구현하도록 할 것 이다.
목차
기본 구성
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 테스트를 진행
테스트 결과 데이터베이스에도 정상적으로 회원 정보가 저장되었고 지정한 시스템 메시지가 정상적으로 반환되는 것도 확인 하였다.
제약조건 및 검증
스펙 상으로 회원 가입(등록)시 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
계정 아이디를 제약조건에 맞지 않게 입력하니 응답이 Bad Request이다. 다른 필드 값들도 제약조건에 맞지 않게 입력하니 Bad Request 응답이 전달됬다(물론 제약조건에 맞게 필드 값을 입력하면 가입 완료 메시지가 출력된다). 제약조건이 잘 적용된 것을 확인 할 수 있었다. 하지만 이것만으로 사용자는 요청에서 무엇이 잘못되었는지 알 수 없다. 시스템 오류에 대한 내용을 사용자가 알 수 있도록 예외 처리를 할 필요가 있다.
수정 이력
- 제약조건 및 검증 추가 (2024.04.30)
'Project > Naver Cafe' 카테고리의 다른 글
[클론 코딩] 네이버 카페 - 네이버 로그인 (0) | 2024.05.10 |
---|---|
[클론 코딩] 네이버 카페 - 네이버 회원 정보 조회(읽기) (0) | 2024.05.03 |
[클론 코딩] 네이버 카페 - 예외 처리 (0) | 2024.05.02 |
[클론 코딩] 네이버 카페 - 프로젝트 설정, 데이터베이스 연결 설정 (0) | 2024.04.27 |
[클론 코딩] 네이버 카페 - 서비스 분석 (0) | 2024.04.23 |