우선적으로 유저 Entity를 리펙토링 해보겠다.
package com.example.testlocal.module.user.domain.entity;
import com.example.testlocal.domain.dto.UserDTO2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.Collections;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = "user")
@Table(name = "user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private long id;
@Column(name = "student_number", nullable = false)
private String studentNumber;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "email", nullable = false)
private String email;
public User(UserDTO2 userDTO) {
this.studentNumber = userDTO.getStudentNumber();
this.password = userDTO.getPassword();
this.name = userDTO.getName();
this.email = userDTO.getEmail();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public String getUsername() {
return studentNumber;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
위 코드는 리펙토링 전 유저 Entity 코드이다.
스프링 시큐리티에서 사용자 정보를 불러오기 위해 UserDetails를 상속 받았다.
의문점이 든다. 왜 Entity에 UserDetails의 구현체를 만들었는지?
→ User Entity는 역할이 Entitiy인데 스프링 시큐리티에서 사용자 정보를 주는 역할까지 하면 책임이 너무 많다고 생각이 든다. 그러므로 밖으로 빼주겠다.
package com.example.testlocal.core.security;
import com.example.testlocal.module.user.domain.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class CustomUserDetails implements UserDetails {
private User user;
public CustomUserDetails(User user){
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
위 코드는 분리시켜준 UserDetails의 구현체이다.
package com.example.testlocal.module.user.application.service;
import com.example.testlocal.core.security.CustomUserDetails;
import com.example.testlocal.module.user.domain.repository.UserRepository2;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository2 userRepository;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
return new CustomUserDetails(userRepository.findById(id)
.orElseThrow(
() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")
)
);
}
}
위 코드는 UserDetails를 반환하는 코드인데, 원래는 userRepository만 참조했지만 그러면 entity가 무거워져서 따로 분리시켜줬다.
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository2 userRepository;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
return new CustomUserDetails(userRepository.findById(id)
.orElseThrow(
() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")
)
);
}
}
테스트 코드도 만들어주겠다.
package com.example.testlocal.module.user.application.service;
import com.example.testlocal.module.user.domain.VO.UserVO;
import com.example.testlocal.module.user.domain.entity.User;
import com.example.testlocal.module.user.domain.repository.UserRepository2;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
@WebAppConfiguration
@SpringBootTest
class CustomUserDetailServiceTest {
@Autowired
private CustomUserDetailService customUserDetailService;
@Autowired
private UserRepository2 userRepository;
@DisplayName("UserDetail 반환하는 비교를 한다.")
@Test
void UserDetail_반환() {
User user = User.builder().id(1).email("tovbskvb@daum.net").name("박태순").password("1234567").studentNumber("17011526").build();
userRepository.save(user);
UserDetails userDetails = customUserDetailService.loadUserByUsername("17011526");
assertThat(userDetails.getUsername()).isEqualTo("박태순");
}
}
package com.example.testlocal.module.user.domain.entity;
import com.example.testlocal.domain.dto.UserDTO2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.Collections;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = "user")
@Table(name = "user")
public class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private long id;
@Column(name = "student_number", nullable = false)
private String studentNumber;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "email", nullable = false)
private String email;
public User(UserDTO2 userDTO) {
this.studentNumber = userDTO.getStudentNumber();
this.password = userDTO.getPassword();
this.name = userDTO.getName();
this.email = userDTO.getEmail();
}
}
의문점이 한 가지 더 생겼다. 왜 Entity만 있고, 순수 도메인 VO는 없는 지? 안정성을 위해서 VO를 생성해주겠다.
package com.example.testlocal.module.user.domain.VO;
import com.example.testlocal.module.user.domain.entity.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Objects;
@Getter
@Builder
public class UserVO {
private String studentNumber;
private String password;
private String name;
private String email;
@Override
public int hashCode() {
return Objects.hash(studentNumber, password, name, email);
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || getClass() != obj.getClass()) return false;
UserVO userVO = (UserVO) obj;
return (userVO.studentNumber == ((UserVO) obj).studentNumber)
&& (userVO.password == ((UserVO) obj).password)
&& (userVO.name == ((UserVO) obj).name)
&& (userVO.email == ((UserVO) obj).email);
}
}
생성자가 4개 이상이라 빌더를 적용해주었고,
VO는 주소 값이 달라도 속성 값들이 같으면 같은 객체라고 여기기 때문에 hashCode와 equals 메소드를 구현해서 사용하였다.
UserVO를 만들었으니, 테스트 코드를 만들어 보겠다.
package com.example.testlocal.module.user.domain.VO;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class UserVOTest {
@DisplayName("VO 동등비교를 한다.")
@Test
void isSameObjects() {
UserVO user1 = UserVO.builder().name("박태순").email("tovbs111@daum.net").password("1234").studentNumber("17011526").build();
UserVO user2 = UserVO.builder().name("박태순").email("tovbs111@daum.net").password("1234").studentNumber("17011526").build();
assertThat(user1).isEqualTo(user2);
assertThat(user1).hasSameHashCodeAs(user2);
}
@DisplayName("VO 같지 않다. 테스트")
@Test
void isNotSameObjects() {
UserVO user1 = UserVO.builder().name("박태리아").email("tovbs111@daum.net").password("1234").studentNumber("17011527").build();
UserVO user2 = UserVO.builder().name("박태순").email("tovbs111@daum.net").password("1234").studentNumber("17011527").build();
assertThat(user1).isNotSameAs(user2);
}
}
VO에 관련된 테스트 코드를 추가해주었다.
다시 User Entity를 보겠다.
일단 User Entity를 리펙토링 하기 전에 테스트 코드부터 추가해주겠다.
JPA를 사용해서 테이블과 매핑하기 위해 사용된 클래스이므로 주의사항을 한 번 보겠다.
@Entity 적용시 주의사항
드디어 User Entity의 책임이 분리가 되었다. 테스트 코드를 만들기 전에 명세를 다시 짜보겠다.
당시 프로젝트할 땐 명세를 정확하게 적지 않아 현재 리펙토링할 때 고생이다.
명세를 적어두면 유지 보수할 때 편하게 진행할 수 있는 걸 느꼈다.
User 명세
1. 관리자, 조교, 일반 회원으로 타입을 구분해야 한다.
2. 유저는 학번, 비밀번호, 이름, 이메일을 가지고 있다.
3. 회원 가입일과 수정일이 있어야 한다.
package com.example.testlocal.module.user.domain.entity;
import com.example.testlocal.module.user.domain.repository.UserRepository2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserTest {
@Autowired
private UserRepository2 userRepository;
private User user1;
private User user2;
@BeforeEach
public void init(){
user1 = User.builder().email("tovbskvb@daum.net").studentNumber("17011526").password("1234").name("손민기").build();
}
@DisplayName("유저 생성, 테스트")
@Test
public void create(){
user2 = userRepository.save(user1);
assertThat(user1.getName()).isEqualTo(user2.getName());
assertThat(user1.getId()).isEqualTo(user2.getId());
assertThat(user1.getPassword()).isEqualTo(user2.getPassword());
assertThat(user1.getRoleType()).isEqualTo(user2.getRoleType());
assertThat(user1.getStudentNumber()).isEqualTo(user2.getStudentNumber());
assertThat(user1.getEmail()).isEqualTo("tovbskvb@daum.net");
}
@DisplayName("유저 업데이트, 테스트")
@Test
public void update(){
user2 = userRepository.save(user1);
user1.setEmail("tovbskvb1@daum.net");
user1.setName("발민기");
user1.setStudentNumber("17011568");
user2 = userRepository.save(user1);
assertThat(user1.getName()).isEqualTo(user2.getName());
assertThat(user1.getId()).isEqualTo(user2.getId());
assertThat(user1.getPassword()).isEqualTo(user2.getPassword());
assertThat(user1.getRoleType()).isEqualTo(user2.getRoleType());
assertThat(user1.getStudentNumber()).isEqualTo(user2.getStudentNumber());
assertThat(user1.getEmail()).isEqualTo("tovbskvb1@daum.net");
}
@DisplayName("유저 삭제")
@Test
public void delete(){
userRepository.delete(user1);
assertThat(userRepository.findByEmail("tovbskvb@daum.net")).isEmpty();
}
}
관리자, 조교, 일반 회원을 나누어줘야하는데, 쪽팔리지만 전에는 조교 테이블을 따로 만들어서 관리를 해주었다.
하지만 그럴 필요가 없기에 enum 타입으로 나누어 주겠다.
상수를 담당하기에 config 패키지에 넣어놨다.
package com.example.testlocal.config;
public enum RoleType {
ADMIN, USER, ASSISTANT
}
package com.example.testlocal.module.user.domain.entity;
import com.example.testlocal.config.RoleType;
import com.example.testlocal.domain.dto.UserDTO2;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.Collections;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name = "user")
@Table(name = "user")
public class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private long id;
@Column(name = "student_number", nullable = false)
private String studentNumber;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "email", nullable = false)
private String email;
public User(UserDTO2 userDTO) {
this.studentNumber = userDTO.getStudentNumber();
this.password = userDTO.getPassword();
this.name = userDTO.getName();
this.email = userDTO.getEmail();
}
// 추가
@Enumerated(EnumType.STRING)
private RoleType roleType;
}
@Enumerated(EnumType.STRING)
EnumType.STRING 타입으로 설정하면 DB 저장할 때 이름으로 저장됨.
예를 들어 ADMIN 타입 → roleType 컬럼에 ADMIN 저장
package com.example.testlocal.module.user.domain.entity;
import com.example.testlocal.config.RoleType;
import com.example.testlocal.module.user.domain.repository.UserRepository2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserTest {
@Autowired
private UserRepository2 userRepository;
private User user1;
private User user2;
@BeforeEach
public void init(){
user1 = User.builder().email("tovbskvb@daum.net").studentNumber("17011526").password("1234").name("손민기").roleType(RoleType.ADMIN).build();
}
@DisplayName("유저 생성, 테스트")
@Test
public void create(){
user2 = userRepository.save(user1);
assertThat(user1.getName()).isEqualTo(user2.getName());
assertThat(user1.getId()).isEqualTo(user2.getId());
assertThat(user1.getPassword()).isEqualTo(user2.getPassword());
assertThat(user1.getRoleType()).isEqualTo(user2.getRoleType());
assertThat(user1.getStudentNumber()).isEqualTo(user2.getStudentNumber());
assertThat(user1.getEmail()).isEqualTo("tovbskvb@daum.net");
assertThat(user1.getRoleType()).isEqualTo(RoleType.ADMIN);
}
@DisplayName("유저 업데이트, 테스트")
@Test
public void update(){
user2 = userRepository.save(user1);
user1.setEmail("tovbskvb1@daum.net");
user1.setName("발민기");
user1.setStudentNumber("17011568");
user1.setRoleType(RoleType.USER);
user2 = userRepository.save(user1);
assertThat(user1.getName()).isEqualTo(user2.getName());
assertThat(user1.getId()).isEqualTo(user2.getId());
assertThat(user1.getPassword()).isEqualTo(user2.getPassword());
assertThat(user1.getRoleType()).isEqualTo(user2.getRoleType());
assertThat(user1.getStudentNumber()).isEqualTo(user2.getStudentNumber());
assertThat(user1.getEmail()).isEqualTo("tovbskvb1@daum.net");
assertThat(user1.getRoleType()).isEqualTo(RoleType.USER);
}
@DisplayName("유저 삭제")
@Test
public void delete(){
userRepository.delete(user1);
assertThat(userRepository.findByEmail("tovbskvb@daum.net")).isEmpty();
}
}
명세 2번은 이미 작성돼있어서 건너뛰겠다.
package com.example.testlocal.config;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
@Data
@MappedSuperclass
@NoArgsConstructor
public class DateTime {
private LocalDateTime createAt;
private LocalDateTime updateAt;
@PrePersist
public void prePersist() {
this.createAt = LocalDateTime.now();
this.updateAt = this.createAt;
}
@PreUpdate
public void preUpdate() {
this.updateAt = LocalDateTime.now();
}
}
이런 식으로 DateTime을 만들어서 엔티티 클래스에서 상속받도록 설계했다.
앞으로 수정할 테이블들도 다 DateTime이 필요해서 따로 클래스로 빼 주었다.
이렇게 User Entity 책임을 많이 덜어주고, 회원 관리를 생각해서 회원 타입과 수정일을 추가해주었다.
자바 ORM 표준 JPA 프로그래밍 - 김영한
잘 정리하셨네요~! 잘보고갑니다