종우의 삶 (전체 공개)

Code refactoring - 2 본문

개발/Spring

Code refactoring - 2

jonggae 2024. 1. 10. 14:22

대상 repository : https://github.com/Jonggae/group-invitation ( Readme 참고 )

 

목표 : 가독성 향상, 부족한 기능 완성, 테스트코드 작성, 완성도 있는 코드 

 

 


 

1.원래 구현 목표에 맞게 클래스를 추가

package com.exam.invitation.authority;

public enum MemberRoleEnum {
    TEMPMEMBER(Authority.TEMPMEMBER),
    MEMBER(Authority.MEMBER);

    private final String authority;

    MemberRoleEnum(String authority) {
        this.authority=authority;
    }
    public String getAuthority() {
        return this.authority;
    }

    public static class Authority{
        public static final String TEMPMEMBER = "ROLE_TEMPMEMBER";
        public static final String MEMBER = "ROLE_MEMBER";
    }
}

 

주요 기능의 로직은 이러하다

 

1. 초대 링크를 생성한다 (uuid를 포함한 무작위 도메인)

2. 초대 링크를 보낼 임시 멤버를 추가한다.

    2- 1. #2에서 멤버의 이름, 이메일을 체크한 뒤에

    2- 2. 임시 멤버를 db에 추가 한다.

 

이러한 기존의 로직에서 임시 멤버에 대한 설정을 추가하기로 하였다.

 

MemberRoleEnum 클래스를 생성하여

이메일을 보내어 초대 링크를 수락하기전 까지는 TEMPMEMBER 상태로 DB에 저장이 되고,

초대 링크를 수락한 후에는 MEMBER로 DB에 저장이 된다.

이 두가지 role이 있으므로 권한을 조절해 주면 나중에 회원간 차이를 둘 수 있을 것이다.

 

연관된 다른 클래스에도 이 권한관련 로직을 추가한다.

 

Member.java (멤버 엔티티)

  @Enumerated(EnumType.STRING)
    private MemberRoleEnum role = MemberRoleEnum.TEMPMEMBER;

    private boolean isActivated = Boolean.FALSE; //임시 회원이므로 활성화 태그를 만들어놓고 활성화 시키지 않음.

 

이후 링크 서비스 로직에서 링크를 눌러 수락하였을 때 activate시키는 로직을 추가한다.

public void acceptInvitationLink(Long id) {
        Member activateMember = tempMemberRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다."));
        activateMember.activate(); // <<추가됨
        tempMemberRepository.save(activateMember);

 

--제언

결과적으로 최소한의 기능을 구현했다고 생각한다. 중간중간 아직 구현하지 않은 부분(링크의 만료같은)이나 사용되지 않는 메서드들이 보이지만 다른 프로젝트나 또다른 리팩토링에서 정리해나가면 될 듯.

 


 

2. 테스트코드 작성

어느정도 기능이 구현되었으니 새롭게 테스트코드를 작성해 본다.

 

모킹을 활용한 테스트로 변경해보았다. 자료를 여러가지 조사해보았으나 딱히 더 좋다고 생각되지 않았다. 테스트코드는 어쨌든 그 기능을 테스트하고 잘 작동되는지 확인을 하면된다고 생각함.

 

구현 기능들을 정리하면서 필요없는 테스트는 정리하고 꼭 필요한 것들만 추려냈다.

 

1. 링크 생성 확인 (단순한 uuid 작동 확인) 테스

2. 임시 회원 생성 + 링크 생성 확인 테스트

3. 컨트롤러 응답 테스트

 

 

1. 링크 생성 확인

package com.exam.invitation.service;

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 static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
class InvitationLinkServiceTest {

    @Autowired
    private InvitationLinkService invitationLinkService;

    @Test
    @DisplayName("회원 초대 링크가 제대로 생성되는지 확인")
    void generateInvitationLink() {
        //given

        //when
        String generatedLink1 = invitationLinkService.generateInvitationLink();
        String generatedLink2 = invitationLinkService.generateInvitationLink();

        //then
        assertNotNull(generatedLink1);
        assertNotNull(generatedLink2);
        System.out.println(generatedLink1);
        System.out.println(generatedLink2);
        assertNotEquals(generatedLink1, generatedLink2);

    }
}

print를 사용한 것은 확인용이었다. 원래는 빼도 된다고 한다. -> 어차피 통과를 하면 터미널에 아무것도 뜨지 않는 것이 낫기 때문. 그냥링크 생성 주소가 궁금해서 넣어봤다.

 

 

2. 임시 회원 생성 + 링크 생성 확인 테스트

package com.exam.invitation.service;

import com.exam.invitation.domain.Member;
import com.exam.invitation.dto.MemberDto;
import com.exam.invitation.repository.MemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;



class MemberServiceTest {

    @InjectMocks
    private MemberService memberService;

    @Mock
    private InvitationLinkService invitationLinkService;
    @Mock
    private MemberRepository memberRepository;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    @DisplayName("초대 링크를 보내기 위해 임시 회원 생성, 링크 생성 확인")
    @Transactional
    void inviteMember() {
        //given
        MemberDto memberDto = createMemberDto();

        when(invitationLinkService.generateInvitationLink()).thenReturn("");
        when(memberRepository.findByName(any())).thenReturn(Optional.empty());
        when(memberRepository.findByEmail(any())).thenReturn(Optional.empty());

        //when
        memberService.createMember(memberDto);

        // then
        verify(invitationLinkService, times(1)).generateInvitationLink();
        verify(memberRepository, times(1)).findByName(any());
        verify(memberRepository, times(1)).findByEmail(any());

        ArgumentCaptor<Member> memberCaptor = ArgumentCaptor.forClass(Member.class);
        verify(memberRepository, times(1)).save(memberCaptor.capture());

        Member savedMember = memberCaptor.getValue();
        System.out.println("Actual Email: " + savedMember.getEmail());  // 추가된 로깅 문
        System.out.println("Actual Phone Number: " + savedMember.getPhoneNumber());  // 추가된 로깅 문

        assertEquals("CJW", savedMember.getName());
        assertEquals("CJW@mail.com", savedMember.getEmail());
        assertEquals("01012345678", savedMember.getPhoneNumber());
    }

    private MemberDto createMemberDto() {
        return MemberDto.of(
                "CJW",
                "CJW@mail.com",
                "01012345678"
        );
    }
}

테스트코드 작성을 찾아보다 발견한 것인데,  Service 단에서 실행되는 메서드들이 정상적으로 작동하는지 테스트해보는 것이다. 

dto로 보낼 정보들을 우선 생성해놓고, 

memberService.createMember(memberDto); 메서드로 임시 멤버를 생성한다.

 

그런다음 then 이후 코드들에서 각 메서드들이 1번씩 실행되는지 확인한다. 

그런 다음 assertEqual를 이용해 값이 제대로 들어갔는지 확인한다.

 

로직은 크게 어렵지는 않는데 이러한 테스트를 만드는 방식이나, 유형 등이 다양하여 무엇을 선택해야할지가 고민이었다.

우선 Mock을 이용한 가짜 객체들의 사용법을 잘 익히는 것이 중요할듯 하다. 이번엔 다양한 자료들을 참고하였지만 다음에는 직접 작성해볼 수 있도록 연습을 하도록하자...

 

 

3. 컨트롤러 응답 테스트

package com.exam.invitation.controller;

import com.exam.invitation.dto.MemberDto;
import com.exam.invitation.service.InvitationLinkService;
import com.exam.invitation.service.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;


@SpringBootTest
class InvitationControllerTest {

    @InjectMocks
    private InvitationController invitationController;
    @Mock
    private InvitationLinkService invitationLinkService;
    @Mock
    private MemberService memberService;

    @Test
    @DisplayName("컨트롤러가 올바른 응답을 하는지 테스트")
    void inviteMemberAndCreateMemberAndReturnResponseEntity() {
        //given
        MemberDto memberDto = createMemberDto();
        //when
        ResponseEntity<String> response = invitationController.inviteMember(memberDto);

        //then
        verify(memberService, times(1)).createMember(memberDto);
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertEquals("초대 링크를 보냈습니다.", response.getBody());
    }

    @Test
    @DisplayName("수락하였을때의 경우")
    void acceptInvitation_shouldCallAcceptInvitationLinkAndReturnResponseEntity() {
        //given
        Long invitationId = 1L;

        //when
        ResponseEntity<String> response = invitationController.acceptInvitation(invitationId);

        //then
        verify(invitationLinkService, times(1)).acceptInvitationLink(invitationId);
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertEquals("멤버가 초대 요청을 수락하였습니다", response.getBody());
    }

    private MemberDto createMemberDto() {
        return MemberDto.of(
                "CJW",
                "CJW@mail.com",
                "01012345678"
        );
    }

}

 

컨트롤러 단에서도 두가지 메서드밖에 없었기때문에 (초대 발신, 수락)

이러한 로직이 잘 굴러가는지 테스트를 할 수 있었다. 간단하게 앞선 2번 테스트와 같은 방식을 사용했다.

 


 

이정도로 이번 프로젝트를 마무리하였다.

 

사실 아직 구현하지 못한 부분이나, 더 보완 할 수 있는 부분들이 있는데, 또 여기에만 집중할 수는 없는 노릇이니

다른 프로젝트를 진행하면서 그때 새로운 기능들을 넣어보기로 하자.

 

1. 링크 만료와 같은 시간이 지난 후 폐기되는 정보들

2. 각 메서드들에 필요한 예외처리 (요청을 보냈을때 오류가 나타나면 어떻게 하는가?)

 - http status 오류나 기타 예상치 못한 오류들을 캐치하여 다른 로직으로 진행되게 하는 코드들

3. 멤버의 권한을 2개로 나누었으니 그에 맞는 활동 범위들을 제한해주는 로직

4. .... etc

 

궁금증

Dto에서 자바 클래스가 아닌 Recode를 사용하게 되는 이유는 무엇인가? 

테스트코드와 관련하여 더 깔끔하고 좋은 테스트코드라는 것이 있을까?

항상 메인 코드들을 수정하면 테스트코드도 변경을 해야 하는가? ->테스트코드의 존재 의의?

 

이런저런 궁금증도 남아있다. 따로 찾아서 진행하도록 하자.

 

 

 

'개발 > Spring' 카테고리의 다른 글

Spring security - 4  (0) 2024.01.17
Spring security - 3 // Security Config  (0) 2024.01.15
Spring security - 2  (0) 2024.01.14
Spring Security - 1  (0) 2024.01.14
Code refactoring - 1  (1) 2023.12.31
Comments