Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_51일차_"Criteria + hr DB"

LEFT 2025. 2. 20. 17:50

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [51]일차

🚀51차에는 Criteria API에 대해 배워보고, hr 데이터베이스를 통해 E-R Diagram 기반으로 엔티티를 설계하는 것을 

실습할 수 있었다.

학습 목표 : ERD를 보고 엔티티를 설계할 수 있는 것과 관계매핑을 할 수 있음

학습 과정 : 회고를 통해 작성


Criteria API

  • Java Persistence API의 일부로써 복잡한 검색 기능을 구현할때
  • SQL이나 JPQL 문자열을 직접 작성하지 않고도 동적으로 쿼리를 생성하고 실행할 수 있게 해줌
  • 따라서 개발자가 프로그램 코드 내에서 SQL과 유사한 연산을 수행할 수 있게하는 객체지향 API를 제공

구성요소

  • 타입 안전성 : 컴파일 시점에 쿼리의 구문 오류를 잡아낼 수 있어서 실행 시간의 오류가능성을 줄여줌
  • 동적쿼리생성 : 실행 시간에 쿼리 구성요소를 결정하여 사용자 입력에 따른 쿼리 변경이 유용
  • 자바 코드 기반 구성
  • CriteriaBuilder : 쿼리를 생성, 수정에 사용하는 팩토리 클래스
  • CriteriaQuery : 생성할 쿼리를 정의, 반환할 엔티티 타입이나 WHERE, GROUP BY, ORDER BY 쿼리 조건 지정 가능
  • Root : 쿼리의 FROM절에 해당하는 주 엔티티를 지정, Root 객체를 사용하여 쿼리의 기준이되는 엔티티 정의

▶️실습 - Criteria : User 엔티티 생성

  • UserRepository 생성 (JpaRepository와 UserRepositoryCustom 을 모두 상속)
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom{ ... }

 

  • UserRepositoryCustom 생성
public interface UserRepositoryCustom {
    List<User> findUsersByName(String name);
}

 

  • UserRepositoryCustomImpl 생성
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @Autowired
    private EntityManager entityManager;

    @Override
    public List<User> findUsersByName(String name) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> user = query.from(User.class);
        // query.select(user).where(cb.equal(user.get("name"), name));
        query.select(user).where(cb.like(user.get("name"), "%"+name+"%"));
        return entityManager.createQuery(query).getResultList();
    }
}
  • 실제로 UserRepositoryCustom의 내부에서 수행할 findUsersByName()메소드 오버라이딩
  • UserRepositoryCustomImpl 클래스는 UserRepositoryCustom 인터페이스를 구현
    즉, 이 클래스는 사용자 정의 리포지토리 기능을 제공

  • @Autowired private EntityManager entityManager;
    ➡️JPA에서 데이터베이스 작업을 수행하는 데 사용
    @Autowired 어노테이션을 통해 Spring이 이 객체를 자동으로 주입

  • CriteriaBuilder : JPA의 Criteria API를 사용하여 동적 쿼리를 생성하는 데 사용

  • CriteriaQuery<User> query = cb.createQuery(User.class);
    ➡️CriteriaQuery 객체를 생성하여 User 엔티티에 대한 쿼리를 작성할 준비

  • Root<User> user = query.from(User.class);
    Root 객체는 쿼리의 시작점을 정의. ➡️User 엔티티를 기준으로 쿼리를 작성

  • query.select(user).where(cb.like(user.get("name"), "%"+name+"%"));
    ➡️select 메서드를 사용하여 user 엔티티를 선택
    where 메서드를 통해 이름이 주어진 name을 포함하는 사용자들을 찾는 조건을 설정

  • 작성한 쿼리를 실행하고, 그 결과로 얻은 사용자 목록을 반환

main 테스트

@SpringBootApplication
public class Application implements CommandLineRunner {
    private static final Logger log = LoggerFactory.getLogger(Application.class);

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Autowired
    UserRepository userRepository;

    @Override
    public void run(String... args) throws Exception {
        // Criteria API를 사용한 사용자 조회 예제
        List<User> usersByNameCriteria = userRepository.findUsersByName("홍길동");
        usersByNameCriteria.forEach(user -> log.info("Criteria API로 찾은 사용자: " + user.getName() + ", 이메일: " + user.getEmail()));
    }
}
  • UserRepository에서 findUsersByName 메서드를 호출하여
    이름이 "홍길동"인 사용자 목록을 조회하고, 그 결과를 usersByNameCriteria에 저장

  • 조회된 사용자 목록을 반복하여 각 사용자의 이름과 이메일을 로그에 출력

  • Criteria API : 동적 쿼리 생성
    ➡️쿼리를 프로그래밍 방식으로 구성하여 실행 시 다양한 검색 조건을 유연하게 적용할 수 있음
    특히 사용자 입력에 따라 변화하는 쿼리의 조건을 처리해야할때 유용

    ⚠️정적 쿼리 (=미리 정의된 쿼리)로만 모든 사용사례를 처리할 수 없기 때문

Criteria API의 동적 쿼리 생성 절차

  1. CriteriaBuilder 인스턴스 생성
    ➡️EntityManager로부터 CriteriaBuilder 객체를 얻음 (쿼리의 조건 및 구조를 정의하는데 사용)

  2. CriteriaQuery 객체 생성
    ➡️CriteriaBuilder를 사용해 CriteriaQuery 객체를 생성, (반환될 결과의 형식을 정의)

  3. Root 정의
    ➡️쿼리의 FROM절에 해당하는 Root객체를 생성 (쿼리의 메인 테이블을 나타냄)

  4. 조건 추가 (WHERE 절)
    ➡️필요한 검색 조건을 CriteriaBuilder를 통해 생성하고 CriteriaQuery에 추가

  5. 쿼리 실행
    ➡️완성된 CriteriaQuery를 EntityManager를 통해 실행함

▶️실습 - 이름과 이메일 주소로 사용자 조회 메소드 추가

  • 사용자 검색 동적 쿼리 생성 (Criteria 활용)

 

UserRepositoryCustom 인터페이스에 메소드 추가

public interface UserRepositoryCustom {
    List<User> findUsersByName(String name);
    List<User> findUsersDynamically(String name, String email);
}

 

UserRepositoryCustomImpl 메소드 기능 오버라이딩 

@Override
public List<User> findUsersDynamically(String name, String email) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class); // createQuery는 대상이 될 엔티티를 넣어줌 (User.class)
    Root<User> user = query.from(User.class);

    List<Predicate> predicates = new ArrayList<>();
    if(name != null){
        predicates.add(cb.equal(user.get("name"), name)); // predicates는 List이므로 List의 add()메소드로 쿼리문을 인자로 받음
    }
    if(email != null){
        predicates.add(cb.equal(user.get("email"), email));
    }

    query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));

    // 엔티티매니저로부터 쿼리를 생성하고 그 쿼리로는 지금까지 만든 query를 넣어준 후 결과 리스트로 반환하는 형태
    return entityManager.createQuery(query).getResultList();
}
    • List<Predicate> predicates = new ArrayList<>();
      ➡️Predicate :
      조건값들을 담는 인터페이스

      조건을 List에 담는데 Predicate는 조건을 리스트에 담도록 할 수 있음
      (=조건이 여러개 있을 수 있으므로 리스트에 담는 것)

      Predicate 인터페이스 내부의 BooleanOperator 메소드가 AND, OR등을 자동으로 붙여서 
      쿼리를 만들어주는 역할을 함

    • Root<User> user = query.from(User.class);
      ➡️
      query.from(User.class);
      : 조회 대상이 되는 엔티티 User를 지정
      ➡️Root<User> : SQL의 FROM 절에 해당 >> User 엔티티를 기준으로 필드를 참조

    • query.select(user).where(cb.and(predicates.toArray(new Predicate[0])));
      ➡️predicates리스트의 toArray를 통해 배열로 꺼내와서 0번째 인덱스로 만들어줄 것

      이 배열에는 
      - cb.equals(user.get("name"), name)
      - cb.equals(user.get("email"), email)
      가 저장되어있음

      이들을 and() 메소드로 AND 조건으로 붙임 (=여러 조건들을 AND 연산으로 붙여줌)
      만약 or() 메소드이면 OR 조건으로 toArray()에 저장된 여러 조건들을 OR 연산으로 붙여줌

      and()일때 경우의 수
      1) name != null, email == null 
      select u from User u where u.name = name; // 쿼리 생성

      2) name == null, email != null
      select u from User u where u.email = email; // 쿼리 생성

      3) name != null, email != null
      select u from User u where u.name = name and u.email = email; // 쿼리 생성

      4) name == null, email == null
      select u from User u; // 쿼리 생성

      자동으로 이러한 쿼리들이 만들어지므로 동적 쿼리라고 부르는 것 
  • cb.and(predicates.toArray(new Predicate[0]))
    ➡️toArray() : 리스트의 요소들을 배열로 변환
    🚀toArray(new Predicate[0])
    : List<Predicate] 를 Predicate[] 배열로 변환
    (= 0은 배열의 크기가 아닌 배열 타입을 맞추는 역할)

    ➡️
    Predicate 리스트를 and()로 묶어 WHERE name = ? AND email = ? 형식으로 설정
    만약 or()를 사용하면 WHERE name = ? OR email = ?

    predicates 리스트의 toArray를 통해 배열로 꺼내와서 0번째 인덱스로 만들어준다는 것
    (=리스트를 Predicate[] 배열로 변환)

    ex. predicates 리스트에 두 개의 조건이 있다고 가정
predicates.add(cb.equal(user.get("name"), "Milos")); 
predicates.add(cb.equal(user.get("email"), "rogers@exam.com"));
  • cb.or(predicates.toArray(new Predicate[0]))
    ➡️이를 통해 SQL 쿼리로 변환 (동적 쿼리 생성)
SELECT * FROM user 
WHERE name = 'Milos' OR email = 'rogers@exam.com';

 

❓toArray(new Predicate[0])을 사용하는 이유

➡️가변적인 리스트 크기에 적합함
만약 toArray(new Predicate[predicates.size()])처럼 리스트 크기만큼 배열을 미리 할당하면,
불필요한 메모리 할당이 발생할 수도 있음.

반면, toArray(new Predicate[0])을 넘기면 Java가 내부적으로 적절한 크기의 배열을 생성하여 반환하여 효율적임


main 테스트

// Criteria 활용 이름, 이메일 주소에 따른 사용자 조회
userRepository.findUsersDynamically("Milos", null)
        .forEach(user -> log.info("user :: {} {}", user.getName(), user.getEmail()));

 

➡️상황 1) ("Milos", null)
name != null, email == null

메소드 내부 : cb.equal(user.get("name"), "Milos") 조건 추가
동적 쿼리 생성 : WHERE name = 'Milos'
출력:
user :: Milos Milos@exam.com
user :: Milos Milos@example.com

>> "Milos"인 사용자 2명이 반환

➡️상황 2) ("Milos", "exam.com")
name != null, email != null

userRepository.findUsersDynamically("Milos", "exam.com");

메소드 내부 : 
cb.equal(user.get("name"), "Milos") 조건 추가
cb.equal(user.get("email"), "exam.com") 조건 추가

동적 쿼리 생성 : WHERE name = 'Milos' AND email = 'exam.com'
출력:
user :: Milos Milos@exam.com

>> "Milos"이면서 "exam.com" 이메일을 가진 사용자만 반환

 

➡️상황 3) ("Milos", "rogers@exam.com")
name != null, email != null

// OR 조건일 경우
query.select(user).where(cb.or(predicates.toArray(new Predicate[0])));
userRepository.findUsersDynamically("Milos", "rogers@exam.com");

메소드 내부 : 
cb.equal(user.get("name"), "Milos") 조건 추가
cb.equal(user.get("email"), "rogers@exam.com") 조건 추가

동적 쿼리 생성 : WHERE name = 'Milos' AND email = 'rogers@exam.com'
출력:
user :: Milos Milos@exam.com
user :: Milos Milos@example.com
user :: Morgan rogers@exam.com

>> 이름이 "Milos"인 사용자 2명과 이메일이 "rogers@exam.com"인 사용자 1명 모두 반환


🚀hr 데이터베이스 실습

 

hr DB의 ERD

 

Region

@Entity
@Getter@Setter@NoArgsConstructor
@Table(name = "regions")
public class Region {
    @Id
    @Column(name = "region_id")
    private Integer id;

    @Column(name = "region_name")
    private String name;
}

 

Employee

@Entity
@Getter@Setter@NoArgsConstructor
@Table(name = "employees")
public class Employee {
    @Id@GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "employee_id")
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    private String email;

    @Column(name = "phone_number")
    private String phoneNumber;

    @Column(name = "hire_date")
    private LocalDate hireDate;

    @ManyToOne
    @JoinColumn(name = "job_id")
    private Job job;

    private Double salary;

    @Column(name = "commission_pct")
    private Double commissionPct;

    @ManyToOne
    @JoinColumn(name = "manager_id")
    private Employee manager;

    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;

    @OneToMany(mappedBy = "manager")
    private Set<Employee> subordinates;

    @OneToMany(mappedBy = "employee")
    private Set<JobHistory> jobHistories;
}
  • 다른 엔티티들과 관계 매핑이 많은 Employee 엔티티까지 생성

 

❓Employee에서 Job엔티티와의 관계가 @ManyToOne인 이유

➡️하나의 직업에 여러 사원이 그 직업을 가질 수 있으므로 이러한 관계 정의

 

❓Employee와 Department와의 관계

// foreignKey = @ForeignKey(name = "FK_department_manager"), nullable = true
@ManyToOne
@JoinColumn(name = "manager_id")
private Employee manager;
  • 한 관리자 사원이 여러 부서를 관리할 수 있다는 것을 표현
  • @ForeignKey처럼 외래키 어노테이션을 사용할 수도 있음

EmployeeRepository 메소드 추가

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByLastName(String lastName);
}

main 테스트

@Bean
public CommandLineRunner run(EmployeeRepository employeeRepository){
    return args -> {
        List<Employee> findLastName = employeeRepository.findByLastName("Patel");
        findLastName.forEach(employee -> log.info("[마지막 이름이 Patel]인 고객 정보 :: {} {} 님", employee.getFirstName(), employee.getLastName()));
    };
}

  • 정상적으로 출력

🚀실습 - 연봉 12000이상 사원 조회

// JPA 실습
List<Employee> findMoreSalary = employeeRepository.findBySalaryGreaterThanEqual(12000d);
findMoreSalary.forEach(employee -> log.info("[연봉 12000 이상]인 고객 : {}의 연봉 [{}]", employee.getFirstName(), employee.getSalary()));


🚀실습 - 사원번호가 176번인 사원 조회

// 2. 사원 번호로 조회
Optional<Employee> findEmpId = employeeRepository.findById(176);
if(findEmpId.isPresent()){
    Employee employee = findEmpId.get();
    log.info("[사원번호 176]인 사원 : {}", employee.getLastName());
}

 

⚠️오류해결

전체 사원번호로 조회하면 176번인 사원이 나오지 않는데

-- 전체 사원번호 조회 
select employee_id from employees;
-- 1-1) 사원번호가 176번인 사원 조회 
select employee_id, last_name from employees where employee_id = 176;

-- 1-2) 사원번호가 176번인 사원 조회
select * from employees where employee_id = 176;
  • 176번인 사원을 직접 접근하도록 조회하면 데이터가 있는 것을 확인 가능 

 

➡️1번 방법. 인덱스 손상 확인

-- 인덱스 손상 확인
check table employees; analyze table employees;

  • 인덱스 매핑이 손상되었는지 확인해보았지만 문제 없음

 

➡️2번 방법. 정렬 후 출력

-- 정렬 후 전체 사원 출력 
SELECT employee_id FROM employees ORDER BY employee_id ASC;
  • 100~149번대까지밖에 사원이 출력되지 않았음

 

➡️3번 방법. 출력 행 제한 확인

-- 출력 행 제한 확인
select @@max_allowed_packet, @@sql_select_limit;

 

  • 이미 충분한 limit를 가지고 있으므로 문제 없음
  • 총 데이터 개수 확인 ➡️select count(*) from employees; -- 107개

 

✅4번 방법. 총 데이터 출력 제한 늘리기

-- 총 데이터 출력 제한 10000개로 늘리기 
select employee_id from employees limit 10000;

-- 전체 사원 번호 재조회
select employee_id from employees;
  • 176번 사원도 포함되어 전체 사원의 id가 나오는 것을 확인 가능
  • limit으로 출력행의 제한을 늘려줌으로써 해결할 수 있음

🚀회고 결과 :
이번 회고에서는 Criteria API에 대한 이해가 부족해서 추가 공부를 하였다.

아직 사용에 익숙치 않지만 개념의 이해를 하는 것에 많은 시간 공부한 것 같다.

또한 실습을 진행하면서 생겼던 오류를 해결할 수 있었다.

- MySQL 데이터 출력 제한 늘리기 (limit 사용)
- ERD로 엔티티 설계

느낀 점 : 
Docker의 버전 오류로 포트 충돌이 일어나서 제대로 실습을 진행하지 못했지만
오류 해결을 겨우 할 수 있었다. 오류 해결 과정은 별도의 회고로 작성해야할 것 같다.
Criteria API를 활용한 다른 실습들을 진행하여 더 Criteria에 대해 알아봐야겠다고 느꼈다.

향후 계획 : 

- Criteria API 추가 공부
- Docker 포트 충돌 문제 - 다른 해결법 
- MySQL Query 연습