Build.gradle에 타임리프, 스프링 부트 웹, Validation, JPA, MariaDB 의존성 추가.
application.properties 에서 DB 의 username 과 password. url 등을 설정해주었다.
사용자의 데이터를 DB에 저장하기 위한 테이블을 생성해야한다. 이 때 JPA 와 같은 ORM은 클래스를 정의하면 이에 맞도록 테이블을 생성해준다.
@Entity 는 JPA의 어노테이션으로 해당 클래스를 바탕으로 테이블을 생성해준다. @Builder 는 lombok이 해당 객체의 Builder 메서드를 생성해준다. Builder 에 대해선 별도의 포스팅으로 정리할 예정이다. 이 때 Builder 패턴을 위해 AllArgs 와 NoArrgs 생성자가 필요하다. @Getter 는 로그인 때 User 의 username 과 password 를 사용하기 위해 추가하였다.
@Id : 테이블의 PK를 지정한다.
@GeneratedValue : JPA가 자동으로 값을 생성해주는 필드이다. strategy 를 IDENTITY 로하면 DB에 Insert 시 마다 자동으로 1씩 증가시켜준다.
@Column : DB 삽입시 Column 제약조건을 정해준다.
DB 의 User 에 접근을 수행할 UserRepository 를 생성하였다.
JpaRepository 인터페이스를 상속받으면 PaginAndSortingRepository 와 CrudRepository 등을 함께 상속받는다. 그 과정에서 save, findAll 과 같은 메서드들을 구현하는데, 이는 후에 JPA 에 대해 학습하며 정리하겠다. 이때 제네릭으로는 도메인 타입과 PK 타입을 사용한다. 대표적인 Save 메서드를 보면 파라미터와 반환형으로 도메인 타입을 사용한다.
회원가입의 Form 데이터 전달에 활용할 SignupDto 객체이다. username, password, name, email 의 필드를 갖는다.
등록이나 수정의 검증 로직이 다를 수도 있고, 모든 도메인의 필드를 폼에서 입력받지 않을 수도 있다. 즉 별도의 검증 로직을 걸어줄 수 있고, 도메인의 불필요한 필드의 사용을 방지하기 위해 Form 마다 Dto를 정의하여 사용한다.
Bean Validation의 어노테이션들을 추가해주었다. Bean Validation 의 글로벌 Validator 가 해당 어노테이션을 보고 검증을 수행하여 매핑된 BindingResult 에 FieldError 를 추가해준다.
toEntity() 메서드는 SignupDto -> User 를 수행하는 메서드이다. 회원가입에서 User 의 모든 필드를 입력받지 않는다. 그런데 UserRepository 의 메서드들은 User 객체를 파라미터로만 수행가능하다. 그래서 SignupDto 를 User로 변환해줄 수 있는 메서드가 필요하므로 Builder 패턴으로 메서드를 정의해놓은 것이다.
회원가입과 로그인 관련 요청을 처리하기 위한 컨트롤러들을 모아놓은 클래스이다. /auth/signin 에 대한 Get,Post 요청과, /auth/signup 에 대한 Get,Post 요청을 처리한다.
/auth/signup 으로 Get 요청이 들어오면 매핑된 signupForm 핸들러가 호출된다. 타임리프의 th:object 속성을 사용하기 위해 Model 에 빈 signupDto 를 담은 뒤 뷰를 렌더링해준다.
Repository 로 회원가입, 로그인 관련 메서드를 갖는 클래스이다. UserRepository 를 주입받는다. JPA 가 제공하는 save 메서드로
AuthService 에서 회원가입 메서드를 정의하였다.
UserRepository.save 를 보면 파라미터도 User 객체이며 반환형도 User 객체다. User 객체가 아닌 다른 객체가 파라미터로 에러가 발생한다. 그런데 우리가 회원가입 폼을 통해 받은 것은 SignupDto 객체이다. 그래서 toEntity 메서드로 User 객체로 변환할 필요가 있는 것이다. 이는 컨트롤러에서 수행한다.
우리가 SignupDto 에 Bean Validation 어노테이션들을 추가해두었다. 글로벌 Validator 는 이를 바탕으로 검증을 수행하고, BindingResult 에 이를 담는다. 그래서 여기서 검증 오류가 존재하면 auth/signup 을 다시 렌더링하도록 하였다. 이때 BindingResult 가 함께 넘어가기 때문에 th:errors 나 th:errorclass 들이 동작하게 되며, th:field 에 의해 사용자가 잘못 입력한 값들이 보존된다.
UserRepository 는 파라미터로 User 객체를 받는다 하였다. 그래서 singupDto 를 toEntity() 로 User 객체로 변환한 뒤 회원가입 메서드의 파라미터로 넣어 회원가입을 수행한다.
회원가입을 성공적으로 수행하면 로그인 화면으로 보내주고 싶다. 이 때 그냥 로그인 뷰로 렌더링을 수행하면 오류가 발생한다. 로그인 뷰에도 Form이 존재하는데, 여기서 th:object 를 사용하기 때문이다. 따라서 빈 SigninDto 객체를 넘겨줘야 된다. 그래서 반드시 GetMapping 을 거친 후 뷰를 받아야하며 이를 위해 /auth/signin 으로 redirect 를 보낸다.
회원가입 POST 성공 후 Login 페이지를 직접 주는 것이 아닌 Redirect 를 후 GET 요청을 하도록 하는 것이 PRG 패턴의 일종으로 볼 수 있다. PRG 패턴을 적용하면, 사용자가 Form 을 제출한 후에 새로고침을 하더라도 다시 GET 요청을 수행하므로 중복 Form 제출을 방지할 수 있다.
뷰의 템플릿엔진은 스프링과 통합이 잘되어있는 타임리프를 사용하였으며, 프론트엔드는 부트스트랩 프레임워크를 찾아가며 활용하였다.
th:object 는 Model 에 담겨진 객체와 전체 Form Input 을 매핑시킨다.
th:field 는 Model 에 담겨진 객체의 필드와 Form 의 각 Input 을 매핑시킨다. 이 때 *{필드명} 문법으로 th:object 의 속성에 접근할 수 있다. ${객체명.필드명} 과 동일하다. th:field 는 id, name, value 를 자동으로 생성해준다.
id 속성은 label 속성의 명시에 활용하며, name 은 서버에게 전송할 때 파라미터의 key로, value 는 해당 폼에 기본 입력될 value 를 지정한다.
th:field 는 오류처리에 유연하게 동작하여 해당 필드에 에러가 발생하였으면 value 에 FieldError 의 잘못 입력된 값을 가져와 입력시킨다.
해당 필드의 필드에러가 BindingResult 에 존재한다면 클래스를 추가해준다. 에러 클래스를 빨간색으로 처리하거나 할 때 사용한다.
해당 필드에 error 가 존재할 경우 속한 태그를 렌더링한다. 이 때 에러메시지도 text 로 출력해준다.
검증 실패시 다시 auth/singup 을 렌더링한다. 이때 SignupDto 와 BindingResult 가 함께 뷰로 넘어간다. 그래서 th:field 는 오류가 발생한 필드에 대해서는 FieldError 에서 잘못된 입력 값을. 제대로 입력된 필드에 대해서는 SignupDto 에서 값을 찾아 출력한다.
th:errors 에서 오류메시지를 렌더링해주고, th:errorclass 에 의해 input 태그가 빨간색으로 변경된다.
검증 성공시 /auth/signin 으로 redirect 된다. 그러면 GET 요청을 수행하여 로그인 페이지를 가져온다.
쿼리를 실행해보면 Service 의 회원가입 메서드가 제대로 수행된 것을 확인할 수 있다.