회원가입에서 만들고자 하는 Request는 총 4개이다.
1. 이메일 인증코드 보내기
2. 인증코드 확인
3. 프로필 이미지 업로드
4. 회원가입
이 포스팅에서는 이메일 인증코드 보내기, 인증코드 확인까지 다룰 것이다.
1. Entity 클래스 작성
1) Members
package com.betweenourclothes.domain.members;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import javax.persistence.*;
@Getter
@DynamicInsert // 기본값 설정을 위한 어노테이션
@NoArgsConstructor
@Entity
@Table(name = "members") // DB에 테이블 이름이 members로 형성됨
public class Members {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //auto_increment를 위한 설정
private Long id;
@Column(length=45, nullable=false)
private String email;
@Column(length=15, nullable=false)
private String password;
@Column(length=10, nullable=false)
private String name;
@Column(length=20, nullable=false)
private String nickname;
@Column(length=11, nullable=false)
private String phone;
@Column(columnDefinition = "varchar(300) default '이미지경로'")
private String image;
@Column(columnDefinition = "varchar(15) default 'USER'")
private String role;
@Builder
public Members(String email, String password, String name, String nickname, String phone, String image){
this.email = email;
this.password = password;
this.nickname = nickname;
this.name = name;
this.phone = phone;
this.image = image;
}
}
Members에는 사용자 정보를 저장한다.
프로그램이 자동으로 부여하는 회원번호, 이메일, 비밀번호, 이름, 전화번호, 프로필 이미지, 역할이 존재한다.
- 프로필 이미지는 경로로 저장한다. 사용자가 프로필 이미지를 설정하지 않는 경우를 대비해 default 이미지를 기본값으로 저장한다.
- 역할은 일단 'USER' 하나라 기본값으로 설정했다. 하지만 관리자가 추가될 수 있기 때문에 enum을 만들어두었는데 db에 저장될 때는 문자열로 저장되게 만들었다.
Default 값을 저장하는 방법은 다음과 같다.
1. @Column(columnDefintion= "")을 이용해 테이블을 만들 때 해당 컬럼이 기본값을 지닌 필드라는 것을 알려준다.
2. Entitiy 클래스에 @DynamicInsert를 붙인다. 이 어노테이션을 붙이면 Insert구문이 만들어질 때 null이 아닌 값들만 포함하게 된다.
3. 데이터를 삽입할 때 값을 넘겨주지 않는다. 그러면 자동으로 null인 필드에 기본값이 채워지게 된다.
2) Email
package com.betweenourclothes.domain.members;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Getter
@NoArgsConstructor
@Entity
@Table(name = "authentication")
public class Email {
@Id
private String email;
@Column(length=6)
private String code;
private String status;
@Builder
public Email(String email, String code, String status){
this.email = email;
this.code = code;
this.status = status;
}
public void update(String status){
this.status = status;
}
}
Email에서는 이메일 인증코드를 받고 나서 회원가입이 완료될 때까지의 정보를 임시로 보관한다.
사용자 이메일, 인증코드, 인증상태를 가지고 있다.
Repository Mapping
두 클래스의 객체는 Repository 인터페이스를 통해 DB와 연결된다.
package com.betweenourclothes.domain.members;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MembersRepository extends JpaRepository<Members, Long> { // 넣을 객체, ID 타입
Optional<Members> findByEmail(String email);
Optional<Members> findByNickname(String nickname);
}
package com.betweenourclothes.domain.members;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface EmailRepository extends JpaRepository<Email, String> {
Optional<Email> findByEmail(String email);
}
Optional<T> 클래스는 T 객체를 한번 더 감싸는 wrapper 클래스로 NullPointerException을 방지가 가능하다.
find메서드의 결과값이 없을 시 null이 리턴되는게 아니기 때문에 NullPointerException까지 이어질 가능성을 막아준다.
2. AuthService
package com.betweenourclothes.service.auth;
import com.betweenourclothes.web.dto.AuthSignUpRequestDto;
import com.betweenourclothes.web.dto.AuthEmailRequestDto;
public interface AuthService {
void sendMail(AuthEmailRequestDto receiver);
void checkAuthCode(AuthEmailRequestDto receiver);
}
package com.betweenourclothes.service.auth;
import com.betweenourclothes.domain.members.Email;
import com.betweenourclothes.domain.members.EmailRepository;
import com.betweenourclothes.domain.members.MembersRepository;
import com.betweenourclothes.exception.ErrorCode;
import com.betweenourclothes.exception.customException.*;
import com.betweenourclothes.web.dto.AuthSignUpRequestDto;
import com.betweenourclothes.web.dto.AuthEmailRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
@RequiredArgsConstructor
@Service
public class AuthServiceImpl implements AuthService{
private final MembersRepository membersRepository;
private final EmailRepository emailRepository;
private final JavaMailSender sender;
@Transactional
@Override
public void sendMail(AuthEmailRequestDto requestDto){
if(membersRepository.findByEmail(requestDto.getEmail()).isPresent()){
throw new DuplicatedDataException(ErrorCode.DUPLICATE_EMAIL);
}
MimeMessage message;
try {
message = createMessage(requestDto);
}catch(Exception e){
throw new MailMsgCreationException(ErrorCode.MAIL_MSG_CREATION_ERROR);
}
try{
System.out.println(message);
sender.send(message);
}catch(Exception e){
e.printStackTrace();
throw new MailRequestException(ErrorCode.MAIL_REQUEST_ERROR);
}
emailRepository.save(requestDto.toEntity());
}
@Transactional
@Override
public void checkAuthCode(AuthEmailRequestDto receiver) {
Email user = emailRepository.findByEmail(receiver.getEmail()).orElseThrow(()->new UserNotFoundException(ErrorCode.USER_NOT_FOUND));
System.out.println("existed code: " + user.getCode());
System.out.println("requested code: " + receiver.getCode());
if(user.getCode().equals(receiver.getCode())){
receiver.setStatusAccepted();
user.update(receiver.getStatus());
return;
}
throw new NotAuthenticatedException(ErrorCode.NOT_AUTHENTICATED);
}
private MimeMessage createMessage(AuthEmailRequestDto requestDto) throws MessagingException, UnsupportedEncodingException {
MimeMessage msg = sender.createMimeMessage();
msg.addRecipients(Message.RecipientType.TO, requestDto.getEmail());
msg.setSubject("너와 내 옷 사이 회원가입 인증번호가 도착했습니다.");
String body = "<div style='margin:100px;'>\n" +
"<h1> 안녕하세요. 너와 내 옷 사이입니다.</h1>\n" +
"<br>\n" +
"<p>아래 인증코드를 회원가입 창으로 돌아가 입력해주세요.<p>\n" +
"<p>감사합니다!<p>\n" +
"<br>\n" +
"<div align='center' style='border:1px solid black; font-family:verdana';>\n" +
"<br>\n" +
"<div style='font-size:130%'>인증코드 : \n" +
"<strong>"+ requestDto.getCode() +"\n" +
"</strong><div><br/></div>";
msg.setText(body, "utf-8", "html");
msg.setFrom(new InternetAddress("gunsong2@gmail.com", "너와 내 옷 사이"));
return msg;
}
}
1) sendMail
@Transactional //db랑 연결되는 서비스는 Transactional을 붙여서 데이터를 보호한다
@Override
public void sendMail(AuthEmailRequestDto requestDto){
// 1. 가입된 이메일이 있는지 체크
if(membersRepository.findByEmail(requestDto.getEmail()).isPresent()){
throw new DuplicatedDataException(ErrorCode.DUPLICATE_EMAIL);
}
// 2. 인증코드 메시지 만들기
MimeMessage message;
try {
message = createMessage(requestDto);
}catch(Exception e){
throw new MailMsgCreationException(ErrorCode.MAIL_MSG_CREATION_ERROR);
}
// 3. 전송
try{
//System.out.println(message);
sender.send(message);
}catch(Exception e){
e.printStackTrace();
throw new MailRequestException(ErrorCode.MAIL_REQUEST_ERROR);
}
// 4. 이메일-인증코드-상태 임시저장
emailRepository.save(requestDto.toEntity());
}
가입된 이메일이 있는지 체크
인증메일을 보내달라고 요청한 메일이 Members 테이블에 이미 존재하는지 확인하고,
존재할 시 Exception을 던진다
메일 전송
메일 전송은 Spring에서 제공하는 API인 JavaMailSender를 이용했다.
메일 전송을 위해서는 application.properties에 관련 조건들을 명시해주어야 한다. (이전 포스팅 참조)
JavaMailSender는 MimeMessage를 정의해 본문이 HTML로 이루어진 메일을 발송할 수 있다.
2) checkCode
@Transactional
@Override
public void checkAuthCode(AuthEmailRequestDto receiver) {
// 인증코드를 보낸 이메일이 있는지 확인
Email user = emailRepository.findByEmail(receiver.getEmail()).orElseThrow(()->new UserNotFoundException(ErrorCode.USER_NOT_FOUND));
//System.out.println("existed code: " + user.getCode());
//System.out.println("requested code: " + receiver.getCode());
// 코드가 일치한다면,
if(user.getCode().equals(receiver.getCode())){
receiver.setStatusAccepted(); // status 'y'로 업데이트
user.update(receiver.getStatus()); // status 테이블에 업데이트
return;
}
throw new NotAuthenticatedException(ErrorCode.NOT_AUTHENTICATED); //일치하지 않으면 Exception 던지기
}
3. Dto
package com.betweenourclothes.web.dto;
import com.betweenourclothes.domain.members.Email;
import lombok.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Random;
@Getter
public class AuthEmailRequestDto {
@NotBlank(message = "이메일을 입력하세요.")
@javax.validation.constraints.Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
@NotNull
private String code;
@NotNull
private String status;
public Email toEntity(){
return Email.builder().email(email).code(code).status(status).build();
}
private String createAuthCode(){
int leftLimit = 48; //아스키코드 숫자0
int rightLimit = 122; //아스키코드 영문자z
int length = 6; //인증코드 길이
Random random = new Random();
String key = random.ints(leftLimit, rightLimit+1)
.filter(i -> (i<=57 || i>=65) && (i <=90 || i>=97)) //영문자, 숫자가 아닌 값 제외
.limit(length)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
return key;
}
//Status 업데이트를 위함
private String booleanConverter(boolean value){
if(value){
return "Y";
} else{
return "N";
}
}
//Status 업데이트를 위함
public void setStatusAccepted(){
this.status = "Y";
}
// 인증코드, 인증상태는 객체가 만들어질 때 생성
public AuthEmailRequestDto(){
this.code = createAuthCode();
this.status = booleanConverter(false);
}
@Builder
public AuthEmailRequestDto(String email){
this.email = email;
this.code = createAuthCode();
this.status = booleanConverter(false);
}
@Builder
public AuthEmailRequestDto(String email, String code){
this.email = email;
this.code = code;
this.status = booleanConverter(false);
}
}
4. AuthApiController
package com.betweenourclothes.web;
import com.betweenourclothes.exception.ErrorCode;
import com.betweenourclothes.exception.customException.RequestFormatException;
import com.betweenourclothes.service.auth.AuthServiceImpl;
import com.betweenourclothes.web.dto.AuthEmailRequestDto;
import com.betweenourclothes.web.dto.AuthSignUpRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/auth")
public class AuthApiController {
private final AuthServiceImpl membersService;
@PostMapping("/sign-up/email")
public ResponseEntity<String> sendEmail(@RequestBody @Valid AuthEmailRequestDto requestDto) throws Exception{
membersService.sendMail(requestDto);
return new ResponseEntity<>("이메일 전송 성공", HttpStatus.OK);
}
@PostMapping("/sign-up/code")
public ResponseEntity<String> checkAuthCode(@RequestBody @Valid AuthEmailRequestDto requestDto) throws Exception{
membersService.checkAuthCode(requestDto);
return new ResponseEntity<>("인증 성공", HttpStatus.OK);
}
}
결과
'백엔드 > Spring' 카테고리의 다른 글
JWT 토큰을 이용한 로그인 구현 (1) (0) | 2022.10.13 |
---|---|
Spring Security 동작과정 (0) | 2022.10.13 |
JWT (Json Web Tokens) (0) | 2022.10.12 |
이메일 인증 회원가입 구현하기 (1): 프로젝트 구성 (0) | 2022.10.11 |
TDD와 단위테스트 (0) | 2022.10.03 |
댓글