→ UserController
- 어쩌고 Controller면 무조건
/어쩌고
가 맵핑이 되어야한다.
한가지 예외가 있다면 HomeController 혹은 RootController는/
로 맵핑한다.
경로 자동완성 되게 하는 방법
- command + ;
: 해당 태그의 주소를
form태그에 action이 있으면 action에 명시된 주소로 요청을 보낸다.
action이 없으면 현재주소로 요청을 보낸다. (똑같은 주소)
/user/register
에 get방식으로 요청을 보내는 form이다.
-> register form
- getParameter은 URL에 존재하는 변수 중 email 이름을 가진 걊을 반환하도록 한다.
- 실제 URL 주소와 비교해보면 이름과 값이 이런식으로 일치가 된다.
→ 가지고온 email을 input의 value로 해주자.
th:value="@{email}"
- 이렇게 처리를 해주면 email input에 전에 적은 email이 들어가게 된다.
- 그런데 어떻게 spring이 바로 email이 string인지 단정을 지을 수 있을까?
${#request.getParameter('email')}
- getParameter를 타고 들어가보면 항상 string을 주고 있기 때문에 단정을 지을 수 있는 것이다.
http://localhost:8080/user/register?email=user01%40sample.com
- 이전 화면에서 작성한 이메일이 그대로 들고와지는 걸 볼 수 있다.
<input autofocus class="input" maxlength="50" name="email" placeholder="이메일 주소" type="email" th:autofocus="${email == null}" th:readonly="${email != null}" th:value="${email}">
th:autofocus="${email == null}" 이메일이 없을 때 이메일에 autofocus th:readonly="${email != null}" 이메일이 있을 때 email을 수정하지 못하게 한다.
<input class="input" maxlength="50" name="password" placeholder="비밀번호" type="password" th:autofocus="${email != null}">
- email이 있다면 비밀번호 input으로 autofocus
- register.html / css → js 에서 회원가입 input에 대한 처리해보자.
<script>
const registerForm = window.document.getElementById('register-form');
registerForm.focusAndSelect = (name) => { // focus 랑 select 를 한 번에 처리하기 위해서 사용.
registerForm[name].focus();
registerForm[name].select(); // 적어놓은 내용을 전체 선택하라는 것.
}
// HTMLInputElement.prototype.focusAndSelect = function () {
// this.focus();
// this.select(); // 모든 input 에 적용이 가능하다. registerForm['email'] 은 결론적으로 가르치는것은 email 의 input 이다.
// }
// 비밀번호 두개 일치 여부
registerForm.onsubmit = e => {
const warning = registerForm.querySelector('[rel=warning]');
warning.show = (message) => {
warning.innerText = message;
warning.classList.add('visible');
}
warning.innerText = '';
warning.classList.remove('visible'); // 혹시 보여져 있다면 숨긴다.
if(registerForm['email'].value === '') {
warning.show('이메일을 입력해주세요.');
registerForm.focusAndSelect('email');
e.preventDefault();
return false;
}
if(registerForm['password'].value === '') {
warning.show('비밀번호를 입력해주세요.');
registerForm.focusAndSelect('password');
e.preventDefault();
return false;
}
if(registerForm['passwordCheck'].value === '') {
warning.show('비밀번호를 다시 한 번 입력해주세요.');
registerForm.focusAndSelect('passwordCheck');
e.preventDefault();
return false;
}
if (registerForm['password'].value !== registerForm['passwordCheck'].value) {
warning.show('입력한 비밀번호가 서로 다릅니다. 다시 확인해 주세요.');
registerForm.focusAndSelect('passwordCheck'); // 비밀번호가 다를 시 다시 적도록 focus
e.preventDefault();
return false;
}
if(registerForm['name'].value === '') {
warning.show('소유자 이름을 입력하세요');
registerForm.focusAndSelect('name');
e.preventDefault();
return false;
}
// check 여부 확인
if(!registerForm['agreeTerm'].checked) {
warning.show('개인정보 처리방침을 읽고 동의하지 않으면 회원가입을 계속할 수 없습니다.');
registerForm['agreeTerm'].focus();
e.preventDefault();
return false;
}
if(!registerForm['agreeEmail'].checked && confirm('매년 매월 매일 매시 매분 매초 혜택이 가득한 쓸 때 없는 이메일을 보내드려요.\n\n다시 한 번 검토해 보시겠어요?')) { // confirm 창이 떴을 때 확인을 누르면 넘어가지 않는 방식이다.
registerForm['agreeEmail'].focus();
e.preventDefault();
return false;
}
};
</script>
<script> registerForm.focusAndSelect = (name) => { // focus 랑 select 를 한 번에 처리하기 위해서 사용. registerForm[name].focus(); registerForm[name].select(); // 적어놓은 내용을 전체 선택하라는 것. } </script>
<script> HTMLInputElement.prototype.focusAndSelect = function () { this.focus(); this.select(); // 모든 input 에 적용이 가능하다. registerForm['email'] 은 결론적으로 가르치는것은 email 의 input 이다. } </script>
회원가입 데이터를 받도록 하자.
넘어온 유저의 데이터를 처리하기 위해서 컨트롤러로 간다.
input태그의 name값 기준으로 requestParam
으로 받아야하는데 귀찮기 때문에 Entity만들어서 Entity의 매개변수로 받자.
Entity를 만들기 전에 먼저 DB부터 설계하자.
-> users
테이블 생성
-> UserEntity
- getter / setter 은 Builder 선택 후 생성.
- Builder로 만드는 이유는 setter메서드의 반환 타입이 UserEntity가 되고 return this를 해준다. 이렇게 했을 때 장점은 뭘까?
- 컨트롤러에서 메서드 체인으로 사용이 가능해진다.
UserEntity userEntity = new UserEntity() .setEmail("~~") .setName("~~") .setPassword("~~");
각각의 setter메서드는 this를 반환한다.
this는 결론적으로 UserEntity의 객체이고 그 객체가 가진 메서드를 싹 다 가져올 수 있게 된다.
- Builder로 getter / setter을 만들지 않고 메서드 체인을 쓰지 않으면 이렇게 메서드를 불러와야한다.
- UserEntity에 build 메서드 생성.
UserEntity userEntity = new UserEntity()
- 이처럼 new키워드를 사용하지 않기 위해서 UserEntity 클래스에서
bulid()
메서드를 만들어준다.
- 이 화면은 두 객체가 같은 가를 비교할 때 대상이 될 멤버를 고르는 화면이다.
두 사용자가 같은 사용자다 라고 판단하기 위해서는 email로 판단을 할 것이다. (보통 기본키를 걸은 것으로 확인을 한다.)
→ UserEntity를 상속받는 그 어떠한 타입도 equals() and hashCode()
메서드를 재정의하지 못하게 하고 여기서 써놓은 대로만 사용할 수 있도록 하기 위해서 final을 사용한다.
- 사용자를 이메일로만 구분하기 위해서
final
을 붙여주자.- 다만 UserEntity을 상속받을 경우가 있기 때문에 UserEntity 자체의 클래스를 final로 만들어서는 안된다.
hashCode는 HashMap에서 두 객체가 동일한가에 대한 여부를 확인 할 떄 사용한다.
지금은 hashCode를 사용하지 않는다.
controller에서 input의 name
이 email
인 것이면 setEmail
메서드 호출해서 값을 넣을 것이다. 그 넣은 값이 Entity 의 email 매개변수에 들어가게 된다.
input name : email → setEmail 호출 → private String email;
회원가입의 결과는 성공 / 실패(이메일 중복) / 실패(알 수 없는 이유)
로 설정한다.
insert에 의해서 영향을 받은 레코드의 갯수가 0일 경우가 있기 때문에 알 수 없는 이유로 실패도 필요로 한다.
회원가입의 결과가 이 3개로 정해져있고 제한적이며 더 이상 추가 되지 않을 것이기 때문에 열거형(enum) 으로 만들자.
-> RegisterResult
SUCCESS
와FAILURE
은 다른 곳에서도 많이 사용할 것 같고FAILURE_DUPLICATE_EMAIL
제한적이기 때문에 따로 만들어서 사용해보자.
-> CommonResult
- CommonResult를 만들어 여러곳에서 사용할 수 있는 enum을 만들어준다.
RegisterResult
은FAILURE_DUPLICATE_EMAIL
만 남긴다.
UserController) 각 enum안에 있는 객체들은 엄밀히 애기해서 타입이 다른데 controller에서 어떤 타입으로 받아야할까?
→ RegisterResult타입일까? CommonResult타입일까?
- Enum 타입으로 받아주면 된다.
저 두 개의 enum안에 있는 객체들은 Enum타입을 상속받고 있고 enum은 Object를 상속받고 있다.
Object 타입으로 받아줘도되지만 목적성이 떨어져 Enum을 사용한다.
그래서 우리가 사용하는 어떠한 행동에 결과값으로 싹 묶어내기 위해서 IResult 인터페이스 생성한다.
IResult 인터페이스 생성
- 각 enum에 IResult를 implements 해준다.
String name();
- 이러한 메서드를 추가를 했는데 인터페이스를 구현하는 Enum은 분명히 있는데 왜 오류가 나지 않을까?
- 이미 Enum 타입은 name() 메서드를 가지고 있기 때문에 가능한 것이다.
Enum 클래스의 name() 메서드는 열거형 선언에 선언된 것과 동일한 이 열거형 상수의 이름을 반환한다.
- name() 메서드는 final이지만 인터페이스에서는 이런 메서드가 있다고 명시만 해두었기 때문에 사용이 가능한 것이다.
- 그렇게 되면 UserController에서 IResult 타입으로 받아낼 수 있게 된다.
- name 메서드 또한 가능하다.
결론적으로
enum
는 내부적으로Enum<T>
를 상속받고 있기 때문에 어떠한 다른 클래스도 더 이상 상속받을 수 없다. 그렇기 때문에 enum 클래스의 기능을 확장시켜 사용하고 싶다면 interface를 구현해 참조하여 사용한다.
→ 따라서 회원가입 결과는 IResult타입의 result로 돌려주자.
먼저, 메인 페이지에서 회원가입 요청이 들어온 이메일이 존재하는지 하지 않는지 확인을 하자. DB에 해당 이메일이 있는지 확인해야한다.
있다면 FAILURE_DUPLICATE_EMAIL
날려주고 아니라면 `SUCCESS으로 return 해주자.
-> UserService getUserCountByEmail / putUser
- SELECT : getUserCountByEmail → 이메일의 존재 여부 확인. 사용자의 갯수를 받아온다.
- INSERT : putUser → 회원가입 절차가 통과가 되었다면 user을 추가한다.
-> UserController
if(this.userService.getUserCountByEmail(user.getEmail()) > 0) { // 기입한 이메일이 이미 가입되있을 경우 result = RegisterResult.FAILURE_DUPLICATE_EMAIL; } else if (this.userService.putUser(user) == 1) { result = CommonResult.SUCCESS; } else { result = CommonResult.FAILURE; }
→ 풀이
if(this.userService.getUserCountByEmail(user.getEmail()) > 0 )
user (=UserEntity) 객체가 가진 getEmail == 사용자가 적은 email을 userService의 getUserCountByEmail에 매개변수로 넘겨주어서 SELECT COUNT 결과가 0 이상이라면 해당 유저가 존재하는 것이기 때문에
RegisterResult.FAILURE_DUPLICATE_EMAIL;
를 돌려준다.
여기서 if가 아닌 else if를 사용하는 이유 : 이미 있는 이메일이라는 결론이 났는데도 불구하고 회원가입을 시키면 (putUser) 오류가 터지게 된다.
- 컨트롤러에서 이렇게 접근하는 방법은 좋은 방법이 아니다.
-> IResult 정적 상수 추가
- 인터페이스의 변수의 접근제한자는 public이며 정적이고 상수이다. 즉, 멤버 변수의 타입은 항상 public static final 이고 생략 가능하다.
→ 인터페이스에서 정적 상수를 만들어 이용한다.
public static final String ATTRIBUTE_NAME = "result";
-> 만들어놓은 멤버 상수에 접근을 하기 위해서는 final이기 때문에 타입으로 접근한다.
modelAndView.addObject(IResult.ATTRIBUTE_NAME, result);
-> IUserMapper 인터페이스 생성
insertUser
selectByUserCountEmail
Param 어노테이션 사용한다.
-> UserService
- IUserMapper 의존성 추가
- 각 메서드의 반환값을 적어준다.
-> UserMapper
- Mapper의
namespace
는 인터페이스의 풀네임으로 한다.
selectByUserCountEmail
- SELECT 는 resultType을 가진다. (INSERT는 resultType: 반환해줘야해줘야 하는 값이 없다.)
resultType="_int"
: 기초 타입의 정수가 result로 나가게 된다.
- 이 두개가 일치해야하고 String 뒤에 변수는 아무런 다르게 적어도 아무런 의미가 없다.
IUserMapper에서 사용한 @Param의 value가 email이 였던 이유
: @Param의 value값이
#{ }
나${ }
에 있는 email과 같으면 그 자리에 문자열 값을 UserEntity에 넣어준다. (#{ }
일 경우에는 홑따옴표도 자동으로 추가된다.)
insertUser
- insertUser가 매개변수로 전달받는 parameterType은 UserEntity임으로 UserEntity 의 풀네임을 작성한다.
#{~}
은 알아서 홑따옴표를 붙여서 처리해준다.
아래와 같이 select와 달리 insertUser에서 따로 value가 어떤것을 가르키는지 지정해준적이 없는데 (= @Param 어노테이션을 사용하지 않음) 값을 어떻게 가지고 오는걸까?
- Param 어노테이션 이용해서 value가 저 4개라는 것을 지정해준적없고 매개변수를 UserEntity 타입을 받고 있다라는 것만 지정해주었다.
parameterType을 지정해주었기 때문에 자동으로 values에 있는 email이 setEmail 바뀌어서 UserEntity의 setEmail메서드를 가르키게 된다.
createdAt
에 대한 처리는 따로 해주어야한다.
→ UserEntity가 가진 createdAt은 기본적으로 null인데 우리가 설정한 created_at 필드는 NOT NULL임으로 오류가 터지기 때문에 지정을 해주자.
- 컨트롤러에서 CreatedAt은 현재시간으로 지정해준다.
- 회원가입을 해보았더니 email값이 중복으로 들어가있는데 index페이지에서 입력한 이메일과 register 페이지에서 입력한 이메일이 중복으로 insert(전달)가 되었기 때문에 중복으로 들어가게 되었다.
- register form 에서
th:action
을 걸어주자.
th:action="@{./register}"
http://localhost:8080/user/register?email=user02%40sample.com
./
은/user/
을 의미한다. (.
은 현재주소를 의미함)
그러고 나서 register을 붙여줌으로써 뒤에 붙어있는 것이 날아가게 된다.↓ http://localhost:8080/user/register
id 속성 : 메서드 이름
parameterType 속성 : 전달받을 매개변수의 타입의 풀 네임.
<mappers>
namespace 속성 : 연결된 인터페이스의 Qualified Name(풀 네임)
<insert>
<select>
resultType 속성 : 반환 타입 속성
→ _int : int 기초 타입
→ 그 외 원하는 타입의 풀 네임
#{ } : 그 자리에 값을 대입한다. 단, 홀따옴표나 보안을 위한 이스케이프를 자동으로 해줌. 절대로 추가적인 것을 작성하지 않는다.
${ } : 그 자리에 그 값 그대로를 대입하는데 보안상의 이유로 사용하지 않는 것이 좋다.