[점프 투 스프링] 회원가입 구현 (with. Spring Security)

Honam Kim·2024년 12월 11일
post-thumbnail

깃허브 : https://github.com/tigerpoint123/jsb-20241210

스프링 시큐리티를 통해 회원가입 기능을 만들어 보아요.

엔티티 생성

@Getter
@Setter
@Entity
public class SiteUser { 
// SiteUser로 한 이유 : 스프링 시큐리티에 이미 User 클래스가 존재. 실습땐 이름을 달리함.
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true) //유일한 값만 저장 (중복 방지)
    private String username;

    private String password;

    @Column(unique = true)
    private String email;
}

Repository, Service 생성

public interface UserRepository extends JpaRepository<SiteUser, Long> {
}
@RequiredArgsConstructor // 생성자 만들어줌.
@Service
public class UserService {

    private final UserRepository userRepository;

// 회원 생성
    public SiteUser create(String username, String email, String password) {
        SiteUser user = new SiteUser();
        user.setUsername(username);
        user.setEmail(email);
        //빈으로 등록한 Password Encoder 객체 주입받아 사용
        user.setPassword(passwordEncoder.encode(password));
        this.userRepository.save(user);
        return user;
    }
}


@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 // (중략)
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

PasswordEncoder(BCryptPasswordEncoder 인터페이스)객체를 빈으로 등록해서,

BCryptPasswordEncoder 객체를 직접 생성하지 않고 빈의 객체를 주입받아 사용함.

회원가입을 위한 정보를 담는 폼을 생성함.

@Getter
@Setter
public class UserCreateForm {
    @Size(min = 3, max = 25)
    @NotEmpty(message = "사용자ID는 필수항목입니다.")
    private String username;

    @NotEmpty(message = "비밀번호는 필수항목입니다.")
    private String password1;

    @NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
    private String password2;

    @NotEmpty(message = "이메일은 필수항목입니다.")
    @Email
    private String email;
}

다음은 회원 가입을 위한 컨트롤러를 생성

@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping("/signup") // 회원가입 페이지 불러오기
    public String signup(UserCreateForm userCreateForm) {
        return "signup_form";
    }

    @PostMapping("/signup") // 가입 로직 실행하기
    public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) { // 가입 중 에러 발생시 원래 페이지로 돌아가기
            return "signup_form";
        }

// 비밀번호 == 비밀번호 확인
        if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
            bindingResult.rejectValue("password2", "passwordInCorrect", 
                    "2개의 패스워드가 일치하지 않습니다.");
            return "signup_form";
        }

        userService.create(userCreateForm.getUsername(), 
                userCreateForm.getEmail(), userCreateForm.getPassword1());

        return "redirect:/";
    }
}

URL이 get으로 요청되면 회원 가입 페이지로 이동이 되고, post 요청이 되면 실제 가입을 진행해요

중간에 비밀번호 + 비밀번호확인 정보를 비교하여 같은지 다른지를 확인할 수 있습니다.

rejectValue의 매개변수는 각각 bindingResult.rejectValue(필드명, 오류 코드, 오류 메시지)를 의미해요.

이제 회원가입 페이지가 필요하겠쥬 ?

<div layout:fragment="content" class="container my-3">
    <div class="my-3 border-bottom">
        <div>
            <h4>회원가입</h4>
        </div>
    </div>
    <form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <div class="mb-3">
            <label for="username" class="form-label">사용자ID</label>
            <input type="text" th:field="*{username}" class="form-control">
        </div>
        <div class="mb-3">
            <label for="password1" class="form-label">비밀번호</label>
            <input type="password" th:field="*{password1}" class="form-control">
        </div>
        <div class="mb-3">
            <label for="password2" class="form-label">비밀번호 확인</label>
            <input type="password" th:field="*{password2}" class="form-control">
        </div>
        <div class="mb-3">
            <label for="email" class="form-label">이메일</label>
            <input type="email" th:field="*{email}" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">회원가입</button>
    </form>
</div>

전체를 form 태그로 감싸고, button의 submit을 post 방식으로 이뤄지게 했습니다.

문득 내용 정리 중 생긴 궁금증

회원가입 페이지에서의 입력값을 받아오는 자바 코드가 없는데,,, 어떻게 UserCreateForm에 저장됨 ?

AI에 질문을 해보니 내용이 다음과 같습니다.

Spring MVC에서 사용자의 입력값을 자바 객체에 자동으로 매핑 = 폼 데이터 바인딩

폼 데이터 바인딩이라는 원리라고 합니다.

  1. HTML 폼 제출 : 사용자의 입력값을 서버로 전송
  2. MVC 요청 처리 : MVC는 HTTP 요청을 가로채고 요청 파라미터를 추출
  3. 컨트롤러 메서드 매핑 : 추출된 파라미터를 바탕으로 적절한 컨트롤러 메서드를 찾는다
  4. 객체 생성 및 속성 설정 : 파라미터로 선언된 객체를 생성하고, 요청 파라미터의 이름과 일치하는 객체의 속성에 값을 설정합니다.

4번의 내용이 제가 궁금했던 내용의 정답 같습니다.

@PostMapping("/user/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
    // ...
}

컨트롤러에서 @Valid를 통해 UserCreateForm 객체에 대한 유효성 검사를 수행합니다.

그 다음 BindingResult 객체에 검사 결과를 담습니다.

즉, UserCreateForm 객체는 HTML 폼의 입력 필드와 1:1로 매핑이 되고 있습니다.

그래서 가입 페이지의 username에 해당하는 입력값은 UserCreateForm 객체의 username속성에 자동으로 설정이 됩니다.

그리고 HTML 코드 중

<form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
    </form>

이 부분에서, th:object="${userCreateForm}" 을 통해 폼에 바인딩할 객체를 지정합니다.

그리고 th:field="*{username}"을 통해 폼의 입력 필드를 객체의 속성에 바인딩합니다.

핵심 포인트

  • 자동 매핑 : Spring MVC는 폼 데이터와 자바 객체의 속성을 자동으로 매핑합니다.
  • 유효성 검사 : @Valid 애노테이션을 사용하여 입력 데이터의 유효성을 검사할 수 있습니다.
  • Thymeleaf 연동 : Thymeleaf를 사용하여 폼을 생성하고 데이터를 바인딩할 수 있습니다.
  • 커스텀 데이터 바인더 : 기본적인 데이터 바인딩 기능 외에도 커스텀 데이터 바인더를 사용하여 복잡한 데이터 변환을 수행할 수 있습니다.
profile
자유로운영혼

0개의 댓글