1. 회원가입
회원가입 코드는 아래 브랜치 참고
https://github.com/happyprogfrog/healthnewbie/tree/002-SIGN-UP
2. healthnewbie-mysql.sh
최신 버전의 mysql 이미지를 컨테이너로 실행
docker run -d \
--name healthnewbie-mysql \
-e MYSQL_ROOT_PASSWORD="healthnewbie" \
-e MYSQL_USER="healthnewbie" \
-e MYSQL_PASSWORD="healthnewbie" \
-e MYSQL_DATABASE="healthnewbie" \
-p 3306:3306 \
mysql:latest
3. 예외 관련 코드
ErrorCode
package me.progfrog.healthnewbie.user.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
ALREADY_REGISTER_USER(HttpStatus.BAD_REQUEST, "이미 가입된 회원입니다."),
NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "일치하는 회원이 없습니다."),
ALREADY_VERIFY(HttpStatus.BAD_REQUEST, "이미 인증이 완료된 회원입니다."),
WRONG_VERIFICATION(HttpStatus.BAD_REQUEST, "잘못된 인증 시도입니다."),
EXPIRE_CODE(HttpStatus.BAD_REQUEST, "인증 시간이 만료되었습니다.");
private final HttpStatus httpStatus;
private final String detail;
}
CustomException
package me.progfrog.healthnewbie.user.exception;
import lombok.Getter;
@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
public CustomException(ErrorCode errorCode) {
super(errorCode.getDetail());
this.errorCode = errorCode;
}
}
ExceptionController
package me.progfrog.healthnewbie.user.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
@Slf4j
public class ExceptionController {
@ExceptionHandler({
CustomException.class
})
public ResponseEntity<ExceptionResponse> customRequestException(final CustomException c) {
log.warn("API Exception: {}", c.getErrorCode());
return ResponseEntity.badRequest().body(new ExceptionResponse(c.getMessage(), c.getErrorCode()));
}
@Getter
@ToString
@AllArgsConstructor
public static class ExceptionResponse {
private String message;
private ErrorCode errorCode;
}
}
4. CustomerRepository
- 이메일로 Customer를 찾을 수 있도록 메서드 추가
package me.progfrog.healthnewbie.user.domain.repository;
import me.progfrog.healthnewbie.user.domain.model.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByEmail(String email);
}
5. ServerProperties
- url과 port는 변경될 수 있는 정보이고, 다른 곳에서도 많이 사용될 것 같아서 @ConfigurationProperties 애노테이션을 사용
- 이 객체를 스프링 빈으로 등록하고, 애플리케이션에서 필요한 곳이 있으면 해당 빈을 주입하여 사용할 예정!
server.url=http://localhost
server.port = 8081
package me.progfrog.healthnewbie.user.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("server")
@Getter
@Setter
public class ServerProperties {
private String url;
private String port;
}
6. 이메일 검증 핵심 로직 구현
build.gradle
- 이메일 검증에 사용할 무작위한 문자열을 생성하기 위해 의존성 추가
- 아래 예제 메서드는 길이가 10이면서 알파벳과 숫자를 모두 포함하는 무작위한 문자열을 생성하여 반환한다.
implementation 'org.apache.commons:commons-lang3:3.12.0'
private String getRandomCode() {
return RandomStringUtils.random(10, true, true);
}
SignUpApplication
package me.progfrog.healthnewbie.user.application;
import lombok.RequiredArgsConstructor;
import me.progfrog.healthnewbie.user.client.MailgunClient;
import me.progfrog.healthnewbie.user.client.mailgun.SendMailForm;
import me.progfrog.healthnewbie.user.config.ServerProperties;
import me.progfrog.healthnewbie.user.domain.dto.auth.SignUpForm;
import me.progfrog.healthnewbie.user.domain.model.Customer;
import me.progfrog.healthnewbie.user.exception.CustomException;
import me.progfrog.healthnewbie.user.service.SignUpCustomerService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import static me.progfrog.healthnewbie.user.exception.ErrorCode.ALREADY_REGISTER_USER;
@Service
@RequiredArgsConstructor
public class SignUpApplication {
private final ServerProperties serverProperties;
private final MailgunClient mailgunClient;
private final SignUpCustomerService signUpCustomerService;
private final static int DAYS_TO_ADD = 1;
private final static String SENDER = "healthnewbie@email.com";
private final static String SUBJECT = "please verify your account!";
public void verifyCustomer(String email, String code) {
signUpCustomerService.verifyEmail(email, code);
}
public void signUpCustomer(SignUpForm signUpForm) {
if (signUpCustomerService.isEmailExist(signUpForm.email())) {
throw new CustomException(ALREADY_REGISTER_USER);
} else {
Customer customer = signUpCustomerService.signUp(signUpForm);
String verificationCode = getRandomCode();
SendMailForm sendMailForm = SendMailForm.builder()
.from(SENDER)
.to(customer.getEmail())
.subject(SUBJECT)
.text(getVerificationEmailHtmlContent(customer.getEmail(), customer.getNickname(), verificationCode))
.build();
mailgunClient.sendEmail(sendMailForm);
signUpCustomerService.changeCustomerVerificationInfo(customer.getId(), verificationCode, DAYS_TO_ADD);
}
}
private String getRandomCode() {
return RandomStringUtils.random(10, true, true);
}
private String getVerificationEmailHtmlContent(String email, String nickname, String code) {
return """
Hello, %s! Please Copy&Paste the link below for verification.
%s?email=%s&code=%s
""".formatted(nickname, getUrl(), email, code);
}
private String getUrl() {
// http://localhost:8081/signup/verify/customer
return serverProperties.getUrl() + ":" + serverProperties.getPort() + "/signup/verify/customer";
}
}
SignUpCustomerService
package me.progfrog.healthnewbie.user.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.healthnewbie.user.domain.dto.auth.SignUpForm;
import me.progfrog.healthnewbie.user.domain.model.Customer;
import me.progfrog.healthnewbie.user.domain.repository.CustomerRepository;
import me.progfrog.healthnewbie.user.exception.CustomException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Optional;
import static me.progfrog.healthnewbie.user.exception.ErrorCode.*;
@Service
@RequiredArgsConstructor
public class SignUpCustomerService {
private final BCryptPasswordEncoder encoder;
private final CustomerRepository customerRepository;
public Customer signUp(SignUpForm signUpForm) {
return customerRepository.save(signUpForm.toEntity(signUpForm, encoder));
}
public boolean isEmailExist(String email) {
return customerRepository.findByEmail(email.toLowerCase(Locale.ROOT))
.isPresent();
}
@Transactional
public void verifyEmail(String email, String code) {
Customer customer = customerRepository.findByEmail(email.toLowerCase(Locale.ROOT))
.orElseThrow(() -> new CustomException(NOT_FOUND_USER));
if (customer.isVerify()) {
throw new CustomException(ALREADY_VERIFY);
} else if (!customer.getVerificationCode().equals(code)) {
throw new CustomException(WRONG_VERIFICATION);
} else if (customer.getVerifyExpiredAt().isBefore(LocalDateTime.now())) {
throw new CustomException(EXPIRE_CODE);
}
customer.updateVerify(true);
}
@Transactional
public void changeCustomerVerificationInfo(Long customerId, String verificationCode, int daysToAdd) {
Optional<Customer> customerOptional = customerRepository.findById(customerId);
if (customerOptional.isEmpty()) {
throw new CustomException(NOT_FOUND_USER);
}
Customer customer = customerOptional.get();
customer.updateVerificationInfo(verificationCode, LocalDateTime.now().plusDays(daysToAdd));
}
}
SignUpController
package me.progfrog.healthnewbie.user.controller;
import lombok.RequiredArgsConstructor;
import me.progfrog.healthnewbie.user.application.SignUpApplication;
import me.progfrog.healthnewbie.user.domain.dto.auth.SignUpForm;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/signup")
@RequiredArgsConstructor
public class SignUpController {
private final SignUpApplication signUpApplication;
@PostMapping
public ResponseEntity<String> customerSignUp(@RequestBody SignUpForm signUpForm) {
signUpApplication.signUpCustomer(signUpForm);
return ResponseEntity.ok("회원 가입용 인증 메일이 발송되었습니다.");
}
@GetMapping("/verify/customer")
public ResponseEntity<String> verifyCustomer(@RequestParam("email") String email, @RequestParam("code") String code) {
signUpApplication.verifyCustomer(email, code);
return ResponseEntity.ok("인증이 완료되었습니다.");
}
}
7. 마무리
http://localhost:8081/swagger-ui/index.html#
/signup API를 호출해서, 이메일이 발송되는 지 확인
이때 작성하는 email은 mailgun에 별도로 등록한 이메일이여야 한다.


is_verify가 1이 되는 것을 확인할 수 있다!
다음 시간은 로그인하는 부분을 구현해보도록 하자.
반응형