4/29(수) 유저 CRUD - 조회, 수정, 삭제 구현

dev_joo·2026년 4월 29일
post-thumbnail

조회 (CQRS + Query DSL)

이전 프로젝트에서와 마찬가지로, CQRS 스타일로 조회를 분리해 구현하기로 했다. (DB는 동일)
대신 이번에는 동작 원리를 확실히 이해하고 넘어가려고 한다.

[쓰기] POST/PATCH/DELETE → UserService → UserRepository (JPA save/delete)
[읽기] GET              → UserQueryService → UserQueryRepository (QueryDSL select)

(CQRS) 왜 분리하나?

  • 읽기는 @Transactional(readOnly=true) → DB 최적화
  • 읽기는 복잡한 동적 조건 검색이 필요 → QueryDSL 특화
  • 쓰기/읽기 로직이 섞이면 서비스가 비대해진다.

QueryDSL 동작 원리:

 @Entity User → 빌드 시 APT가(아파트 아님 주의: 어노테이션 프로세싱 툴) QUser 자동 생성
                              ↓
 JPAQueryFactory.selectFrom(QUser.user)
                 .where(QUser.user.name.contains("김"))  // 반환 메서드가 null을 반환하면
      				//QueryDSL이 해당 where 조건을 자동으로 무시 → 동적 쿼리
                 .offset(0).limit(10)
                 .fetch()

1. JPAQueryFactory (쿼리를 짜는 펜)

JPAQueryFactoryueryDSL이 제공하는 쿼리 생성 빌더입니다.

역할: select, from, where 같은 SQL 문법을 자바 메서드 형태로 작성할 수 있게 해줍니다.

특징: EntityManager를 내부에서 사용하여 실제 DB에 쿼리를 날립니다.

핵심 메서드:

selectFrom(QUser.user): 전체 조회

where(...): 조건절 (null 입력 시 자동 무시)

orderBy(...): 정렬

fetch(): 리스트 반환 / fetchOne(): 단건 반환

2. Q-Class: QUser (쿼리용 재료)

QUser는 우리가 만든 @Entity User 클래스를 바탕으로 APT가 자동 생성한 클래스입니다.

역할: 엔티티의 필드들을 QueryDSL이 이해할 수 있는 메타데이터 형태로 제공합니다.

  • 왜 필요한가?:

User 엔티티의 name 필드는 그냥 String일 뿐이지만,

QUser의 name은 StringPath라는 특수 타입입니다. 이 타입 안에는 .eq(), .contains(), .like() 같은 쿼리용 메서드들이 미리 정의되어 있습니다.

정적 인스턴스: 보통 QUser.user라는 기본 인스턴스를 사용하는데, 이는 SQL의 Alias(별칭) 역할을 합니다. (예: select u from User u에서 u)

3. QueryDSL 엔진

자바 코드를 JPQL로 변환 (최종적으로 SQL로 변환됨)

4. EntityManager

완성된 쿼리를 DB에 날리고 결과를 가져옴

의존성 추가

dependencies {
    implementation "com.pagely:common:2.0.0"
    // QueryDSL (core는 common에서 전파, jpa+apt만 추가)
    implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
./gradlew clean compileJava

경로에 Q엔티티 파일이 생겨야 한다.

APT (Annotation Processing Tool)?
APT는 자바 컴파일러의 일종의 '플러그인'입니다. 코드에 붙어있는 @Entity 같은 어노테이션을 찾아내서, 컴 파일 시점에 새로운 소스 코드(java 파일)를 자동으로 만들어주는 역할을 합니다.

1. 발견: 컴파일러가 소스 코드를 읽다가 @Entity 를 발견합니다.
2. 호출: "어! 여기 엔티티 있다! APT 일해라!" 하고 APT를 깨웁니다.
3. 생성: APT는 해당 엔티티 정보를 바탕으로 auser. java 같은 Q-Class를 생성합니다.
4. 완성: 개발자는 이 생성된 Q-Class를 사용해 Type-safe한 쿼리를 작성하게 됩니다.
5. 활용: 개발자가 user.nume이라고 오타를 내도 컴파일러가 바로 "그런 필드 없는데?"하고 잡아줍니다.

QueryDSLConfig

@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

테스트

외부 API

내부 API

내부 API 테스트는 FeignClient를 호출하는 어플리케이션을 추가로 만들어 테스트했다.

외부 API의 Page size 방어 로직은 Pagerequest의 size를 강제하는 방식으로 했는데, 내부 API의 경우 page size의 사용이 더 자유로워야한다고 생각했고, size를 지정하지 않았을 때 상한을 1000으로 지정했다.
나중에 알게 된 사실인데, 별도의 프로퍼티(spring.data.web.pageable.max-page-size)설정 없이 size를 크게 지정하면 spring 자체 상한값으로 2000을 가진다고 한다.

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글