포스트

05.회원 가입 로직

데이터베이스 종류와 ORM

  • 회원정보를 저장하기 위한 데이터베이스는 MySQL 엔진의 데이터베이스를 사용한다
  • 그리고 접근은 Spring Data JPA를 사용한다

회원 가입 로직

  • 회원정보를 통해 인증 인가 작업을 진행하기 때문에 사용자로부터 회원가입을 진행한 뒤 데이터베이스에 회원정보를 저장해야 한다.
회원가입 페이지
1
2
3
4
5
<form action="/joinProc" method="post" name="joinForm">
    <input type="text" name="username" placeholder="Username"/>
    <input type="password" name="password" placeholder="Password"/>
    <input type="submit" value="Join"/>
</form>
JoinDTO
  • form 양식으로 넘어온 데이터를 이동시킬 객체
1
2
3
4
5
6
7
8
9
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class JoinDTO {
  private String username;
  private String password;
}
JoinController
  • 회원가입 로직을 시작하는 컨트롤러
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.service.JoinService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class JoinController {

  @Autowired
  private JoinService joinService;

  @GetMapping("/join")
  public String joinP() {
    return "join";
  }

  @PostMapping("/joinProc")
  public String joinProcess(JoinDTO joinDTO) {
    joinService.joinProcess(joinDTO);
    return "redirect:/login";
  }
}
  • joinP() 회원가입 페이지 호출
  • joinProcess() 회원가입 로직 진행
  • 현재는 편의상 의존성 주입 방식을 필드 주입 방식으로 하였지만, 실무에서는 final 키워드를 달고 생성자 주입방식으로 하는 것을 권장함
JoinService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class JoinService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public void joinProcess(JoinDTO joinDTO) {
      //db에 이미 동일한 username을 가진 회원이 존재하는지?

      UserEntity data = new UserEntity();

      data.setUsername(joinDTO.getUsername());
      data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
      data.setRole("ROLE_USER");

      userRepository.save(data);
    }
}
  • 서비스에서 동일한 아이디 조회하는 로직도 짜도록 해보자
  • setRole()을 통해 강제적으로 역할을 세팅하도록 하고, 나중에 필요하면 ENUM을 만들어서 불러와도 좋고, 로직을 추가해도 좋다.
  • setPassword()의 경우는 단방향 해시 암호화를 하여 저장해야 함으로 bCryptPasswordEncoder를 통해 암호화 하여 저장하도록 하자
UserEntity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Setter
@Getter
public class UserEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  private String username;
  private String password;

  private String role;
}
  • Entity를 만들고 Hibernate 설정을 통해 테이블 생성을 세팅해두자
UserRepository
1
2
3
4
5
6
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Integer> {

}
  • JPA를 사용하기 위해 인터페이스로 만들고 JpaRepository를 상속 받도록 하자
SecurityConfig 접근권한
1
2
3
4
5
6
7
http
          .authorizeHttpRequests((auth) -> auth
                  .requestMatchers("/", "/login", "/loginProc", "/join", "/joinProc").permitAll()
                  .requestMatchers("/admin").hasRole("ADMIN")
                  .requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
                  .anyRequest().authenticated()
          );
  • requestMatchers(“/”, “/login”, “/loginProc”, “/join”, “/joinProc”).permitAll() 를 통해 모두 접근 가능한 경로를 추가하자

회원 중복 검증 및 처리

  • username에 대해서 중복된 가입이 발생하면 서비스에서 아주 치명적인 문제가 발생하기 때문에 백엔드단에서 중복 검증과 중복 방지 로직을 작성해야 한다.
UserEntity Unique 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Setter
@Getter
public class UserEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private int id;

  @Column(unique = true)
  private String username;
  private String password;

  private String role;
}
JoinService 중복 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class JoinService {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private BCryptPasswordEncoder bCryptPasswordEncoder;


  public void joinProcess(JoinDTO joinDTO) {

    //db에 이미 동일한 username을 가진 회원이 존재하는지?
    boolean isUser = userRepository.existsByUsername(joinDTO.getUsername());
    if (isUser) {
        return;
    }

    UserEntity data = new UserEntity();

    data.setUsername(joinDTO.getUsername());
    data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
    data.setRole("ROLE_USER");

    userRepository.save(data);
  }
}
  • existsByUsername()의 경우 개발자가 커스텀하여 메소드를 만들어준다.
UserRepository
1
2
3
4
5
6
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Integer> {
  boolean existsByUsername(String username);
}
front
  • httpXMLRequest 메소드를 통해 백엔드에 미리 구현해둔 API에 이미 존재하는 username인지 검증하는 로직을 추가해야 한다.
  • 또한 정규식 처리를 통해 올바른 형식의 입력값을 받아야 한다
    • 아이디의 자리수
    • 아이디의 특수문자 포함 불가
    • admin과 같은 아이디 사용 불가
    • 비밀번호 자리수
    • 비밀번호 특수문자 포함 필수

트러블 슈팅

  • 난 MSA 연습으로 프로젝트를 구성하였다.
  • MSA이기 때문에 gateway에서 유저 서버를 호출하여 회원가입을 진행하려고 했고, 회원에 시큐리티가 걸려있었다.
  • 또한, 컨트롤러역시 RestController로 설정하였기 때문에 일반적인 Controller로 하여 html 파일로 호출하는 것이 안되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {

  private final UserService userService;
  private ModelAndView mav;
    
  // 회원가입 페이지
  @GetMapping("/join")
  public ModelAndView joinP(){
    ModelAndView mav = new ModelAndView("join");
    return mav;
  }

  // 회원가입
  @PostMapping("/joinProc")
  public ModelAndView joinProccess(JoinDto joinDto){

    userService.joinProccess(joinDto);

    mav = new ModelAndView("login");
    return mav;
  }

}    
  • 문제점1
  • 페이지 이동이 안됨. Controller 처럼 반환값을 String으로 하여 페이지 이동이 안됨.
  • 해결
  • ModelAndView 객체를 이용하여 해결
    • 문제점1-1
    • Circular view path [join]: would dispatch back to the current handler URL [/api/users/join] again. Check your ViewResolver setup! 에러가 발생하였다
    • 해결
    • 이것은 join.html을 만들어서 resource의 static 패키지에 넣어놨는데 파일을 인식하지 못하여서 발생하는 문제였다.
    • 부트에서 따로 html을 인식하게 하는것이 있는 줄은 모르겠는데
    • 내가 찾아본 정보들은 기본적으로 String을 반환타입으로 하여 join.html로 보내면 static 폴더에서 찾아서 연결되는 듯 했다.
    • 하지만 나는 RestController였고 String을 반환타입으로 하면 String이 그대로 반환되었다.
    • ModelAndView를 이용하여 “join.html”로 하여도 되지 않았다.
    • mustache를 의존성에 추가하고, templates 패키지에 join.mustache 파일을 추가 하였다.

  • 문제점2
  • 회원가입 페이지까지는 열렸으나 그 다음에 회원가입 로직이 진행이 되지 않았다.
  • 해결
  • 문제는 회원가입하고 form을 날리는 url을 /joinProc로 해놨는데, /joinProc는 /api/users/joinProc 였으므로 form action 값을 변경하였다.
  • 또한 SecurityConfig에서 requestMatchers에 permitAll()을 해놓은 값에도 requestMatchers(“/”, “/api/users/login”, “/api/users/join” , “/api/users/joinProc”).permitAll()를 추가하였다.
참고 사이트

개발자 유미