본문 바로가기

Server Programming/Spring Boot Backend Programming

8장-3. 회원 데이터 처리 (+ UserDetailsService, enum, MSA)

반응형

요구사항

  • Spring Data JPA로 회원 데이터 구성
  • UserDetailsService를 이용해 사용자 정보 로딩
  • @ElementCollection을 이용해 여러 개의 권한을 갖는 회원 엔티티 구성

회원 데이터의 구성

  • mid 회원 아이디
  • del 탈퇴여부
  • mpw 패스워드
  • regDate, modDate 등록일/수정일
  • email 이메일 
  • social 소셜 로그인 자동 회원 가입 여부

1. Spring Data JPA로 회원 데이터 구성

 

1. MemberRole enum 클래스로 이용해 사용자의 두 가지 권한 설정

package org.zerock.b01.domain;

public enum MemberRole {
    USER, ADMIN;
}

 

2. Member 엔티티 작성

  • 엔티티 명 : Member
  • 멤버 변수 : @Id mid, mpw, email, del, social, @ElementCollection Set<Role> roleSet
  • 메서드 : change={Password, Email ,Del, Social}, add={Role}, clear ={Role}
package org.zerock.b01.domain;

import lombok.*;

import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import java.util.HashSet;
import java.util.Set;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "roleSet")
public class Member extends BaseEntity{
    
    @Id
    private String mid;
    
    private String mpw;
    private String email;
    private boolean del;
    
    private boolean social;
    
    //enum 클래스를 이용해 권한을 가지는 자료구조 생성
    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private Set<MemberRole> roleSet= new HashSet<>();
    
    //변경 가능한 정보들 메서드로 생성
    public void changePassword(String mpw){
        this.mpw=mpw;
    }
    public void changeEmail(String email){
        this.email=email;
    }
    public void changeDel(boolean del){
        this.del=del;
    }
    public void changeSocial(boolean social){
        this.social=social;
    }
    //추가 가능한 정보 메서드로 생성
    public void addRole(MemberRole memberRole){
        this.roleSet.add(memberRole);
    }
    //초기화 가능한 정보들 메서드로 생성
    public void clearRoles(){
        this.roleSet.clear();
    }

}

 

3. MemberRepository와 테스트 코드 작성

 

(1) MemberRepository 선언

-MemberRepository에 로그인 시에 MemberRole을 로딩하는 메서드 작성

package org.zerock.b01.repository;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.zerock.b01.domain.Member;

import javax.persistence.Entity;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, String> {
    
    @EntityGraph(attributePaths = "roleSet")
    @Query("select m from Member m where m.mid= :mid and m.social = false")
    Optional<Member> getWithRoles(String mid);
    
}

 

(2) 일반 회원 추가 테스트 코드 작성

-MemeberRepositoryTests 클래스 작성

-PasswordEncoder로 mpw 처리한 일반 회원 데이터 추가하는 테스트 

 

package org.zerock.b01.repository;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.zerock.b01.domain.Member;
import org.zerock.b01.domain.MemberRole;

import java.util.stream.IntStream;

@SpringBootTest
@Log4j2
public class MemberRepositoryTests {
    
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Test
    public void insertMembers(){
        IntStream.rangeClosed(1, 100).forEach(i->{

            Member member=Member.builder()
                    .mid("member"+i)
                    .mpw(passwordEncoder.encode("1111"))
                    .email("email"+i+"@aaa.bbb")
                    .build();
            
            member.addRole(MemberRole.USER);
            
            if(i>=90){
                member.addRole(MemberRole.ADMIN);
            }
            memberRepository.save(member);
        });
    }
}

 

(3) 회원 조회 테스트

-MemberRole과 함께 로딩하는지 확인

//회원 조회시 멤버의 권한도 조회하는지 확이하는 테스트
@Test
public void testRead(){
    Optional<Member> result = memberRepository.getWithRoles("member100");

    Member member=result.orElseThrow();

    log.info(member);
    log.info(member.getRoleSet());

    member.getRoleSet().forEach(memberRole -> log.info(memberRole.name()));
}

 

Member(mid=member100, mpw=$2a$10$IY45qFsPN9FnIkN2EwN/Z.akWdpdbnXNSEMV4seAYq219YMKFgLom, email=email100@aaa.bbb, del=false, social=false)

[ADMIN, USER]
ADMIN
USER

 


2. 회원 서비스와 DTO처리

스프링 시큐리티에서 회원 DTO는 해당 API인 UserDetails라는 타입에 맞게 작성되어야 한다.

따라서, /security/dto/MemberSecurityDTO 클래스를 정의한다.

 

(1) MemberSecurityDTO 작성

-상속한 User 클래스를 이용한 생성자를 통해 UserDetails 타입으로 객체를 생성한다.

  • DTO 명 : MemberSecurityDTO
  • 멤버 변수 : mid, mpw, email, del, social
  • 상속 : UserDetails 인테페이스를 구현한 User 클래스
  • 생성자 : 상속한 User 클래스의 생성자 호출(username, password, authorities)
package org.zerock.b01.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Getter
@Setter
@ToString
public class MemberSecurityDTO extends User {

    private String mid;
    private String mpw;
    private String email;
    private boolean del;
    private boolean social;


    public MemberSecurityDTO(String username, String password, String email, boolean del, boolean social, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);

        this.mid=username;
        this.mpw=password;
        this.email= email;
        this.del=del;
        this.social=social;
    }
}

 

(2) CustomUserDetailsService 수정

-실제 로그인 처리 담당하는 CustomUserDetailsService

-MemberRepository 주입받아 MemberSecurityDTO 반환

 

변경 전, CustomUserDetailsService

package org.zerock.b01.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.zerock.b01.domain.Member;
import org.zerock.b01.repository.MemberRepository;


@Log4j2
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    //패스워드 인코더 의존성 주입
    private final PasswordEncoder passwordEncoder;

    //로그인 처리를 위한 의존성 주입
    private final MemberRepository memberRepository;
    public CustomUserDetailsService(){
        this.passwordEncoder=new BCryptPasswordEncoder();
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.info("loadUserByUsername: " + username);
//        return null;

        UserDetails userDetails = User.builder()
                .username("user1")
                //.password("1111")
                .password(passwordEncoder.encode("1111"))
                .authorities("ROLE_USER")
                .build();

        return userDetails;
    }

}

 

변경 후, CustomUserDetailsService

package org.zerock.b01.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.zerock.b01.domain.Member;
import org.zerock.b01.repository.MemberRepository;
import org.zerock.b01.security.dto.MemberSecurityDTO;

import javax.swing.text.html.Option;
import java.util.Optional;
import java.util.stream.Collectors;


@Log4j2
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    //패스워드 인코더 의존성 주입
//    private final PasswordEncoder passwordEncoder;



//    public CustomUserDetailsService(){
//        this.passwordEncoder=new BCryptPasswordEncoder();
//    }

//    @Override
//    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//
//        log.info("loadUserByUsername: " + username);
////        return null;
//
//        //사용자 확인
//        Optional<Member> result = memberRepository.getWithRoles(username);
//
//
//        UserDetails userDetails = User.builder()
//                .username("user1")
//                //.password("1111")
//                .password(passwordEncoder.encode("1111"))
//                .authorities("ROLE_USER")
//                .build();
//
//        return userDetails;
//    }
//로그인 처리를 위한 의존성 주입
private final MemberRepository memberRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.info("loadUserByUsername: " + username);

        Optional<Member> result = memberRepository.getWithRoles(username);

        if (result.isEmpty()) { //해당 아이디를 가진 사용자가 없다면
            throw new UsernameNotFoundException("username not found...");
        }

        Member member = result.get();

        MemberSecurityDTO memberSecurityDTO =
                new MemberSecurityDTO(
                        member.getMid(),
                        member.getMpw(),
                        member.getEmail(),
                        member.isDel(),
                        false,
                        member.getRoleSet()
                                .stream().map(memberRole -> new SimpleGrantedAuthority("ROLE_" + memberRole.name()))
                                .collect(Collectors.toList())
                );

        log.info("memberSecurityDTO");
        log.info(memberSecurityDTO);

        return memberSecurityDTO;
    }
}

 

 

 


 

회원 도메인과 연관관계

  • 모놀리틱
    • 하나의 서비스에 회원과 주문이 모두 같은 컨텍스트로 구성
    • Member 도메인을 Board나 Reply에 연결해 다대일 연관관계 구성(@ManyToOne)
      • Member 데이터를 모든 서비스에 참조해서 사용하는 구조
      • 하나의 서비스에 각각의 소규모 서비스가 같은 컨텍스트로 구성
  • MSA
    • 여러 개의 독립 서비스를 연계해 하나의 큰 서비스 구성
    • 연관관계 없이 각각의 서비스가 다른 컨텍스트로 구성돼 필요하다면 서비스를 연계해서 구성

 

 

반응형