본문 바로가기
Programming/TDD Project

Pull Request 023. 인증된 사용자 객체 사용 TDD로 구현하기 - 자유 게시판 등록 시 작성자 등록하기

by JKROH 2023. 12. 8.
반응형

 지난 게시글까지 Spring Security와 JWT를 적용하여 로그인과 권한 확인의 과정까지 마쳤다. 그런데 로그인은 왜 할까? 로그인을 한 사용자의 정보를 좀 가져다 쓰고 싶어서 하는 것 아닐까? 그냥 로그인만 하고 아무것도 안하면 로그인을 애써서 구현할 이유가 없다. 이번에는 자유 게시판 작성자의 정보를 로그인 한 사용자의 정보에서 가져와서 등록하게 기능을 수정할 것이다.

 

 Spring Security의 사용자 정보를 가져오는 방법은 크게 세 가지가 있다. 관련해서는 해당 링크를 통해 확인할 수 있다. 이번에는 링크에 제시된 세 번째 방법인 @AuthenticaitonPrincipal을 사용할 것이다. 딱히 어려운 방법을 학습해야 하는 것이 아니고, 사용해야 하는 이유도 그냥 편하니까라서 딱히 별도로 포스팅하지는 않았다. 어짜피 Spring Security 프레임워크에서 제공해주는 기능 중에 취사선택해서 가져오는 건데, 그냥 편리한 방법으로 하는게 제일 낫지 않나?

 

 기존의 FreeBoardController#postFreeBoard(); 메서드의 코드는 다음과 같았다.

@PostMapping
public ResponseEntity<FreeBoardDto.Response> postFreeBoard(@RequestBody FreeBoardDto.Post dto) {
    FreeBoardDto.Response responseDto = freeBoardService.postFreeBoard(dto);
    return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}

 

 앞서 설정했듯이, Post 메서드는 인증된 사용자만 이용가능하다. 따라서, 인자로 인증된 사용자 정보를 넣어줘도 NullPointException이 발생할 이유가 없다. 인증 안 된 사용자가 해당 요청을 보내면 필터에서 걸려서 예외처리되기 때문이다. 사실, 해당 메서드의 테스트 메서드는 수정하지 않아도 된다. 놀랍게도. 해당 메서드에서 바뀔 부분은 인자로 @AuthenticationPrincipal이 넘어오게 바뀌는 것이 끝이고, Controller의 테스트는 Spring Security와 분리되어 있어서 그 부분은 Null 처리 될 것이기 때문이다.

 

 그럼 @AuthenticationPrincipal이 적용되게 메서드를 수정만 하자.

@PostMapping
public ResponseEntity<FreeBoardDto.Response> postFreeBoard(@AuthenticationPrincipal Member writer,
                                                           @RequestBody FreeBoardDto.Post dto) {
    FreeBoardDto.Response responseDto = freeBoardService.postFreeBoard(writer, dto);
    return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}

 

 작성자 정보가 넘어오기 때문에 writer라고 변수 명을 붙였다. 이제 해당 인자를 postFreeBoard에 넘겨주어야 할 것이다. postFreeBoard에서는 기존의 post dto -> entity /  저장 / entity -> response dto 로직 중간에 작성자 정보를 엔티티에 저장한다는 로직이 추가되어야 한다. 그런데, 해당 로직은 Member객체에서 직접 처리한다. 결국 FreeBoardService에서는 또 테스트 할 메서드가 없다. 대신, Member에서 작성자 정보를 추가하는 부분은 테스트 해봐야겠다.

class MemberTest {

    @Test
    @DisplayName("게시글 작성 시 자신의 게시글 리스트에 게시글이 등록되며, 게시글에 자신이 작성자로 등록되야 한다")
    void testWriteFreeBoard(){
        Member member = createMember();

        FreeBoard freeBoard = FreeBoard.builder()
                .title("test")
                .content("content")
                .build();

        member.writeFreeBoard(freeBoard);

        assertThat(freeBoard.getWriter()).isEqualTo(member);
        assertThat(member.getFreeBoards().get(0)).isEqualTo(freeBoard);
    }

    private Member createMember() {
        return Member.builder()
                .email("jk@gmail.com")
                .password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123"))
                .name("JKROH")
                .phoneNumber("010-1111-2222")
                .roles(List.of("BARBER", "CUSTOMER"))
                .build();
    }
}

 

 Member와 FreeBoard는 일대다 양방향 연관관계를 맺고 있다. 회원 페이지에 들어가면 작성한 게시글을 확인할 수 있어야 하고, 게시글에는 작성자의 정보가 나타나야 하기 때문이다. Member#writeFreeBoard()를 통해 Member는 자신이 작성한 게시글을 List에 추가하고, FreeBoard에 Member정보를 넣어준다. 따라서 writeFreeBoard이후에는 Member의 List에 FreeBoard가 들어있어야 하며, FreeBoard의 Member는 작성한 사용자여야 한다. 또한 이렇게 연관 관계를 설정하는 메서드를 한 군데에만 이유는 무한 루프에 빠질 수 있기 때문이다. 해당 기능을 정의해보자.

    public void writeFreeBoard(FreeBoard freeBoard) {
        this.freeBoards.add(freeBoard);
        if (freeBoard.getWriter() != this) {
            freeBoard.setWriter(this);
        }
    }

 

  테스트는 야무지게 통과한다.

 

 이제 FreeBoardService#postFreeBoard()를 살짝 수정하자.

@Override
@Transactional
public FreeBoardDto.Response postFreeBoard(Member writer, FreeBoardDto.Post postDto) {
    FreeBoard entity = freeBoardConverter.convertPostDtoToEntity(postDto);
    writer.writeFreeBoard(entity);
    FreeBoard savedEntity = freeBoardCommandService.create(entity);
    return freeBoardConverter.convertEntityToResponseDto(savedEntity);
}

 

 여기서 고민한건 writer.writeFreeBoard()의 위치였는데, FreeBoard 엔티티를 먼저 persist한 다음 연관관계를 맺어줄지, 아니면 연관관계를 먼저 맺어주고 persist 해야 할지였다. 나는 코드를 좀 더 직관적으로 작성하기 위해서 위와 같은 방법을 선택했다. 또한 먼저 FreeBoard를 persist하고 연관관계를 맺으면 update쿼리가 추가적으로 실행돼서 위와 같은 방법을 택했다. 사실 저렇게 하면 Member에도 update쿼리가 발생해야 할 것 같았는데(id값이 없는 엔티티가 먼저 연관되고, 그 다음에 연관 객체에 id가 생기니까) 그렇지 않더라. 이 부분은 따로 공부를 좀 해봐야겠다.

 

 FreeBoardConverter#converPostDtoToEntity()와 FreeBoardCommandService#create()에는 변화가 없다. 대신, FreeBoardCommandService에서 호출하는 FreeBoardRepository#save()에 작성자 정보가 잘 담기는지는 테스트해볼만 하다. 테스트 코드를 작성해보자.

@RepositoryTest
class FreeBoardRepositoryTest {

    @Autowired
    private FreeBoardRepository freeBoardRepository;
    
    ...

    @Test
    @DisplayName("게시글 등록 테스트")
    void testPostFreeBoard(){
        Member member = Member.builder()
                .name("name")
                .build();
        member.setId(1L);

        FreeBoard entity = FreeBoard.builder()
                .title("title")
                .content("content")
                .build();
        member.writeFreeBoard(entity);

        FreeBoard savedEntity = freeBoardRepository.save(entity);

        assertThat(savedEntity.getTitle()).isEqualTo(entity.getTitle());
        assertThat(savedEntity.getContent()).isEqualTo(entity.getContent());
        assertThat(savedEntity.getWriter()).isEqualTo(member);
    }
}

 

 사실 해당 메서드는 내가 커스텀하는게 아니라서 별도의 로직 수정은 없다. 테스트 결과 잘 통과함을 알 수 있다. @RepositoryTest는 커스텀 애너테이션이다.

 

 마지막으로  FreeBoardConverter#entityToResponseDto()메서드를 수정해야 한다. 작성자 정보를 넣어준 만큼, 이제는 FreeBoardDto.Response에 사용자의 정보도 담겨야하기 때문이다. 담을 정보는 사용자 정보를 얻을 수 있는 MemberId와 사용자의 닉네임 정도가 있다. 이 부분에 대한 테스트를 먼저 작성해보자.

@Test
@DisplayName("Entity를 Response DTO로 변환한다.")
void convertEntityToResponseDtoTest(){
    Member member = Member.builder()
            .name("name")
            .build();
    member.setId(1L);

    FreeBoard entity = FreeBoard.builder()
            .title("title")
            .content("content")
            .build();
    member.writeFreeBoard(entity);

    FreeBoardDto.Response responseDto = freeBoardConverter.convertEntityToResponseDto(entity);

    assertThat(entity.getTitle()).isEqualTo(responseDto.title);
    assertThat(entity.getContent()).isEqualTo(responseDto.content);
    assertThat(entity.getWriter().getId()).isEqualTo(responseDto.writerId);
    assertThat(entity.getWriter().getName()).isEqualTo(responseDto.writerName);
}

 

 이에 맞춰서 ResponseDto와 해당 기능을 수정해주자.

public class FreeBoardDto {

    ...

    @Builder
    public static class Response{
        public long boardId;
        public String title;
        public String content;
        public int viewCount;
        public String createdAt;
        public String modifiedAt;
        public long writerId;
        public String writerName;
    }

    ...
}

/////////////////////////

@Service
public class FreeBoardConverter {

    ...

    public FreeBoardDto.Response convertEntityToResponseDto(FreeBoard entity) {
        return FreeBoardDto.Response.builder()
                .boardId(entity.getId())
                .title(entity.getTitle())
                .content(entity.getContent())
                .viewCount(entity.getViewCount())
                .createdAt(entity.getCreatedAt().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)))
                .modifiedAt(entity.getModifiedAt().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)))
                .writerId(entity.getWriter().getId())
                .writerName(entity.getWriter().getName())
                .build();
    }
}

 

 테스트를 다시 수행하면 통과한다. 그런데, 이렇게 Response DTO를 수정하니 문제가 생긴 부분이 있다. 바로 Controller 테스트다. 기존의 테스트 메서드에는 작성자 id와 닉네임에 대한 정보가 담겨있지 않다. 이를 문서화하면 오류가 발생한다. 또한 FreeBoardService#postFreeBoard()에 Member 인자가 추가적으로 전달되어야 한다. 이 역시 수정해주자.

@Test
@DisplayName("정상적인 게시글 등록 요청 테스트")
void postFreeBoardTest() throws Exception {
    // given
    ...

    FreeBoardDto.Response testResponse = FreeBoardDto.Response
            .builder()
            
            ...
            
            .writerId(1L)
            .writerName("name")
            .build();

    ...

    given(freeBoardService.postFreeBoard(any(), any())).willReturn(testResponse);

    // when // then
    mockMvc.perform(
            ...
            
            .andDo(document(
                    
                    ...
                    
                    responseFields(
                            List.of(
                                    ...
                                    
                                    fieldWithPath("writerId").type(JsonFieldType.NUMBER).description("작성한 사용자 ID"),
                                    fieldWithPath("writerName").type(JsonFieldType.STRING).description("작성한 사용자 닉네임")
                            )
                    )
            ));
}

 

 수정이 필요한 부분 외에는 생략했다. 다시 테스트를 수행하면 잘 통과함을 알 수 있다. 마지막으로 Postman을 이용해 해당 과정을 전체적으로 수행했고, 잘 수행됨을 알 수 있다.

 

 전체 코드는 링크에서 확인할 수 있다.

 

반응형

댓글