🎯 Tuple 이란 무엇일까 ?
Tuple 은 QueryDSL 에서 여러 엔티티나 값들을 한 번에 조회하고 그 결과를 하나의 객체로 묶어서 반환할 때 사용되는 자료형 입니다. 이를 통해 여러 컬림이나 객체를 하나의 결과를 처리할 수 있습니다. 특히 Tuple 은 SQL 쿼리에서 여러개의 값을 조회할 때 유용하게 쓰입니다.
또한 Tuple 은 단순한 데이터 타입 뿐만 아니라, 엔티티 객체와 값 객체 등 다양한 타입을 포함할 수 있습니다. 그리고 Tuple 은 여러 컬럼을 조회한 결과를 각각의 값으로 나누지 않고, 하나의 객체로 묶어서 반환하기 때문에 여러 개의 값을 처리 할 때 유용합니다.
🎯 루트 애그리거트(Aggregate Root) 로 설계된 도메인
먼저 User(사용자) 도메인과 Home(집 게시글) 도메인은 각각 독립적인 루트 애그리거트 입니다. 즉, Entity 를 설계할때 두 도메인은 각각 객체를 참조하지 않고, PK 값만을 갖게 설계했습니다.
✅ Home 엔티티
@Entity
@Getter
@Builder
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Home extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "home_id", nullable = false)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@JsonIgnore
@OneToMany(mappedBy = "home", cascade = CascadeType.PERSIST, orphanRemoval = true, fetch = FetchType.LAZY)
private List<HomeImage> images;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "home_address_id")
private HomeAddress homeAddress;
@Embedded
private HomeInfo homeInfo;
@Enumerated(EnumType.STRING)
private HomeStatus homeStatus;
...
}
✅ User 엔티티
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "`user`")
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id", nullable = false)
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "nickname", nullable = false)
private String nickname;
@Setter
@Column(name = "password", nullable = false)
private String password;
@Setter
@Column(name = "profile_url", nullable = true)
private String profileUrl;
@Column(name = "fcm_token", nullable = true)
private String fcmToken;
@Column(name = "job", nullable = true)
private String job;
@Enumerated(EnumType.STRING)
@Column(name = "user_state", nullable = true)
private UserState userState;
@Enumerated(EnumType.STRING)
@Column(name = "gender", nullable = true)
private Gender gender;
@Enumerated(EnumType.STRING)
@Column(name = "signup_type", nullable = false)
private SignupType signupType;
@Column(name = "nationality", nullable = true)
private String nationality;
@Column(name = "introduce", nullable = true)
private String introduce;
@Setter
@Column(name = "refreshToken", nullable = true)
private String refreshToken;
public boolean isExistProfile(){
return profileUrl != null;
}
}
즉, Home 은 userId 를 필드값으로 가지고 있습니다.
✅ Tuple 을 사용한 User 와 Home 을 동시에 조회하는 메서드
@Override
public List<HomeOverviewResponse> findAllSellHome() {
Set<Long> seenHomeIds = new HashSet<>();
List<Tuple> tuples = query.select(qHome, qUser)
.from(qHome)
.join(qUser).on(qHome.userId.eq(qUser.id))
.leftJoin(qHome.images, qHomeImage).fetchJoin()
.where(qHome.homeStatus.eq(HomeStatus.FOR_SALE))
.fetch()
.stream()
.filter(tuple -> {
Home home = tuple.get(QHome.home);
return seenHomeIds.add(home.getId());
})
.toList();
return tuples.stream()
.map(tuple -> {
Home home = tuple.get(QHome.home);
User user = tuple.get(QUser.user);
return HomeMapper.INSTANCE.toSimpleHomeDto(home, user);
})
.toList();
}
원래는 Service 계층에서 Home과 User를 각각 조회했지만, 이 방식은 여러 번의 쿼리가 DB에서 발생하게 되어 성능 저하를 일으키고, 또한 하나의 트랜잭션으로 처리되지 않아 불안정한 메서드라고 생각했습니다.
이 문제를 해결하기 위해 QueryDSL에서 Tuple과 join을 활용하여, 필요한 User와 Home 정보를 한 번의 쿼리로 조회하도록 쿼리를 최적화했습니다. Tuple을 사용해 Home과 User를 함께 조회하고, 그 결과를 DTO인 HomeOverviewResponse로 변환하여 반환하도록 했습니다.
현재 쿼리는 모든 데이터를 한 번에 조회합니다. 이는 데이터 양이 많을 경우 성능 저하를 초래할 수 있지만, 클라이언트에서 모든 집 게시글을 사용해 Map에 뿌려주는 기능이 있기 때문에 fetchJoin과 Tuple을 적절히 사용하여 쿼리 성능을 최적화했습니다.
QueryDSL의 Tuple과 join을 활용한 방법은 여러 번의 DB 쿼리 호출을 줄여주고, 하나의 트랜잭션으로 데이터를 안정적으로 처리할 수 있게 도와주었습니다. 다만, 데이터가 많을 경우 성능이 저하될 수 있기 때문에, 향후 데이터 양을 고려한 추가적인 최적화가 필요할 수 있습니다.
'Spring > JPA' 카테고리의 다른 글
[QueryDSL] fetchJoin 과 동작하지 않는 distinct (0) | 2025.04.13 |
---|---|
[JPA] 벌크 연산(Bulk Operation)이란 무엇일까? (1) | 2024.09.07 |