목표
1. 회원가입 페이지에서 필요 정보를 입력한다 (이름, 별명, 아이디, 비밀번호).
2. 별명과 아이디는 Unique값이기 때문에 입력과 동시에 중복체크를 하여 사용자에게 보여준다.
3. 입력 후 가입하기 버튼을 입력하면 비밀번호를 암호화 하여 데이터베이스에 저장한다.
4. 회원가입이 정상적으로 됐으면 메인페이지로 이동하고 실패하면 다시 회원가입 페이지로 돌아가서 오류를 출력한다.
Version
Spring boots : 3.1.4
java : 17
Database : H2
gradle
코드 및 설명
1. Database - h2
먼저 회원을 저장 할 데이터베이스가 필요하기 Member라는 table을 만들었다. 내가 저장할 정보는 유저 고유 번호, 이름, 별명, 아이디, 비밀번호이다. Long타입의 userId를 PK로 설정했고 이 값은 자동 증가하게 설정했다. 데이터 베이스는 H2 Database를 사용했다.
비밀번호의 길이는 암호화해서 들어가기 때문에 넉넉하게 100을 잡아줬다.
2-1. Home.html
Home.html은 기본 메인 페이지이며 나중에 만들 기능인 식단 추천해주는 기능과 자유게시판을 들어갈 수 있는 네비바를 추가했다. 그리고 로그인을 했을 때 인증이 되어있으면 로그아웃 버튼을, 로그인이 안되어있으면 로그인과 회원가입 버튼을 랜더링하도록 설정했다.
로그인을 했을 때는 로그인과 로그아웃 버튼이 사라지고 로그아웃 버튼이 나온다. 로그인 과정에 대해서는 다음 포스팅에서 작성 할 예정이다.
<!doctype html>
<html lang="en" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Home</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
</head>
<body>
<header class="p-3 text-bg-primary" style="background-color: #a7ca5d !important;">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
<use xlink:href="#bootstrap" />
</svg>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><a href="/dietrecommendation" class="nav-link px-2 text-secondary">식단 추천</a></li>
<li><a href="/freeboard" class="nav-link px-2 text-white">자유게시판</a></li>
<li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
<li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
<li><a href="#" class="nav-link px-2 text-white">About</a></li>
</ul>
<div sec:authorize="isAuthenticated()" style="display: flex; justify-content: center;
align-items: center;" >
<div sec:authentication="name" class="me-3"></div>
<button type="button" class="btn btn-outline-light me-2"
onclick="location.href='/signout.do'">로그아웃</button>
</div>
<div class="text-end" th:if="${!isSignedIn}">
<button type="button" class="btn btn-outline-light me-1"
onclick="location.href='/signin'">로그인</button>
<button type="button" class="btn btn-outline-light me-1"
onclick="location.href='/signup'">회원가입</button>
</div>
</div>
</div>
</header>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
crossorigin="anonymous"></script>
</body>
</html>
2-2. signup.html
회원가입 폼은 그냥 기본 정보만 받을 수 있게 기본으로 짰다..ㅋㅋㅋㅋ 현재 리액트도 공부를 하고 있어서 나중에 더 이쁘게 변경해보려고 한다. 닉네임과 아이디는 중복이 안되기 때문에 값을 입력하면 바로 중복처리가 되도록 했다.
3.Member - entity
엔티티 객체는 처음에 봤던 sql table의 컬럼과 같이 맞춰줬고, @GeneratedValue 어노테이션을 사용하여 자동 증가를 표시해줬다.
@NoArgsConstructor
@Getter
@Table(name = "member")
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userid")
private Long userId;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String nickname;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Builder
public Member(String name, String nickname, String username, String password) {
this.name = name;
this.nickname = nickname;
this.username = username;
this.password = password;
}
public static Member createMember(MemberFormDTO memberFormDTO, PasswordEncoder passwordEncoder) {
Member member = Member.builder()
.name(memberFormDTO.getName())
.nickname(memberFormDTO.getNickname())
.username(memberFormDTO.getUsername())
.password(passwordEncoder.encode(memberFormDTO.getPassword()))
.build();
return member;
}
}
제일 아래 createMember에서 보이는 passwordEncoder는 회원가입을 할 때 개인의 비밀번호는 관리자도 알면 안되기 때문에 암호화를 해서 저장하기 위해 만들었다. 해당 로직은 MemberController에서 signup 폼에서 넘어온 정보를 MemberService로 넘기기 전 암호화를 할 때 사용한다.
5. MemberDTO
회원가입 폼에서 받은 정보를 바로 보내는게 아니라 DTO객체 안에 담아서 보내는 이유는 간단하게 비밀번호를 누가 채가지 않게 DTO객체를 사용해서 보내고 내가 필요할 때 꺼내서 쓰면 된다.
DTO에 대해서도 더 자세하게 설명해서 글을 올려볼 것이다.
@NotBlank에 있는 message 값을 controller에서 에러메세지로 반환할 것이다.
@NoArgsConstructor
@Getter
@Setter
public class MemberFormDTO {
@NotBlank(message = "이름은 필수 입력값입니다.")
private String name;
@NotBlank(message = "별명은 필수 입력값입니다.")
private String nickname;
@NotBlank(message = "아이디는 필수 입력값입니다.")
private String username;
@NotBlank(message = "비밀번호는 필수 입력값입니다.")
private String password;
@Builder
public MemberFormDTO(String name, String nickname, String username, String password) {
this.name = name;
this.nickname = nickname;
this.username = username;
this.password = password;
}
}
6. MemberController
/signup url에서 Post방식으로 넘겼을 때 받는 어노테이션이다.
먼저 사용한 어노테이션들을 보자면
@Valid : 폼에서 컨트롤러로 들어온 DTO 정보가 비어있을 때 DTO클래스의 NotBlank에 입력된 메세지가 반환된다.
NotBlank에 입력된 메세지를 Model객체의 model.addAttribute에 담아서 view에 key, value형식으로 보내준다.
@ModelAttribute : memberFormDTO객체를 자동으로 생성해준다.
@PostMapping("/signup")
public String postSignUp(@Validated @ModelAttribute("memberFormDTO") MemberFormDTO memberFormDTO, BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
return "member/signup";
}
Member member = Member.createMember(memberFormDTO, passwordEncoder);
memberService.saveMember(member);
return "redirect:/";
}
7. MemberServiceImple.java
다음 포스팅에서 볼 내용이지만 로그인을 하기 위해 UserDetailsService 를 implement했다. 그래서 loadUserByUsername메소드가 있는것이다.
MemberRepository에 인자를 넘겨주고 값을 받아와서 처리하는 서비스클래스이다. Optional로 받아준 이유는 값이 없을 때 null이 반환되는데 null을 받아주기 위해서 사용했다. 보통 NullPointerException을 방지하기 위해 사용한다.
@Transactional
public Member saveMember(Member member) {
duplicateDuplicateMember(member);
return memberRepository.save(member);
}
public void duplicateDuplicateMember(Member member) {
Optional<Member> findMember = memberRepository.findByUsernameAndNickname(member.getUsername(), member.getNickname());
if(findMember.isPresent()) {
throw new IllegalStateException("이미 존재하는 정보입니다.");
}
}
public boolean usernameDuplicateCheck(String username) {
return memberRepository.existsByUsername(username);
}
public boolean nicknameDuplicateCheck(String nickname) {
return memberRepository.existsByNickname(nickname);
}
8. MemberRepository.java
JpaRepository를 상속받아 사용하기 때문에 메서드의 이름에 맞춰 Hibernate가 쿼리를 자동으로 구성하여 DB에서 정보를 가져와준다.
findByUsernameOrNickname 메서드는 username과 nickname중에 하나라도 중복이 있으면 해당 member값을 반환한다. 둘다
8. Test Code
테스트 코드는 내가 생각한 오류가 날법한 상황을 가정하고 짰다.
회원가입이 잘 되는지
중복 체크가 동작 하는지
username 또는 nickname중 한곳에서라도 중복이 있는걸 걸러낼 수 있는지 를 체크했다.
@SpringBootTest
public class MemberServiceImplTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private MemberServiceImpl memberServiceImpl;
@BeforeEach
void addMember() {
Member member1 = new Member("z", "z", "z", "z");
memberServiceImpl.saveMember(member1);
}
@AfterEach
void deleteRepository() {
memberRepository.deleteAll();
}
@Test
void testSaveMember() {
Member member1 = new Member("t", "t", "t", "t");
Member member = memberServiceImpl.saveMember(member1);
Long mem1Check = memberRepository.findByUsernameOrNickname(member1.getUsername(), member1.getNickname()).get().getUserId();
Long checkUid = member.getUserId();
assertThat(mem1Check).isEqualTo(checkUid);
}
@Test
void 중복없는거체크() {
Member member1 = new Member("3", "3", "3", "3");
Optional<Member> checkValidate = memberRepository.findByUsernameOrNickname(member1.getUsername(), member1.getNickname());
assertThat(checkValidate).isEqualTo(Optional.empty());
}
@Test
void 둘중하나라도중복있는거체크() {
Member member1 = new Member("a", "z", "a", "a");
Optional<Member> checkValidate = memberRepository.findByUsernameOrNickname(member1.getUsername(), member1.getNickname());
assertThat(member1.getNickname()).isEqualTo(checkValidate.get().getNickname());
Member member2 = new Member("y", "y", "z", "y");
Optional<Member> checkValidate2 = memberRepository.findByUsernameOrNickname(member2.getUsername(), member2.getNickname());
assertThat(member2.getUsername()).isEqualTo(checkValidate2.get().getUsername());
}
}
테스트 코드 모두 통과 완료!
이렇게 해서 회원가입 로직이 완성됐다. 데이터 베이스를 보니 비밀번호도 암호와 되서 잘 들어가는 것을 볼 수 있다.
만들고 싶은 서비스를 스스로 공부하고 만들어 보면서 기록하는 개인 공부 블로그입니다.
내용 중 최적화가 가능한 부분 혹은 궁금한 점은 언제든지 댓글로 남겨주세요🧐
'Projects > 식단 짜주는 웹' 카테고리의 다른 글
[Spring/식단 추천 API] 게시판 글 수정 기능 구현 - 6 (0) | 2023.11.18 |
---|---|
[Spring/식단 추천 API] 게시판 글 상세보기 기능 구현 - 5 (0) | 2023.11.16 |
[Spring/식단 추천 API] 게시판 글 작성 기능 구현 - 4 (1) | 2023.11.16 |
[Spring/식단 추천 API] 게시판 글 목록 페이지 구현 - 3 (0) | 2023.11.16 |
[Spring/식단 추천 API] 로그인 기능 구현 - 2 (1) | 2023.11.11 |