Spring boot rest api 회원가입 - spring boot rest api hoewongaib

  • 시큐리티를 이용한 JSON 데이터로 로그인 (완료)
  • JWT를 이용한 인증 (완료)
  • 도메인, 테이블 설계, 엔티티 생성 (완료)
  • 댓글 삭제 로직 구현 (완료)
  • 회원가입 + 정보수정 등 회원 서비스 구현 (진행 중)
  • 게시판 서비스 구현
  • 댓글 서비스 구현 (1댓글 -> *(무한) 대댓글 구조)
  • 예외 처리
  • 예외 메세지 국제화
  • 카테고리별 게시판 분류
  • 게시글 페이징
  • 동적인 검색 조건을 사용한 검색
  • 사용자 간 쪽지 기능
  • 무한 쪽지 스크롤
  • 게시물 & 댓글에 대한 알람
  • 쪽지에 대한 알람
  • 접속한 사용자 간 실시간 채팅
  • 회원가입 시 검증(예: XX대학교 XX과가 아니면 가입할 수 없게)
  • Swagger를 사용한 API 문서 만들기
  • 신고 & 블랙리스트 기능
  • AOP를 통한 로그
  • 어드민 페이지
  • 캐시
  • 배포 (+ 무중단 배포)
  • 배포 자동화
  • 포트와 어댑터 설계를 따르는 패키지 구조 설계하기
  • ...

회원 서비스 개발하기

우선 로그인은 이전에 개발해 두었기 때문에, 회원가입과 정보수정, 회원탈퇴, 정보조회를 구현하도록 하겠습니다.

현재 Member 클래스의 상태입니다.

@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@AllArgsConstructor
@Builder
public class Member extends BaseTimeEntity {

    @Id @GeneratedValue(strategy = IDENTITY)
    @Column(name = "member_id")
    private Long id; //primary Key

    @Column(nullable = false, length = 30, unique = true)
    private String username;//아이디

    private String password;//비밀번호

    @Column(nullable = false, length = 30)
    private String name;//이름(실명)

    @Column(nullable = false, length = 30)
    private String nickName;//별명

    @Column(nullable = false, length = 30)
    private Integer age;//나이

    @Enumerated(STRING)
    @Column(nullable = false, length = 30)
    private Role role;//권한 -> USER, ADMIN


    @Column(length = 1000)
    private String refreshToken;//RefreshToken


    //== 회원탈퇴 -> 작성한 게시물, 댓글 모두 삭제 ==//
    @OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
    private List<Post> postList = new ArrayList<>();

    @OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
    private List<Comment> commentList = new ArrayList<>();





    //== 연관관계 메서드 ==//
    public void addPost(Post post){
        //post의 writer 설정은 post에서 함
        postList.add(post);
    }

    public void addComment(Comment comment){
        //comment의 writer 설정은 comment에서 함
        commentList.add(comment);
    }









    //== 정보 수정 ==//
    public void updatePassword(PasswordEncoder passwordEncoder, String password){
        this.password = passwordEncoder.encode(password);
    }

    public void updateName(String name){
        this.name = name;
    }

    public void updateNickName(String nickName){
        this.nickName = nickName;
    }

    public void updateAge(int age){
        this.age = age;
    }

    public void updateRefreshToken(String refreshToken){
        this.refreshToken = refreshToken;
    }
    public void destroyRefreshToken(){
        this.refreshToken = null;
    }



    //== 패스워드 암호화 ==//
    public void encodePassword(PasswordEncoder passwordEncoder){
        this.password = passwordEncoder.encode(password);
    }

}

다음 코드를 추가해주겠습니다.

//비밀번호 변경, 회원 탈퇴 시, 비밀번호를 확인하며, 이때 비밀번호의 일치여부를 판단하는 메서드입니다.
public boolean matchPassword(PasswordEncoder passwordEncoder, String checkPassword){
    return passwordEncoder.matches(checkPassword, getPassword());
}


//회원가입시, USER의 권한을 부여하는 메서드입니다.
public void addUserAuthority() {
    this.role = Role.USER;
}

그리고 현재 MemberRepository의 상태입니다.

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByUsername(String username);

    boolean existsByUsername(String username);

    Optional<Member> findByRefreshToken(String refreshToken);
}

이제 MemberService를 개발해보도록 하겠습니다.

MemberService

위치는 다음과 같습니다.

Spring boot rest api 회원가입 - spring boot rest api hoewongaib
public interface MemberService {

    /**
     * 회원가입
     * 정보수정
     * 회원탈퇴
     * 정보조회
     */

    void signUp(MemberSignUpDto memberSignUpDto) throws Exception;

    void update(MemberUpdateDto memberUpdateDto) throws Exception;

    void updatePassword(String checkPassword, String toBePassword) throws Exception;

    void withdraw(String checkPassword) throws Exception;

    MemberInfoDto getInfo(Long id) throws Exception;
    
    MemberInfoDto getMyInfo() throws Exception;


}

DTO

사용할 DTO들은 다음 위치에 생성했습니다.

Spring boot rest api 회원가입 - spring boot rest api hoewongaib

MemberInfoDto

@Data
public class MemberInfoDto {

    private final String name;
    private final String nickName;
    private final String username;
    private final Integer age;


    @Builder
    public MemberInfoDto(Member member) {
        this.name = member.getName();
        this.nickName = member.getNickName();
        this.username = member.getUsername();
        this.age = member.getAge();
    }
}

MemberSignUpDto

public record MemberSignUpDto(String username, String password, String name,
                              String nickName, Integer age) {

    public Member toEntity() {
        return Member.builder().username(username).password(password).name(name).nickName(nickName).age(age).build();
    }
}

MemberUpdateDto

public record MemberUpdateDto(Optional<String> name, Optional<String> nickName,
                              Optional<Integer> age) {
}

SecurityUtil

SecurityContextHolder에서 username을 꺼내오는 코드를, 해당 클래스를 통해서 작성한 후, 편하게 사용하도록 하겠습니다.

public class SecurityUtil {
    public static String getLoginUsername(){
        UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getUsername();
    }
}

MemberServiceImpl

@Service
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;



    @Override
    public void signUp(MemberSignUpDto memberSignUpDto) throws Exception {
        Member member = memberSignUpDto.toEntity();
        member.addUserAuthority();
        member.encodePassword(passwordEncoder);

        if(memberRepository.findByUsername(memberSignUpDto.username()).isPresent()){
            throw new Exception("이미 존재하는 아이디입니다.");
        }

        memberRepository.save(member);
    }

    @Override
    public void update(MemberUpdateDto memberUpdateDto) throws Exception {
        Member member = memberRepository.findByUsername(SecurityUtil.getLoginUsername()).orElseThrow(() -> new Exception("회원이 존재하지 않습니다"));

        memberUpdateDto.age().ifPresent(member::updateAge);
        memberUpdateDto.name().ifPresent(member::updateName);
        memberUpdateDto.nickName().ifPresent(member::updateNickName);
    }


    @Override
    public void updatePassword(String checkPassword, String toBePassword) throws Exception {
        Member member = memberRepository.findByUsername(SecurityUtil.getLoginUsername()).orElseThrow(() -> new Exception("회원이 존재하지 않습니다"));

        if(!member.matchPassword(passwordEncoder, checkPassword) ) {
            throw new Exception("비밀번호가 일치하지 않습니다.");
        }

        member.updatePassword(passwordEncoder, toBePassword);
    }


    @Override
    public void withdraw(String checkPassword) throws Exception {
        Member member = memberRepository.findByUsername(SecurityUtil.getLoginUsername()).orElseThrow(() -> new Exception("회원이 존재하지 않습니다"));

        if(!member.matchPassword(passwordEncoder, checkPassword) ) {
            throw new Exception("비밀번호가 일치하지 않습니다.");
        }

        memberRepository.delete(member);
    }




    @Override
    public MemberInfoDto getInfo(Long id) throws Exception {
        Member findMember = memberRepository.findById(id).orElseThrow(() -> new Exception("회원이 없습니다"));
        return new MemberInfoDto(findMember);
    }

    @Override
    public MemberInfoDto getMyInfo() throws Exception {
        Member findMember = memberRepository.findByUsername(SecurityUtil.getLoginUsername()).orElseThrow(() -> new Exception("회원이 없습니다"));
        return new MemberInfoDto(findMember);
    }
}

설명

더보기

1. signUp()

회원가입을 진행하는 메서드입니다.

회원가입 시 컨트롤러 단에서 엔티티로 변환하여 받아오는 것이 아니라, 서비스 단에서 DTO를 엔티티로 변환하였습니다.

변환 후 USER라는 권한을 설정하였고, 이후 중복된 아이디가 있는지 체크합니다.

없다면 회원가입을 진행합니다.

2. update()

회원정보를 수정합니다.

MemberUpdateDto는 Optional 필드들을 가지고 있으며,

ifPresent를 통해 필드가 존재하는 경우에만 업데이트를 진행하도록 작성하였습니다.

3. updatePassword()

비밀번호를 변경하는 메서드입니다.

비밀번호는 다른 회원정보들과 다르게 무조건 따로 업데이트 해야 하며,

비밀번호 변경시에는 현재 비밀번호를 입력받아 보안을 강화합니다.

Member 클래스에 matchPassword()라는 메서드를 만들어 비밀번호가 일치하는지 확인하는 메서드를 생성하였고, 일치한다면, 변경하고자 하는 비밀번호 (toBePassword)로 변경합니다

4. withdraw()

회원탈퇴를 진행하는 메서드입니다.

비밀번호를 재입력받아 비밀번호가 일치해야만 회원탈퇴를 진행합니다.

5. getInfo()

id를 받아와서 해당 회원의 정보를 조회하는 메서드입니다.

MemberInfoDto의 형태로 감싸서 반환하며 이는 이후에 비공개 계정과 공개 계정을 나누어 비공개 계정일 경우 정보조회를 할 수 없도록 만들고 싶습니다.

6. getMyInfo()

나의 정보를 가져오는 메서드입니다.

현재 나의 정보는 로그인 한 경우 SecurityContextHolder에 들어있기 때문에 따로 입력받지 않아도 인증만 되어있다면 정보 조회가 가능합니다.

테스트코드

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceTest {


    @Autowired EntityManager em;
    @Autowired
    MemberRepository memberRepository;

    @Autowired MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    String PASSWORD = "password";

    private void clear(){
        em.flush();
        em.clear();
    }

    private MemberSignUpDto makeMemberSignUpDto() {
        return new MemberSignUpDto("username",PASSWORD,"name","nickNAme",22);
    }

    private MemberSignUpDto setMember() throws Exception {
        MemberSignUpDto memberSignUpDto = makeMemberSignUpDto();
        memberService.signUp(memberSignUpDto);
        clear();
        SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();

        emptyContext.setAuthentication(new UsernamePasswordAuthenticationToken(User.builder()
                .username(memberSignUpDto.username())
                .password(memberSignUpDto.password())
                .roles(Role.USER.name())
                .build(),
                null, null));

        SecurityContextHolder.setContext(emptyContext);
        return memberSignUpDto;
    }


    @AfterEach
    public void removeMember(){
        SecurityContextHolder.createEmptyContext().setAuthentication(null);
    }







    /**
     * 회원가입
     *    회원가입 시 아이디, 비밀번호, 이름, 별명, 나이를 입력하지 않으면 오류
     *    이미 존재하는 아이디가 있으면 오류
     *    회원가입 후 회원의 ROLE 은 USER
     *
     *
     */
	@Test
	public void 회원가입_성공() throws Exception {
    	//given
    	MemberSignUpDto memberSignUpDto = makeMemberSignUpDto();

    	//when
    	memberService.signUp(memberSignUpDto);
    	clear();

    	//then  TODO : 여기 MEMBEREXCEPTION으로 고치기
   		Member member = memberRepository.findByUsername(memberSignUpDto.username()).orElseThrow(() -> new Exception("회원이 없습니다"));
    	assertThat(member.getId()).isNotNull();
    	assertThat(member.getUsername()).isEqualTo(memberSignUpDto.username());
    	assertThat(member.getName()).isEqualTo(memberSignUpDto.name());
    	assertThat(member.getNickName()).isEqualTo(memberSignUpDto.nickName());
    	assertThat(member.getAge()).isEqualTo(memberSignUpDto.age());
    	assertThat(member.getRole()).isSameAs(Role.USER);

	}

    @Test
	public void 회원가입_실패_원인_아이디중복() throws Exception {
    	//given
    	MemberSignUpDto memberSignUpDto = makeMemberSignUpDto();
    	memberService.signUp(memberSignUpDto);
    	clear();

    	//when, then TODO : MemberException으로 고쳐야 함
    	assertThat(assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto)).getMessage()).isEqualTo("이미 존재하는 아이디입니다.");

	}


    @Test
    public void 회원가입_실패_입력하지않은_필드가있으면_오류() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto1 = new MemberSignUpDto(null,passwordEncoder.encode(PASSWORD),"name","nickNAme",22);
        MemberSignUpDto memberSignUpDto2 = new MemberSignUpDto("username",null,"name","nickNAme",22);
        MemberSignUpDto memberSignUpDto3 = new MemberSignUpDto("username",passwordEncoder.encode(PASSWORD),null,"nickNAme",22);
        MemberSignUpDto memberSignUpDto4 = new MemberSignUpDto("username",passwordEncoder.encode(PASSWORD),"name",null,22);
        MemberSignUpDto memberSignUpDto5 = new MemberSignUpDto("username",passwordEncoder.encode(PASSWORD),"name","nickNAme",null);


        //when, then

        assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto1));

        assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto2));

        assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto3));

        assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto4));

        assertThrows(Exception.class, () -> memberService.signUp(memberSignUpDto5));
    }


    /**
     * 회원정보수정
     * 회원가입을 하지 않은 사람이 정보수정시 오류 -> 시큐리티 필터가 알아서 막아줄거임
     * 아이디는 변경 불가능
     * 비밀번호 변경시에는, 현재 비밀번호를 입력받아서, 일치한 경우에만 바꿀 수 있음
     * 비밀번호 변경시에는 오직 비밀번호만 바꿀 수 있음
     *
     * 비밀번호가 아닌 이름,별명,나이 변경 시에는, 3개를 한꺼번에 바꿀 수도 있고, 한,두개만 선택해서 바꿀수도 있음
     * 아무것도 바뀌는게 없는데 변경요청을 보내면 오류
     *
     */



    @Test
    public void 회원수정_비밀번호수정_성공() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();


        //when
        String toBePassword = "1234567890!@#!@#";
        memberService.updatePassword(PASSWORD, toBePassword);
        clear();

        //then
        Member findMember = memberRepository.findByUsername(memberSignUpDto.username()).orElseThrow(() -> new Exception());
        assertThat(findMember.matchPassword(passwordEncoder, toBePassword)).isTrue();

    }



    @Test
    public void 회원수정_이름만수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        String updateName = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.of(updateName),Optional.empty(), Optional.empty()));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getName()).isEqualTo(updateName);
            assertThat(member.getAge()).isEqualTo(memberSignUpDto.age());
            assertThat(member.getNickName()).isEqualTo(memberSignUpDto.nickName());
        }));

    }
    @Test
    public void 회원수정_별명만수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        String updateNickName = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.empty(), Optional.of(updateNickName), Optional.empty()));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getNickName()).isEqualTo(updateNickName);
            assertThat(member.getAge()).isEqualTo(memberSignUpDto.age());
            assertThat(member.getName()).isEqualTo(memberSignUpDto.name());
        }));

    }

    @Test
    public void 회원수정_나이만수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        Integer updateAge = 33;
        memberService.update(new MemberUpdateDto(Optional.empty(),  Optional.empty(), Optional.of(updateAge)));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getAge()).isEqualTo(updateAge);
            assertThat(member.getNickName()).isEqualTo(memberSignUpDto.nickName());
            assertThat(member.getName()).isEqualTo(memberSignUpDto.name());
        }));
    }


    @Test
    public void 회원수정_이름별명수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        String updateNickName = "변경할래요옹";
        String updateName = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.of(updateName),Optional.of(updateNickName),Optional.empty()));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getNickName()).isEqualTo(updateNickName);
            assertThat(member.getName()).isEqualTo(updateName);

            assertThat(member.getAge()).isEqualTo(memberSignUpDto.age());
        }));

    }

    @Test
    public void 회원수정_이름나이수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        Integer updateAge = 33;
        String updateName = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.of(updateName),Optional.empty(),Optional.of(updateAge)));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getAge()).isEqualTo(updateAge);
            assertThat(member.getName()).isEqualTo(updateName);

            assertThat(member.getNickName()).isEqualTo(memberSignUpDto.nickName());
        }));


    }
    @Test
    public void 회원수정_별명나이수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        Integer updateAge = 33;
        String updateNickname = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.empty(),Optional.of(updateNickname),Optional.of(updateAge)));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getAge()).isEqualTo(updateAge);
            assertThat(member.getNickName()).isEqualTo(updateNickname);

            assertThat(member.getName()).isEqualTo(memberSignUpDto.name());
        }));

    }

    @Test
    public void 회원수정_이름별명나이수정() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        Integer updateAge = 33;
        String updateNickname = "변경할래용";
        String updateName = "변경할래용";
        memberService.update(new MemberUpdateDto(Optional.of(updateName),Optional.of(updateNickname),Optional.of(updateAge)));
        clear();

        //then
        memberRepository.findByUsername(memberSignUpDto.username()).ifPresent((member -> {
            assertThat(member.getAge()).isEqualTo(updateAge);
            assertThat(member.getNickName()).isEqualTo(updateNickname);
            assertThat(member.getName()).isEqualTo(updateName);
        }));
    }

    /**
     * 회원탈퇴
     * 비밀번호를 입력받아서 일치하면 탈퇴 가능
     */

    @Test
    public void 회원탈퇴() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        memberService.withdraw(PASSWORD);

        //then
        assertThat(assertThrows(Exception.class, ()-> memberRepository.findByUsername(memberSignUpDto.username()).orElseThrow(() -> new Exception("회원이 없습니다"))).getMessage()).isEqualTo("회원이 없습니다");

    }

	@Test
	public void 회원탈퇴_실패_비밀번호가_일치하지않음() throws Exception {
    	//given
    	MemberSignUpDto memberSignUpDto = setMember();

    	//when, then TODO : MemberException으로 고쳐야 함
    	assertThat(assertThrows(Exception.class ,() -> memberService.withdraw(PASSWORD+"1")).getMessage()).isEqualTo("비밀번호가 일치하지 않습니다.");

	}




    @Test
    public void 회원정보조회() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();
        Member member = memberRepository.findByUsername(memberSignUpDto.username()).orElseThrow(() -> new Exception());
        clear();

        //when
        MemberInfoDto info = memberService.getInfo(member.getId());

        //then
        assertThat(info.getUsername()).isEqualTo(memberSignUpDto.username());
        assertThat(info.getName()).isEqualTo(memberSignUpDto.name());
        assertThat(info.getAge()).isEqualTo(memberSignUpDto.age());
        assertThat(info.getNickName()).isEqualTo(memberSignUpDto.nickName());
    }

    @Test
    public void 내정보조회() throws Exception {
        //given
        MemberSignUpDto memberSignUpDto = setMember();

        //when
        MemberInfoDto myInfo = memberService.getMyInfo();

        //then
        assertThat(myInfo.getUsername()).isEqualTo(memberSignUpDto.username());
        assertThat(myInfo.getName()).isEqualTo(memberSignUpDto.name());
        assertThat(myInfo.getAge()).isEqualTo(memberSignUpDto.age());
        assertThat(myInfo.getNickName()).isEqualTo(memberSignUpDto.nickName());

    }

}

3개의 메서드만 살펴보겠습니다.

private MemberSignUpDto makeMemberSignUpDto() {
    return new MemberSignUpDto("username",PASSWORD,"name","nickNAme",22);
}

MemberSignUpDto를 반환하는 메서드입니다. Http요청을 보내는 것이 아니기때문에 임의로 생성해 주었습니다.

private MemberSignUpDto setMember() throws Exception {
    MemberSignUpDto memberSignUpDto1 = makeMemberSignUpDto();
    memberService.signUp(memberSignUpDto1);
    clear();
    SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();

    emptyContext.setAuthentication(new UsernamePasswordAuthenticationToken(User.builder()
            .username(memberSignUpDto1.username())
            .password(memberSignUpDto1.password())
            .roles(Role.USER.name())
            .build(),
            null, null));
    SecurityContextHolder.setContext(emptyContext);
    return memberSignUpDto1;
}

회원가입을 진행한 후 SecurityContextHolder에 인증된 회원정보를 저장하는 메서드입니다.

반환은 회원가입 시 사용했던 MemberSignUpDto를 반환합니다.

@AfterEach
public void removeMember(){
    SecurityContextHolder.createEmptyContext().setAuthentication(null);
}

테스트가 끝날 때 마다, SecurityContextHolder의 Authentication(인증)정보를 비워줍니다.

회원 서비스는 구현이 끝났습니다. 이제 Controller를 만들고, 테스트를 진행해보도록 하겠습니다.

전체 코드는 깃허브에서 확인하실 수 있습니다.

https://github.com/ShinDongHun1/SpringBoot-Board-API

GitHub - ShinDongHun1/SpringBoot-Board-API

Contribute to ShinDongHun1/SpringBoot-Board-API development by creating an account on GitHub.

github.com

Spring boot rest api 회원가입 - spring boot rest api hoewongaib