웹 개발 Spring Day8 회원가입 enum + interface, MyBatis, Mapper

김지원·2022년 8월 10일
0

WebDevelop2

목록 보기
28/34

→ UserController

  • 어쩌고 Controller면 무조건 /어쩌고 가 맵핑이 되어야한다.
    한가지 예외가 있다면 HomeController 혹은 RootController는 / 로 맵핑한다.

경로 자동완성 되게 하는 방법

  • command + ;

th:src="링크"

: 해당 태그의 주소를


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>

select / focus 한번에 처리하는 방법 (2가지)

<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 테이블 생성

Entity → getter / setter Builder

-> UserEntity

  • getter / setter 은 Builder 선택 후 생성.
  • Builder로 만드는 이유는 setter메서드의 반환 타입이 UserEntity가 되고 return this를 해준다. 이렇게 했을 때 장점은 뭘까?
  • 컨트롤러에서 메서드 체인으로 사용이 가능해진다.
UserEntity userEntity = new UserEntity()
        .setEmail("~~")
        .setName("~~")
        .setPassword("~~");

각각의 setter메서드는 this를 반환한다.
this는 결론적으로 UserEntity의 객체이고 그 객체가 가진 메서드를 싹 다 가져올 수 있게 된다.

  • Builder로 getter / setter을 만들지 않고 메서드 체인을 쓰지 않으면 이렇게 메서드를 불러와야한다.

Entity build 메서드

  • UserEntity에 build 메서드 생성.
UserEntity userEntity = new UserEntity()
  • 이처럼 new키워드를 사용하지 않기 위해서 UserEntity 클래스에서 bulid() 메서드를 만들어준다.

equals() and hashCode() 메서드 오버라이드 + final

  • 이 화면은 두 객체가 같은 가를 비교할 때 대상이 될 멤버를 고르는 화면이다.
    두 사용자가 같은 사용자다 라고 판단하기 위해서는 email로 판단을 할 것이다. (보통 기본키를 걸은 것으로 확인을 한다.)

→ UserEntity를 상속받는 그 어떠한 타입도 equals() and hashCode() 메서드를 재정의하지 못하게 하고 여기서 써놓은 대로만 사용할 수 있도록 하기 위해서 final을 사용한다.

  • 사용자를 이메일로만 구분하기 위해서 final을 붙여주자.
  • 다만 UserEntity을 상속받을 경우가 있기 때문에 UserEntity 자체의 클래스를 final로 만들어서는 안된다.

hashCode는 HashMap에서 두 객체가 동일한가에 대한 여부를 확인 할 떄 사용한다.
지금은 hashCode를 사용하지 않는다.

controller에서 input의 nameemail 인 것이면 setEmail 메서드 호출해서 값을 넣을 것이다. 그 넣은 값이 Entity 의 email 매개변수에 들어가게 된다.

input name : email → setEmail 호출 → private String email;

Result

회원가입의 결과는 성공 / 실패(이메일 중복) / 실패(알 수 없는 이유) 로 설정한다.
insert에 의해서 영향을 받은 레코드의 갯수가 0일 경우가 있기 때문에 알 수 없는 이유로 실패도 필요로 한다.

회원가입의 결과가 이 3개로 정해져있고 제한적이며 더 이상 추가 되지 않을 것이기 때문에 열거형(enum) 으로 만들자.

-> RegisterResult

  • SUCCESSFAILURE은 다른 곳에서도 많이 사용할 것 같고 FAILURE_DUPLICATE_EMAIL 제한적이기 때문에 따로 만들어서 사용해보자.

-> CommonResult

  • CommonResult를 만들어 여러곳에서 사용할 수 있는 enum을 만들어준다.
  • RegisterResultFAILURE_DUPLICATE_EMAIL만 남긴다.

UserController) 각 enum안에 있는 객체들은 엄밀히 애기해서 타입이 다른데 controller에서 어떤 타입으로 받아야할까?
→ RegisterResult타입일까? CommonResult타입일까?

  • Enum 타입으로 받아주면 된다.
    저 두 개의 enum안에 있는 객체들은 Enum타입을 상속받고 있고 enum은 Object를 상속받고 있다.
    Object 타입으로 받아줘도되지만 목적성이 떨어져 Enum을 사용한다.

그래서 우리가 사용하는 어떠한 행동에 결과값으로 싹 묶어내기 위해서 IResult 인터페이스 생성한다.

확장 가능한 enum을 위한 interface 구현 + name() 사용

IResult Interface

IResult 인터페이스 생성

  • 각 enum에 IResult를 implements 해준다.

String name();
  • 이러한 메서드를 추가를 했는데 인터페이스를 구현하는 Enum은 분명히 있는데 왜 오류가 나지 않을까?
  • 이미 Enum 타입은 name() 메서드를 가지고 있기 때문에 가능한 것이다.

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

MyBatis

0. 공통 속성

id 속성            : 메서드 이름
parameterType 속성 : 전달받을 매개변수의 타입의 풀 네임.

1. <mappers>

namespace 속성 : 연결된 인터페이스의 Qualified Name(풀 네임)

2. <insert>

3. <select>

resultType 속성 : 반환 타입 속성
	→  _int : int 기초 타입
	→  그 외 원하는 타입의 풀 네임 

4. 표현식

#{ } : 그 자리에 값을 대입한다. 단, 홀따옴표나 보안을 위한 이스케이프를 자동으로 해줌. 절대로 추가적인 것을 작성하지 않는다.
${ } : 그 자리에 그 값 그대로를 대입하는데 보안상의 이유로 사용하지 않는 것이 좋다. 
profile
Software Developer : -)

0개의 댓글