안녕하세요.
오늘은 저번 JPA CRUD와 JPA 1 : N 관계 생성에 이어
스프링 시큐리티로 회원가입을 해보겠습니다.
2편 : Spring Security로 회원가입을 해보자 (2) - SecurityConfig 및 회원가입
3편 : Spring Security로 회원가입을 해보자 (3) - SecurityConfig 및 로그인
사실 예전에 스프링 시큐리티로 로그인 구현을 올린 적이 있었는데요,
다시 공부할 겸 새로 올립니다.
저도 공부하면서 실시간으로 기록하는 거라,
시리즈로 올라갈 것 같네요.
그리고 코드 변동도 많이 있을 것 같습니다.
특히나 DTO 구조를 좀 세분화하고 싶기도 하고...
TestCode도 좀 더 많이 짜고 싶은 마음에 이것저것 해보고 있는데요,
그래서 코드 변동이 많이 있을 것 같다는 겁니다.
틀린 부분도 있을 수 있습니다.
📌 목표
- SpringSecurity 회원가입
- 회원가입 시 DTO 세분화
- TestCode 많이 짜보기
- Custom Exception
- JWT 적용해 보기 (임시?)
✔️ 요구사항 분석
- 이메일, 비밀번호, 닉네임을 입력받는다.
- 이메일은 중복될 수 없다. (Unique)
- 비밀번호와 닉네임은 변경할 수 있다.
- 비밀번호는 암호화 저장한다.
- 모든 엔티티는 등록시간과 수정시간이 삽입된다.
- 회원 권한은 USER와 ADMIN이 있다.
✔️ BaseTimeEntity 생성
JPA Auditing으로 생성시간/수정시간을 자동화 등록해 주기 위해 BaseTimeEntity 클래스를 생성해 줍니다.
저는 domain 패키지에 생성할 겁니다.
[BaseTimeEntity]
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
- BaseTimeEntity 클래스는 모든 Entity의 상위 클래스가 되어, Entity들의 createDate(생성시간), modifiedDate(수정시간)을 자동으로 관리해 줍니다.
- MappedSuperclass
- JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우, 필드들도 칼럼으로 인식하도록 합니다.
- @EntityListeners(AuditingEntityListener.class)
- BaseTimeEntity 클래스에 Auditing 기능을 포함시킵니다.
- @CreatedDate 및 @LastModifiedDate
- Entity의 시간을 자동 저장합니다.
다음은 Application 클래스에 활성화 어노테이션을 추가합니다.
@EnableJpaAuditing // JPA Auditing 활성화
@SpringBootApplication
public class RollingApplication {
public static void main(String[] args) {
SpringApplication.run(RollingApplication.class, args);
}
}
테스트코드 및 DB 확인은 밑에서 하겠습니다.
다음은 회원 엔티티를 생성합니다.
BaseTimeEntity를 상속받습니다.
✔️ 회원 엔티티 생성
먼저 회원 엔티티를 생성하겠습니다.
User로 하시는 분들도 있는데, 저는 Member로 하겠습니다.
MySQL을 사용할 때 User로 하면 내부의 무언가와 이름이 겹쳐서 안 되나 봐요.
그래서 따로 @Table(name = "Users") 설정을 해야 하는데,
나중에 시큐리티 관련 설정할 때 User와 헷갈릴 수도 있기도 하고, 번거롭기도 하고...
저는 그냥 Member로 하겠습니다.
[Memer 엔티티 & Role enum]
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
public class Member extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false, length = 30)
private String nickname;
@Enumerated(EnumType.STRING)
private Role role;
/* 패스워드 암호화 */
public void encodePassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(password);
}
@Builder
public Member(String password, String email, String nickname, Role role) {
this.password = password;
this.email = email;
this.nickname = nickname;
this.role = role;
}
public void updatePassword(PasswordEncoder passwordEncoder, String password) {
this.password = passwordEncoder.encode(password);
}
public void updateNickname(String nickname) {
this.nickname = nickname;
}
}
public enum Role {
USER, ADMIN;
}
- email 필드를 unique로 설정함으로써, 유일값을 갖게 합니다.
이제 한번 확인해 보겠습니다.
createdDate와 modifiedDate가 잘 들어와 있습니다.
다음은 MemberRepository를 생성하겠습니다.
✔️ MemberRepostiory
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
}
- findByEmail
- 이메일로 회원을 찾습니다.
이제 MemberRepository의 Test Code를 작성하겠습니다.
✔️ MemberRepostioryTest
먼저 기본 설정을 하겠습니다.
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@AfterEach
private void after(){
em.clear();
}
private void clear(){
em.flush();
em.clear();
}
// 테스트 코드 추가
}
- @AfterEach
- 각 Test들이 서로에게 영향을 미치 않게 하기 위해 설정합니다.
- em.flush()
- flush를 하면 강제로 DB에 insert 쿼리를 날리게 됩니다.
- em.clear()
- DB에 쿼리를 다 날리고 JPA 영속성 컨텍스트에 있는 캐시를 다 날립니다.
- 그래서 깔끔하게 다 확인을 할 수 있습니다.
- Test 코드는 테스트 완료 직후 rollback 되어, 실제로는 저장되지 않습니다.
다음은 회원저장 테스트코드 입니다.
@Test
public void 회원저장_성공() throws Exception {
// given
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
// when
Member saveMember = memberRepository.save(member);
// then
Member findMember = memberRepository.findById(saveMember.getId())
.orElseThrow(() -> new IllegalArgumentException("해당 회원 없습니다. userId = " + saveMember.getId()));
assertThat(findMember).isSameAs(saveMember);
assertThat(findMember).isSameAs(member);
}
- 저장한 member와 findById를 해서 찾은 findMember가 같은지 확인합니다.
성공합니다.
다음 테스트부터는 성공화면 캡처는 하지 않겠습니다.
다음은 회원가입 시 이메일이 없을 때 오류가 발생하는 테스트입니다.
이메일은 unique와 nullable=false로 설정했기 때문에, 반드시 존재해야 합니다.
@Test
public void 오류_회원가입시_이메일이_없음() throws Exception {
// given
Member member = Member.builder()
.password("1234567")
.nickname("ummchicken")
.role(Role.USER)
.build();
// when, then
assertThrows(Exception.class, () -> memberRepository.save(member));
}
마찬가지로 테스트 통과를 했고, 어떤 에러 메세지를 내나 봤더니
이런 메세지가 나오네요.
EMAIL은 null이 허용되지 않는다는 문구입니다.
다음은 닉네임이 Null일 때 테스트입니다.
@Test
public void 오류_회원가입시_닉네임이_없음() throws Exception {
// given
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.role(Role.USER)
.build();
// when, then
assertThrows(Exception.class, () -> memberRepository.save(member));
}
닉네임 필드는 nullable을 false로 지정했기 때문에 마찬가지로 반드시 존재해야 합니다.
위와 같은 에러가 납니다.
다음은 이메일 중복 에러입니다.
이메일은 unique로 지정했기 때문에, 겹치면 안 됩니다.
@Test
public void 오류_회원가입시_중복된_이메일이_있음_unique() throws Exception {
// given
Member member1 = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
Member member2 = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
memberRepository.save(member1);
clear();
// when, then
assertThrows(Exception.class, () -> memberRepository.save(member2));
}
이런 에러 메세지가 나오네요.
중복 테스트는 여기서 끝낼까 했지만...
제가 의심병이 도져서
이메일을 제외한 다른 필드들은 중복으로 저장하지 않아 보겠습니다.
혹시 이메일 때문이 아니라 다른 필드들이 겹쳐서 위의 테스트 코드가 통과한 걸 수도 있으니까요ㅋ;;
저도 공부 중이라 제 코드를 완전히 신뢰하진 못합니다ㅋ
@Test
public void 오류_회원가입시_중복된_이메일이_있음_unique_다른필드는다름() throws Exception {
// given
Member member1 = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
Member member2 = Member.builder()
.password("1234567777")
.email("aaa@naver.com")
.nickname("ummchicken222")
.role(Role.ADMIN)
.build();
memberRepository.save(member1);
clear();
// when, then
assertThrows(Exception.class, () -> memberRepository.save(member2));
}
마찬가지로 같은 에러가 납니다.
다음은 회원정보 수정 코드입니다.
@Test
public void 성공_회원정보수정() throws Exception {
// given
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
memberRepository.save(member);
clear();
String updatePassword = "updatePassword";
String updateNickname = "updateNickname";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// when
Member findMember = memberRepository.findById(member.getId())
.orElseThrow(() -> new IllegalArgumentException("해당 회원 없습니다. memberId = " + member.getId()));
findMember.updatePassword(passwordEncoder, updatePassword);
findMember.updateNickname(updateNickname);
em.flush();
// then
Member updateMember = memberRepository.findById(findMember.getId())
.orElseThrow(() -> new IllegalArgumentException("해당 회원 없습니다. userId = " + findMember.getId()));
assertThat(updateMember).isSameAs(findMember);
assertThat(passwordEncoder.matches(updatePassword, updateMember.getPassword())).isTrue();
assertThat(updateMember.getNickname()).isEqualTo(updateNickname);
assertThat(updateMember.getNickname()).isNotEqualTo(member.getNickname());
}
다음은 회원 정보 삭제 코드입니다.
@Test
public void 성공_회원삭제() throws Exception {
// given
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
memberRepository.delete(member);
clear();
// then
assertThrows(Exception.class, () -> memberRepository.findById(member.getId())
.orElseThrow(() -> new Exception()));
}
다음은 findByEmail을 테스트하는 코드입니다.
@Test
public void findByEmail() throws Exception {
// given
String email = "aaa@naver.com";
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
memberRepository.save(member);
clear();
// when, then
assertThat(memberRepository.findByEmail(email).get().getNickname()).isEqualTo(member.getNickname());
assertThat(memberRepository.findByEmail(email).get().getId()).isEqualTo(member.getId());
assertThrows(Exception.class, () -> memberRepository.findByEmail(email+"123")
.orElseThrow(() -> new Exception()));
}
- 이메일로 찾은 회원의 정보가 맞는 정보인지 확인합니다.
다음은 BaseTimeEntity 기능 확인 코드입니다.
@Test
public void BaseTimeEntity_등록() {
// given
LocalDateTime now = LocalDateTime.of(2023, 2, 1, 0, 0, 0);
Member member = Member.builder()
.password("1234567")
.email("aaa@naver.com")
.nickname("ummchicken")
.role(Role.USER)
.build();
memberRepository.save(member);
clear();
// when
Member findMember = memberRepository.findById(member.getId())
.orElseThrow(() -> new IllegalArgumentException("해당 회원 없습니다. memberId = " + member.getId()));
// then
assertThat(findMember.getCreatedDate().isAfter(now));
assertThat(findMember.getModifiedDate().isAfter(now));
}
- now로 설정한 날짜보다 현재 저장한 회원 정보의 생성/수정시간이 후에 있는지 확인합니다.
여기까지 이번 포스팅은 끝입니다.
다음 포스팅은 MemberService와 SequrityConfig 설정이 될 것 같습니다.
- 회원 엔티티 설정 끝 -
'프로젝트' 카테고리의 다른 글
Spring Security로 회원가입을 해보자 (3) - SecurityConfig 및 로그인 (0) | 2023.03.06 |
---|---|
Spring Security로 회원가입을 해보자 (2) - SecurityConfig 및 회원가입 (0) | 2023.03.02 |
JPA 1 : N 관계를 분석해보자 (0) | 2023.02.27 |
스프링 시큐리티를 분석해보자 (0) | 2023.02.25 |
Session 기반 인증과 Token 기반 인증의 차이가 뭘까? (2) | 2023.02.20 |