import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
@Getter
@Setter
@Entity
@Table(name = "members")
public class Member extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer memberId;
@Column(unique = true, nullable = false)
private String email;
@Column(length = 20, nullable = false)
private String password;
//이하생략
}
생성시간인 createdAt는 회원뿐만 아니라 주요 엔티티가 모두 사용하기 때문에
공용으로 코드를 재활용해서 사용하기 위해서 추상클래스로 만들고 extends(상속)받았다.
그리고 SpringApplication에 @EnableJpaAuditing 붙이면 잘 작동한다.
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@CreatedDate
@Column(name= "createdAt", updatable = false)
private LocalDate createdAt;
@LastModifiedDate
@Column(name= "modifiedAt")
private LocalDate modifiedAt;
}
자바 17을 처음 사용해보기 때문에 자바 11에는 없었던 record 키워드를 사용하여 DTO를 만들었다.
record란 불변 데이터 객체로 생성자를 작성하지 않아도 되고 toString, equals, hashCode 메소드에 대한 구현을 자동으로 제공하며 코드가 더 간결해진다.
하단에는 bulder를 사용하여 DTO를 Entity로 변환해주는 코드를 만들었다.
build gradle에 implementation 'org.springframework.boot:spring-boot-starter-validation'를 추가하면 @NotBlank이나, @Email 등을 사용하여 데이터를 검증할 수 있다.
예를들어 비밀번호는 영문, 숫자를 포함한 8자 이상 20자 이하라는 요구사항이 있을 경우, 프론트에서도 검증을 하겠지만 백엔드에서도 검증을 거치면 데이터 품질을 높일 수 있다.
import jakarta.validation.constraints.*;
public record JoinRequest(
@NotNull(message = "이메일은 필수 입력 사항입니다.")
@NotBlank(message = "이메일은 공백일 수 없습니다.")
@Email(message = "올바른 이메일 주소 형식으로 작성해주세요.")
@Size(max = 50, message = "이메일은 50글자 이내로 입력해주세요.")
String email,
@NotNull(message = "비밀번호는 필수 입력 사항입니다.")
@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).{8,20}$",
message = "영문, 숫자를 포함한 8자 이상 20자 이하의 비밀번호를 입력해주세요.")
String password,
//이하생략
) {
public Member toEntity() {
return Member.builder()
.email(email())
.password(password())
.name(name())
...
.build();
}
}
회원가입 이후에 자동 로그인을 하는 경우와 로그인 페이지로 이동시키는 경우가 있는데
자동 로그인의 경우에는 자신의 아이디와 비번에 대해서 까먹게 되는 경우가 다수 발생하고,
로그인시 '아이디 기억하기' 기능을 사용할 수 없게 되므로 로그인 계정의 확인 과정으로서
회원가입이 완료되었다는 정보를 주는 것으로 요구사항이 결정되었다.
따라서 Controller 코드는 간단했고, Dto를 Entity로 바꾸어서 Service로 전달하였다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@PostMapping("/join")
public ResponseEntity<String> join(@RequestBody @Valid JoinRequest joinRequest) {
memberService.createNewMember(joinRequest.toEntity());
return ResponseEntity.ok("회원가입이 성공적으로 완료되었습니다.");
}
서비스에서는 이미 회원가입된 이메일로는 회원가입이 불가한 점을 고려하여
데이터베이스에 email을 한번 확인 한 후에 중복이 있을 경우엔 예외처리를 하였다.
비밀번호는 사용자가 입력한 그대로 데이터베이스에 저장하는 것이 아니라 암호화로 해싱처리를 해야하는데 현재 security 설정을 하지 않았기 때문에 우선 주석처리하였다.
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// private final PasswordEncoder passwordEncoder;
public void createNewMember(Member member) {
// 이메일 중복 체크
if (isEmailAlreadyExists(member.getEmail())) {
throw new BusinessLogicException(ExceptionCode.MEMBER_EMAIL_ALREADY_EXISTS);
}
// String encryptedPassword = passwordEncoder.encode(member.getPassword());
// member.setPassword(encryptedPassword);
memberRepository.save(member);
}
private boolean isEmailAlreadyExists(String email) {
Optional<Member> optionalMember = memberRepository.findByEmail(email);
return optionalMember.isPresent();
}
}
HTTP post, 엔트포인트, json타입의 바디를 작성하여 테스트해보았다.
성공시 상태코드 200 OK와, 완료문구를 보여준다.
동일한 요청을 한번 더 보내보면 이메일 중복으로 인해 요청이 실패되는데 기본 응답메세지로는 원인을 상세하게 알기가 어렵다.
enum ExceptionCode를 작성하고, ErrorResponse class를 만들어서 커스텀 응답메세지를 호출하게끔 변경하여 에러 원인을 정확히 파악할 수 있도록 하였다.