인프런 커뮤니티 질문&답변

wxxsox lee님의 프로필 이미지
wxxsox lee

작성한 질문수

실전! Querydsl

다대다 관계에서 효율적인 조회 쿼리 작성

작성

·

1.1K

1

안녕하세요. 강사님.

항상 친절한 답변 감사합니다^^

다대다 관계에서 효율적인 조회 쿼리 작성에 대해 질문이 있습니다.

현재 저는 다음과 같은 엔티티 클래스를 개발했습니다.^^

다대다 관계를 가지는 세개의 엔티티를 지니고있고 TagProjectOutput Entity는 다대다 연결을 엔티티로 두 엔티티를 필드를 가지고있습니다. 또한, Tag, ProjectOutput 엔티티와 다대일 단방향 매핑 연결을 했습니다.

ProjectOuutput 목록을 조회할 때 가지고 있는 Tag 정보도 동시에 조회하는 메소드를 개발하는 중에 고민되는 부분이 있어서요^^
ProjectOutput과 ProjectOutput이 지닌 Tag 목록을 보여주는 Dto를 만들어 List로 뽑아내야 하는 상황인데요.
현재는 다대다 연결 엔티티인 TagProjectOutput List를 뽑아와 Key는 ProjectOutput, Value는 List<Tag>인 Map으로 가공 후 Service Layer에서 Dto로 변환하는 코드를 사용하고 있습니다.

    /**
     * 다대다 연결 엔티티인 TagProjectOutput List를 뽑아와 Key는 ProjectOutput, Value는 List<Tag>인 Map으로 가공 후 Service Layer에서 Dto로 변환
     */
    @Override
    public LinkedHashMap<ProjectOutput, List<Tag>> findMapByTagNameList(TagProjectOutputSearchCondition condition) {
	// 다대다 연결 엔티티 목록 조회
	List<TagProjectOutput> fetched = queryFactory.selectFrom(tagProjectOutput)
		.where(tagProjectOutput.dataStatusCode.eq(DataStatusCode.USE), tagNamesEq(condition.getTagNames()))
		.join(tagProjectOutput.projectOutput, projectOutput).fetchJoin().join(tagProjectOutput.tag, tag)
		.fetchJoin().fetch();
	
	// Key : ProjectOutput, Value : Tag List 형식의 LinkedHashMap 자료구조로 가공
	LinkedHashMap<ProjectOutput, List<Tag>> map = new LinkedHashMap<>();
	
	for (TagProjectOutput tagProjectOutput : fetched) {
	    ProjectOutput projectOutput = tagProjectOutput.getProjectOutput();
	    if (map.containsKey(projectOutput)) {
		List<Tag> tags = map.get(projectOutput);
		tags.add(tagProjectOutput.getTag());
		map.replace(projectOutput, tags);
	    } else {
		List<Tag> tags = new ArrayList<>();
		tags.add(tagProjectOutput.getTag());
		map.put(projectOutput, tags);
	    }
	}
	return map;
    }

    /**
     * Tag명 리스트 조건으로 TagProjectOutput를 조회하기 위한 동적 쿼리 메소드
     */
    private BooleanExpression tagNamesEq(List<String> tagNames) {
	return isEmpty(tagNames) ? null : tagProjectOutput.tag.tagName.in(tagNames);
    }


위와 같은 메소드를 Querydsl로 작성해 사용 중인데, 지금은 데이터가 얼마 되지않아 큰 문제는 없지만, 나중에 List를 가져와 다시 한 번, LinkedHashMap 자료 구조로 변환하는 과정이 성능에 문제가 될까 염려가 되네요.

TagProjectOutput과 ProjectOutput 간 양방향 매핑은 ProjectOutput에 다른 컬렉션 필드가 추가될 수 있어서 왠만하면 단방향 매핑인 상태로 개발해야할 것 같은데, 추가적으로 고려해야하는 부분이 있을까요?

답변 4

1

wxxsox lee님의 프로필 이미지
wxxsox lee
질문자

앞으로 개발하면서 크게 도움되는 지식을 배웠네요.
감사합니다^^

1

김영한님의 프로필 이미지
김영한
지식공유자

wxxsox lee님 테스트까지 해보셨군요^^

2.838568246초 / 1000000 = 0.0000028이네요. 결국 1ms도 안되는 매우 작은 숫자입니다.

이번에 테스트를 해보셔서 아시겠지만, 성능에서는 암달의 법칙이라는 것이 중요합니다.

예를 들어서 API 호출에서 List를 LinkedHashMap로 바꾸는데 0.0000028초가 걸리고 + 쿼리 조회 결과가 0.1초(100ms)가 걸린다고 했을 때 전체 시간은 0.1000028초가 걸리는 것입니다. 그렇다면 애플리케이션 메모리 호출은 아무리 최적화를 해도 도움이 안되겠지요^^

애플리케이션은 대부분 외부 네트워크 통신이 이슈가 되지 이렇게 메모리에서 몇백건 돌린다고 이슈가 되는 일은 거의 없습니다^^

보통 내부 메서드 호출 한번 하는 것 보다 외부 네트워트 한번 통신하는 것이 5~10만배 정도 더 오래 걸리는 작업니다.

도움이 되셨길 바래요.

0

wxxsox lee님의 프로필 이미지
wxxsox lee
질문자

항상 친절하신 답변에 이렇게 경험해볼 기회까지 주신다니 감사합니다^^

질문 주신대로 100개의 List를 데이터를 LinkedHashMap으로 변환하는 for 루프를 1,000,000번 돌리는 테스트 코드를 작성했습니다.

    @Test
    @DisplayName("100개의 List 데이터를 LinkedHashMap으로 변환하는 시간을 측정")
    void ListToLinkedHashMapTest() {
	// given

	// 100개의 TagProjectOutput 데이터를 가져오기
	List<TagProjectOutput> tagProjectOutputs = tagProjectOutputRepository.findAll();
	assertThat(tagProjectOutputs.size()).isEqualTo(100);

	// when

	// 100개의 List를 LinkedHashMap으로 1,000,000번 변환하는 수행 시간 측정
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	for (int i = 0; i < 1000000; i++) {
	    LinkedHashMap<ProjectOutput, List<Tag>> map = new LinkedHashMap<>();
	    for (TagProjectOutput tagProjectOutput : tagProjectOutputs) {
		ProjectOutput projectOutput = tagProjectOutput.getProjectOutput();
		if (map.containsKey(projectOutput)) {
		    List<Tag> tags = map.get(projectOutput);
		    tags.add(tagProjectOutput.getTag());
		    map.replace(projectOutput, tags);
		} else {
		    List<Tag> tags = new ArrayList<>();
		    tags.add(tagProjectOutput.getTag());
		    map.put(projectOutput, tags);
		}
	    }
	}
	stopWatch.stop();
	
	// then
	System.out.println("수행 시간 = " + stopWatch.getTotalTimeSeconds());
	System.out.println(stopWatch.prettyPrint());
    }


StopWatch를 이용해서 시간을 측정했고 그 결과 제 로컬 환경에서 1,000,000번의 루프를 수행하는데 
2.838568246초 정도  측정되네요. 2.838568246를 1000000로 나누면 매우 적은 시간이 들 것으로 예상이 되는데..
이정도 시간은 문제가 없다고 생각해도 괜찮을까요?^^ 관련 경험이 없어 판단하기가 어렵네요.


감사합니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. wxxsox lee님 좋은 질문입니다.

먼저 List를 가져와 다시 한 번, LinkedHashMap 자료 구조로 변환하는 것에 대한 성능 문제 걱정이 있으시군요^^

제가 답을 바로 드릴 수 있지만! 그러면 wxxsox lee님의 실력이 늘지 않으니 제가 반대로 질문을 드릴께요.

성능 문제에 대한 걱정은 사실 성능 테스트를 해보아야 합니다^^! 너무 고민하기 보다는 코드로 테스트를 딱! 해보고 테스트 결과를 얻어봐야 합니다. 그래야 본인 실력이 쌓이거든요.

예를 들어서 본인 애플리케이션 요구사항 중에 이 API는 초당 100번 정도 호출되어도 문제가 없어야 한다 라는 성능 요구사항이 있어야 겠지요. 그러면 데이터를 한번에 100개씩 있다고 가정하고 1초에 100번 해당 메서드를 호출해도 이 부분에 문제가 없는지 검증해보면 되겠지요?

지금 검증하고 싶은 부분은 단순히 List를 LinkedHashMap으로 변환하는 것이니 100개의 List 데이터를 LinkedHashMap으로 변환 하는데, 시간이 얼마정도 소요되는지 측정해보시면 좋습니다^^! 단순히 for 루프로 1000000번 정도 돌리고, 결과에 시간/1000000을 하시면 한번 돌리는데 걸리는 시간을 대략 알 수 있습니다. 그리고 그 결과를 여기 답글에 남겨주세요.

두번째로 연관관계 매핑은 우선 최대한 단뱡향으로 해보고, 꼭 양뱡향이 필요하면 그때 추가해도 됩니다^^

그럼 답글 기다릴께요.

wxxsox lee님의 프로필 이미지
wxxsox lee

작성한 질문수

질문하기