스프링 공부/게시판 프로젝트 만들기

[스프링] 21. MemberCustomRepository : 조회를 엔티티로 반환하는게 과연 좋을까?

장아장 2023. 2. 5. 16:41

이전에 만든 검색기능을 조금 더 최적화 시켜야 할 필요가 있다.

이러한 생각이 든 이유는, 

  • 멤버를 검색하는 기능에서 엔티티가 바로 클라이언트 단으로 나가도 괜찮을까?
  • 일반 사용자가 다른 사용자를 검색했을 때 나와야 할 것은 기본적인 정보(아이디, 닉네임, 이메일)외에 다른 게 없는데 굳이 많은 데이터를 끌어올 필요가 있을까?

정도였다. 

 

그래서, 이번엔 바로 dto로 데이터를 검색시키는 방법으로 최적화를 시키려고 한다. 

일단, 

@QueryProjection
public SearchMemberDto(String username, String nickname, String email) {
    this.username = username;
    this.nickname = nickname;
    this.email = email;
}

SearchMemberDto에 @QueryProjection 어노테이션을 넣어주었다. 

이는 QueryDsl에서 제공하는 어노테이션으로, 

@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QSearchMemberDto extends ConstructorExpression<SearchMemberDto> {

    private static final long serialVersionUID = -1741614864L;

    public QSearchMemberDto(com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<String> nickname, com.querydsl.core.types.Expression<String> email) {
        super(SearchMemberDto.class, new Class<?>[]{String.class, String.class, String.class}, username, nickname, email);
    }

}

이런식으로 querydsl 패키지에 Q객체를 만들어준다. 이를 활용해 querydsl내에서 해당 클래스의 생성자를 사용할 수 있게 해준다. 

이렇게 처리해준 후, 

한번더 compile해주면 된다. 

 

그러면 어떻게 쓸까?

query
        .select(new QSearchMemberDto(member.username, member.nickname, member.email))
        .from(member)
        .where(usernameEq(searchMemberDto.getUsername()),
                nicknameEq(searchMemberDto.getNickname()),
                emailEq(searchMemberDto.getEmail()))
        .fetch();

이런식으로 사용하면 된다. 

일반적인 자바 생성자처럼 사용하면서, 원래 있던 이름에 Q하나 붙여주면 된다. 

이렇게 되면 fetch()를 했을 때, List<SearchMemberDto>로 반환해준다.(너무달다!)

 

이제 테스트코드를 건드려야 한다. 

기존의 테스트코드는 전부 List<Member>를 기준으로 만들어두었기 때문이다. 

이참에 클래스도 따로 분리해서 만들자. 기능들이 너무 많다. 

 

@SpringBootTest
@Transactional
public class MemberRepository_SearchTest {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private EntityManager em;

    @BeforeEach
    void createData() {
        for(int index = 0; index < 10; index++){
            RegisterRequestDto registerRequestDto = RegisterRequestDto.builder()
                    .username("testUser" + index)
                    .nickname("test" + index)
                    .email("test" + index + "@test.com")
                    .password("password" + index)
                    .passwordCheck("password" + index)
                    .build();
            Member member = new Member(registerRequestDto);
            memberRepository.save(member);
            em.flush();
            em.clear();
        }
    }

기존에는 모든 테스트메서드마다 createData를 불러오게 했는데, 이젠 @BeforeEach로 메서드를 따로 쓰지 않아도 작동하게 했다. 

@BeforeEach는 각 테스트 이전에 해당 메서드가 동작하게 해준다. 

 

@Test
@DisplayName("아이디로만 검색했을 때 검색한 멤버의 닉네임이 같은 번호로 나온다. ")
public void searchByUsernameTest() throws Exception{
    //given
    SearchMemberDto searchMemberDto = new SearchMemberDto("testUser3", null, null);

    //when
    List<SearchMemberDto> search = memberRepository.search(searchMemberDto);
    //then
    assertThat(search.get(0).getNickname()).isEqualTo("test3");
}

테스트 중에 하나를 예시로 들자면, 모든 테스트에서 given부분에 createData가 없어도 된다. 

또한, when부분에 List<SearchMemberDto>로 반환 타입이 변경되어야 한다. (안그러면 빨간줄 난다. )

 

모든 테스트가 정상 동작한다. 

 

그런데, 우리가 하나 생각해야 하는 것이 있다. 

일단, 

  • dto로 가져온 데이터는 자바에서 변경해도 데이터베이스에 반영되지 않는다. (심지어 지금의 기준으론 setter나 update기능도 만들지 않아 수정할 수 없다)
  • 조회해온 데이터를 가지고 있던 객체가 캐시 메모리에 들어오지 않는다. 캐시 메모리는 우리가 가져온 객체들을 담아두는 곳인데, 
    지금 만든 방식은, 쉽게 말하면 데이터베이스에서 Member모양으로 가져온게 아니라, 가져올 때 부터 테이블을 다른 모양으로 짜서 담아온 방식이다. 즉, 재사용할 수 없다. 

그럼에도 이렇게 가져온 이유도 분명 존재한다. 

  • Member를 검색하는 기능은 다른 멤버가 하게 된다. 그렇다면 그들은 다른 멤버를 검색해서 이에 대한 데이터를 조작할 일이 없다. 
  • 트래픽이 많아졌을 경우, 모든 데이터를 끌어오는 것보단, 이런식으로 단순하게 가져오는게 더 효율적이기 때문이다. (그래서 그런지 다른 사람들도 이런 방식을 최적화라고 한다) 

 

실제로 dto로 안전하게 데이터를 컨트롤러로 보내기 위해, 항상 stream을 이용해 엔티티를 dto로 매핑해서 반환하는 동작을 추가적으로 수행했었다. 이럴 경우 엔티티의 모든 데이터를 가져오기에 db에서 가져오는 시간과, 프로젝트 내에서 매핑하는데 까지 쓰이는 메모리와 시간까지 쓰이게 된다. 

이번 프로젝트에서는 이런 부분까지 조금 더 신경써서, 기본적인 게시판 만들기 이더라도 더 공들여 만들어 보아야 겠다.