프로젝트

Spring Security로 회원가입을 해보자 (1) - 회원 엔티티 설계

ummchicken 2023. 3. 1. 22:50

안녕하세요.

오늘은 저번 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 설정이 될 것 같습니다.

 

 

 

 

 

- 회원 엔티티 설정 끝 -