[Spring Security] 온라인 시험 사이트 구현(1. 도메인 설계:학교 학생)

WOOK JONG KIM·2022년 12월 7일
0

패캠_java&Spring

목록 보기
90/103
post-thumbnail

사이트 시나리오

  • 학교의 선생님과 학생이 사이트에 가입한다.
  • 선생님은 학교와 학급을 입력하고 가입을 할 수 있다.
  • 학생은 학교와 선생님을 입력하고 가입을 할 수 있다.
  • 선생님은 시험지를 만들 수 있다.
  • 선생님은 시험지를 만들어서 학생들에게 시험지를 낼 수 있다.
  • 학생은 자신에게 온 시험지를 조회하고 시험을 볼 수 있다.
  • 학생은 시험을 본 후, 제출해서 시험 점수를 확인할 수 있다.
  • 선생님은 학생리스트와 시험 점수를 확인할 수 있다.

학교 도메인 설계

  • 학교를 생성한다.
  • 학교 이름을 수정한다.
  • 지역 목록을 가져온다.
  • 지역으로 학교 목록을 가져온다.

PaperUserModule.java
-> 모듈의 기본설정은 Config 패키지에서 작업

@Configuration
@ComponentScan("com.sp.fc.user")
@EnableJpaRepositories(basePackages = {
        "com.sp.fc.user.repository"
})
@EntityScan(basePackages = {
        "com.sp.fc.user.domain"
})
public class PaperUserModule {

}

Application에서 이 모듈에 대한 Bean과 특징들을 스캔할때, 스캔해가는 정보들을 취합

School.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "sp_school")
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long schoolId;

    private String name;

    private String city;

    @Column(updatable = false)
    private LocalDateTime created;

    private LocalDateTime updated;
}

SchoolRepository.java

public interface SchoolRepository extends JpaRepository<School, Long> {

    @Query("select distinct(city) from School")
    List<String> getCities();

    List<School> findAllByCity(String city);
}

SchoolService.java

@Service
@Transactional // DB 작업이기 때문
@RequiredArgsConstructor
public class SchoolService {

    private final SchoolRepository schoolRepository;

    public School save(School school){ // 학교 저장
        if(school.getSchoolId() == null){
            school.setCreated(LocalDateTime.now());
        }
        school.setUpdated(LocalDateTime.now());
        return schoolRepository.save(school);
    }

    // 어느 지역의 학교인지 리스트상에 노출하기 위해
    public List<String> cities(){
        return schoolRepository.getCities();
    }

    public Optional<School> updateName(Long schoolId, String name){
        return schoolRepository.findById(schoolId).map(school -> {
            school.setName(name);
            schoolRepository.save(school);
            return school;
        });
    }

    public List<School> findAllByCity(String city) {
        return schoolRepository.findAllByCity(city);
    }
}

SchoolTestHelper.java

@RequiredArgsConstructor
public class SchoolTestHelper {

    private final SchoolService schoolService;

    public static School makeSchool(String name, String city){
        return School.builder()
                .name(name)
                .city(city)
                .build();
    }

    public School createSchool(String name, String city){
        return schoolService.save(makeSchool(name, city));
    }

    public static void assertSchool(School school, String name, String city){
        assertNotNull(school.getSchoolId());
        assertNotNull(school.getCreated());
        assertNotNull(school.getUpdated());

        assertEquals(school.getCity(), city);
        assertEquals(school.getName(), name);
    }
}

PaperUserTestApp.java

@SpringBootApplication
public class PaperUserTestApp {

    public static void main(String[] args){
        SpringApplication.run(PaperUserTestApp.class, args);
    }

    @Configuration
    @ComponentScan("com.sp.fc.user")
    @EnableJpaRepositories(basePackages = {
            "com.sp.fc.user.repository"
    })
    @EntityScan(basePackages = {
            "com.sp.fc.user.domain"
    })
    class Config{
        // server쪽 Application에서는 스캔이 잘되지만 테스트 시에는 잘 안되는 문제가 있어 Config 빈을
        // 만들어 설정하였음
    }
}

SchoolTest.java

@DataJpaTest // DB에 데이터를 넣고 빼는 작업을 진행(Integration Test)
// 위 어노테이션 선언 시 DB 데이터 소스를 H2 DB에 인메모리 방식으로 만들고
// Repository는 스프링 컨테이너에서 기본적으로 만들어 줌 -> 서비스는 안만들어줌
public class SchoolTest {

    @Autowired
    private SchoolRepository schoolRepository;

    private SchoolService schoolService;

    private SchoolTestHelper schoolTestHelper;

    School school;

    @BeforeEach
    void before() {
        this.schoolRepository.deleteAll(); // Repository는 테스트 실행 시 초기화 하는 것이 좋음
        this.schoolService = new SchoolService(schoolRepository);
        this.schoolTestHelper = new SchoolTestHelper(this.schoolService);
        school = this.schoolTestHelper.createSchool("테스트 학교", "서울");
    }

    @Test
    void test1(){
        // 학교 생성 테스트
      List<School> list = schoolRepository.findAll();
      assertEquals(1, list.size());

      SchoolTestHelper.assertSchool(list.get(0), "테스트 학교", "서울");
    }

    @Test
    void test2(){
        //학교 이름 수정
        schoolService.updateName(school.getSchoolId(), "테스트2 학교");
        assertEquals(schoolRepository.findAll().get(0).getName(), "테스트2 학교");
    }

    @Test
    void test3(){
        // 지역 목록 가져오기
        List<String> list = schoolService.cities();
        assertEquals(1, list.size());
        assertEquals("서울", list.get(0));

        school = this.schoolTestHelper.createSchool("부산 학교", "부산");
        list = schoolService.cities();
        assertEquals(2, list.size());
    }

    @Test
    void test_4(){
        List<School> list = schoolService.findAllByCity("서울");
        assertEquals(1, list.size());
    }
}

선생님, 학생 도메인 설계

유저

  • 사용자를 생성 한다.
  • 이름을 수정할 수 있다.
  • 권한을 주면 권한이 주어진다.
  • 권한을 취소하면 권한이 취소된다.
  • email로 검색할 수 있다.
  • role이 중복해서 추가되지 않는다.
  • email이 중복되어서 등록되지 않는다.

선생님

  • 선생님을 등록한다.
  • 선생님으로 등록한 학생 리스트를 조회한다.
  • 선생님 리스트를 조회 한다.
  • 학교로 선생님이 조회된다.

학생

  • 학습자 등록 한다.
  • 선생님을 등록하면 선생님의 학생으로 조회된다
  • 학교로 학생이 조회된다.

구조

코드 예시

Authority.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "sp_authority")
@IdClass(Authority.class)
public class Authority implements GrantedAuthority {

    public static final String ROLE_TEACHER = "ROLE_TEACHER";
    public static final String ROLE_STUDENT = "ROLE_STUDENT";

    @Id
    private Long userId;

    @Id
    private String authority;
}

User.java(UserDetails)

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name="sp_user")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    private String name;

    @Column(unique = true)
    private String email;

    private String password;


    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(foreignKey = @ForeignKey(name = "userId"))
    private Set<Authority> authorities;

    private String grade;

    @ManyToOne(fetch = FetchType.EAGER)
    private User teacher;

    @ManyToOne(fetch = FetchType.EAGER)
    private School school;

    private boolean enabled;

    @Column(updatable = false)
    private LocalDateTime created;

    private LocalDateTime updated;

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return enabled;
    }

    @Override
    public boolean isAccountNonLocked() {
        return enabled;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return enabled;
    }

}

UserRepository.java

public interface UserRepository extends JpaRepository<User, Long> {

    @Modifying(clearAutomatically = true)
    @Query("update User set name = ?2 , updated = ?3 where userId = ?1")
    void updateUserName(Long userId, String userName, LocalDateTime update);

    Optional<User> findByEmail(String username);

    @Query("select a from User a, Authority b where a.userId = b.userId and b.authority=?1")
    List<User> findAllByAuthoritiesIn(String authority);

    // UI에서 구현할려면 Paging 된 리스트를 받을 수 있어야 함
    @Query("select a from User a, Authority b where a.userId = b.userId and b.authority=?1")
    Page<User> findAllByAuthoritiesIn(String authority, Pageable pageable);

    @Query("select a from User a, Authority b where a.school.schoolId = ?1 and a.userId = b.userId and b.authority = ?2")
    List<User> findAllBySchool(Long schoolId, String authority);

    @Query("select a from User a, User b where a.teacher.userId = b.userId and b.userId = ?1")
    List<User> findAllByTeacherUserId(Long userId);

    @Query("select count(a) from User a, User b where a.teacher.userId = b.userId and b.userId = ?1")
    long countByAllTeacherUserId(Long userId);

    @Query("select count(a) from User a, Authority b where a.userId = b.userId and b.authority = ?1")
    long countAllByAuthoritiesIn(String authority);

    @Query("select count(a) from User a, Authority b where a.school.schoolId = ?1 and a.userId = b.userId and b.authority = ?2")
    long countAllByAuthoritiesIn(long schoolId, String authority);

}

UserService.java

@Service
@RequiredArgsConstructor
@Transactional
public class UserService {

    private final SchoolRepository schoolRepository;
    private final UserRepository userRepository;

    public User save(User user) throws DataIntegrityViolationException{
        if(user.getUserId() == null){
            user.setCreated(LocalDateTime.now());
        }
        user.setUpdated(LocalDateTime.now());
        return userRepository.save(user);
    }

    public Optional<User> findUser(Long userId){
        return userRepository.findById(userId);
    }

    public Page<User> listUser(int pageNum, int size){
        return userRepository.findAll(PageRequest.of(pageNum-1, size));
    }

    public Map<Long,User> getUsers(List<Long> userIds){
        return StreamSupport.stream(userRepository.findAllById(userIds).spliterator(), false)
                .collect(Collectors.toMap(User::getUserId, Function.identity()));
    }

    public void addAuthority(Long userId, String authority){
        userRepository.findById(userId).ifPresent(user -> {
            Authority newRole = new Authority(user.getUserId(), authority);
            // @EqualsAnd~~ 어노테이션과 함께 참고
            if(user.getAuthorities() == null){
                HashSet<Authority> authorities = new HashSet<>();
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            } else if(!user.getAuthorities().contains(newRole)) {
                HashSet<Authority> authorities = new HashSet<>();
                authorities.addAll(user.getAuthorities());
                authorities.add(newRole);
                user.setAuthorities(authorities);
                save(user);
            }
        });
    }

    public void removeAuthority(Long userId, String authority){
        userRepository.findById(userId).ifPresent(user ->{
            if(user.getAuthorities() == null) return;
            Authority targetRole = new Authority(user.getUserId(), authority);
            if(user.getAuthorities().contains(targetRole)){
                user.setAuthorities(
                        user.getAuthorities().stream().filter(auth -> !auth.equals(targetRole))
                                .collect(Collectors.toSet())
                );
                save(user);
            }

        });
    }

    public void updateUsername(Long userId, String userName){
        userRepository.updateUserName(userId, userName, LocalDateTime.now());
    }

    public Optional<User> findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    public List<User> findTeacherList() {
        return userRepository.findAllByAuthoritiesIn(Authority.ROLE_TEACHER);
    }

    public List<User> findStudentList() {
        return userRepository.findAllByAuthoritiesIn(Authority.ROLE_STUDENT);
    }

    public List<User> findTeacherStudentList(Long userId) {
        return userRepository.findAllByTeacherUserId(userId);
    }

    public Long findTeacherStudentCount(Long userId) {
        return userRepository.countByAllTeacherUserId(userId);
    }

    public List<User> findBySchoolStudentList(Long schoolId) {
        return userRepository.findAllBySchool(schoolId, Authority.ROLE_STUDENT);
    }

    public List<User> findBySchoolTeacherList(Long schoolId) {
        return userRepository.findAllBySchool(schoolId, Authority.ROLE_TEACHER);
    }

    public void updateUserSchoolTeacher(Long userId, Long schoolId, Long teacherId) {
        // 선생님 소속 바꾸기
        userRepository.findById(userId).ifPresent(user->{
            if(!user.getSchool().getSchoolId().equals(schoolId)) {
                schoolRepository.findById(schoolId).ifPresent(school -> user.setSchool(school));
            }
            if(!user.getTeacher().getUserId().equals(teacherId)){
                findUser(teacherId).ifPresent(teacher->user.setTeacher(teacher));
            }
            if(user.getSchool().getSchoolId() != user.getTeacher().getSchool().getSchoolId()){
                throw new IllegalArgumentException("해당 학교의 선생님이 아닙니다.");
            }
            save(user);
        });
    }

    public long countTeacher() {
        return userRepository.countAllByAuthoritiesIn(Authority.ROLE_TEACHER);
    }
    public long countTeacher(long schoolId) {
        return userRepository.countAllByAuthoritiesIn(schoolId, Authority.ROLE_TEACHER);
    }

    public long countStudent() {
        return userRepository.countAllByAuthoritiesIn(Authority.ROLE_STUDENT);
    }
    public long countStudent(long schoolId) {
        return userRepository.countAllByAuthoritiesIn(schoolId, Authority.ROLE_STUDENT);
    }

    public Page<User> listStudents(Integer pageNum, Integer size) {
        return userRepository.findAllByAuthoritiesIn(Authority.ROLE_STUDENT, PageRequest.of(pageNum-1, size));
    }

    public Page<User> listTeachers(Integer pageNum, Integer size) {
        return userRepository.findAllByAuthoritiesIn(Authority.ROLE_TEACHER, PageRequest.of(pageNum-1, size));
    }

}

UserSecurityService.java

@Service
@RequiredArgsConstructor
@Transactional
public class UserSecurityService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByEmail(username).orElseThrow(()->new IllegalArgumentException(username+" 사용자가 존재하지 않습니다"));
    }
}

테스트 코드 예시

UserTestHelper.java

@RequiredArgsConstructor
public class UserTestHelper {

    private final UserService userService;

    private final PasswordEncoder passwordEncoder; //  = NoOpPasswordEncoder.getInstance();


    public static User makeUser(School school, String name){
        return User.builder()
                .school(school)
                .name(name)
                .email(name+"@test.com")
                .enabled(true)
                .build();
    }

    public User createUser(School school, String name){
        User user = makeUser(school, name);
        user.setPassword(passwordEncoder.encode(name+"123"));
        return userService.save(user);
    }

    public User createUser(School school, String name, String ... authorities){
        User user = createUser(school, name);
        Stream.of(authorities).forEach(auth->userService.addAuthority(user.getUserId(), auth));
        return user;
    }

    public User createTeacher(School school, String name){
        User teacher = createUser(school, name);
        userService.addAuthority(teacher.getUserId(), Authority.ROLE_TEACHER);
        return teacher;
    }

    public static void assertTeacher(School school, User teacher, String name){
        assertUser(school, teacher, name, Authority.ROLE_TEACHER);
    }

    public User createStudent(School school, User teacher, String name, String grade){
        User student = User.builder()
                .school(school)
                .name(name)
                .password(passwordEncoder.encode(name+"123"))
                .email(name+"@test.com")
                .teacher(teacher)
                .grade(grade)
                .enabled(true)
                .build();
        student = userService.save(student);
        userService.addAuthority(student.getUserId(), Authority.ROLE_STUDENT);
        return student;
    }

    public static void assertStudent(School school, User teacher, User student, String name, String grade){
        assertUser(school, student, name, Authority.ROLE_STUDENT);
        assertEquals(teacher.getUserId(), student.getTeacher().getUserId());
        assertEquals(grade, student.getGrade());
    }


    public static void assertUser(School school, User user, String name){
        assertNotNull(user.getUserId());
        assertNotNull(user.getCreated());
        assertNotNull(user.getUpdated());
        assertTrue(user.isEnabled());
        assertEquals(school.getSchoolId(), user.getSchool().getSchoolId());
        assertEquals(name, user.getName());
        assertEquals(name+"@test.com", user.getEmail());
    }


    public static void assertUser(School school, User user, String name, String ... authorities){
        assertUser(school, user, name);
        assertTrue(user.getAuthorities().containsAll(
                Stream.of(authorities).map(auth->new Authority(user.getUserId(), auth)).collect(Collectors.toSet())
        ));
    }
}

WithUserTest.java

public class WithUserTest {

    @Autowired
    protected SchoolRepository schoolRepository;
    @Autowired
    protected UserRepository userRepository;

    protected SchoolService schoolService;
    protected UserService userService;
    protected UserSecurityService userSecurityService;

    protected SchoolTestHelper schoolTestHelper;
    protected UserTestHelper userTestHelper;
    protected School school;

    private boolean prepared;

    protected void prepareUserServices (){
        if(prepared) return;
        prepared = true;

        this.schoolRepository.deleteAll();
        this.userRepository.deleteAll();
        this.schoolService = new SchoolService(schoolRepository);
        this.userService = new UserService(schoolRepository, userRepository);
        this.userSecurityService = new UserSecurityService(userRepository);

        this.userTestHelper = new UserTestHelper(userService, NoOpPasswordEncoder.getInstance());
        this.schoolTestHelper = new SchoolTestHelper(schoolService);
        this.school = this.schoolTestHelper.createSchool("테스트 학교", "서울");
    }

}

UserTest.java

@DataJpaTest
public class UserTest extends WithUserTest {

    @BeforeEach
    protected void before (){
        prepareUserServices();
    }

    @DisplayName("1. 사용자 생성")
    @Test
    void 사용자_생성 () {
        userTestHelper.createUser(school, "user1");
        List<User> list = StreamSupport.stream(userRepository.findAll().spliterator(), false)
                .collect(Collectors.toList());
        assertEquals(1,list.size());
        UserTestHelper.assertUser(school, list.get(0), "user1");
    }

    @DisplayName("2. 이름 수정")
    @Test
    void 이름_수정 () {
        User user = userTestHelper.createUser(school, "user1");
        userService.updateUsername(user.getUserId(), "user2");
        List<User> list = StreamSupport.stream(userRepository.findAll().spliterator(), false).collect(Collectors.toList());
        assertEquals("user2", list.get(0).getName());
    }


    @DisplayName("3. 권한 부여")
    @Test
    void 권한_부여하기 () {
        User user = userTestHelper.createUser(school, "user1", Authority.ROLE_STUDENT);
        userService.addAuthority(user.getUserId(), Authority.ROLE_TEACHER);
        User savedUser = userService.findUser(user.getUserId()).get();
        userTestHelper.assertUser(school, savedUser, "user1", Authority.ROLE_STUDENT, Authority.ROLE_TEACHER);
    }

    @DisplayName("4. 권한 취소")
    @Test
    void 권한_취소하기 () {
        User user1 = userTestHelper.createUser(school, "admin", Authority.ROLE_STUDENT, Authority.ROLE_TEACHER);
        userService.removeAuthority(user1.getUserId(), Authority.ROLE_STUDENT);
        User savedUser = userService.findUser(user1.getUserId()).get();
        userTestHelper.assertUser(school, savedUser, "admin", Authority.ROLE_TEACHER);
    }

    @DisplayName("5. email검색")
    @Test
    void 이메일_검색기능 () {
        User user1 = userTestHelper.createUser(school, "user1");
        User saved = (User) userSecurityService.loadUserByUsername("user1@test.com");
        userTestHelper.assertUser(school, saved, "user1");
    }

    @DisplayName("6. role 중복해서 추가되지 않는다.")
    @Test
    void Role_중복_문제 () {
        User user1 = userTestHelper.createUser(school, "user1", Authority.ROLE_STUDENT);
        userService.addAuthority(user1.getUserId(), Authority.ROLE_STUDENT);
        userService.addAuthority(user1.getUserId(), Authority.ROLE_STUDENT);
        User savedUser = userService.findUser(user1.getUserId()).get();
        assertEquals(1, savedUser.getAuthorities().size());
        userTestHelper.assertUser(school, savedUser, "user1", Authority.ROLE_STUDENT);
    }

    @DisplayName("7. email이 중복되어서 들어가는가?")
    @Test
    void 이메일_중복문제 () {
        // jpa 기능이라 굳이..테스트 할필요는 없을듯 , 만약 unique가 없는 경우 문제 생길것 테스트
        userTestHelper.createUser(school, "user1");
        assertThrows(DataIntegrityViolationException.class, ()->{
            userTestHelper.createUser(school, "user1");
        });
    }

}

TeacherTest.java

@DataJpaTest
public class TeacherTest extends WithUserTest {

    User teacher;

    @BeforeEach
    void before(){
        prepareUserServices();
        this.teacher = this.userTestHelper.createTeacher(school, "teacher1");
    }

    @DisplayName("1. 선생님을 등록한다. ")
    @Test
    void 선생님_등록_테스트 () {
        List<User> teacherList = userService.findTeacherList();
        assertEquals(1, teacherList.size());
        UserTestHelper.assertTeacher(school, teacherList.get(0), "teacher1");
    }

    @DisplayName("2. 선생님으로 등록한 학생 리스트를 조회한다.")
    @Test
    void 선생님으로_등록한_학생_리스트_조회 () {
        this.userTestHelper.createStudent(school, teacher, "study1", "1");
        this.userTestHelper.createStudent(school, teacher, "study2", "1");
        this.userTestHelper.createStudent(school, teacher, "study3", "1");
        assertEquals(3, userService.findTeacherStudentList(teacher.getUserId()).size());
    }

    @DisplayName("3. 선생님 리스트를 조회 한다.")
    @Test
    void 선생님_리스트_조회 () {
        this.userTestHelper.createUser(school, "teacher2", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "teacher3", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "teacher4", Authority.ROLE_TEACHER);
        assertEquals(4, userService.findTeacherList().size());
    }


    @DisplayName("4. 학교로 선생님이 조회된다.")
    @Test
    void 학교로_선생님_리스트_조회 () {
        List<User> teacherList = userService.findBySchoolTeacherList(school.getSchoolId());
        assertEquals(1, teacherList.size());
        UserTestHelper.assertTeacher(school, teacher, "teacher1");

        this.userTestHelper.createUser(school, "teacher2", Authority.ROLE_TEACHER);
        this.userTestHelper.createUser(school, "teacher3", Authority.ROLE_TEACHER);
        assertEquals(3, userService.findTeacherList().size());
    }
}

StudentTest.java

@DataJpaTest
public class StudentTest extends WithUserTest {

    User teacher;
    User student;

    @BeforeEach
    void before(){
        prepareUserServices();
        this.teacher = this.userTestHelper.createTeacher(school, "teacher1");
        this.student = this.userTestHelper.createStudent(school, teacher, "student1", "1");
    }

    @DisplayName("1. 학습자 등록 한다. ")
    @Test
    void 햑생_등록_테스트 () {
        List<User> studentList = userService.findStudentList();
        assertEquals(1, studentList.size());
        UserTestHelper.assertStudent(school, teacher, studentList.get(0), "student1", "1");
    }

    @DisplayName("2. 선생님으로 등록하면 선생님의 학습자가 조회된다 ")
    @Test
    void 선생님의_학생으로_조회된다 () {
        List<User> studentList = userService.findTeacherStudentList(teacher.getUserId());
        assertEquals(1, studentList.size());
        UserTestHelper.assertStudent(school, teacher, studentList.get(0), "student1", "1");
    }

    @DisplayName("3. 학교로 학습자가 조회된다.")
    @Test
    void 학교의_학생으로_조회된다 () {
        List<User> studentList = userService.findBySchoolStudentList(school.getSchoolId());
        assertEquals(1, studentList.size());
        UserTestHelper.assertStudent(school, teacher, studentList.get(0), "student1", "1");
    }
}

profile
Journey for Backend Developer

0개의 댓글