이번 응용예제는
JPA기본 개념을 실제 코드 흐름으로 확인하는 파트이다.
앞에서는Entity,EntityManager, 영속성 컨텍스트,Transaction,JPQL, 연관관계, 지연 로딩,N+1,Fetch Join을 개념 중심으로 정리했다.
이제는 코드예제가 어떤 개념을 확인하는지 하나씩 연결해서 본다.
응용예제는 파일을 하나씩 따로 외우는 방식으로 보면 어렵다.
먼저 파일의 역할을 나누어야 한다.
어떤 파일은Entity구조를 보여 주고, 어떤 파일은EntityManager생성 흐름을 보여 주며, 어떤 파일은 저장, 조회, 삭제 같은 실제 실행 흐름을 보여 준다.
응용예제는 코드 파일을 실행 순서와 역할 기준으로 묶어야 흐름이 보인다.
응용예제를 읽기 전에 잡아야 할 기준
응용예제는 새 개념을 무작정 추가하는 파트가 아니다.
앞에서 배운 개념이 실제 코드에서 어떤 순서로 연결되는지 확인하는 파트이다.
그래서 코드를 볼 때는 먼저 이 파일이 어떤 역할인지부터 구분해야 한다.
이 파일이Entity구조를 보여 주는 파일인지, 직접 실행하는 파일인지, 데이터베이스 작업을 모아 둔DAO파일인지 먼저 봐야 한다.
이 기준이 잡히면 뒤에서 나오는persist(),find(),JPQL,remove(),Fetch Join,DAO호출 흐름이 어느 위치에서 실행되는지 이해하기 쉬워진다.
응용예제의 핵심은 코드 한 줄을 따로 외우는 것이 아니라, 각 파일이 어떤 개념을 확인하는 역할인지 연결해서 보는 것이다.
이번 응용예제에서 계속 확인할 기준
- 이 파일이 실행용 파일인지 확인한다.
- 이 파일이 테이블과 연결되는
Entity인지 확인한다.- 이 파일이 데이터베이스 작업을 모아 둔
DAO인지 확인한다.- 이 파일이 조회 결과를 담는
DTO인지 확인한다.- 이 파일이 설정 파일인지 확인한다.
- 실행 결과가 어떤 코드 흐름에서 나온 것인지 확인한다.
이 기준으로 보면 파일이 많아도 흐름이 끊기지 않는다.
각 파일을 따로 외우는 것이 아니라,JPA실행 과정 안에서 어떤 역할을 맡는지 연결해서 볼 수 있다.
파일은 역할별로 나누어 봐야 한다
파일은 크게 실행 파일,
Entity파일,DAO파일,DTO파일, 설정 파일로 나누어 볼 수 있다.
이 구분이 먼저 되어야 코드 흐름이 정리된다.
실행 파일
실행 파일은 직접 실행해서 결과를 확인하는 코드이다.
main()메서드가 들어 있고,EntityManager를 만들거나DAO메서드를 호출해서 실제 결과를 출력한다.
HelloJPA1,HelloJPA2는JPA실행 객체 생성 흐름을 확인한다.HelloJPA3부터HelloJPA6까지는Emp조회, 연관 필드 접근,N+1,Fetch Join흐름을 확인한다.EntityTestApp계열은 기본 저장, 조회,flush,rollback, 삭제 흐름을 확인한다.MemberTeamTest계열은Member,Team,Locker의 연관관계 저장과 조회를 확인한다.EmpApp계열은EmpDAO의 여러 조회 기능을 실행한다.VisitorApp,MeetingApp은 콘솔 메뉴로DAO기반 프로그램 흐름을 확인한다.실행 파일은 결과를 직접 만드는 파일이다.
그래서 나중에 결과 이미지나 영상을 붙일 때는 대부분 실행 파일을 기준으로 보면 된다.
Entity 파일
Entity파일은 데이터베이스 테이블과 연결되는 객체 구조를 정의한다.
실행 파일에서 저장하거나 조회하는 대상이 바로Entity이다.
EntityTest계열은 기본 엔티티 매핑, 컬럼 설정, 날짜·시간 매핑을 보여 준다.Emp,Dept,Locations는 사원, 부서, 지역의 연관관계를 보여 준다.Member,Team,Locker는 회원, 팀, 락커의 다대일과 일대일 관계를 보여 준다.Visitor,Meeting,Reply,Book은 실제 프로그램 구조에 가까운 엔티티 예제이다.
Entity파일을 먼저 확인해야 실행 파일의persist(),find(),JPQL이 어떤 테이블과 연결되는지 이해할 수 있다.
DAO 파일
DAO파일은 데이터베이스 작업을 메서드 단위로 모아 둔 코드이다.
DAO는Data Access Object의 줄임말이고, 데이터 접근 객체라는 뜻이다.
EmpDAO는 다양한JPQL조회 기능을 보여 준다.VisitorDAO는 방명록 조회, 검색, 저장, 수정, 삭제를 처리한다.MeetingDAO는 미팅 글 조회, 검색, 저장, 수정, 삭제와 댓글 조회·저장을 처리한다.
DAO를 사용하면 실행 앱이JPA코드를 전부 직접 들고 있지 않아도 된다.
데이터베이스 접근 코드를DAO에 모아 두기 때문에 프로그램 구조가 더 분리된다.
DTO 파일과 설정 파일
DTO파일은 조회 결과를 필요한 형태로 담기 위한 객체이다.
DTO는Data Transfer Object의 줄임말이고, 데이터를 옮기기 위한 객체라는 뜻이다.
EmpFreqDTO는Emp전체 엔티티가 아니라 필요한 값만 묶어서 조회하는 흐름에서 사용된다.설정 파일은
JPA실행 설정을 담당한다.
실행 코드에서"entitytest"나"emptest"같은 이름을 사용하면, 해당 설정 묶음을 기준으로EntityManagerFactory가 만들어진다.
파일 역할을 먼저 나누면 실행 코드, 데이터 구조, 데이터베이스 작업 코드가 서로 어떻게 연결되는지 보인다.
전체 흐름은 실행 준비에서 DAO 구조까지 이어진다
응용예제를 읽을 때는 파일 이름 순서만 따라가면 흐름이 잘 보이지 않을 수 있다.
먼저 실행 준비 파일을 보고, 그다음Entity구조를 확인한 뒤, 실제 실행 파일을 보는 순서가 좋다.
첫 번째 흐름은 JPA 실행 준비이다
HelloJPA1,HelloJPA2는EntityManagerFactory와EntityManager가 어떻게 만들어지는지 확인한다.
실제 저장이나 조회 전에JPA실행 객체가 어떻게 준비되는지 보는 단계이다.
두 번째 흐름은 기본 Entity 매핑과 기본 데이터 작업이다
EntityTest계열에서는 기본Entity매핑, 기본키 자동 생성, 컬럼 설정, 날짜·시간 매핑, 간단한 연관관계를 확인한다.
EntityTestApp계열에서는 저장, 전체 조회,flush,rollback(), 삭제 요청과commit()여부를 확인한다.
세 번째 흐름은 Emp 기반 JPQL과 로딩 전략이다
Emp,Dept,Locations와HelloJPA3부터HelloJPA6까지의 조회 예제에서는JPQL조회, 연관 필드 접근, 지연 로딩,N+1,Fetch Join흐름을 확인한다.
이 구간은 연관관계가 있는 엔티티를 조회할 때 실제SQL흐름이 어떻게 달라질 수 있는지 보는 구간이다.
네 번째 흐름은 DAO 기반 조회 심화이다
EmpDAO,EmpApp1,EmpApp2에서는JPQL조건 조회,like,DTO조회, 페이징, 집계 함수를DAO메서드로 나누어 실행하는 구조를 확인한다.
여기서는 쿼리 하나만 보는 것이 아니라, 여러 조회 기능을 메서드로 분리해 사용하는 흐름을 본다.
다섯 번째 흐름은 Member 기반 연관관계 조회이다
Member,Team,Locker와MemberTeamTest계열에서는 다대일, 일대일 관계를 가진 엔티티를 저장하고 조회한다.
이 구간에서는 연관 필드 조건 조회, 단건 조회,null관계,Native SQL과JPQL차이를 확인한다.
마지막 흐름은 실제 프로그램 구조에 가까운 DAO 예제이다
Visitor,Meeting,Reply,Book예제에서는 실제 프로그램 구조에 가까운DAO기반CRUD흐름과 댓글의 다대일 관계를 확인한다.
CRUD는 생성, 조회, 수정, 삭제를 의미한다.
실제 프로그램에서 가장 기본이 되는 데이터 처리 흐름이다.
결과물은 코드 흐름과 함께 봐야 한다
응용예제 결과물은 대부분 콘솔 출력이나
SQL로그로 확인한다.
일부 예제는 실행 결과가 짧지만,EmpApp,VisitorApp,MeetingApp처럼 결과가 길게 나오는 예제도 있다.
결과가 길게 나오면 전체를 한 번에 외우려고 하면 안 된다.
구분선, 입력 메뉴, 출력 메시지,SQL로그를 기준으로 어느 코드가 실행된 결과인지 나누어 봐야 한다.
결과물이 길게 나오는 대표 예제
EntityTestApp3은 저장 요청,JPQL조회, 추가 저장 요청, 다시 조회, 마지막rollback()흐름으로 결과가 길어진다.EmpApp1,EmpApp2는 여러DAO메서드를 순서대로 호출하기 때문에 결과가 여러 구간으로 나뉜다.VisitorApp,MeetingApp은 메뉴 번호에 따라 결과 흐름이 달라진다.결과가 길어도 핵심은 출력 줄 수가 아니다.
어떤 코드가 실행되었고, 그 결과 어떤 콘솔 출력이나SQL로그가 나온 것인지 연결해서 봐야 한다.
응용예제 결과물은 출력 줄 수보다 어떤 코드 흐름이 어떤 결과를 만든 것인지 연결해서 보는 것이 중요하다.
핵심 정리
이번 응용예제 전체는 파일을 역할별로 나누어 읽어야 한다.
실행 파일만 보면 어떤 테이블과 연결되는지 알기 어렵고,Entity만 보면 실제로 어떤 작업을 하는지 알기 어렵다.
그래서Entity구조와 실행 흐름을 함께 봐야 한다.
정리하면 아래와 같다.
Entity는 데이터 구조와 테이블 매핑을 담당한다.- 실행 파일은
EntityManager를 사용해 저장, 조회, 삭제 흐름을 확인한다.DAO는 데이터베이스 접근 코드를 메서드 단위로 분리한다.DTO는 조회 결과를 필요한 형태로 담는다.- 콘솔 앱은 사용자 입력과 실행 흐름을 담당한다.
- 설정 파일은
JPA실행 설정 묶음을 제공한다.응용예제는 파일명을 외우는 것이 아니라, 각 파일이
JPA실행 흐름의 어느 단계를 맡는지 확인하는 방식으로 읽어야 한다.
HelloJPA1.java와HelloJPA2.java는JPA실행 흐름의 시작점을 확인하는 예제이다.
이 예제는 데이터를 저장하거나 조회하는 코드가 아니다.
JPA를 사용하기 위해 필요한 실행 객체가 어떤 순서로 만들어지는지 확인한다.
이전 기본개념에서EntityManagerFactory는EntityManager를 만드는 공장이고,EntityManager는 실제Entity작업을 처리하는 객체라고 정리했다.
이번 예제에서는 그 개념이 실제 코드에서 어떻게 등장하는지 확인한다.
이 예제의 핵심은EntityManagerFactory를 먼저 만들고, 그다음EntityManager를 만든다는 실행 준비 순서를 확인하는 것이다.
이전 기본개념과 연결하기
이전 기본개념에서
JPA실행 흐름은Persistence에서 시작한다고 정리했다.
Persistence.createEntityManagerFactory()를 호출하면persistence.xml에 적힌 설정 묶음을 읽고EntityManagerFactory가 만들어진다.
EntityManagerFactory는 이름 그대로EntityManager를 만드는 공장 역할이다.
공장은 실제 작업을 직접 처리하는 객체라기보다, 작업 객체를 만들어 주는 기준 객체로 보면 된다.
EntityManager는 실제로Entity를 저장, 조회, 수정, 삭제할 때 사용하는 객체이다.
앞으로 나오는persist(),find(),remove(),createQuery()같은 메서드는 대부분EntityManager를 통해 사용한다.
기본개념에서 다시 잡아야 할 흐름
Persistence는JPA실행 설정을 읽는 시작점이다.EntityManagerFactory는EntityManager를 만드는 공장이다.EntityManager는 실제 데이터 작업을 처리하는 객체이다.EntityManagerFactory를 먼저 만들고, 그다음EntityManager를 만든다.- 사용이 끝나면
close()로 자원을 정리한다.이 흐름을 알고 보면
HelloJPA1.java와HelloJPA2.java는 매우 단순하다.
두 파일은 저장이나 조회가 아니라,JPA실행 준비 단계만 확인하는 예제이다.
예제의 목표
이 예제의 목표는
JPA실행 객체 생성 순서를 확인하는 것이다.
HelloJPA1.java는EntityManagerFactory만 만든다.
HelloJPA2.java는EntityManagerFactory를 만든 뒤EntityManager까지 만든다.
두 파일 모두 출력 결과로 객체의 실제 클래스명을 보여 준다.
코드에서 사용하는 타입은JPA표준 인터페이스이지만, 실제 실행 객체는 구현체가 만들어 준 객체이다.
여기서는Hibernate가 구현체 역할을 한다.
이 예제에서 확인할 내용
"entitytest"설정 묶음으로EntityManagerFactory를 만든다.EntityManagerFactory에서EntityManager를 만든다.getClass().getName()으로 실제 생성된 객체의 클래스명을 확인한다.- 사용이 끝난 객체는
close()로 정리한다.표준 타입으로 코드를 작성하지만, 실제 실행 객체는
Hibernate같은 구현체가 만들어 준다는 점을 확인해야 한다.
코드 흐름
HelloJPA1.java는EntityManagerFactory생성만 확인한다.
이 파일은EntityManager를 만들지 않는다.
따라서 실제 데이터 작업도 하지 않는다.// HelloJPA1.java public class HelloJPA1 { public static void main(String[] args) { EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 System.out.println("EntityManagerFactory 객체 : " + factory.getClass().getName()); // 실제 구현 클래스명 출력 factory.close(); // Factory 자원 정리 } }
Persistence.createEntityManagerFactory("entitytest")는persistence.xml안의"entitytest"설정 묶음을 읽는다.
그리고 그 설정을 기준으로EntityManagerFactory를 만든다.
factory.getClass().getName()은 생성된 객체의 실제 클래스명을 문자열로 가져온다.
이 출력 결과를 통해JPA표준 타입 뒤에서 어떤 구현 객체가 만들어졌는지 확인할 수 있다.
HelloJPA2.java는 여기서 한 단계 더 나아간다.
EntityManagerFactory를 만든 뒤,factory.createEntityManager()로EntityManager를 만든다.// HelloJPA2.java public class HelloJPA2 { public static void main(String[] args) { EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 System.out.println("EntityManagerFactory 객체 : " + factory.getClass().getName()); // Factory 구현 클래스명 출력 EntityManager em = factory.createEntityManager(); // EntityManager 생성 System.out.println("EntityManager 객체 : " + em.getClass().getName()); // EntityManager 구현 클래스명 출력 em.close(); // EntityManager 자원 정리 factory.close(); // Factory 자원 정리 } }
factory.createEntityManager()는 실제 작업 객체인EntityManager를 만든다.
앞으로Entity저장, 조회, 수정, 삭제는 이EntityManager를 통해 처리된다.
HelloJPA2.java에서도 아직persist()나find()는 나오지 않는다.
이 예제의 목적은 데이터 작업이 아니라, 데이터 작업을 하기 전 필요한 객체 생성 순서를 확인하는 것이다.
핵심 코드 해석
Persistence.createEntityManagerFactory
Persistence.createEntityManagerFactory("entitytest")는JPA실행의 시작점이다.
여기서"entitytest"는 설정 묶음 이름이다.
이 이름은 아무 문자열이 아니다.
persistence.xml에 정의된 설정 이름과 연결된다.
그래서"entitytest"라는 이름을 잘못 쓰면JPA가 어떤 설정을 읽어야 하는지 찾지 못할 수 있다.
이 코드는 아래처럼 이해하면 된다.
persistence.xml에서"entitytest"설정을 찾는다.- 해당 설정을 기준으로 데이터베이스 연결과
JPA구현체 설정을 준비한다.EntityManagerFactory객체를 만든다.
EntityManagerFactory는 무거운 객체로 이해하면 된다.
실습용main()에서는 실행할 때 만들고 끝날 때 닫지만, 실제 애플리케이션에서는 보통 자주 만들고 닫는 객체로 쓰지 않는다.
factory.createEntityManager
factory.createEntityManager()는EntityManager를 만든다.
EntityManager는 실제 작업 객체이다.
앞으로 나오는 저장, 조회, 수정, 삭제 흐름은 거의 모두EntityManager와 연결된다.
예를 들어em.persist(entity)는 저장 요청이고,em.find(Entity.class, id)는 기본키 조회이다.
이 예제에서는 아직 그런 작업을 하지 않는다.
하지만HelloJPA2.java에서EntityManager를 생성해 보기 때문에, 뒤 예제에서em.persist()가 나와도 흐름이 끊기지 않는다.
getClass().getName
getClass().getName()은 객체의 실제 클래스명을 확인하는 코드이다.
초보자 입장에서는 이 부분이 조금 낯설 수 있다.
코드에서는 변수를EntityManagerFactory타입으로 선언한다.
하지만 실제 객체는Hibernate가 만든 구현 클래스일 수 있다.
그래서getClass().getName()을 출력하면 표준 타입 이름이 아니라 실제 구현 클래스명이 길게 출력될 수 있다.
이 코드로 확인하는 흐름은 아래와 같다.
- 변수 타입은
EntityManagerFactory이다.- 실제 생성된 객체는 구현체의 클래스이다.
getClass().getName()으로 실제 클래스명을 확인한다.인터페이스 타입으로 선언해도 실제 실행 객체는 구현체가 만든 클래스일 수 있다.
close와 연결 풀 정리 로그
close()는 사용한 자원을 정리하는 메서드이다.
EntityManager와EntityManagerFactory는 데이터베이스 연결과 관련된 자원을 사용할 수 있다.
그래서 실습 코드에서는 사용이 끝난 뒤 닫아 주는 흐름을 잡는 것이 좋다.
닫는 순서도 중요하게 보면 좋다.
HelloJPA2.java에서는EntityManager를 먼저 닫고, 그다음EntityManagerFactory를 닫는다.
작업 객체를 먼저 정리하고, 공장 객체를 나중에 정리하는 흐름이다.
실행 결과 아래쪽에는Cleaning up connection pool로그가 보일 수 있다.
이 로그는 오류가 아니다.
EntityManagerFactory를 닫으면서Hibernate가 데이터베이스 연결 자원을 정리한다는 의미이다.
마지막에Process finished with exit code 0이 출력되면 프로그램이 정상 종료된 것이다.
즉, 빨간색 로그가 보인다고 해서 무조건 오류로 판단하면 안 된다.
정리하면 아래와 같다.
EntityManager는 실제 작업 객체이므로 사용 후 닫는다.EntityManagerFactory는 공장 객체이므로 마지막에 닫는다.Cleaning up connection pool은 연결 자원 정리 로그이다.exit code 0은 프로그램이 정상 종료되었다는 의미이다.
결과물은 어떻게 나오는가
HelloJPA1.java를 실행하면 콘솔에EntityManagerFactory객체의 실제 클래스명이 출력된다.
제공한 첫 번째 실행 결과에서는EntityManagerFactory 객체 : org.hibernate.internal.SessionFactoryImpl이 출력된다.
SessionFactoryImpl은Hibernate가 만든 실제 구현 클래스이다.
즉, 코드에서는EntityManagerFactory라는 표준 타입으로 받았지만, 내부 실행 객체는Hibernate구현 객체로 만들어졌다는 뜻이다.
HelloJPA2.java를 실행하면 두 종류의 클래스명이 출력된다.
제공한 두 번째 실행 결과에서는EntityManagerFactory 객체 : org.hibernate.internal.SessionFactoryImpl과EntityManager 객체 : org.hibernate.internal.SessionImpl이 함께 출력된다.
SessionImpl은Hibernate가 만든 실제EntityManager구현 객체로 볼 수 있다.
왼쪽은
HelloJPA1.java실행 결과이고, 오른쪽은HelloJPA2.java실행 결과이다.
왼쪽 결과에서는EntityManagerFactory구현 객체만 확인한다.
오른쪽 결과에서는EntityManagerFactory가 먼저 생성되고, 그 공장에서 실제 작업 객체인EntityManager가 생성된 것을 함께 확인한다.
출력 아래쪽의Cleaning up connection pool로그는 연결 자원 정리 로그이다.
마지막의Process finished with exit code 0은 프로그램이 정상 종료되었다는 뜻이다.
결과가 길게 보여도 기준은 단순하다.
EntityManagerFactory가 먼저 만들어지고, 그다음EntityManager가 만들어졌는지 확인하면 된다.
// 출력결과 // HelloJPA1.java // EntityManagerFactory 객체 : org.hibernate.internal.SessionFactoryImpl // Cleaning up connection pool 로그 // Process finished with exit code 0 // HelloJPA2.java // EntityManagerFactory 객체 : org.hibernate.internal.SessionFactoryImpl // EntityManager 객체 : org.hibernate.internal.SessionImpl // Cleaning up connection pool 로그 // Process finished with exit code 0결과 화면에서 봐야 할 핵심은 클래스명 전체가 아니라
EntityManagerFactory와EntityManager가 실제 구현 객체로 생성되었다는 점이다.
핵심 정리
HelloJPA1.java와HelloJPA2.java는JPA실행 준비 흐름을 확인하는 예제이다.
이 단계에서는 아직 데이터 저장이나 조회를 하지 않는다.
핵심은 아래와 같다.
Persistence.createEntityManagerFactory("entitytest")로 설정 묶음을 읽는다.EntityManagerFactory는EntityManager를 만드는 공장이다.factory.createEntityManager()로 실제 작업 객체인EntityManager를 만든다.getClass().getName()으로 실제 구현 클래스명을 확인할 수 있다.Cleaning up connection pool은 연결 자원 정리 로그이다.Process finished with exit code 0은 정상 종료를 의미한다.- 사용이 끝난 뒤에는
close()로 자원을 정리한다.이 예제를 이해하면 뒤에서 나오는 모든
EntityManager기반 저장, 조회, 수정, 삭제 코드의 시작점을 알 수 있다.
EntityTest계열 파일은JPA에서Entity가 어떻게 테이블과 연결되는지 확인하는 예제이다.
앞에서Entity는 데이터베이스 테이블과 매핑되는Java클래스라고 정리했다.
이번 예제에서는 그 개념이 실제 코드에서 어떻게 작성되는지 확인한다.
실행 파일에서persist()나find()를 호출하더라도, 어떤 테이블에 저장되고 어떤 컬럼으로 조회되는지는Entity클래스의 매핑 정보가 결정한다.
따라서 저장이나 조회 예제를 보기 전에Entity구조를 먼저 읽어야 한다.
Entity매핑 구조를 먼저 이해해야 뒤에서 나오는 저장, 조회, 수정, 삭제 코드가 어떤 테이블을 대상으로 동작하는지 알 수 있다.
이전 기본개념과 연결하기
이전 기본개념에서
Entity는JPA가 관리하는 객체라고 정리했다.
@Entity가 붙은 클래스는 데이터베이스 테이블과 연결될 수 있고,@Id가 붙은 필드는 기본키 역할을 한다.
기본키는 각 데이터를 구분하는 값이다.
또@Table,@Column,@GeneratedValue,@Lob,@Temporal,@ManyToOne같은 어노테이션도 함께 봤다.
어노테이션은 코드에 붙이는 설정 표시이다.
JPA는 이 표시를 보고 어떤 테이블과 어떤 컬럼을 사용할지 판단한다.
이번 응용예제에서는 기본개념에서 배운 매핑 어노테이션이 실제Entity클래스 안에서 어떻게 쓰이는지 확인한다.
기본개념에서 다시 잡아야 할 흐름
@Entity는JPA관리 대상 클래스라는 뜻이다.@Table은 연결할 테이블 이름을 지정한다.@Id는 기본키 필드를 지정한다.@GeneratedValue는 기본키 자동 생성 전략을 지정한다.@Column은 컬럼 이름, 길이,null허용 여부 같은 세부 조건을 지정한다.@Lob은 큰 데이터 저장에 사용한다.@Temporal은 오래된 날짜 타입에서 날짜, 시간, 날짜와 시간을 구분할 때 사용한다.@ManyToOne은 여러 엔티티가 하나의 엔티티와 연결되는 관계를 나타낸다.이 기준을 가지고
EntityTest1.java부터MyMyTest.java까지 보면, 각 파일이 어떤 매핑 개념을 확인하는 예제인지 구분할 수 있다.
예제의 목표
이 예제의 목표는 여러
Entity클래스의 매핑 구조를 비교해서 보는 것이다.
EntityTest1.java하나만 보면 기본적인Entity구조만 보인다.
하지만EntityTest2.java,EntityTest3.java,EntityTest4.java,EntityTest5.java,MyMyTest.java까지 함께 보면 기본형과 참조형, 컬럼 설정, 날짜·시간 매핑, 연관관계, 이름 변환 규칙까지 흐름이 넓어진다.
이 예제에서 확인할 내용
EntityTest1.java: 가장 기본적인Entity구조와IDENTITY기본키 자동 생성EntityTest2.java: 기본형int와 참조형Integer차이EntityTest3.java:@Column세부 설정과@LobEntityTest4.java: 날짜와 시간 타입 매핑EntityTest5.java:@ManyToOne을 이용한 간단한 연관관계MyMyTest.java: 클래스명과 필드명이 실제 테이블명, 컬럼명으로 바뀌는 흐름이 묶음은 실행 결과보다
Entity클래스 자체의 매핑 정보를 읽는 것이 핵심이다.
EntityTest1은 기본 저장과 조회 예제의 기준 Entity이다
EntityTest1.java는 뒤에서 나오는EntityTestApp1.java부터EntityTestApp4.java까지에서 계속 사용된다.
즉, 저장, 조회,flush,rollback(), 삭제 예제의 기준이 되는Entity이다.
코드 구조는 아래와 같다.// EntityTest1.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @Entity // JPA가 관리할 Entity 클래스 @Table(name="entitytesttbl") // entitytesttbl 테이블과 매핑 public class EntityTest1 { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; private String name; // 이름 private int age; // 나이 private LocalDateTime birthday; // 날짜와 시간 }
@Entity가 있으므로 이 클래스는JPA가 관리하는 엔티티가 된다.
@Table(name="entitytesttbl")이 있으므로 실제 테이블 이름은entitytesttbl로 지정된다.
id에는@Id와@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있다.
@Id는 기본키이고,IDENTITY전략은 기본키 값을 데이터베이스가 자동으로 만들게 하는 방식이다.
name,age,birthday에는 별도의@Column설정이 없다.
이런 경우에는 기본 매핑 규칙에 따라 필드명이 컬럼명으로 사용될 수 있다.
birthday는LocalDateTime타입이므로 날짜와 시간을 함께 담을 수 있다.
EntityTest1에서 봐야 할 핵심
@Entity로JPA관리 대상이 된다.@Table(name="entitytesttbl")로 테이블명을 직접 지정한다.id는 기본키이다.IDENTITY전략으로 기본키를 데이터베이스가 자동 생성한다.@ToString덕분에 조회 결과를 출력할 때 객체 내용이 보기 좋게 출력될 수 있다.뒤의 저장, 조회, 삭제 예제는 모두
EntityTest1의 매핑 정보를 기준으로 동작한다.
EntityTest1은entitytesttbl테이블과 매핑된다.
결과에서id컬럼에auto_increment가 붙은 것을 확인할 수 있다.
이는@GeneratedValue(strategy = GenerationType.IDENTITY)설정이 실제 테이블 구조에 반영된 결과이다.
EntityTest2는 기본형과 참조형 차이를 확인한다
EntityTest2.java는 구조가 단순하지만,int와Integer가 함께 나온다는 점이 중요하다.
둘 다 숫자를 담을 수 있지만 의미가 다르다.
코드 구조는 아래와 같다.// EntityTest2.java @Entity // JPA가 관리할 Entity 클래스 public class EntityTest2 { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; private String name; // 이름 private int number1; // 기본형 int private Integer number2; // 참조형 Integer }
int는 기본형이다.
기본형은 값이 없다는 상태를 표현하기 어렵다.
값을 따로 넣지 않으면 기본값0이 들어갈 수 있다.
Integer는 참조형이다.
참조형은 값이 없다는 의미로null을 가질 수 있다.
null은 아직 어떤 객체나 값이 연결되지 않았다는 뜻이다.
이 차이는 데이터베이스의NULL과 연결해서 생각해야 한다.
숫자 값이 반드시 있어야 하는 필드라면int를 사용할 수 있다.
하지만 값이 없을 수 있는 숫자라면Integer처럼null을 표현할 수 있는 타입이 더 자연스러울 수 있다.
int와 Integer 비교
int: 기본형이고 값이 없으면 기본값0이 들어갈 수 있다.Integer: 참조형이고null을 가질 수 있다.0은 실제 숫자 값이다.null은 값이 없다는 뜻이다.숫자 필드를 만들 때
0과null은 같은 의미가 아니므로 기본형과 참조형 차이를 구분해야 한다.
EntityTest3은 Column 세부 설정과 Lob을 확인한다
EntityTest3.java는@Column설정을 자세히 확인하는 예제이다.
필드가 컬럼으로 매핑될 때 이름, 길이,null허용 여부, 숫자 자리수, 컬럼 정의 같은 조건을 세밀하게 지정할 수 있다.
코드 구조는 아래와 같다.// EntityTest3.java @Entity // JPA가 관리할 Entity 클래스 @Getter // getter 자동 생성 @Setter // setter 자동 생성 public class EntityTest3 { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; @Column(nullable = false, length=6) // null 불가, 길이 6 private String myName; @Column(columnDefinition = "varchar(30) default '녹색'") // 컬럼 정의 직접 지정 private String favoriteColor; @Column(name="age", nullable = false) // age 컬럼과 매핑, null 불가 private int number1; @Column(name="score", precision = 6, scale = 2) // 전체 6자리, 소수 2자리 private BigDecimal number2; @Lob // 큰 데이터 매핑 private byte[] content1; @Lob // 큰 문자 데이터 매핑 private char[] content2; @Lob // 긴 문자열 매핑 private String content3; }
@Column(nullable = false, length=6)은myName컬럼이null을 허용하지 않고, 문자열 길이는6으로 제한된다는 뜻이다.
nullable = false는 반드시 값이 있어야 한다는 의미이다.
@Column(columnDefinition = "varchar(30) default '녹색'")은 데이터베이스 컬럼 정의를 직접 작성한 것이다.
favoriteColor컬럼을varchar(30)형태로 만들고, 데이터베이스 컬럼 정의에 기본값을 넣는 설정이다.
여기서 주의해야 한다.
이 설정은 테이블 컬럼 정의에 기본값을 넣는다는 의미이지,Java객체의favoriteColor필드가 자동으로"녹색"이 된다는 뜻은 아니다.
실제 저장할 때 기본값이 적용되는지는insert문장이 어떻게 만들어지는지, 해당 컬럼 값이 실제로 생략되는지에 따라 달라질 수 있다.
즉, 초보자 단계에서는 이렇게 이해하면 된다.
columnDefinition은 데이터베이스 컬럼 모양을 직접 정하는 설정이고, 객체 필드 기본값을 자동으로 넣어 주는 기능으로 단정하면 안 된다.
@Column(name="age", nullable = false)는number1필드를 데이터베이스에서는age컬럼으로 사용하겠다는 뜻이다.
필드명과 컬럼명이 반드시 같을 필요는 없다.
precision = 6,scale = 2는 숫자 자리수를 지정한다.
전체 자리수는6이고, 소수점 아래는2자리이다.
예를 들어 점수, 금액처럼 자리수 관리가 필요한 값에 사용할 수 있다.
@Lob은 큰 데이터를 저장할 때 사용한다.
byte[]는 바이트 데이터,char[]와String은 문자 데이터와 연결해서 생각할 수 있다.
EntityTest3에서 봐야 할 핵심
nullable = false는 값이 반드시 있어야 한다는 뜻이다.length는 문자열 길이를 제한한다.name은 실제 컬럼명을 지정한다.columnDefinition은 컬럼 정의를 직접 지정한다.columnDefinition의 기본값 설정을Java객체 필드 기본값으로 바로 오해하면 안 된다.precision과scale은 숫자 자리수를 지정한다.@Lob은 큰 데이터를 저장할 때 사용한다.
@Column은 단순히 컬럼 이름만 정하는 것이 아니라 컬럼의 제약 조건과 저장 형태를 세밀하게 조정하는 설정이다.
EntityTest3에서는@Column,columnDefinition,precision,scale,@Lob설정이 실제 컬럼 타입과 제약 조건으로 반영된 것을 확인할 수 있다.
myName은 길이 제한이 있는varchar(6)으로 생성되고,score는decimal(6,2)로 생성된다.
또@Lob이 붙은 필드는tinyblob,tinytext계열 타입으로 만들어진 것을 확인할 수 있다.
EntityTest4는 날짜와 시간 매핑을 확인한다
EntityTest4.java는 날짜와 시간 타입을 다양하게 보여 준다.
날짜와 시간은 타입마다 표현하는 범위가 다르기 때문에 구분해서 봐야 한다.
코드 구조는 아래와 같다.// EntityTest4.java @Entity // JPA가 관리할 Entity 클래스 public class EntityTest4 { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; @Temporal(TemporalType.TIME) // 시간만 저장 private java.util.Date utilTime; @Temporal(TemporalType.DATE) // 날짜만 저장 private java.util.Date utilDate; @Temporal(TemporalType.TIMESTAMP) // 날짜와 시간 저장 private java.util.Date utilTimestamp; private java.util.Date utilPlainDate; // Temporal 없이 사용한 util Date private java.sql.Date sqlPlainDate; // SQL Date @Column(columnDefinition = "TIME") // DB TIME 컬럼 지정 private LocalTime localTime1; private LocalTime localTime2; // 시간만 표현 @Column(columnDefinition = "DATE") // DB DATE 컬럼 지정 private LocalDate localDate1; private LocalDate localDate2; // 날짜만 표현 @Column(columnDefinition = "TIMESTAMP") // DB TIMESTAMP 컬럼 지정 private LocalDateTime localDateTime1; private LocalDateTime localDateTime2; // 날짜와 시간 표현 }
java.util.Date를 사용할 때는@Temporal로 저장 범위를 지정할 수 있다.
TemporalType.TIME은 시간만 저장한다.
TemporalType.DATE는 날짜만 저장한다.
TemporalType.TIMESTAMP는 날짜와 시간을 함께 저장한다.
utilPlainDate는@Temporal을 붙이지 않은java.util.Date필드이다.
이 필드는@Temporal을 붙인 경우와 붙이지 않은 경우의 차이를 비교해 보기 위한 필드로 보면 된다.
오래된 날짜 타입을 사용할 때는 어떤 범위로 저장할지 명확히 지정하는 것이 중요하다.
java.sql.Date는SQL의 날짜 표현과 연결되는 타입이다.
java.util.Date와 이름이 비슷하지만, 실제 의미와 사용 흐름은 다를 수 있으므로 구분해서 봐야 한다.
LocalTime,LocalDate,LocalDateTime은 이름에서 의미가 더 분명하다.
LocalTime은 시간만 표현한다.
LocalDate는 날짜만 표현한다.
LocalDateTime은 날짜와 시간을 함께 표현한다.
@Column(columnDefinition = "TIME"),@Column(columnDefinition = "DATE"),@Column(columnDefinition = "TIMESTAMP")는 데이터베이스 컬럼 타입을 직접 지정한 것이다.
날짜와 시간 타입 비교
LocalTime: 시간만 표현한다.LocalDate: 날짜만 표현한다.LocalDateTime: 날짜와 시간을 함께 표현한다.TemporalType.TIME:java.util.Date에서 시간만 저장한다.TemporalType.DATE:java.util.Date에서 날짜만 저장한다.TemporalType.TIMESTAMP:java.util.Date에서 날짜와 시간을 함께 저장한다.utilPlainDate:@Temporal을 붙이지 않은 경우를 비교하기 위한 필드로 볼 수 있다.날짜와 시간 매핑은 어떤 값을 저장하려는지에 따라 타입과 컬럼 정의를 맞춰야 한다.
EntityTest4에서는 날짜와 시간 타입이date,time,timestamp,datetime계열 컬럼으로 매핑되는 흐름을 확인할 수 있다.
LocalDate는 날짜를 저장하는date계열로,LocalTime은 시간을 저장하는time계열로,LocalDateTime은 날짜와 시간을 함께 저장하는timestamp또는datetime계열로 연결된다.
EntityTest5는 간단한 ManyToOne 관계를 보여 준다
EntityTest5.java는Team을 참조하는 간단한 연관관계 예제이다.
여기서는@ManyToOne이 사용된다.
코드 구조는 아래와 같다.// EntityTest5.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @Entity // JPA가 관리할 Entity 클래스 public class EntityTest5 { @Id // 기본키 필드 int id; private String username; // 사용자 이름 @ManyToOne // 여러 EntityTest5가 하나의 Team과 연결될 수 있음 private Team a; // Team 참조 public EntityTest5() { // 기본 생성자 } public EntityTest5(String username, Team team) { // 이름과 팀을 받는 생성자 this.username = username; // 사용자 이름 저장 a = team; // Team 참조 저장 } }
@ManyToOne은 여러 엔티티가 하나의 엔티티와 연결될 수 있다는 뜻이다.
예를 들어 여러 사용자가 하나의 팀에 속할 수 있다.
필드 이름이a라서 처음 보면 의미가 잘 보이지 않을 수 있다.
하지만 중요한 것은 필드 이름이 아니라, 이 필드의 타입이Team이고@ManyToOne이 붙어 있다는 점이다.
즉,EntityTest5는Team을 참조한다.
또 하나 봐야 할 점은id에@GeneratedValue가 없다는 점이다.
@Id만 있으면 기본키 필드라는 뜻은 되지만, 기본키 값을 자동으로 생성한다는 뜻은 아니다.
따라서 이 구조에서는 저장할 때id값을 직접 넣어야 할 수 있다.
이 예제는 기본키 자동 생성보다@ManyToOne연관관계 모양을 확인하는 데 초점을 두고 보면 된다.
이 예제에는@JoinColumn이 없다.
@JoinColumn은 외래키 컬럼명을 직접 지정할 때 사용하는 어노테이션이다.
따라서EntityTest5에서는 외래키 컬럼명이 명시적으로 정해진 것이 아니라,JPA의 기본 규칙에 따라 만들어질 수 있다.
이 부분은 뒤에서 나오는Member예제와 비교하면 더 분명해진다.
Member예제에서는@JoinColumn(name = "TEAM_ID")처럼 외래키 컬럼명을 직접 지정한다.
반면EntityTest5는@ManyToOne만 두고 기본 규칙에 맡긴 형태이다.
이 예제는 연관관계의 기본 모양을 짧게 보여 주는 용도이다.
뒤에서Member,Team,Locker예제로 다대일과 일대일 관계를 더 자세히 확인한다.
EntityTest5에서 봐야 할 핵심
EntityTest5는Team을 참조한다.@ManyToOne은 여러EntityTest5가 하나의Team과 연결될 수 있다는 뜻이다.id에는@GeneratedValue가 없으므로 기본키 자동 생성 예제가 아니다.- 이 예제에는
@JoinColumn이 없으므로 외래키 컬럼명은 기본 규칙에 따라 정해질 수 있다.- 필드 이름보다 필드 타입과 어노테이션을 먼저 봐야 한다.
연관관계 매핑을 볼 때는 참조 대상 엔티티, 관계 어노테이션, 기본키 자동 생성 여부, 외래키 컬럼명을 직접 지정했는지 여부를 함께 확인해야 한다.
EntityTest5에서는@GeneratedValue가 없어서id에auto_increment가 없는 것을 확인할 수 있다.
또@JoinColumn이 없어도 기본 규칙으로a_TEAM_ID외래키가 생성된 것을 확인할 수 있다.
즉, 필드명a와 참조 대상 기본키인TEAM_ID가 조합되어 외래키 컬럼명이 만들어진 흐름으로 볼 수 있다.
MyMyTest는 이름 변환 규칙을 확인하기 좋다
MyMyTest.java는 구조가 매우 단순하다.
하지만 클래스 이름과 필드 이름이 실제 테이블명, 컬럼명으로 어떻게 바뀔 수 있는지 확인하기 좋다.
코드 구조는 아래와 같다.// MyMyTest.java @Entity // JPA가 관리할 Entity 클래스 public class MyMyTest { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; private String myName; // 설정에 따라 컬럼명으로 연결됨 }
Java에서는MyMyTest,myName처럼 단어가 이어질 때 중간 단어를 대문자로 시작하는 방식을 자주 사용한다.
이런 표기 방식을camel case라고 한다.
데이터베이스에서는my_my_test,my_name처럼 단어 사이에 밑줄을 넣는 방식을 자주 사용한다.
이런 표기 방식을snake case라고 한다.
다만camel case가 항상snake case로 자동 변환되는 것은 아니다.
자동 변환 규칙은 프로젝트 설정에 따라 달라진다.
이번 실행 결과에서는 테이블명이MyMyTest그대로 생성되고, 컬럼명도myName그대로 생성되었다.
따라서 이름 변환은 항상 자동으로 일어난다고 단정하면 안 된다.
실제 테이블명과 컬럼명은 데이터베이스에서 직접 확인하는 것이 가장 정확하다.
명확하게 지정하고 싶다면@Table이나@Column을 사용해 이름을 직접 지정할 수 있다.
이름 변환에서 주의할 점
Java코드는 보통camel case를 사용한다.- 데이터베이스는 보통
snake case를 사용하는 경우가 많다.camel case가 항상snake case로 자동 변환되는 것은 아니다.- 현재 실행 결과에서는
MyMyTest,myName이 그대로 생성되었다.- 명확하게 지정하고 싶으면
@Table,@Column을 사용할 수 있다.클래스명과 필드명이 실제 테이블명과 컬럼명으로 어떻게 바뀌는지는 설정과 매핑 어노테이션을 함께 확인해야 한다.
현재 설정에서는
MyMyTest테이블명과myName컬럼명이 그대로 생성되었다.
따라서camel case가 항상snake case로 자동 변환된다고 단정하면 안 된다.
이름을 명확하게 고정하고 싶다면@Table이나@Column으로 직접 지정하는 방식이 더 안전하다.
핵심 정리
이번 묶음에서는 여러
Entity매핑 구조를 확인했다.
각 파일은 단순히 클래스 하나를 보여 주는 것이 아니라, 서로 다른 매핑 개념을 확인하는 역할을 한다.
정리하면 아래와 같다.
EntityTest1은 기본Entity구조와IDENTITY전략을 보여 준다.EntityTest2는int와Integer차이를 보여 준다.EntityTest3은@Column세부 설정과@Lob을 보여 준다.EntityTest4는 날짜와 시간 타입 매핑을 보여 준다.EntityTest5는@ManyToOne연관관계를 보여 주고,@GeneratedValue와@JoinColumn이 없을 때의 매핑 흐름을 확인하게 해 준다.MyMyTest는 이름 변환 규칙을 확인하기 좋다.
Entity매핑 구조를 읽을 수 있어야 이후 실행 파일에서 어떤 테이블과 컬럼이 사용되는지 자연스럽게 이해할 수 있다.
EntityTestApp1.java는EntityTest1객체를 여러 개 만들고 저장하는 예제이다.
앞에서Entity매핑 구조를 확인했다면, 이번에는 그Entity객체가 실제 저장 흐름에서 어떻게 사용되는지 확인한다.
이 예제의 핵심은persist()와commit()의 역할을 나누어 보는 것이다.
persist()는 저장할Entity를JPA가 관리하도록 등록하는 흐름이고,commit()은Transaction을 최종 확정하는 흐름이다.
이 예제는 여러Entity객체를 저장하면서persist(),Transaction,commit(),IDENTITY전략의 실행 흐름을 함께 확인하는 예제이다.
이전 기본개념과 연결하기
이전 기본개념에서 데이터베이스에 값을 저장하려면
EntityManager와Transaction이 필요하다고 정리했다.
EntityManager는 실제Entity작업을 처리하는 객체이고,Transaction은 데이터 변경 작업을 하나의 작업 단위로 묶는 기능이다.
저장, 수정, 삭제처럼 데이터베이스 상태를 바꾸는 작업은 보통Transaction안에서 처리한다.
작업을 시작할 때는begin()을 호출하고, 작업을 확정할 때는commit()을 호출한다.
중간에 문제가 있거나 확정하지 않으려면rollback()으로 되돌릴 수 있다.
또EntityTest1.java에서id필드에@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있었다.
IDENTITY전략은 기본키 값을 데이터베이스가 자동으로 만들어 주는 방식이다.
따라서 저장 흐름을 볼 때 기본키가 언제 만들어지고,insert쿼리가 언제 보이는지도 함께 확인해야 한다.
기본개념에서 다시 잡아야 할 흐름
EntityManager는Entity저장, 조회, 삭제 같은 실제 작업을 처리한다.Transaction은 데이터 변경 작업을 하나의 단위로 묶는다.begin()은Transaction시작이다.persist()는Entity를 저장 대상으로 등록한다.commit()은Transaction안의 작업을 최종 확정한다.IDENTITY전략은 기본키 값을 데이터베이스가 자동 생성한다.저장 예제는
persist()만 보면 안 되고,Transaction시작과commit()확정까지 함께 봐야 한다.
예제의 목표
이 예제의 목표는
EntityTest1객체5개를 만들어 데이터베이스에 저장하는 흐름을 확인하는 것이다.
단순히 데이터가 저장된다는 사실만 보는 예제가 아니다.
반복문으로 객체를 만들고, 값을 넣고,persist()로 저장 요청한 뒤, 마지막에commit()으로 확정하는 순서를 보는 예제이다.
또 코드 중간에는Scanner로 엔터 입력을 기다리는 부분이 있다.
이 부분은 실행 흐름을 잠시 멈춰서commit()전후 상태를 확인하기 위한 장치로 볼 수 있다.
이 예제에서 확인할 내용
EntityManagerFactory와EntityManager를 생성한다.Transaction을 시작한다.- 반복문으로
EntityTest1객체를5개 만든다.- 각 객체에
name,age,birthday값을 저장한다.persist()로 저장 요청한다.- 엔터 입력으로 중간 상태를 확인할 시간을 만든다.
commit()으로 저장 작업을 확정한다.이 흐름을 잡아두면 뒤에서 나오는
flush,rollback(),remove()예제도 훨씬 쉽게 이어진다.
코드 흐름
EntityTestApp1.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음Transaction을 시작하고, 반복문 안에서EntityTest1객체를 생성한다.
핵심 코드는 아래와 같다.// EntityTestApp1.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 EntityTest1 et; // 저장할 Entity 참조 변수 em.getTransaction().begin(); // Transaction 시작 for(int i=1; i < 6; i++) { // 1부터 5까지 반복 et = new EntityTest1(); // Entity 객체 생성 et.setName("둘리"+i); // 이름 저장 et.setAge(10+i); // 나이 저장 et.setBirthday(LocalDateTime.now()); // 현재 날짜와 시간 저장 em.persist(et); // 저장 요청 } System.out.println("엔터키....."); // 중간 확인 안내 출력 Scanner scan = new Scanner(System.in); // 입력 도구 생성 scan.nextLine(); // 엔터 입력 대기 scan.close(); // Scanner 닫기 em.getTransaction().commit(); // Transaction 확정 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기반복문은
i가1부터5까지 변하면서 총5번 실행된다.
그래서EntityTest1객체가5개 만들어진다.
각 객체에는 이름, 나이, 현재 날짜와 시간이 들어간다.
이름은둘리1,둘리2,둘리3,둘리4,둘리5형태로 만들어진다.
나이는11부터15까지 들어간다.
birthday에는 실행 시점의 현재 날짜와 시간이 들어간다.
핵심 코드 해석
반복문으로 Entity 객체를 여러 개 만든다
이 예제에서는 객체를 하나만 저장하지 않는다.
반복문을 사용해서EntityTest1객체를 여러 개 만든다.
// EntityTestApp1.java for(int i=1; i < 6; i++) { // 1부터 5까지 반복 et = new EntityTest1(); // 새 Entity 객체 생성 et.setName("둘리"+i); // 이름 저장 et.setAge(10+i); // 나이 저장 et.setBirthday(LocalDateTime.now()); // 현재 날짜와 시간 저장 em.persist(et); // 저장 요청 }
new EntityTest1()은 새로운Entity객체를 만드는 코드이다.
반복문이 한 번 돌 때마다 새로운 객체가 생성된다.
setName(),setAge(),setBirthday()는 객체의 필드 값을 채우는 메서드이다.
이 값들이 나중에 데이터베이스 컬럼 값으로 저장될 수 있다.
여기서 중요한 점은 객체를 만들기만 해서는 데이터베이스에 저장되지 않는다는 것이다.
new EntityTest1()은 메모리에 객체를 만든 것이고,em.persist(et)를 호출해야JPA저장 흐름에 들어간다.
et변수는 반복문 안에서 계속 같은 이름으로 사용된다.
하지만 반복이 돌 때마다new EntityTest1()로 새 객체가 만들어진다.
따라서 변수 이름은 하나여도 실제로는 서로 다른EntityTest1객체5개가persist()로 저장 대상에 등록된다.
persist는 저장 대상으로 등록하는 흐름이다
em.persist(et)는EntityTest1객체를 저장 대상으로 등록한다.
이때 객체는JPA가 관리하는 상태가 된다.
// EntityTestApp1.java em.persist(et); // Entity를 저장 대상으로 등록초보자가 가장 많이 헷갈리는 부분은
persist()를 “데이터베이스 저장 완료”라고 바로 이해하는 것이다.
이 단계에서는 저장 요청 또는 저장 대상 등록으로 이해하는 것이 더 안전하다.
일반적인 흐름에서는persist()로 영속성 컨텍스트에 저장 대상이 등록되고, 이후flush와commit()흐름을 거쳐 데이터베이스에 반영된다.
다만 이 예제의EntityTest1은IDENTITY전략을 사용한다.
IDENTITY전략은 데이터베이스가 기본키를 만들어 주는 방식이다.
그래서JPA가 기본키 값을 알아야 하는 상황에서는persist()시점에insert쿼리가 먼저 실행될 수 있다.
따라서 콘솔에insert쿼리가 보였다고 해서 바로 최종 저장이 끝났다고 보면 안 된다.
현재 흐름에서는 아직Transaction이 끝나지 않았기 때문에, 최종 저장 여부는commit()이후 데이터베이스에서 확인하는 것이 가장 안전하다.
정리하면 아래처럼 봐야 한다.
persist()는Entity를 저장 대상으로 등록한다.- 일반적으로 최종 확정은
commit()에서 이루어진다.IDENTITY전략에서는 기본키 값을 얻기 위해persist()시점에insert쿼리가 보일 수 있다.insert쿼리가 보여도 최종 저장 여부는commit()이후에 확인하는 것이 안전하다.
persist()의 의미와IDENTITY전략 때문에 쿼리가 빨리 실행될 수 있는 흐름을 구분해야 한다.
Scanner는 commit 전 상태를 확인하기 위한 대기 코드이다
코드 중간에는
Scanner로 엔터 입력을 기다리는 부분이 있다.
이 코드는 사용자에게 값을 입력받아 저장하려는 목적이 아니다.
실행을 잠시 멈춰서 중간 상태를 확인하기 위한 장치이다.
// EntityTestApp1.java System.out.println("엔터키....."); // 중간 확인 안내 출력 Scanner scan = new Scanner(System.in); // 입력 도구 생성 scan.nextLine(); // 엔터 입력 대기이 시점은
persist()를 모두 호출한 뒤이고,commit()은 아직 실행되기 전이다.
따라서 엔터를 누르기 전후를 나누어 보면persist()이후와commit()이후의 흐름을 비교할 수 있다.
다만 앞에서 말한 것처럼IDENTITY전략에서는persist()시점에insert쿼리가 이미 보일 수 있다.
그렇더라도 최종적으로 작업을 확정하는 흐름은commit()과 연결해서 봐야 한다.
이 예제에서는Scanner가 데이터 저장의 핵심 기능은 아니다.
실행 흐름을 잠시 멈춰서 관찰하기 위한 코드로 보면 된다.
commit은 Transaction을 최종 확정한다
마지막에는
commit()이 실행된다.
commit()은Transaction안에서 진행한 데이터 변경 작업을 최종 확정하는 메서드이다.
// EntityTestApp1.java em.getTransaction().commit(); // 저장 작업 확정
persist()가 저장 대상으로 등록하는 흐름이라면,commit()은 그 작업을 확정하는 흐름이다.
그래서 저장, 수정, 삭제 예제를 볼 때는 마지막에commit()이 있는지 반드시 확인해야 한다.
이 예제에서는commit()이 실행되므로 최종적으로EntityTest1객체5개가 데이터베이스에 저장되는 흐름으로 이어진다.
데이터 변경 작업은persist()호출만 보는 것이 아니라, 마지막에commit()으로 확정되는지까지 확인해야 한다.
결과물은 어떻게 나오는가
이 예제를 실행하면 콘솔에 먼저
엔터키.....가 출력된다.
프로그램은 이 지점에서 사용자가 엔터를 누를 때까지 기다린다.
엔터를 누르면commit()이 실행되고 프로그램이 종료된다.
실행 중에는Hibernate가 생성한insert쿼리가 보일 수 있다.
특히EntityTest1의 기본키는IDENTITY전략이므로, 기본키 값을 얻기 위해persist()시점에insert쿼리가 실행되는 흐름을 볼 수 있다.
하지만insert쿼리가 보였다는 사실만으로 최종 저장까지 확정되었다고 보면 안 된다.
이 예제에서는 엔터 입력 후commit()이 실행되므로, 최종 확인은commit()이후entitytesttbl을 조회해서 확인하는 것이 안전하다.
EntityTestApp1.java를 실행하면 반복문 안에서persist()가 여러 번 호출되고, 콘솔에insert쿼리가 출력될 수 있다.
마지막에는엔터키.....에서 실행이 멈춘다.
이 시점은persist()호출 이후이지만 아직commit()전 상태이다.
엔터를 입력하면commit()이 실행되어 저장 작업이 확정된다.
데이터베이스에서entitytesttbl을 조회하면둘리1부터둘리5까지 저장된 데이터를 확인할 수 있다.
id값은 데이터베이스가 자동으로 생성한 값이고,name,age,birthday는 코드에서 넣은 값이다.
entitytesttbl을 조회하면둘리1부터둘리5까지 저장된 데이터를 확인할 수 있다.
현재 화면에는 같은 예제를 여러 번 실행한 결과가 누적되어25개 행이 보인다.
각 실행마다EntityTest1객체5개가 저장되며,id는IDENTITY전략에 따라 데이터베이스가 자동 생성한다.
// 출력결과 // 엔터키..... // 엔터 입력 후 commit 실행 // entitytesttbl 테이블에 둘리1~둘리5 데이터 저장결과를 볼 때는 단순히 데이터가 들어갔는지만 보면 부족하다.
persist()가 반복문 안에서 여러 번 호출되고, 마지막에commit()으로 작업이 확정된다는 흐름을 함께 봐야 한다.
결과물을 확인할 때 봐야 할 기준
- 콘솔에서
엔터키.....가 출력되는지 확인한다.- 엔터 입력 전에는
commit()전 상태임을 이해한다.insert쿼리가 언제 출력되는지 확인한다.insert쿼리 출력과 최종 저장 확정은 구분해서 본다.- 엔터 입력 후
commit()이 실행되는 흐름을 확인한다.- 데이터베이스에서
entitytesttbl에 데이터가 저장되었는지 확인한다.이 예제의 결과물은 저장된 데이터 자체보다, 저장 요청과 최종 확정 시점을 나누어 보는 것이 중요하다.
핵심 정리
EntityTestApp1.java는EntityTest1객체를 여러 개 저장하면서JPA저장 흐름을 확인하는 예제이다.
반복문으로 객체를 만들고, 값을 넣고,persist()로 저장 대상으로 등록한 뒤,commit()으로 최종 확정한다.
정리하면 아래와 같다.
EntityTest1은entitytesttbl테이블과 매핑된다.- 반복문으로
EntityTest1객체5개를 만든다.et변수는 하나지만 반복마다 새 객체가 만들어진다.- 각 객체에 이름, 나이, 현재 날짜와 시간을 넣는다.
persist()는 저장 대상으로 등록하는 흐름이다.IDENTITY전략에서는persist()시점에insert쿼리가 보일 수 있다.insert쿼리 출력과 최종commit()확정은 구분해서 봐야 한다.Scanner는commit()전 상태를 확인하기 위한 대기 코드로 볼 수 있다.commit()은Transaction안의 저장 작업을 최종 확정한다.이 예제를 이해하면 이후
flush,rollback(),remove()예제에서 데이터 변경 작업이 언제 반영되고 언제 확정되는지 더 쉽게 이해할 수 있다.
EntityTestApp2.java는EntityTest1데이터를 전체 조회하는 예제이다.
앞의 예제에서는EntityTest1객체를 새로 만들어 저장했다.
이번 예제에서는 이미 저장된entitytesttbl데이터를JPQL로 조회해서EntityTest1객체 목록으로 가져온다.
이 예제의 핵심은JPQL조회 문장,TypedQuery,getResultList(),List,stream().forEach()흐름을 연결해서 보는 것이다.
저장 예제와 달리 데이터를 변경하지 않으므로Transaction을 직접 시작하지 않는다.
이 예제는 데이터베이스에 저장된 행을Entity객체 목록으로 조회하는 기본JPQL조회 흐름을 확인하는 예제이다.
이전 기본개념과 연결하기
이전 기본개념에서
JPQL은 테이블이 아니라Entity를 기준으로 작성하는 조회 언어라고 정리했다.
즉, 실제 테이블명이entitytesttbl이어도JPQL에서는EntityTest1이라는 엔티티 이름을 사용한다.
또EntityManager는 조회 작업도 처리한다고 정리했다.
저장할 때는persist()를 사용했고, 기본키로 한 건을 찾을 때는find()를 사용할 수 있다.
여러 데이터를 조건이나 목록 형태로 조회할 때는createQuery()를 사용한다.
이번 예제에서는createQuery()로JPQL을 만들고,getResultList()로 여러 개의 결과를 가져온다.
가져온 결과는List<EntityTest1>형태로 저장된다.
기본개념에서 다시 잡아야 할 흐름
JPQL은 데이터베이스 테이블명이 아니라Entity이름을 기준으로 작성한다.select t from EntityTest1 t에서EntityTest1은 엔티티 이름이다.t는 쿼리 안에서EntityTest1을 짧게 부르기 위한 별칭이다.TypedQuery는 조회 결과 타입이 정해진 쿼리 객체이다.getResultList()는 여러 건의 조회 결과를List로 가져온다.- 조회만 하는 예제이므로
begin()과commit()이 나오지 않는다.조회 예제에서는
JPQL이 실제 테이블명이 아니라 엔티티 이름을 사용한다는 점을 가장 먼저 확인해야 한다.
예제의 목표
이 예제의 목표는
EntityTest1데이터를 전체 조회하고 출력하는 흐름을 확인하는 것이다.
앞에서EntityTestApp1.java로둘리1부터둘리5까지 저장했다면, 이번 예제는 그 데이터를 다시 읽어오는 흐름이다.
즉,EntityTestApp1.java가 저장 예제라면EntityTestApp2.java는 조회 예제이다.
저장과 조회는 사용하는 메서드와 실행 흐름이 다르다.
저장은persist()와commit()이 중요하고, 조회는JPQL,TypedQuery,getResultList()가 중요하다.
이 예제에서 확인할 내용
"entitytest"설정으로EntityManagerFactory를 만든다.EntityManager를 만든다.JPQL문장으로EntityTest1전체를 조회한다.- 조회 결과를
List<EntityTest1>로 받는다.stream().forEach()로 목록을 하나씩 출력한다.- 조회 예제이므로
Transaction을 시작하지 않는다.이 흐름을 이해하면 뒤에서 나오는 조건 조회, 연관 필드 조회,
DAO조회 메서드 흐름도 더 쉽게 이어진다.
코드 흐름
EntityTestApp2.java는EntityManagerFactory와EntityManager를 만든 뒤,JPQL로EntityTest1전체 목록을 조회한다.
조회 결과는List로 받고, 목록의 각 요소를 콘솔에 출력한다.
핵심 코드는 아래와 같다.// EntityTestApp2.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<EntityTest1> q = em.createQuery( "select t from EntityTest1 t", EntityTest1.class ); // EntityTest1 전체 조회 JPQL 생성 List<EntityTest1> list = q.getResultList(); // 조회 결과를 List로 받음 list.stream().forEach(e -> System.out.println(e)); // 목록을 하나씩 출력 factory.close(); // Factory 자원 정리
Persistence.createEntityManagerFactory("entitytest")는"entitytest"설정 묶음을 읽어EntityManagerFactory를 만든다.
그다음factory.createEntityManager()로 실제 조회 작업에 사용할EntityManager를 만든다.
이 예제에서 가장 중요한 코드는em.createQuery("select t from EntityTest1 t", EntityTest1.class)이다.
이 한 줄에서JPQL문장을 만들고, 결과 타입을EntityTest1.class로 지정한다.
q.getResultList()는 조회 결과를 여러 건 가져온다.
반환 결과는List<EntityTest1>이다.
이 목록 안에는 데이터베이스 행이 그대로 들어 있는 것이 아니라, 조회 결과로 만들어진EntityTest1객체들이 들어 있다.
제공된 예제 코드에서는 마지막에factory.close()로EntityManagerFactory를 정리한다.
다만 일반적인 자원 정리 흐름에서는EntityManager도 사용 후em.close()로 닫아 주는 것이 좋다.
이후 예제들을 볼 때는 작업 객체인EntityManager와 공장 객체인EntityManagerFactory를 모두 정리하는 흐름도 함께 기억하면 된다.
핵심 코드 해석
select t from EntityTest1 t는 Entity 기준 조회이다
이 예제의
JPQL문장은 아래와 같다.// EntityTestApp2.java "select t from EntityTest1 t" // EntityTest1 전체 조회
select t는 조회 결과로t전체를 가져오겠다는 뜻이다.
여기서t는EntityTest1엔티티의 별칭이다.
from EntityTest1 t는EntityTest1엔티티를 조회 대상으로 삼고, 그 엔티티를 쿼리 안에서t라고 부르겠다는 뜻이다.
여기서 중요한 점은entitytesttbl이라는 테이블명을 쓰지 않는다는 것이다.
EntityTest1.java에는@Table(name="entitytesttbl")이 있었지만,JPQL에서는 테이블명이 아니라 엔티티 클래스 이름인EntityTest1을 사용한다.
SQL과 JPQL의 차이
SQL: 실제 테이블명을 기준으로 작성한다.JPQL:Entity이름과 필드명을 기준으로 작성한다.- 실제 테이블명은
entitytesttbl이다.JPQL조회 대상은EntityTest1이다.
JPQL에서EntityTest1을 사용한다고 해서 테이블 이름이EntityTest1이라는 뜻은 아니다.
TypedQuery는 결과 타입을 정해 둔 Query이다
TypedQuery<EntityTest1>은 조회 결과가EntityTest1타입이라는 뜻이다.
Typed는 타입이 정해져 있다는 의미로 이해하면 된다.
// EntityTestApp2.java TypedQuery<EntityTest1> q = em.createQuery( "select t from EntityTest1 t", EntityTest1.class ); // 결과 타입이 EntityTest1인 Query
createQuery()의 두 번째 인자인EntityTest1.class는 조회 결과 타입을 알려 준다.
그래서 결과를 받을 때List<EntityTest1>로 자연스럽게 받을 수 있다.
만약 결과 타입을 정하지 않는 일반Query를 사용하면, 나중에 결과를 꺼낼 때 타입을 직접 확인하거나 형변환해야 하는 상황이 생길 수 있다.
초보자 단계에서는 결과 타입이 명확한 조회라면TypedQuery가 더 읽기 쉽다.
TypedQuery에서 봐야 할 핵심
- 조회 결과 타입이
EntityTest1로 정해져 있다.EntityTest1.class가 결과 타입 기준이 된다.- 결과를
List<EntityTest1>로 받을 수 있다.- 타입이 명확하므로 코드를 읽기 쉽다.
getResultList는 여러 건의 결과를 List로 가져온다
getResultList()는 조회 결과가 여러 건일 때 사용하는 메서드이다.
이 예제에서는 전체 데이터를 조회하므로 결과가 여러 개일 수 있다.
// EntityTestApp2.java List<EntityTest1> list = q.getResultList(); // 여러 건의 결과 조회
List는 여러 값을 순서대로 담는 자료구조이다.
여기서는 조회된EntityTest1객체들이List안에 들어간다.
앞의 저장 예제에서 데이터를 여러 개 넣었다면, 이번 조회 결과도 여러 개가 될 수 있다.
따라서getSingleResult()처럼 한 건만 가져오는 메서드가 아니라,getResultList()를 사용한다.
getResultList와 getSingleResult 차이
getResultList(): 결과가 여러 건일 때 사용한다.getSingleResult(): 결과가 한 건이라고 예상할 때 사용한다.- 전체 조회는 보통 여러 건이므로
getResultList()가 자연스럽다.전체 조회처럼 결과가 여러 개일 수 있는 경우에는
getResultList()를 사용해야 한다.
stream().forEach는 목록을 하나씩 출력한다
조회 결과는
List<EntityTest1>이다.
목록 안에는 여러 개의EntityTest1객체가 들어 있다.
이 객체들을 콘솔에 하나씩 출력하기 위해stream().forEach()를 사용한다.
// EntityTestApp2.java list.stream().forEach(e -> System.out.println(e)); // 목록 요소를 하나씩 출력
stream()은 목록 데이터를 하나씩 처리하기 위한 흐름을 만든다.
forEach()는 그 흐름 안의 요소를 하나씩 꺼내 실행한다.
e -> System.out.println(e)는 목록에서 꺼낸 요소e를 출력하겠다는 뜻이다.
이 코드는 일반 반복문으로 쓰면 아래 흐름과 비슷하다.// EntityTestApp2ForLoop.java for (EntityTest1 e : list) { // 목록 요소를 하나씩 꺼냄 System.out.println(e); // 객체 출력 }두 코드 모두
list안의EntityTest1객체를 하나씩 출력한다.
차이는 표현 방식이다.
초보자 단계에서는stream().forEach()를 “목록을 하나씩 돌면서 출력한다” 정도로 이해하면 된다.
System.out.println(e)를 실행하면EntityTest1객체가 문자열로 출력된다.
EntityTest1.java에는@ToString이 붙어 있으므로 객체의 필드 값이 보기 좋은 형태로 출력될 수 있다.
조회만 하므로 Transaction을 시작하지 않는다
EntityTestApp2.java에는em.getTransaction().begin()이 없다.
또commit()도 없다.
이유는 이 예제가 데이터를 변경하지 않고 조회만 하기 때문이다.
조회는 데이터베이스 상태를 바꾸지 않는다.
그래서 이 코드에서는Transaction을 직접 시작하지 않고JPQL조회만 수행한다.
앞의 저장 예제와 비교하면 차이가 분명하다.// SaveFlow.java em.getTransaction().begin(); // 저장 작업 시작 em.persist(entity); // 저장 요청 em.getTransaction().commit(); // 저장 확정// ReadFlow.java TypedQuery<EntityTest1> q = em.createQuery( "select t from EntityTest1 t", EntityTest1.class ); // 조회 Query 생성 List<EntityTest1> list = q.getResultList(); // 조회 실행저장 흐름에는
Transaction시작과 확정이 있다.
조회 흐름에는JPQL을 만들고 결과를 가져오는 흐름이 중심이다.
저장 예제와 조회 예제를 비교할 때는Transaction이 필요한 이유가 데이터 변경 여부와 연결된다는 점을 봐야 한다.
결과물은 어떻게 나오는가
이 예제를 실행하면
entitytesttbl에 저장된 데이터들이 콘솔에 한 줄씩 출력된다.
출력 형식은EntityTest1객체의toString()결과 형태로 나온다.
앞에서EntityTestApp1.java를 여러 번 실행했다면 데이터가 누적되어 출력 줄이 많을 수 있다.
이것은 오류가 아니다.
EntityTestApp1.java를 실행할 때마다EntityTest1객체5개가 저장되기 때문에, 조회 결과도 그만큼 늘어난다.
EntityTestApp2.java를 실행하면JPQL의select t from EntityTest1 t가 실행된다.
코드에서는EntityTest1엔티티를 기준으로 조회하지만, 실제SQL로그에서는entitytesttbl테이블을 대상으로select가 실행되는 것을 확인할 수 있다.
조회 결과는EntityTest1(id=..., name=..., age=..., birthday=...)형태로 콘솔에 출력된다.
현재 화면에는 이전 저장 예제를 여러 번 실행한 데이터가 누적되어 여러 줄로 출력된다.
예상 출력 흐름은 아래와 같다.// 출력결과 // Hibernate: // select ... // from entitytesttbl ... // EntityTest1(id=1, name=둘리1, age=11, birthday=...) // EntityTest1(id=2, name=둘리2, age=12, birthday=...) // EntityTest1(id=3, name=둘리3, age=13, birthday=...) // ...결과에서 봐야 할 핵심은 출력 줄 수가 아니다.
JPQL로 전체 조회를 실행했고, 조회된 각 행이EntityTest1객체로 출력된다는 점이다.
결과물을 확인할 때 봐야 할 기준
select t from EntityTest1 t가 실행되는지 확인한다.- 실제
SQL로그에서는entitytesttbl이 조회되는지 확인한다.- 콘솔에
EntityTest1(...)형태로 객체들이 출력되는지 확인한다.id,name,age,birthday값이 출력되는지 확인한다.- 이전 저장 예제를 여러 번 실행했다면 데이터가 누적되어 출력될 수 있음을 이해한다.
- 조회 예제이므로
begin()과commit()이 없다는 점을 확인한다.이 예제의 결과물은
JPQL조회 결과가Entity객체 목록으로 출력된다는 점을 확인하는 데 초점을 둔다.
핵심 정리
EntityTestApp2.java는JPQL로EntityTest1전체 목록을 조회하는 예제이다.
앞의 저장 예제에서 데이터베이스에 넣은 데이터를 다시Entity객체 목록으로 읽어오는 흐름이다.
정리하면 아래와 같다.
JPQL은 테이블명이 아니라Entity이름을 기준으로 작성한다.- 실제 테이블명은
entitytesttbl이지만,JPQL에서는EntityTest1을 사용한다.TypedQuery<EntityTest1>은 결과 타입이EntityTest1로 정해진 조회 객체이다.getResultList()는 여러 건의 조회 결과를List로 가져온다.stream().forEach()는 목록을 하나씩 출력하는 흐름이다.@ToString덕분에 객체 필드 값이 콘솔에 보기 좋게 출력된다.- 제공된 코드에서는
factory.close()로 마무리하지만, 일반적으로는EntityManager도 사용 후 닫아 주는 것이 좋다.- 조회만 하는 예제이므로
Transaction을 직접 시작하지 않는다.이 예제를 이해하면
JPQL로 조회한 데이터가 단순 행이 아니라Entity객체 목록으로 다루어진다는 흐름을 잡을 수 있다.
EntityTestApp3.java는 하나의Transaction안에서 저장 요청, 조회, 추가 저장 요청, 다시 조회, 마지막rollback()까지 확인하는 예제이다.
앞에서는persist()로 저장 대상을 등록하고commit()으로 최종 확정하는 흐름을 봤다.
이번 예제에서는commit()이 아니라rollback()으로 끝날 때 어떤 관점으로 결과를 봐야 하는지 확인한다.
이 예제는 단순히 데이터를 저장하고 조회하는 코드가 아니다.
Transaction안에서 조회 쿼리가 실행될 때 저장 요청된 데이터가 어떻게 보일 수 있는지, 그리고 마지막에rollback()하면 최종 저장이 취소된다는 점을 함께 확인한다.
이 예제의 핵심은 중간 조회 결과에 데이터가 보이더라도, 마지막에rollback()되면 최종 저장은 확정되지 않는다는 점이다.
이전 기본개념과 연결하기
이전 기본개념에서
Transaction은 데이터 변경 작업을 하나의 작업 단위로 묶는다고 정리했다.
begin()으로 작업 단위를 시작하고,commit()으로 확정하거나rollback()으로 되돌린다.
또flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 보내는 과정이라고 정리했다.
여기서 중요한 점은flush와commit()은 같은 개념이 아니라는 것이다.
flush는 변경 내용을 데이터베이스에 보내는 과정이고,commit()은 그 작업을 최종 확정하는 과정이다.
이번 예제에는em.flush()가 직접 쓰이지 않는다.
하지만JPQL조회가 실행될 때, 현재Transaction안에서 저장 요청된 내용이 조회 결과와 맞도록 반영되는 흐름을 확인할 수 있다.
특히 기본 설정에서는 조회 쿼리를 실행하기 전에 필요한flush가 일어날 수 있다.
다만 이 예제의EntityTest1은IDENTITY전략을 사용한다.
IDENTITY전략은 데이터베이스가 기본키 값을 만들어 주는 방식이다.
그래서persist()시점에 기본키를 얻기 위해insert쿼리가 먼저 실행될 수 있다.
또JPQL조회를 실행할 때도 조회 결과를 맞추기 위해 변경 내용이 반영될 수 있다.
중요한 것은insert쿼리 출력이나 중간 조회 결과가 최종 저장 확정을 의미하지는 않는다는 점이다.
기본개념에서 다시 잡아야 할 흐름
persist()는Entity를 저장 대상으로 등록한다.IDENTITY전략에서는persist()시점에insert쿼리가 보일 수 있다.JPQL조회는Entity이름과 필드명을 기준으로 실행된다.- 조회 쿼리 실행 전에는 영속성 컨텍스트의 변경 내용이 반영될 수 있다.
flush는 데이터베이스에SQL을 보내는 흐름이다.commit()은 변경 작업을 최종 확정한다.rollback()은 현재Transaction안의 변경 작업을 되돌린다.
insert쿼리가 보이거나 중간 조회 결과에 데이터가 보여도, 마지막에commit()되지 않으면 최종 저장으로 확정된 것이 아니다.
예제의 목표
이 예제의 목표는
Transaction안에서 저장 요청과 조회가 섞였을 때 결과를 어떻게 해석해야 하는지 확인하는 것이다.
처음에는도우너11부터도우너15까지 저장 요청한다.
그다음 엔터 입력 후 전체 조회를 실행한다.
이후에는또치21부터또치25까지 추가로 저장 요청한다.
다시 엔터 입력 후 전체 조회를 실행한다.
마지막에는commit()이 아니라rollback()을 실행한다.
이 예제에서 확인할 내용
- 하나의
Transaction안에서 여러 번persist()를 호출한다.- 중간에
JPQL전체 조회를 실행한다.- 첫 번째 조회에서는 기존 데이터와
도우너데이터가 함께 보일 수 있다.- 두 번째 조회에서는 기존 데이터,
도우너데이터,또치데이터가 함께 보일 수 있다.- 마지막에
rollback()을 실행한다.- 최종적으로는 이번
Transaction에서 저장 요청한 데이터가 확정되지 않는다.rollback()이후 자동 증가 번호가 건너뛴 것처럼 보일 수 있다.이 예제는 결과가 길게 나올 수 있다.
그래서 출력 줄 하나하나를 외우기보다, 저장 요청 → 조회 → 추가 저장 요청 → 조회 →rollback()순서로 읽어야 한다.
코드 흐름
EntityTestApp3.java는Transaction을 시작한 뒤, 첫 번째 반복문에서도우너데이터를 저장 요청한다.
그다음 엔터 입력을 기다리고,JPQL로 전체 조회를 실행한다.
핵심 코드는 아래와 같다.// EntityTestApp3.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 EntityTest1 et; // 저장할 Entity 참조 변수 em.getTransaction().begin(); // Transaction 시작 for(int i=11; i < 16; i++) { // 11부터 15까지 반복 et = new EntityTest1(); // 새 Entity 객체 생성 et.setName("도우너"+i); // 이름 저장 et.setAge(10+i); // 나이 저장 et.setBirthday(LocalDateTime.now()); // 현재 날짜와 시간 저장 em.persist(et); // 저장 요청 } Scanner scan = new Scanner(System.in); // 입력 도구 생성 System.out.println("엔터키....."); // 중간 확인 안내 출력 scan.nextLine(); // 엔터 입력 대기 TypedQuery<EntityTest1> q = em.createQuery("select t from EntityTest1 t", EntityTest1.class); // 전체 조회 JPQL List<EntityTest1> list = q.getResultList(); // 조회 실행 list.stream().forEach(System.out::println); // 조회 결과 출력첫 번째 반복문은 총
5번 실행된다.
i가11부터15까지 변하므로 이름은도우너11,도우너12,도우너13,도우너14,도우너15가 된다.
나이는21부터25까지 들어간다.
그다음Scanner로 엔터 입력을 기다린다.
이 대기 코드는 중간 상태를 관찰하기 위한 장치이다.
엔터를 누르면JPQL전체 조회가 실행되고, 현재 조회 가능한EntityTest1목록이 출력된다.
이후 두 번째 반복문이 실행된다.
이번에는또치21부터또치25까지 저장 요청한다.// EntityTestApp3.java for(int i=21; i < 26; i++) { // 21부터 25까지 반복 et = new EntityTest1(); // 새 Entity 객체 생성 et.setName("또치"+i); // 이름 저장 et.setAge(10+i); // 나이 저장 et.setBirthday(LocalDateTime.now()); // 현재 날짜와 시간 저장 em.persist(et); // 저장 요청 } System.out.println("엔터키....."); // 중간 확인 안내 출력 scan.nextLine(); // 엔터 입력 대기 q = em.createQuery("select t from EntityTest1 t", EntityTest1.class); // 전체 조회 JPQL 다시 생성 list = q.getResultList(); // 다시 조회 실행 list.stream().forEach(System.out::println); // 조회 결과 출력 int a=1; // 별 의미 없는 변수 System.out.println("엔터키....."); // rollback 전 확인 안내 출력 scan.nextLine(); // 엔터 입력 대기 em.getTransaction().rollback(); // Transaction 되돌리기 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기 scan.close(); // Scanner 닫기두 번째 조회에서는 첫 번째 조회 때보다 결과가 더 많아질 수 있다.
두 번째 반복문에서또치데이터까지 저장 대상으로 등록했기 때문이다.
하지만 마지막에rollback()이 실행된다.
따라서 중간 조회에서 데이터가 보였더라도, 이번Transaction에서 새로 저장 요청한도우너와또치데이터는 최종 확정되지 않는다.
핵심 코드 해석
하나의 Transaction 안에서 여러 번 저장 요청한다
이 예제는
begin()을 한 번만 호출한다.
그 뒤에도우너저장 요청, 첫 번째 조회,또치저장 요청, 두 번째 조회,rollback()이 모두 이어진다.
// EntityTestApp3.java em.getTransaction().begin(); // 하나의 Transaction 시작 em.persist(et); // 도우너 저장 요청 흐름 List<EntityTest1> list = q.getResultList(); // 중간 조회 em.persist(et); // 또치 저장 요청 흐름 list = q.getResultList(); // 다시 중간 조회 em.getTransaction().rollback(); // 전체 작업 되돌리기여기서 중요한 점은
Transaction이 중간에 끝나지 않는다는 것이다.
중간에 조회가 있어도commit()이 실행된 것은 아니다.
마지막에rollback()이 실행되기 전까지는 같은 작업 단위 안에 있다.
하나의Transaction안에서 조회 결과가 보인다고 해서 변경 작업이 최종 확정된 것은 아니다.
JPQL 조회 전에는 변경 내용이 반영될 수 있다
첫 번째 반복문에서
도우너데이터를persist()한 뒤, 바로JPQL전체 조회를 실행한다.
이때 조회 결과에도우너데이터가 보일 수 있다.
// EntityTestApp3.java TypedQuery<EntityTest1> q = em.createQuery( "select t from EntityTest1 t", EntityTest1.class ); // EntityTest1 전체 조회 List<EntityTest1> list = q.getResultList(); // 조회 실행이 흐름은
JPA가 조회 결과를 현재 영속성 컨텍스트의 변경 내용과 맞추기 위해 처리하는 과정과 연결된다.
기본 설정에서는JPQL조회가 실행되기 전에 필요한 변경 내용이 데이터베이스에 전달될 수 있다.
다만 이 예제는IDENTITY전략도 함께 사용한다.
그래서 일부insert쿼리는 조회 시점이 아니라persist()시점에 이미 보일 수 있다.
즉, 이 예제에서 쿼리가 보이는 이유를 하나로만 단정하면 안 된다.
IDENTITY전략 때문에 먼저insert가 실행될 수도 있고, 조회 실행 전 변경 내용 반영 흐름도 함께 볼 수 있다.
하지만 어떤 시점에insert쿼리가 보였든 핵심은 같다.
조회를 위해SQL이 실행되거나 데이터가 조회 결과에 보이더라도, 마지막에rollback()하면 이번Transaction의 변경 작업은 되돌아간다.
조회 결과가 보이는 것과 최종 저장은 다르다
- 중간 조회 결과에
도우너데이터가 보일 수 있다.- 두 번째 조회 결과에
또치데이터까지 보일 수 있다.insert쿼리가persist()시점에 먼저 보일 수도 있다.- 하지만 마지막이
rollback()이면 최종 저장은 확정되지 않는다.- 최종 확인은
rollback()이후 데이터베이스를 다시 조회해서 판단해야 한다.중간 조회에서 보이는 데이터와 최종 저장된 데이터는 같은 의미가 아니다.
rollback은 현재 Transaction의 변경 작업을 되돌린다
rollback()은 현재Transaction안에서 진행한 변경 작업을 되돌린다.
이 예제에서는commit()이 아니라rollback()이 호출된다.
// EntityTestApp3.java em.getTransaction().rollback(); // 저장 요청한 작업 되돌리기이 코드가 실행되면 현재
Transaction안에서 저장 요청한 내용은 최종 확정되지 않는다.
즉,도우너11부터도우너15,또치21부터또치25는 중간 조회에서는 보일 수 있지만 최종 저장 데이터로 남지 않을 수 있다.
이 부분이EntityTestApp1.java와 가장 큰 차이이다.
EntityTestApp1.java는 마지막에commit()을 호출했다.
그래서 저장 작업이 확정되었다.
반면EntityTestApp3.java는 마지막에rollback()을 호출한다.
그래서 이번 실행에서 새로 저장 요청한 작업은 되돌아간다.
여기서 하나 더 주의할 점이 있다.
rollback()이후에는도우너,또치데이터가 최종 저장되지 않는다.
다만IDENTITY전략의 자동 증가 번호는 이미 증가한 뒤 되돌아가지 않을 수 있다.
그래서 나중에 다시 데이터를 저장하면id가 중간에 건너뛴 것처럼 보일 수 있다.
이것은 데이터가 남아 있다는 뜻이 아니라, 자동 증가 번호가 이미 사용된 결과로 보면 된다.
rollback()은 데이터 변경을 되돌리지만, 데이터베이스의 자동 증가 번호까지 항상 이전 상태로 되돌린다고 보면 안 된다.
int a=1은 실행 흐름에 큰 의미가 없다
코드 중간에는 아래 문장이 있다.
// EntityTestApp3.java int a=1; // 별 의미 없는 변수이 변수는 저장, 조회,
rollback()흐름에 영향을 주는 핵심 코드가 아니다.
주석에도 별 의미 없다고 되어 있다.
따라서 이 예제를 볼 때int a=1에 집중할 필요는 없다.
중요한 것은 그 아래의 엔터 대기와rollback()흐름이다.
초보자는 코드에 있는 모든 줄이 같은 중요도를 가진다고 생각할 수 있다.
하지만 예제를 읽을 때는 핵심 흐름과 보조 코드를 구분해야 한다.
이 예제에서 핵심은int a=1이 아니라, 마지막에rollback()으로Transaction을 되돌린다는 점이다.
결과물은 어떻게 나오는가
이 예제를 실행하면 콘솔 출력이 길게 나올 수 있다.
먼저도우너11부터도우너15까지 저장 요청한 뒤, 엔터 입력을 기다린다.
엔터를 누르면 첫 번째 전체 조회 결과가 출력된다.
그다음또치21부터또치25까지 추가 저장 요청한 뒤 다시 엔터 입력을 기다린다.
엔터를 누르면 두 번째 전체 조회 결과가 출력된다.
두 번째 결과는 첫 번째 결과보다 더 많아 보일 수 있다.
마지막 엔터를 누르면rollback()이 실행된다.
따라서 중간 조회 결과에도우너와또치가 보였더라도, 최종적으로 데이터베이스에 남는지는rollback()이후 다시 조회해서 확인해야 한다.
왼쪽은 첫 번째
JPQL조회 결과이다.
도우너11부터도우너15까지 저장 요청한 뒤 조회했기 때문에 중간 조회 결과에도우너데이터가 보인다.
오른쪽은 두 번째JPQL조회 결과이다.
또치21부터또치25까지 추가로 저장 요청한 뒤 다시 조회했기 때문에또치데이터까지 함께 보인다.
하지만 아직 마지막rollback()전 중간 조회 결과이므로, 이 데이터가 최종 저장되었다고 보면 안 된다.
rollback()이후도우너또는또치로 시작하는 데이터를 다시 조회하면Empty set이 출력된다.
이는 중간 조회 결과에는도우너,또치데이터가 보였지만, 마지막rollback()으로 이번Transaction의 저장 요청이 최종 취소되었다는 뜻이다.
// 출력결과 // 엔터키..... // 첫 번째 조회 결과 출력 // 도우너11~도우너15가 조회 결과에 보일 수 있음 // 엔터키..... // 두 번째 조회 결과 출력 // 또치21~또치25가 추가로 조회 결과에 보일 수 있음 // 엔터키..... // rollback 실행 // rollback 이후 도우너, 또치 조건 조회 결과는 Empty set결과를 볼 때는 “중간 조회에서 보였다”와 “최종 저장되었다”를 분리해서 봐야 한다.
이 예제는 그 차이를 확인하기 위해 만들어진 흐름이다.
또rollback()이후 다음 저장 예제를 실행하면id값이 연속되지 않고 건너뛴 것처럼 보일 수 있다.
이것은 중간 데이터가 실제로 남아 있어서가 아니라,IDENTITY전략의 자동 증가 번호가 이미 사용되었기 때문일 수 있다.
결과물을 확인할 때 봐야 할 기준
- 첫 번째 엔터 전에는
도우너저장 요청 이후 상태이다.- 첫 번째 조회 결과에
도우너데이터가 보이는지 확인한다.- 두 번째 엔터 전에는
또치저장 요청 이후 상태이다.- 두 번째 조회 결과에
또치데이터까지 보이는지 확인한다.- 마지막에는
rollback()이 실행된다.rollback()이후 데이터베이스에도우너,또치가 최종 저장되어 있는지 다시 확인한다.- 이후 저장 시
id가 건너뛰어 보이면 자동 증가 번호 사용 여부를 함께 생각한다.이 예제의 결과물은 중간 조회 결과보다 마지막
rollback()이후 데이터베이스 상태를 함께 봐야 정확하다.
핵심 정리
EntityTestApp3.java는 저장 요청과 조회가 같은Transaction안에서 섞일 때 결과를 어떻게 해석해야 하는지 보여 주는 예제이다.
중간 조회 결과에 데이터가 보일 수 있지만, 마지막이rollback()이면 최종 저장은 확정되지 않는다.
정리하면 아래와 같다.
도우너11부터도우너15까지 먼저 저장 요청한다.- 첫 번째
JPQL조회로 현재 조회 가능한 목록을 출력한다.또치21부터또치25까지 추가 저장 요청한다.- 두 번째
JPQL조회로 다시 목록을 출력한다.IDENTITY전략 때문에persist()시점에insert쿼리가 보일 수 있다.- 조회 결과에 데이터가 보여도 최종 저장을 의미하지는 않는다.
- 마지막에
rollback()이 실행되므로 이번Transaction의 변경 작업은 되돌아간다.rollback()이후에도 자동 증가 번호는 되돌아가지 않을 수 있다.- 최종 데이터베이스 상태는
rollback()이후 다시 조회해서 확인해야 한다.이 예제를 이해하면
flush,JPQL조회,rollback(),IDENTITY자동 증가 흐름을 서로 구분할 수 있다.
EntityTestApp4.java는EntityTest1데이터를 조회한 뒤 삭제 요청하는 예제이다.
앞에서는persist()로 저장 대상을 등록하고,rollback()으로 변경 작업을 되돌리는 흐름을 확인했다.
이번에는find()로 삭제할Entity를 먼저 찾고,remove()로 삭제 요청하는 흐름을 확인한다.
이 예제에서 가장 중요한 점은 삭제 요청과 최종 삭제 확정을 구분하는 것이다.
코드에는em.remove(finalData)가 있지만, 마지막commit()은 주석 처리되어 있다.
따라서 삭제 요청이 있더라도 최종 반영 여부는commit()실행 여부와 함께 봐야 한다.
이 예제의 핵심은remove()가 삭제 요청이고, 실제 삭제 확정은Transaction의commit()과 연결된다는 점을 확인하는 것이다.
이전 기본개념과 연결하기
이전 기본개념에서
EntityManager는Entity를 저장, 조회, 삭제할 때 사용하는 객체라고 정리했다.
저장할 때는persist()를 사용했고, 기본키로 한 건을 찾을 때는find()를 사용했다.
삭제할 때는remove()를 사용한다.
하지만remove()만 보고 바로 데이터베이스에서 삭제가 끝났다고 이해하면 안 된다.
저장, 수정, 삭제처럼 데이터베이스 상태를 바꾸는 작업은Transaction안에서 처리되고, 마지막에commit()으로 확정되어야 한다.
이번 예제는remove()를 호출하지만commit()이 주석 처리되어 있다.
그래서 삭제 요청이 있었더라도 최종 삭제가 확정되지 않을 수 있다는 점을 확인해야 한다.
기본개념에서 다시 잡아야 할 흐름
find()는 기본키로Entity한 건을 조회한다.remove()는 조회된Entity를 삭제 대상으로 등록한다.- 삭제도 데이터 변경 작업이므로
Transaction안에서 처리한다.commit()이 실행되어야 삭제 작업이 최종 확정된다.commit()이 주석 처리되어 있으면 삭제 요청이 최종 반영되지 않을 수 있다.삭제 예제는
remove()호출 여부뿐 아니라 마지막에commit()이 실행되는지까지 반드시 확인해야 한다.
예제의 목표
이 예제의 목표는
Entity삭제 흐름을 확인하는 것이다.
먼저find()로 삭제할EntityTest1객체를 찾는다.
그다음remove()로 삭제 요청을 한다.
이후JPQL전체 조회로 현재 조회 결과를 출력한다.
다만 이 코드에서는commit()이 주석 처리되어 있다.
즉, 삭제 요청을 했지만 최종 확정 단계는 막혀 있는 상태이다.
이 구조를 통해remove()와commit()의 차이를 확인할 수 있다.
이 예제에서 확인할 내용
Transaction을 시작한다.find()로 삭제할EntityTest1을 찾는다.remove()로 삭제 요청한다.JPQL로 전체 데이터를 다시 조회한다.- 엔터 입력으로 중간 상태를 확인한다.
commit()이 주석 처리되어 있음을 확인한다.- 삭제 요청과 최종 삭제 확정은 다르다는 점을 확인한다.
이 예제는 정상 삭제 결과를 확인하는 예제라기보다, 삭제 요청과
commit()여부를 비교해서 보는 예제에 가깝다.
코드 흐름
EntityTestApp4.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음Transaction을 시작하고, 삭제할 데이터를find()로 조회한다.
핵심 코드는 아래와 같다.// EntityTestApp4.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 EntityTest1 et; // Entity 참조 변수 em.getTransaction().begin(); // Transaction 시작 System.out.println("엔티티 삭제"); // 삭제 예제 안내 출력 EntityTest1 finalData = em.find(EntityTest1.class, 0); // 두 번째 아규먼트에 삭제할 기본키 값 설정 em.remove(finalData); // 조회된 Entity 삭제 요청 TypedQuery<EntityTest1> q = em.createQuery("select t from EntityTest1 t", EntityTest1.class); // 전체 조회 JPQL List<EntityTest1> list = q.getResultList(); // 조회 결과를 List로 받음 list.stream().forEach(System.out::println); // 조회 결과 출력 System.out.println("엔터키....."); // 중간 확인 안내 출력 Scanner scan = new Scanner(System.in); // 입력 도구 생성 scan.nextLine(); // 엔터 입력 대기 // em.getTransaction().commit(); // commit이 주석 처리되어 있음 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기 scan.close(); // Scanner 닫기코드 흐름은 삭제 대상 조회 → 삭제 요청 → 전체 조회 → 엔터 대기 → 종료 순서로 이어진다.
하지만commit()이 실제로 실행되지 않는다.
//로 주석 처리되어 있기 때문이다.
삭제 테스트를 할 때는find(EntityTest1.class, 0)의 두 번째 인자는 삭제할 기본키 값을 넣는 자리이다.
원본 코드에서는0이 들어 있으므로, 이 값이 삭제 대상 조회 조건으로 사용된다.
따라서 삭제 요청이 있더라도 최종 삭제 결과를 확인하려면commit()주석 처리 여부를 반드시 함께 봐야 한다.
핵심 코드 해석
find는 삭제할 Entity를 먼저 찾는 코드이다
삭제하려면 먼저 어떤 데이터를 삭제할지 알아야 한다.
JPA에서는 기본키를 알고 있을 때find()로 해당Entity를 조회할 수 있다.
// EntityTestApp4.java EntityTest1 finalData = em.find(EntityTest1.class, 0); // 기본키 0번 Entity 조회첫 번째 인자인
EntityTest1.class는 조회할Entity타입이다.
두 번째 인자인0은 조회할 기본키 값이다.
즉, 이 코드는EntityTest1중에서 기본키가0인 데이터를 찾으려는 코드이다.
원본 코드에서는0이 삭제 대상 기본키 값으로 들어가 있다.
find()는 해당 기본키의 데이터가 있으면Entity객체를 반환하지만, 없으면null을 반환할 수 있다.
null은 조회 결과가 없다는 뜻이다.
find에서 주의할 점
find(EntityTest1.class, 0)은 기본키가0인 데이터를 찾는다.- 실제 데이터에 해당
id가 없으면 결과는null일 수 있다.- 코드 주석의 “두 번째 아규먼트 설정”은 삭제할 기본키 값을 넣는 자리라는 의미로 볼 수 있다.
- 이 값이
remove()로 넘길 삭제 대상 조회 기준이 된다.삭제 예제를 실행할 때는
find()의 두 번째 인자에 실제 존재하는 기본키 값을 넣어야 한다.
remove는 조회된 Entity를 삭제 대상으로 등록한다
remove()는Entity를 삭제 대상으로 등록하는 메서드이다.
// EntityTestApp4.java em.remove(finalData); // 조회된 Entity 삭제 요청여기서 중요한 점은
remove()에 넘기는 값이Entity객체여야 한다는 것이다.
삭제할 데이터의id숫자만 넘기는 것이 아니다.
먼저find()로Entity객체를 조회한 뒤, 그 객체를remove()에 넘긴다.
만약finalData가null이면 문제가 생길 수 있다.
즉, 조회 결과가 없는데remove(null)을 호출하면 정상 삭제 흐름이 아니라 오류 흐름으로 이어질 수 있다.
따라서 이 예제를 실행할 때는 삭제할 데이터가 실제로 있는지 먼저 확인하는 것이 좋다.
원본 코드에서는find(EntityTest1.class, 0)으로 삭제 대상을 조회한다.
remove에서 봐야 할 핵심
remove()는 삭제할Entity객체를 인자로 받는다.- 삭제할
id값을 직접 넘기는 메서드가 아니다.find()로 먼저 삭제 대상 객체를 가져와야 한다.find()결과가null이면remove()흐름이 정상적으로 진행되지 않을 수 있다.
remove()는 삭제할 객체를 넘겨야 하므로, 삭제 전에find()로 실제Entity를 조회하는 흐름이 필요하다.
JPQL 조회는 삭제 요청 후 현재 목록을 확인하기 위한 코드이다
remove()를 호출한 뒤에는JPQL로 전체 목록을 다시 조회한다.
이 코드는 삭제 요청 이후 현재 조회 결과가 어떻게 보이는지 확인하기 위한 흐름이다.
// EntityTestApp4.java TypedQuery<EntityTest1> q = em.createQuery( "select t from EntityTest1 t", EntityTest1.class ); // 전체 조회 JPQL List<EntityTest1> list = q.getResultList(); // 조회 실행 list.stream().forEach(System.out::println); // 결과 출력
select t from EntityTest1 t는EntityTest1전체 조회JPQL이다.
앞 예제에서 봤듯이JPQL은 실제 테이블명이 아니라Entity이름을 기준으로 작성한다.
삭제 요청 후 조회를 실행하면 현재Transaction안에서 삭제 대상으로 등록된 객체가 조회 결과에서 보이지 않을 수 있다.
하지만 이것을 최종 삭제 확정으로 바로 해석하면 안 된다.
마지막에commit()이 실행되는지 확인해야 한다.
삭제 요청 후 조회 결과에서 데이터가 안 보이더라도, 최종 삭제 확정은commit()실행 여부로 판단해야 한다.
commit이 주석 처리되어 있다
이 예제에서 가장 중요한 부분은 마지막의
commit()이다.
정확히는commit()이 실행되지 않는다는 점이다.
// EntityTestApp4.java // em.getTransaction().commit(); // commit이 주석 처리되어 있음앞에
//가 붙어 있으므로 이 줄은 실행되지 않는다.
Java에서//는 한 줄 주석이다.
주석 처리된 코드는 프로그램 실행에 포함되지 않는다.
즉, 이 예제는Transaction을 시작하고 삭제 요청까지 했지만, 마지막에 확정하지 않는다.
따라서 최종 데이터베이스 상태에서는 삭제가 반영되지 않을 수 있다.
여기서 앞 예제들과 비교하면 흐름이 분명해진다.
EntityTestApp1.java: 마지막에commit()이 있어서 저장이 확정된다.EntityTestApp3.java: 마지막에rollback()이 있어서 저장 요청이 취소된다.EntityTestApp4.java:commit()이 주석 처리되어 있어서 삭제 요청이 확정되지 않는다.데이터 변경 예제에서는 마지막 줄이
commit()인지,rollback()인지, 주석 처리인지가 결과를 결정한다.
결과물은 어떻게 나오는가
이 예제를 실행하면 먼저
엔티티 삭제가 출력된다.
그다음find()로 삭제 대상을 찾고,remove()로 삭제 요청을 한다.
이후JPQL전체 조회 결과가 콘솔에 출력된다.
단, 삭제 테스트에서는find()의 두 번째 인자에 실제 존재하는id값을 넣어야 한다.
존재하지 않는id를 넣으면find()결과가null이 될 수 있고,remove()흐름이 정상적으로 진행되지 않을 수 있다.
또 마지막에commit()이 주석 처리되어 있으므로, 삭제 요청이 최종 반영되지 않을 수 있다.
삭제 후 목록에서 데이터가 빠진 것처럼 보이더라도, 프로그램 종료 후 데이터베이스를 다시 조회하면 그대로 남아 있을 수 있다.
EntityTestApp4.java를 실행하면 먼저find()가 실행되면서 삭제할 대상을 기본키로 조회한다.
콘솔의where et1_0.id=?는find(EntityTest1.class, id)가 실제로는 기본키 조건 조회로 실행된다는 것을 보여 준다.
이후 코드에서는remove()로 삭제 요청을 하지만, 마지막commit()은 주석 처리되어 있다.
프로그램 실행 후
entitytesttbl을 다시 조회해도 기존 데이터가 그대로 남아 있다.
remove()로 삭제 요청은 했지만commit()이 실행되지 않았기 때문에 최종 삭제가 확정되지 않은 것이다.
// 출력결과 // 엔티티 삭제 // find()가 기본키 조건으로 삭제 대상을 조회함 // remove()로 삭제 요청 // commit은 주석 처리되어 실행되지 않음 // 프로그램 종료 후 DB를 다시 조회하면 기존 데이터가 그대로 남아 있음결과를 볼 때는
remove()호출 여부만 보면 안 된다.
삭제 대상이 실제로 조회되었는지,remove()가 정상 실행되었는지, 마지막에commit()이 실행되었는지까지 함께 봐야 한다.
결과물을 확인할 때 봐야 할 기준
find()의 두 번째 인자에 실제 존재하는id를 넣었는지 확인한다.- 콘솔에서
where et1_0.id=?형태의 기본키 조건 조회가 실행되는지 확인한다.remove()는 삭제 요청일 뿐이라는 점을 확인한다.commit()이 주석 처리되어 있으므로 최종 삭제 확정은 되지 않는다는 점을 확인한다.- 프로그램 종료 후 데이터베이스를 다시 조회했을 때 삭제 대상이 남아 있는지 확인한다.
이 예제의 결과물은 삭제 요청이 보이는지보다
commit()이 주석 처리되어 최종 삭제가 확정되지 않는다는 점을 확인하는 데 초점을 둔다.
핵심 정리
EntityTestApp4.java는Entity삭제 요청 흐름을 확인하는 예제이다.
find()로 삭제할 객체를 찾고,remove()로 삭제 대상으로 등록한다.
하지만 마지막commit()이 주석 처리되어 있으므로 최종 삭제 확정은 일어나지 않을 수 있다.
정리하면 아래와 같다.
- 삭제하려면 먼저
find()로 삭제 대상Entity를 조회한다.find()의 두 번째 인자는 기본키 값이다.- 실제 존재하는
id로 맞춰야 정상 테스트가 가능하다.remove()는 삭제할Entity객체를 인자로 받는다.remove()는 삭제 요청이지 최종 삭제 확정이 아니다.- 삭제도 데이터 변경 작업이므로
commit()이 필요하다.- 이 예제에서는
commit()이 주석 처리되어 있으므로 삭제가 최종 확정되지 않는다.삭제 흐름에서는
find()로 삭제 대상을 찾고,remove()로 삭제 요청한 뒤,commit()으로 확정되는 전체 흐름을 함께 봐야 한다.
Emp.java,Dept.java,Locations.java는 사원, 부서, 지역 정보를JPA엔티티로 연결한 예제이다.
앞에서는EntityTest계열로 기본 매핑, 컬럼 설정, 날짜·시간 타입, 간단한@ManyToOne구조를 확인했다.
이번에는 실제 업무 데이터에 가까운Emp,Dept,Locations구조로 연관관계 매핑을 확인한다.
이 예제의 핵심은Emp가Dept를 참조하고,Dept가Locations를 참조하는 흐름을 읽는 것이다.
테이블 기준으로 보면 사원 테이블에는 부서 번호가 있고, 부서 테이블에는 지역 코드가 있다.
객체 기준으로 보면Emp객체 안에Dept객체가 들어가고,Dept객체 안에Locations객체가 들어간다.
이 예제는 외래키 값을 숫자나 문자열로만 들고 있는 구조가 아니라, 외래키로 연결된 대상을 객체로 참조하는 구조를 확인하는 예제이다.
이전 기본개념과 연결하기
이전 기본개념에서
@ManyToOne은 여러 엔티티가 하나의 엔티티와 연결되는 관계라고 정리했다.
예를 들어 여러 사원이 하나의 부서에 속할 수 있다.
이때 사원 입장에서는 부서와 다대일 관계가 된다.
또@JoinColumn은 외래키 컬럼을 지정하는 어노테이션이라고 정리했다.
외래키는 한 테이블의 값이 다른 테이블의 기본키를 참조하는 컬럼이다.
테이블에서는 외래키 컬럼으로 연결하지만, 객체에서는 참조 필드로 연결한다.
이번 예제에서는 이 흐름이 두 번 이어진다.
Emp는Dept를 참조하고,Dept는Locations를 참조한다.
따라서 객체 흐름은Emp → Dept → Locations순서로 이어진다.
기본개념에서 다시 잡아야 할 흐름
@Entity는JPA가 관리할 클래스라는 뜻이다.@Id는 기본키 필드를 지정한다.@Column은 컬럼의 길이, 타입 같은 세부 설정을 지정한다.@ManyToOne은 현재 엔티티 여러 개가 대상 엔티티 하나와 연결될 수 있다는 뜻이다.@JoinColumn은 외래키 컬럼명을 지정한다.- 테이블은 외래키 값으로 연결하고, 객체는 참조 필드로 연결한다.
연관관계 매핑을 볼 때는 테이블의 외래키 컬럼과 객체의 참조 필드가 어떻게 연결되는지 함께 봐야 한다.
예제의 목표
이 예제의 목표는
Emp,Dept,Locations엔티티가 어떤 방향으로 연결되는지 확인하는 것이다.
단순히 필드 목록을 외우는 것이 아니라, 사원에서 부서로, 부서에서 지역으로 이동할 수 있는 객체 구조를 읽는 것이 중요하다.
Emp는 사원 정보를 담는다.
Dept는 부서 정보를 담는다.
Locations는 지역 정보를 담는다.
이 세 엔티티는 따로 떨어진 객체가 아니라, 외래키 관계를 통해 연결된다.
이 예제에서 확인할 내용
Emp의 기본키는empno이다.Emp는deptno외래키 컬럼을 통해Dept를 참조한다.Dept의 기본키는deptno이다.Dept는loc_code외래키 컬럼을 통해Locations를 참조한다.Locations의 기본키는loc_code이다.Emp → Dept → Locations흐름으로 사원, 부서, 지역 정보를 객체로 따라갈 수 있다.이 구조를 이해하면 뒤에서 나오는
Emp조회, 연관 필드 접근, 로딩 전략,N+1,Fetch Join흐름을 더 쉽게 이해할 수 있다.
Emp는 사원 정보와 부서 참조를 가진 Entity이다
Emp.java는 사원 정보를 표현하는 엔티티이다.
사원 번호, 이름, 직무, 관리자 번호, 입사일, 급여, 커미션 정보를 필드로 가진다.
그리고 부서 정보는 단순한 숫자가 아니라Dept객체로 참조한다.
코드 구조는 아래와 같다.// Emp.java @Entity // JPA가 관리할 Entity 클래스 @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 public class Emp { @Id // 기본키 필드 private int empno; // 사원 번호 @Column(length = 14) // 문자열 길이 14 private String ename; // 사원 이름 @Column(length = 30) // 문자열 길이 30 private String job; // 직무 private Integer mgr; // 관리자 번호, null 가능 private java.sql.Date hiredate; // 입사일 private int sal; // 급여 private Integer comm; // 커미션, null 가능 @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정 @JoinColumn(name="deptno") // emp 테이블의 deptno 외래키와 연결 private Dept deptno; // 부서 Entity 참조 }
empno는Emp의 기본키이다.
기본키는 각 사원을 구분하는 값이다.
ename과job에는@Column(length = ...)가 붙어 있다.
이는 문자열 컬럼의 길이를 제한하는 설정이다.
ename은 길이14,job은 길이30으로 지정되어 있다.
mgr와comm은Integer타입이다.
Integer는 참조형이므로null을 가질 수 있다.
관리자가 없거나 커미션이 없는 사원을 표현하려면0이 아니라null이 필요할 수 있다.
그래서int가 아니라Integer를 사용한 것으로 이해할 수 있다.
가장 중요한 부분은deptno필드이다.
필드 이름은deptno라서 부서 번호처럼 보이지만, 실제 타입은Dept이다.
즉, 객체 입장에서는 부서 번호 숫자만 들고 있는 것이 아니라 부서 객체 자체를 참조한다.
@JoinColumn(name="deptno")는 현재 엔티티인Emp쪽에서 사용할 외래키 컬럼명을 지정한다.
테이블에는deptno컬럼이 있지만, 객체 코드에서는 그 컬럼 값으로 연결되는Dept객체를 필드에 담는다.
Emp에서 봐야 할 핵심
empno는 사원 엔티티의 기본키이다.mgr,comm은null을 표현할 수 있도록Integer를 사용한다.deptno필드는 이름만 보면 번호처럼 보이지만 실제 타입은Dept이다.@JoinColumn(name="deptno")는emp테이블의deptno외래키 컬럼과 연결한다.@ManyToOne은 여러Emp가 하나의Dept와 연결되는 다대일 관계를 뜻한다.
Emp에서 가장 중요한 점은deptno필드가 단순 번호가 아니라Dept객체 참조라는 점이다.
Emp테이블 구조를 보면EMPNO가 기본키이고,DEPTNO컬럼이 존재한다.
화면에서는 컬럼명이 대문자로 보이지만, 코드의@JoinColumn(name="deptno")와 연결되는 부서 외래키 컬럼으로 보면 된다.
객체에서는 이DEPTNO값을 직접 숫자로 다루기보다Dept타입의deptno필드로 부서 객체를 참조한다.
Dept는 부서 정보와 지역 참조를 가진 Entity이다
Dept.java는 부서 정보를 표현하는 엔티티이다.
부서 번호와 부서명을 가지고, 해당 부서가 어느 지역에 있는지는Locations객체로 참조한다.
코드 구조는 아래와 같다.// Dept.java @Entity // JPA가 관리할 Entity 클래스 @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 public class Dept { @Id // 기본키 필드 private int deptno; // 부서 번호 @Column(length = 20) // 문자열 길이 20 private String dname; // 부서명 @ManyToOne // 여러 Dept가 하나의 Locations와 연결될 수 있음 @JoinColumn(name="loc_code", referencedColumnName = "loc_code") // 외래키와 참조 대상 컬럼 지정 private Locations loc_code; // 지역 Entity 참조 }
deptno는Dept의 기본키이다.
각 부서를 구분하는 값이다.
dname은 부서명이다.
@Column(length = 20)이 붙어 있으므로 부서명 컬럼의 길이는20으로 제한된다.
loc_code필드는 지역 정보를 참조한다.
필드 이름은loc_code라서 지역 코드 문자열처럼 보이지만, 실제 타입은Locations이다.
즉, 부서 객체 안에는 지역 코드 값 하나만 있는 것이 아니라 지역 엔티티 객체를 참조하는 구조이다.
@JoinColumn(name="loc_code", referencedColumnName = "loc_code")는 두 가지를 알려 준다.
name="loc_code"는 현재 엔티티인Dept쪽 외래키 컬럼 이름이다.
referencedColumnName = "loc_code"는 참조 대상인Locations쪽 컬럼 이름이다.
JoinColumn에서 봐야 할 부분
name="loc_code"는 현재 엔티티 쪽 외래키 컬럼명이다.referencedColumnName = "loc_code"는 참조 대상 엔티티 쪽 컬럼명이다.Dept는loc_code외래키로Locations를 참조한다.- 객체에서는
Dept.loc_code필드를 통해Locations객체로 이동할 수 있다.
Dept의loc_code도 단순 문자열 코드가 아니라Locations객체를 참조하는 필드로 매핑되어 있다.
Dept테이블 구조를 보면DEPTNO가 기본키이고,loc_code컬럼이 존재한다.
이loc_code컬럼은Locations와 연결되는 외래키 역할을 한다.
코드에서는@JoinColumn(name="loc_code", referencedColumnName = "loc_code")로 이 관계를 지정하고, 객체에서는Locations타입의loc_code필드로 지역 객체를 참조한다.
Locations는 지역 정보를 가진 기준 Entity이다
Locations.java는 지역 정보를 표현하는 엔티티이다.
지역 코드와 도시명을 가진다.
이 엔티티는Dept에서 참조하는 대상이 된다.
코드 구조는 아래와 같다.// Locations.java @Entity // JPA가 관리할 Entity 클래스 @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 public class Locations { @Id // 기본키 필드 @Column(columnDefinition = "char(2)") // char(2) 컬럼으로 지정 private String loc_code; // 지역 코드 @Column(length = 20) // 문자열 길이 20 private String city; // 도시명 }
loc_code는Locations의 기본키이다.
지역을 구분하는 코드이다.
@Column(columnDefinition = "char(2)")는 컬럼 정의를 직접 지정한 것이다.
loc_code는 길이가 고정된char(2)컬럼으로 생성되도록 설정되어 있다.
city는 도시명이다.
@Column(length = 20)이 붙어 있으므로 도시명 컬럼 길이는20으로 제한된다.
Locations는 다른 엔티티를 참조하지 않는다.
하지만Dept가Locations를 참조하므로, 전체 구조에서는 지역 정보를 제공하는 기준 엔티티 역할을 한다.
Locations에서 봐야 할 핵심
loc_code는 지역 엔티티의 기본키이다.loc_code는char(2)형태로 매핑된다.city는 도시명을 저장한다.Dept가Locations를 참조한다.
Locations는 직접 다른 객체를 참조하지 않지만,Dept가 참조하는 대상 엔티티로 사용된다.
Locations테이블 구조를 보면LOC_CODE가 기본키이고,CITY가 도시명을 저장하는 컬럼이다.
Dept는loc_code를 통해Locations를 참조하므로, 전체 객체 흐름은Emp → Dept → Locations로 이어진다.
세 Entity는 Emp에서 Locations까지 이어지는 구조이다
이 세 엔티티는 각각 따로 존재하지만, 실제 조회 흐름에서는 이어져서 사용된다.
Emp에서 시작하면Dept로 이동할 수 있고,Dept에서 다시Locations로 이동할 수 있다.
객체 흐름은 아래와 같다.// EmpDeptLocationsFlow.java Emp emp = ...; // 사원 Entity Dept dept = emp.getDeptno(); // 사원이 속한 부서 Entity Locations loc = dept.getLoc_code(); // 부서가 속한 지역 Entity String city = loc.getCity(); // 도시명 조회테이블 기준으로 보면
emp.deptno가dept.deptno를 참조한다.
그리고dept.loc_code가locations.loc_code를 참조한다.
객체 기준으로 보면Emp가Dept객체를 참조하고,Dept가Locations객체를 참조한다.
즉, 같은 관계를 테이블과 객체가 서로 다른 방식으로 표현한다.
테이블은 외래키 컬럼으로 연결하고, 객체는 참조 필드로 연결한다.
이름은deptno나loc_code처럼 번호와 코드로 보일 수 있다.
하지만 필드 타입이 각각Dept,Locations이면 실제로는 단순 값이 아니라 연결된 엔티티 객체를 참조하는 구조이다.
테이블 흐름과 객체 흐름 비교
- 테이블 흐름:
emp.deptno → dept.deptno,dept.loc_code → locations.loc_code- 객체 흐름:
Emp.deptno → Dept,Dept.loc_code → Locations- 테이블은 값으로 연결한다.
- 객체는 객체 참조로 연결한다.
JPA연관관계 매핑은 테이블의 외래키 관계를 객체 참조 구조로 바꿔서 다룰 수 있게 해 준다.
로딩 전략은 이 구조 설명에서 고정해서 다루지 않는다
이 파트의 목적은
Emp,Dept,Locations가 어떤 방향으로 연결되는지 이해하는 것이다.
따라서 여기서는 특정 로딩 전략을 기준으로 고정해서 설명하지 않는다.
중요한 것은Emp가Dept를 참조하고,Dept가Locations를 참조한다는 구조이다.
이 구조가 있어야 뒤에서Emp를 조회할 때 부서와 지역 정보가 어떤 흐름으로 연결되는지 이해할 수 있다.
여기서 기억할 정도
Emp는Dept를@ManyToOne으로 참조한다.Dept는Locations를@ManyToOne으로 참조한다.- 로딩 전략은 연관 엔티티를 언제 가져올지 정하는 설정이다.
- 이 파트에서는 로딩 전략보다 엔티티 사이의 참조 구조를 먼저 이해한다.
이번 파트에서는 로딩 전략 자체보다
Emp,Dept,Locations의 연관관계 구조를 먼저 이해하는 것이 중요하다.
테이블 구조는 어떤 기준으로 확인하면 좋은가
이 묶음은 직접 실행 결과보다 엔티티 구조와 테이블 관계를 확인하는 것이 중요하다.
따라서 콘솔 출력보다Emp,Dept,Locations테이블 구조와 외래키로 사용되는 컬럼을 확인하는 화면이 더 적절하다.
확인할 때는Emp테이블의deptno컬럼,Dept테이블의loc_code컬럼,Locations테이블의loc_code기본키를 보면 된다.
이 세 부분을 코드의@JoinColumn설정과 함께 보면Emp → Dept → Locations흐름이 성립하는 이유를 이해할 수 있다.
확인 기준은 아래와 같다.
Emp테이블에deptno외래키 컬럼이 있는지 확인한다.Dept테이블에loc_code외래키 컬럼이 있는지 확인한다.Locations테이블에loc_code기본키가 있는지 확인한다.Emp의deptno가Dept참조 필드로 매핑되는지 확인한다.Dept의loc_code가Locations참조 필드로 매핑되는지 확인한다.이 테이블 구조 확인은 뒤에서
Emp를 조회했을 때 부서와 지역 정보가 어떻게 따라오는지 이해하기 위한 준비 단계로 보면 된다.
핵심 정리
이번 묶음에서는
Emp,Dept,Locations엔티티의 연관관계 구조를 확인했다.
세 엔티티는 사원, 부서, 지역 정보를 표현하고, 외래키 관계를 객체 참조로 매핑한다.
정리하면 아래와 같다.
Emp는 사원 정보를 표현한다.Emp는@ManyToOne과@JoinColumn(name="deptno")로Dept를 참조한다.Dept는 부서 정보를 표현한다.Dept는@ManyToOne과@JoinColumn(name="loc_code", referencedColumnName = "loc_code")로Locations를 참조한다.Locations는 지역 정보를 표현한다.- 객체 흐름은
Emp → Dept → Locations로 이어진다.- 테이블 흐름은 외래키 컬럼으로 이어진다.
- 로딩 전략 실행 차이는 뒤의 조회 예제에서 확인한다.
이 구조를 이해해야 이후
Emp조회 예제에서 왜 부서와 지역 조회 쿼리가 함께 나타나는지 이해할 수 있다.
HelloJPA3.java는Emp엔티티를JPQL로 전체 조회하는 예제이다.
앞에서는Emp,Dept,Locations엔티티가Emp → Dept → Locations방향으로 연결된 구조를 확인했다.
이번에는Emp를 조회하더라도 연관 객체를 직접 사용하지 않으면 조회 흐름이 어떻게 단순하게 유지되는지 확인한다.
이 예제의 출력 코드는elem.getEname()만 사용한다.
즉,Emp는 조회하지만 연관 객체인Dept의 필드는 직접 사용하지 않는다.
이 예제의 핵심은Emp전체 조회와 사원 이름 출력 흐름을 확인하고, 연관 객체를 사용하지 않는 코드에서는 부서 정보가 출력되지 않는다는 점을 보는 것이다.
이전 기본개념과 연결하기
이전 기본개념에서
JPQL은 실제 테이블명이 아니라Entity이름을 기준으로 작성한다고 정리했다.
따라서 사원 테이블을 조회하더라도JPQL에서는 테이블명 대신Emp엔티티 이름을 사용한다.
또 이전 예제에서Emp는Dept를 참조하고,Dept는Locations를 참조한다고 정리했다.
테이블은 외래키 컬럼으로 연결되지만, 객체는 참조 필드로 연결된다.
즉,Emp객체 안에는Dept객체 참조가 있다.
다만 연관관계가 있다고 해서 항상 연관 객체의 필드까지 사용하는 것은 아니다.
이번 예제는Emp전체를 조회한 뒤 사원 이름만 출력한다.
그래서 조회 결과에서 봐야 할 중심은 부서 정보가 아니라 사원 이름 목록이다.
기본개념에서 다시 잡아야 할 흐름
JPQL은 테이블명이 아니라Entity이름을 기준으로 작성한다.Emp는Dept를@ManyToOne으로 참조한다.Dept는Locations를@ManyToOne으로 참조한다.- 이번 출력 코드는
Emp자신의 필드인ename만 사용한다.- 연관 객체인
Dept의 필드는 이 예제에서 직접 출력하지 않는다.연관관계가 있다는 사실과 코드에서 연관 객체를 실제로 사용한다는 사실은 구분해야 한다.
예제의 목표
이 예제의 목표는
Emp전체 조회 결과를List<Emp>로 받아 사원 이름만 출력하는 흐름을 확인하는 것이다.
Emp가Dept를 참조하고 있어도, 반복문 안에서getEname()만 호출하면 출력 결과는 사원 이름 중심으로 나온다.
이 예제에서 확인할 내용
"entitytest"설정으로EntityManagerFactory를 만든다.EntityManager를 생성한다.JPQL로Emp전체를 조회한다.- 조회 결과를
List<Emp>로 받는다.- 반복문에서
elem.getEname()으로 사원 이름만 출력한다.- 마지막에 조회된 데이터 개수를 출력한다.
이 흐름을 이해하면 뒤에서 연관 필드를 실제로 사용하는 예제와 비교하기 쉬워진다.
코드 흐름
HelloJPA3.java는EntityManagerFactory와EntityManager를 만든 뒤,JPQL로Emp전체 목록을 조회한다.
그다음 조회 결과를 반복하면서 사원 이름만 출력한다.
핵심 코드는 아래와 같다.// HelloJPA3.java public class HelloJPA3 { public static void main(String[] args) { EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 JPQL 생성 List<Emp> empList = q.getResultList(); // 조회 결과를 List로 받음 for (Emp elem : empList) { // 조회된 Emp를 하나씩 꺼냄 System.out.println(elem.getEname()); // 사원 이름만 출력 } System.out.println("데이터 갯수 : " + empList.size()); // 조회된 데이터 개수 출력 em.close(); // EntityManager 자원 정리 factory.close(); // Factory 자원 정리 } }
select t from Emp t는Emp엔티티 전체를 조회하는JPQL이다.
여기서Emp는 테이블명이 아니라 엔티티 이름이다.
t는 쿼리 안에서Emp를 짧게 부르기 위한 별칭이다.
조회 결과는empList에 저장된다.
그다음 반복문에서elem.getEname()만 호출한다.
즉, 사원 이름만 사용하고elem.getDeptno()처럼 부서 객체를 직접 꺼내지 않는다.
이 코드는
Emp전체를 조회한 뒤elem.getEname()으로 사원 이름만 출력하는 흐름이다.
부서 객체를 직접 사용하지 않으므로 출력 결과는 사원 이름과 데이터 개수 중심으로 확인한다.
핵심 코드 해석
select t from Emp t는 Emp 엔티티 전체 조회이다
이 예제의
JPQL은Emp전체 조회 문장이다.
실제 데이터베이스 테이블명이나 컬럼명을 직접 쓰지 않고, 엔티티 이름과 별칭을 사용한다.
// HelloJPA3.java Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회
select t는 조회 결과로t전체를 가져오겠다는 뜻이다.
from Emp t는Emp엔티티를 조회 대상으로 삼고, 그 엔티티를 쿼리 안에서t라고 부르겠다는 뜻이다.
JPQL의Emp는Java엔티티 이름이다.
실제 실행 시에는Hibernate가 이JPQL을 데이터베이스가 이해할 수 있는SQL로 바꿔 실행한다.
elem.getEname만 출력하면 사원 이름만 사용한다
조회 결과를 출력하는 코드는 아래와 같다.
// HelloJPA3.java for (Emp elem : empList) { // Emp 객체를 하나씩 꺼냄 System.out.println(elem.getEname()); // 사원 이름만 출력 }이 반복문은
empList안의Emp객체를 하나씩 꺼낸다.
그리고 각 객체에서getEname()만 호출한다.
getEname()은 사원 이름을 가져오는 메서드이다.
여기서 중요한 점은getDeptno()를 호출하지 않는다는 것이다.
deptno는Emp가 참조하는Dept객체이다.
현재 출력 코드는 부서 객체를 사용하지 않고 사원 이름만 출력한다.
같은Emp조회라도 반복문 안에서 어떤 필드를 사용하는지에 따라 확인해야 할 결과가 달라진다.
결과물은 어떻게 나오는가
이 예제를 실행하면
Emp전체 조회SQL이 실행된다.
그리고 콘솔에는 사원 이름이 한 줄씩 출력된다.
예를 들어SMITH,ALLEN,WARD,JONES같은 값이 출력된다.
마지막에는데이터 갯수 : 14가 출력된다.
실행 결과를 보면
Emp전체 조회SQL이 실행되고, 콘솔에는 사원 이름만 출력된다.
이 예제에서는 부서명이나 지역명을 출력하지 않는다.
코드에서elem.getEname()만 호출하기 때문이다.
// 출력결과 // Hibernate: // select ... // from Emp ... // SMITH // ALLEN // WARD // JONES // ... // 데이터 갯수 : 14결과물을 확인할 때 봐야 할 기준
select t from Emp t에 해당하는Emp조회가 실행되는지 확인한다.- 콘솔에 사원 이름이 한 줄씩 출력되는지 확인한다.
- 출력 코드가
elem.getEname()만 사용한다는 점을 확인한다.- 마지막에
데이터 갯수 : 14가 출력되는지 확인한다.이 예제의 결과물은
Emp전체 조회 후 사원 이름만 출력되는 흐름을 확인하는 데 초점을 둔다.
핵심 정리
HelloJPA3.java는JPQL로Emp전체 목록을 조회하는 예제이다.
단순 목록 조회처럼 보이지만,Emp가Dept를 참조한다는 구조를 알고 있어야 뒤의 연관 필드 접근 예제를 이해하기 쉽다.
정리하면 아래와 같다.
JPQL은 테이블명이 아니라Entity이름을 기준으로 작성한다.select t from Emp t는Emp전체 조회이다.- 조회 결과는
List<Emp>로 받는다.- 현재 출력 코드는
elem.getEname()으로 사원 이름만 출력한다.- 부서 객체를 직접 사용하는 코드는 이 예제에 없다.
- 이 흐름은 뒤에서 연관 필드를 실제로 사용하는 예제와 비교하는 기준이 된다.
Emp전체 조회 예제는 연관관계를 사용하기 전, 먼저 기본 조회와 사원 이름 출력 흐름을 확인하는 출발점이다.
HelloJPA3_1.java는Emp를 전체 조회한 뒤, 각 사원이 참조하는Dept객체가 어떤 클래스 형태로 준비되어 있는지 확인하는 예제이다.
앞의HelloJPA3.java에서는Emp를 조회하고elem.getEname()만 출력했다.
이번 예제에서는elem.getDeptno().getClass().getName()을 출력해서Emp가 참조하는Dept객체의 실제 클래스명을 확인한다.
이 코드는 부서명을 출력하는 코드가 아니다.
Dept참조 객체가 어떤 형태로 준비되어 있는지 관찰하는 코드이다.
이 예제의 핵심은 연관 객체가 실제 엔티티 객체로 바로 준비되는지, 아니면 프록시 객체로 준비되는지 확인하는 것이다.
이전 기본개념과 연결하기
앞 개념에서
JPA는 객체와 테이블을 매핑하고, 연관관계는 외래키 값을 객체 참조로 다룰 수 있게 해 준다고 정리했다.
Emp는Dept를 참조하고,Dept는 다시Locations를 참조할 수 있다.
객체 흐름으로 보면Emp → Dept → Locations로 이어질 수 있다.
다만 이번 예제에서 직접 확인하는 대상은Locations가 아니다.
이번 예제에서 직접 확인하는 대상은Emp가 참조하는Dept객체의 클래스명이다.
즉,Emp에서Dept참조로 이동했을 때 그 참조 객체가 실제Dept인지, 프록시인지 확인한다.
프록시는 실제 객체를 대신해서 서 있는 대리 객체이다.
실제 데이터 조회를 미루거나 필요한 순간에 이어서 처리할 수 있도록 준비된 객체로 이해하면 된다.
기본개념에서 다시 잡아야 할 흐름
Emp는Dept를@ManyToOne으로 참조한다.elem.getDeptno()는Emp에서Dept참조로 이동하는 코드이다.- 프록시는 실제 객체를 대신하는 대리 객체이다.
getClass().getName()은 객체의 실제 클래스명을 확인한다.- 프록시 객체라면 클래스명에
HibernateProxy같은 프록시 관련 이름이 보일 수 있다.지연 로딩을 이해하려면 연관 객체 참조가 있다는 사실과 실제 연관 데이터가 조회되었다는 사실을 구분해야 한다.
코드 흐름
HelloJPA3_1.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음JPQL로Emp전체 목록을 조회한다.
조회 결과를 반복하면서 사원 이름과 부서 객체의 클래스명을 출력한다.
핵심 코드는 아래와 같다.// HelloJPA3_1.java public class HelloJPA3_1 { public static void main(String[] args) { EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 JPQL 생성 List<Emp> empList = q.getResultList(); // Emp 목록 조회 for (Emp elem : empList) { // 조회된 Emp를 하나씩 꺼냄 System.out.print(elem.getEname() + " : "); // 사원 이름 출력 System.out.println( elem.getDeptno() != null ? elem.getDeptno().getClass().getName() // 부서 객체의 실제 클래스명 출력 : "힝 ㅠㅠ~~" // 부서가 없을 때 출력 ); } System.out.println("데이터 갯수 : " + empList.size()); // 조회된 Emp 개수 출력 em.close(); // EntityManager 자원 정리 factory.close(); // EntityManagerFactory 자원 정리 } }
select t from Emp t는Emp엔티티 전체를 조회하는JPQL이다.
조회 결과는List<Emp>형태로 받아서for반복문으로 하나씩 출력한다.
각 줄은 사원 이름과 부서 객체의 클래스명이 함께 출력되는 구조이다.
elem.getDeptno() != null조건이 들어간 이유는 부서가 없는 사원이 있을 수 있기 때문이다.
부서가 없는 사원은deptno가null일 수 있다.
이 상태에서elem.getDeptno().getClass()를 바로 호출하면 오류가 발생할 수 있다.
그래서 먼저null인지 확인한다.
연관 객체가 없을 수 있는 경우에는 객체 참조를 따라가기 전에 반드시null여부를 먼저 확인해야 한다.
핵심 코드 해석
elem.getDeptno는 Emp에서 Dept 참조로 이동하는 코드이다
Emp엔티티에는Dept를 참조하는deptno필드가 있다.
필드 이름은deptno라서 숫자처럼 보이지만, 실제 타입은Dept이다.
// Emp.java @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정 @JoinColumn(name="deptno") // emp 테이블의 deptno 외래키와 연결 private Dept deptno; // Dept Entity 참조
@JoinColumn(name="deptno")는Emp테이블의deptno외래키 컬럼을 사용한다는 뜻이다.
하지만 객체 코드에서는 외래키 숫자만 다루지 않는다.
Emp객체 안에서Dept객체를 참조하는 구조로 다룬다.
그래서elem.getDeptno()는 사원의 부서 번호를 단순히 꺼내는 코드가 아니다.
Emp객체에서 연관된Dept객체 참조로 이동하는 코드이다.
elem.getDeptno()는 외래키 값을 직접 꺼내는 것이 아니라,Emp에서 연관된Dept객체 참조로 이동하는 코드이다.
getClass().getName은 객체의 실제 클래스명을 확인한다
이 예제의 핵심 코드는 아래 부분이다.
// HelloJPA3_1.java elem.getDeptno().getClass().getName() // Dept 참조 객체의 실제 클래스명 확인이 코드는 세 단계로 나누어 읽으면 쉽다.
elem.getDeptno()는Emp가 참조하는Dept객체를 가져온다.
getClass()는 그 객체가 실제로 어떤 클래스의 객체인지 확인한다.
getName()은 그 클래스 이름을 문자열로 가져온다.
출력된 클래스명에 프록시 관련 이름이 포함되어 있다면, 실제 엔티티 대신 프록시 객체가 준비된 흐름으로 볼 수 있다.
여기서 중요한 점은 클래스명 전체를 외우는 것이 아니다.
출력된 클래스명이 프록시인지 실제 엔티티인지 구분하는 것이다.
getClass().getName()은 연관 객체가 실제 엔티티인지 프록시인지 확인하기 위한 관찰용 코드이다.
결과물은 어떻게 나오는가
이 예제를 실행하면
Emp전체 조회가 먼저 실행된다.
그다음 콘솔에는 사원 이름과 부서 참조 객체의 클래스명이 함께 출력된다.
실행 결과에서는 사원명 오른쪽에
jpaexam2.model.entity.Dept$HibernateProxy...형태의 클래스명이 출력될 수 있다.
이는Emp가 참조하는Dept가 실제 엔티티 객체 대신 프록시 객체 형태로 준비될 수 있다는 것을 보여 준다.
ADAMS는"힝 ㅠㅠ~~"로 출력된다.
이는ADAMS의deptno가null이기 때문이다.
즉, 참조할Dept객체가 없으므로getClass().getName()을 호출하지 않고 삼항 연산자의 거짓 결과가 출력된 것이다.
// 출력결과 // SMITH : jpaexam2.model.entity.Dept$HibernateProxy... // ALLEN : jpaexam2.model.entity.Dept$HibernateProxy... // ... // ADAMS : 힝 ㅠㅠ~~ // 데이터 갯수 : 14결과물을 확인할 때 봐야 할 기준
Emp전체 조회SQL이 실행되는지 확인한다.- 사원명 오른쪽에
Dept$HibernateProxy...같은 프록시 클래스명이 보이는지 확인한다.ADAMS처럼deptno가null인 데이터는"힝 ㅠㅠ~~"가 출력될 수 있음을 확인한다.이 예제의 결과물은 연관 객체가 프록시 형태로 준비될 수 있음을 확인하는 데 초점을 둔다.
핵심 정리
HelloJPA3_1.java는Emp전체 조회 후 연관 객체인Dept가 어떤 클래스 형태로 준비되어 있는지 확인하는 예제이다.
앞의HelloJPA3.java가 사원 이름만 출력했다면, 이번 예제는Emp에서Dept참조로 이동해 객체의 실제 클래스명을 확인한다.
정리하면 아래와 같다.
select t from Emp t는Emp전체 조회JPQL이다.- 조회 결과는
List<Emp>로 받는다.- 반복문에서 사원 이름과 부서 객체 클래스명을 출력한다.
elem.getDeptno()는 외래키 값을 꺼내는 것이 아니라Dept객체 참조로 이동하는 코드이다.getClass().getName()은 객체의 실제 클래스명을 확인한다.getClass().getName()은 부서명 같은 실제 필드 값을 읽는 코드가 아니라 객체 형태를 확인하는 코드이다.- 프록시 클래스명이 보이면 연관 객체가 대리 객체 형태로 준비된 흐름으로 볼 수 있다.
deptno가null인ADAMS는 클래스명을 출력하지 않고"힝 ㅠㅠ~~"를 출력한다.이 예제를 이해하면 지연 로딩이 실제 객체 대신 프록시를 사용해 연관 데이터 조회를 미룰 수 있다는 흐름을 코드 결과로 확인할 수 있다.
HelloJPA3_2.java,HelloJPA4.java,HelloJPA5.java는Emp에서 연관된Dept로 이동해 실제 부서 정보를 사용하는 예제이다.
앞의HelloJPA3_1.java에서는getClass().getName()으로Dept참조 객체가 실제 엔티티인지 프록시인지 확인했다.
이번에는 클래스명 확인에서 끝나지 않고,getDname()이나toString()을 통해 연관 엔티티의 실제 필드를 사용할 수 있는 흐름을 확인한다.
이 예제들은Emp목록을 조회한 뒤 반복문에서Dept필드를 실제로 사용할 때 추가 조회가 발생할 수 있음을 보여 준다.
이때 추가 조회가 반복되면N+1문제 흐름과 연결된다.
이 예제의 핵심은Emp목록 조회 후 반복문에서 연관 엔티티 필드를 실제로 사용할 때 추가 조회가 발생할 수 있고, 이 흐름이N+1문제와 연결된다는 점이다.
이전 기본개념과 연결하기
앞 개념에서
Emp는Dept를@ManyToOne으로 참조한다고 정리했다.
테이블 기준으로 보면Emp테이블의deptno외래키가Dept테이블의 기본키와 연결된다.
객체 기준으로 보면Emp객체 안에Dept객체 참조가 들어 있다.
그래서Emp에서 부서 정보를 사용하려면elem.getDeptno()로Dept참조에 접근할 수 있다.
그리고elem.getDeptno().getDname()처럼 한 단계 더 들어가면Dept의 부서명을 사용할 수 있다.
앞 예제에서 사용한getClass().getName()은 객체의 클래스 형태를 확인하는 코드였다.
부서명 같은 실제 필드 값을 읽는 코드는 아니었다.
반면 이번 예제의getDname()은Dept의 실제 필드 값을 읽는 코드이다.
이 차이를 구분해야 한다.
기본개념에서 다시 잡아야 할 흐름
Emp는Dept를 객체 참조로 가진다.elem.getDeptno()는Emp에서Dept참조로 이동하는 코드이다.elem.getDeptno().getDname()은Dept의 부서명 필드를 실제로 사용하는 코드이다.getClass().getName()은 객체 형태 확인이고,getDname()은 실제 필드 값 사용이다.- 반복문 안에서 연관 필드를 계속 사용하면 추가 조회가 반복될 수 있다.
- 이 흐름이
N+1문제와 연결된다.System.out.println(elem)도 내부적으로toString()을 호출하므로,toString()설정에 따라 연관 필드 접근이 발생할 수 있다.연관관계 성능 문제는 연관관계 매핑 자체보다, 목록 조회 후 반복문에서 연관 필드를 실제로 사용하는 순간에 잘 드러난다.
공통 코드 흐름
HelloJPA3_2.java,HelloJPA4.java,HelloJPA5.java는 모두 먼저Emp전체 목록을 조회한다.
조회 문장은 같은 흐름을 가진다.
차이는 반복문 안에서Emp를 어떻게 사용하는지이다.
기본 조회 흐름은 아래와 같다.// EmpListBasicFlow.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 JPQL 생성 List<Emp> empList = q.getResultList(); // Emp 목록 조회 for (Emp elem : empList) { // 조회된 Emp를 하나씩 꺼냄 // 예제별로 출력 방식이 달라짐 } System.out.println("데이터 갯수 : " + empList.size()); // 조회된 Emp 개수 출력 em.close(); // EntityManager 자원 정리 factory.close(); // EntityManagerFactory 자원 정리여기까지는
Emp목록을 조회하는 공통 흐름이다.
select t from Emp t는Emp엔티티 전체를 조회하는JPQL이다.
이제 중요한 부분은 반복문 안이다.
반복문 안에서 사원 이름만 출력하면Emp자신의 필드만 사용한다.
하지만elem.getDeptno().getDname()처럼 연관된Dept의 필드를 사용하면Dept조회가 추가로 발생할 수 있다.
같은Emp목록 조회라도 반복문 안에서 어떤 필드를 사용하느냐에 따라 실제SQL흐름이 달라질 수 있다.
HelloJPA3_2는 부서명을 직접 출력한다
HelloJPA3_2.java는Emp목록을 조회한 뒤 각 사원의 이름과 부서명을 출력한다.
앞의HelloJPA3_1.java가getClass().getName()으로 객체 형태를 확인했다면, 이번 파일은getDname()으로 실제 부서명을 사용한다.
핵심 코드는 아래와 같다.// HelloJPA3_2.java Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 List<Emp> empList = q.getResultList(); // 결과 목록 조회 for (Emp elem : empList) { // 사원 목록 반복 System.out.print(elem.getEname() + " : "); // 사원 이름 출력 if (elem.getDeptno() != null) { // 부서 참조가 있으면 System.out.println(elem.getDeptno().getDname()); // 부서명 출력 } else { System.out.println("부서미정"); // 부서 참조가 없으면 출력 } } System.out.println("데이터 갯수 : " + empList.size()); // 데이터 개수 출력
elem.getDeptno().getDname()은 두 단계로 읽어야 한다.
먼저elem.getDeptno()로Emp에서Dept참조로 이동한다.
그다음getDname()으로Dept의 부서명 필드를 읽는다.
이 코드는Dept객체의 실제 필드 값을 사용한다.
따라서Dept가 아직 준비되지 않았다면 이 시점에 부서 조회가 발생할 수 있다.
HelloJPA3_2.java실행 결과이다.
SMITH : RESEARCH,ALLEN : SALES처럼 사원명 오른쪽에 부서명이 출력된다.
ADAMS : 부서미정은deptno가null이라서 참조할Dept객체가 없을 때 실행되는 분기 결과이다.
HelloJPA3_2에서 봐야 할 핵심
Emp목록을 먼저 조회한다.- 반복문 안에서 사원 이름을 출력한다.
getDeptno()로Dept참조에 접근한다.getDname()으로Dept의 실제 부서명 필드를 읽는다.ADAMS는deptno가null이라서"부서미정"이 출력된다.
getClass().getName()은 객체 형태 확인이고,getDname()은 연관 엔티티의 실제 필드 값을 읽는 코드이다.
HelloJPA4는 시간 간격을 두고 연관 필드 접근 시점을 보여 준다
HelloJPA4.java는 직원명을 먼저 출력하고,Thread.sleep(1000)으로1초 기다린 뒤 부서명을 출력한다.
이 구조는 직원 정보 출력과 부서 정보 출력 사이의 흐름을 눈으로 구분하기 쉽게 만든다.
핵심 코드는 아래와 같다.// HelloJPA4.java Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 List<Emp> empList = q.getResultList(); // 결과 목록 조회 for (Emp elem : empList) { // 사원 목록 반복 System.out.println("직원명 : " + elem.getEname()); // 직원명 먼저 출력 Thread.sleep(1000); // 1초 대기 if (elem.getDeptno() != null) { // 부서 참조가 있으면 System.out.println(" 부서명 : " + elem.getDeptno().getDname()); // 부서명 출력 } else { System.out.println(" 부서명 : 없음"); // 부서 참조가 없으면 출력 } } System.out.println("데이터 갯수 : " + empList.size()); // 데이터 개수 출력
Thread.sleep(1000)은 로딩 전략을 설정하는 코드가 아니다.
Thread는 실행 흐름을 의미하고,sleep()은 잠시 멈추는 메서드이다.
여기서는1초 동안 출력 흐름을 멈춰서 직원명 출력과 부서명 출력의 시점을 구분하기 쉽게 만든다.
즉,HelloJPA4.java의 핵심은 시간 지연 자체가 아니다.
직원명 출력과 부서명 출력 사이를 나누어 보면서, 연관 필드에 접근하는 시점을 확인하는 것이다.
위 결과는
HelloJPA4.java실행 흐름이다.
Emp의 직원명을 먼저 출력한 뒤, 부서명을 출력하려고Dept의dname필드에 접근한다.
Thread.sleep(1000)은 직원명 출력과 부서명 출력 사이에 시간 간격을 만들어서, 언제 연관 필드에 접근하는지 눈으로 보기 쉽게 만든 코드이다.
deptno가null인 사원에게는"부서명 : 없음"이 출력된다.
이는else구문에서 부서 참조가 없을 때 출력하도록 작성한 분기 결과이다.
HelloJPA4에서 봐야 할 핵심
Thread.sleep(1000)은 지연 로딩 문법이 아니다.- 출력 사이에 시간 간격을 만들기 위한 코드이다.
- 직원명은
Emp자신의 필드이다.- 부서명은 연관된
Dept의 필드이다.- 부서명에 접근하는 순간 연관 필드가 실제로 사용된다.
ADAMS는deptno가null이므로"부서명 : 없음"이 출력된다.
HelloJPA4.java는 시간 지연 자체가 핵심이 아니라, 직원명 출력 후 부서명 필드에 접근하는 시점을 관찰하게 해 주는 예제이다.
HelloJPA5는 toString 호출로 연관 필드가 사용될 수 있음을 보여 준다
HelloJPA5.java는 각Emp객체를 그대로 출력한다.
즉, 반복문 안에서System.out.println(elem)을 호출한다.
겉으로 보기에는Emp객체 하나를 출력하는 코드처럼 보인다.
하지만System.out.println(elem)은 내부적으로elem.toString()을 호출한다.
Emp클래스에@ToString이 붙어 있다면, 출력 문자열을 만들 때 여러 필드가 함께 사용될 수 있다.
핵심 코드는 아래와 같다.// HelloJPA5.java Query q = em.createQuery("select t from Emp t", Emp.class); // Emp 전체 조회 List<Emp> empList = q.getResultList(); // 결과 목록 조회 for (Emp elem : empList) { // 사원 목록 반복 System.out.println(elem); // toString() 호출로 객체 정보 출력 } System.out.println("데이터 갯수 : " + empList.size()); // 데이터 개수 출력이 코드에는
getDeptno().getDname()이 직접 보이지 않는다.
하지만toString()결과에deptno필드가 포함되면 연관 객체 정보가 출력 문자열을 만드는 과정에서 사용될 수 있다.
즉, 직접 연관 필드 접근 코드를 작성하지 않아도, 객체 출력 과정에서 연관 필드가 사용될 수 있다.
HelloJPA5.java실행 결과이다.
System.out.println(elem)으로Emp객체를 출력했지만, 내부적으로는toString()이 호출된다.
현재toString()결과에는deptno=Dept(...)가 포함되어 있으므로, 객체 출력만으로도 연관 객체 정보가 함께 출력된다.
ADAMS는 부서 참조가 없기 때문에deptno=null로 출력된다.
HelloJPA5에서 봐야 할 핵심
System.out.println(elem)은 내부적으로toString()을 호출한다.@ToString이 있으면 객체의 여러 필드가 출력 문자열에 포함될 수 있다.toString()안에서 연관 필드가 사용되면 연관 조회가 발생할 수 있다.- 실행 결과에서
deptno=Dept(...)와loc_code=Locations(...)가 함께 출력된다.ADAMS는 부서 참조가 없으므로deptno=null로 출력된다.객체 출력도 내부적으로
toString()을 호출하므로,toString()안에서 연관 필드를 사용하면 로딩이 발생할 수 있다.
반복문과 연관 필드 접근은 N+1 문제와 연결된다
HelloJPA3_2.java,HelloJPA4.java,HelloJPA5.java는 모두Emp목록 조회 후 반복문 안에서 연관 엔티티를 사용할 수 있는 구조이다.
이 구조는N+1문제를 이해하는 데 중요하다.
기본 흐름은 아래와 같다.
Emp목록을 한 번 조회한다.- 조회된 사원 수만큼 반복문을 돈다.
- 반복문 안에서
Dept정보를 사용한다.- 연관 엔티티가 아직 준비되지 않았다면 추가 조회가 발생할 수 있다.
이때 처음
Emp목록 조회가1번이다.
그다음 반복문 안에서 연관된Dept조회가 추가로 발생할 수 있다.
이처럼 목록 조회1번 이후 연관 데이터 조회가 여러 번 이어지는 흐름이N+1문제와 연결된다.
다만 여기서 주의해야 한다.
추가 조회가 사원 수만큼 무조건 발생한다고 단정하면 안 된다.
여러 사원이 같은 부서를 참조할 수 있고, 이미 영속성 컨텍스트에 조회된Dept는 다시 조회하지 않을 수 있기 때문이다.
그래서 실제 추가 조회 횟수는 참조되는 부서의 종류와 영속성 컨텍스트 상태에 따라 달라질 수 있다.
출력 형태를 정리하면 아래와 같다.// 출력결과 // HelloJPA3_2.java // SMITH : RESEARCH // ALLEN : SALES // WARD : SALES // ... // ADAMS : 부서미정 // 데이터 갯수 : 14 // HelloJPA4.java // 직원명 : SMITH // 부서명 : RESEARCH // 직원명 : ALLEN // 부서명 : SALES // ... // 직원명 : ADAMS // 부서명 : 없음 // 데이터 갯수 : 14 // HelloJPA5.java // Emp(empno=7369, ename=SMITH, ..., deptno=Dept(... loc_code=Locations(...))) // ... // Emp(empno=7876, ename=ADAMS, ..., deptno=null) // 데이터 갯수 : 14결과를 볼 때는 단순히 출력 문자열만 보면 부족하다.
출력 문자열 오른쪽에 부서명이 나오거나,Emp객체 안에Dept정보가 함께 출력된다면 연관 엔티티 필드가 사용된 것이다.
이때SQL로그에서Dept조회가 언제 발생하는지도 함께 확인해야 한다.
N+1 흐름에서 봐야 할 핵심
- 처음 목록 조회가
1번 실행된다.- 반복문 안에서 연관 필드를 사용한다.
- 연관 엔티티가 아직 준비되지 않았다면 추가 조회가 발생할 수 있다.
- 추가 조회 횟수는 참조키의 유니크 값과 영속성 컨텍스트 상태에 따라 달라질 수 있다.
SQL로그를 보면서 실제 조회 횟수를 확인해야 한다.
N+1문제는 연관관계를 매핑했다는 사실만으로 생기는 것이 아니라, 목록 조회 후 연관 데이터를 반복해서 사용하는 흐름에서 잘 드러난다.
핵심 정리
HelloJPA3_2.java,HelloJPA4.java,HelloJPA5.java는 모두Emp목록 조회 후 연관된Dept필드를 실제로 사용하는 예제이다.
정리하면 아래와 같다.
HelloJPA3_2.java는getDeptno().getDname()으로 부서명을 직접 출력한다.HelloJPA4.java는 직원명 출력 후 시간 간격을 두고 부서명을 출력한다.Thread.sleep(1000)은 출력 흐름을 보기 쉽게 만드는 대기 코드이다.HelloJPA5.java는System.out.println(elem)으로toString()호출 흐름을 확인한다.toString()에 연관 필드가 포함되면 객체 출력 과정에서도 연관 필드 접근이 발생할 수 있다.deptno가null인 사원은부서미정,부서명 : 없음,deptno=null처럼 분기되어 출력된다.- 목록 조회 후 반복문 안에서 연관 필드를 사용하면 추가 조회가 발생할 수 있다.
- 이 흐름이
N+1문제와 연결된다.이 묶음을 이해하면 연관 객체를 실제로 사용하는 순간부터
SQL흐름이 달라질 수 있다는 점을 확인할 수 있다.
HelloJPA6.java는Fetch Join을 사용해서Emp,Dept,Locations를 함께 조회하는 예제이다.
앞 예제에서는Emp목록을 조회한 뒤 반복문 안에서Dept의 부서명을 사용했다.
그 과정에서FetchType.LAZY상태라면 부서명을 실제로 사용하는 시점에 추가 조회가 발생할 수 있었다.
이번 예제는 필요한 연관 데이터를 처음 조회할 때 함께 가져오도록JPQL에 명시한다.
즉,Emp를 조회하면서Dept와Locations까지 함께 준비하려는 흐름이다.
Fetch Join은 연관 엔티티를 실제로 함께 사용할 것이 확실할 때 처음 조회 쿼리에서 함께 가져오는 방법이다.
이전 기본개념과 연결하기
앞에서
Emp,Dept,Locations는 서로 이어진 엔티티라고 정리했다.
Emp는Dept를 참조하고,Dept는Locations를 참조한다.
객체 흐름으로 보면Emp → Dept → Locations이다.
앞 예제에서는 이 흐름을 반복문 안에서 사용했다.
Emp목록을 먼저 조회하고, 각Emp에서getDeptno()로Dept에 접근했다.
그리고getDname()으로Dept의 부서명을 읽었다.
이때FetchType.LAZY상태라면 연관 엔티티를 처음부터 바로 가져오지 않는다.
그래서 반복문 안에서Dept의 실제 필드를 사용할 때 추가 조회가 발생할 수 있다.
이 흐름이 반복되면N+1문제와 연결된다.
이번 예제의Fetch Join은 이 문제를 줄이기 위한 방법이다.
처음부터Emp만 조회하지 않고, 앞으로 사용할Dept와Locations까지 함께 가져오도록JPQL에 적는다.
기본개념에서 다시 잡아야 할 흐름
Emp는Dept를 참조한다.Dept는Locations를 참조한다.FetchType.LAZY상태에서는 연관 엔티티를 실제로 사용할 때 추가 조회가 발생할 수 있다.- 반복문 안에서 연관 필드를 계속 사용하면 추가 조회가 반복될 수 있다.
Fetch Join은 처음 조회할 때 필요한 연관 엔티티를 함께 가져오는 방법이다.- 일반
join과fetch join은 목적이 다르다.fetch join은 연관 엔티티를 함께 조회하는 데 초점이 있다.
Fetch Join은N+1흐름을 줄이기 위해 필요한 연관 데이터를 처음 조회 시점에 함께 가져오도록 만드는 조회 방식이다.
예제의 목표
이 예제의 목표는
Fetch Join을 사용했을 때Emp,Dept,Locations가 한 조회 흐름에서 함께 준비되는지 확인하는 것이다.
앞 예제처럼 반복문 안에서 연관 필드를 사용할 때마다 조회가 추가되는 흐름과 비교해서 봐야 한다.
HelloJPA6.java는select t from Emp t처럼Emp만 조회하지 않는다.
left join fetch t.deptno로Dept를 함께 가져오고,left join fetch t.deptno.loc_code로Locations까지 함께 가져온다.
즉, 이번 예제는Emp목록을 조회하면서 사원, 부서, 지역 정보를 함께 사용할 준비를 하는 코드이다.
이 예제에서 확인할 내용
JPQL문자열에left join fetch가 들어간다.t.deptno를fetch join해서Emp와Dept를 함께 조회한다.t.deptno.loc_code를fetch join해서Dept와Locations까지 함께 조회한다.left join을 사용하므로 부서가 없는Emp도 결과에서 빠지지 않을 수 있다.- 조회 결과의 중심은
Emp이다.- 조회 결과는
List<Emp>로 받는다.- 반복문에서
System.out.println(elem)을 실행한다.toString()결과에Dept,Locations정보가 함께 출력될 수 있다.SQL로그에서join이 어떻게 만들어졌는지 확인한다.이번 예제는 출력 문자열보다
JPQL의fetch join이 실제SQL에서 연관 테이블을 함께 조회하는 흐름으로 바뀌는지 확인하는 데 목적이 있다.
코드 흐름
HelloJPA6.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음Fetch Join이 포함된JPQL문자열을 만들고, 그 쿼리를 실행한다.
조회된Emp목록은 반복문에서 그대로 출력한다.
핵심 코드는 아래와 같다.// HelloJPA6.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 String fetchJoinJpql = "select t " + "from Emp t left join fetch t.deptno " + "left join fetch t.deptno.loc_code"; // Emp, Dept, Locations 함께 조회 Query q = em.createQuery(fetchJoinJpql, Emp.class); // Emp 타입 기준으로 Fetch Join JPQL 실행 준비 List<Emp> empList = q.getResultList(); // Emp 목록 조회 for (Emp elem : empList) { // 사원 목록 반복 System.out.println(elem); // Emp 객체 출력 } System.out.println("데이터 갯수 : " + empList.size()); // 데이터 개수 출력 em.close(); // EntityManager 자원 정리 factory.close(); // EntityManagerFactory 자원 정리이 코드에서 가장 중요한 부분은
fetchJoinJpql문자열이다.
JPQL안에left join fetch가 두 번 들어간다.
첫 번째는Emp와Dept를 함께 조회하기 위한 것이고, 두 번째는Dept와Locations를 함께 조회하기 위한 것이다.
조회 결과는 여전히Emp목록이다.
즉,select t의 기준은Emp이다.
다만Emp만 따로 가져오는 것이 아니라,Emp가 참조하는 연관 엔티티까지 함께 준비한다.
여기서 변수 타입은Query로 받았지만,createQuery(fetchJoinJpql, Emp.class)처럼Emp.class를 함께 넘겼다.
이 말은 조회 결과를Emp타입 기준으로 다루겠다는 뜻이다.
TypedQuery<Emp>로 받으면 타입 의미가 더 분명하게 보이지만, 이 예제에서는Query변수로 받아도 결과 흐름은List<Emp>중심으로 이해하면 된다.
Fetch Join을 사용해도 조회 결과의 중심은select에 적은 엔티티이고, 함께 조회되는 연관 엔티티는 그 엔티티 안에 채워지는 구조로 이해하면 된다.
핵심 코드 해석
select t는 Emp를 조회 결과의 중심으로 둔다
HelloJPA6.java의JPQL은 아래처럼 시작한다.// HelloJPA6.java String fetchJoinJpql = "select t " + "from Emp t left join fetch t.deptno " + "left join fetch t.deptno.loc_code"; // Fetch Join JPQL
select t는t가 가리키는Emp를 조회 결과로 받겠다는 뜻이다.
from Emp t는Emp엔티티를 조회 대상으로 두고, 쿼리 안에서t라는 별칭으로 부르겠다는 뜻이다.
그래서 결과 타입은Emp이다.
코드에서도 결과를List<Emp>로 받는다.
// HelloJPA6.java Query q = em.createQuery(fetchJoinJpql, Emp.class); // Emp 타입 기준으로 조회 List<Emp> empList = q.getResultList(); // Emp 목록으로 받음여기서
Dept와Locations를 함께 조회하더라도 결과 목록 자체가List<Dept>나List<Locations>로 바뀌는 것은 아니다.
조회의 중심은 여전히Emp이다.
Emp.class를 넘겼다는 것은 조회 결과를Emp타입으로 기대한다는 뜻이다.
그래서 초보자 기준에서는 “조회 결과의 중심은Emp이고,Dept와Locations는 각Emp안에 함께 채워지는 연관 객체”라고 이해하면 된다.
select t가Emp를 가리키므로 결과 목록은Emp목록이고,Dept와Locations는 각Emp안의 연관 객체로 함께 채워진다.
left join fetch t.deptno는 Emp와 Dept를 함께 조회한다
left join fetch t.deptno는Emp를 조회하면서Emp가 참조하는Dept도 함께 조회하겠다는 뜻이다.
여기서t는Emp이다.
t.deptno는Emp안에 있는Dept참조 필드이다.
따라서left join fetch t.deptno는Emp → Dept관계를 따라가면서Dept를 함께 가져오는 코드이다.
// HelloJPA6.java "from Emp t left join fetch t.deptno " // Emp와 Dept를 함께 조회
fetch가 붙어 있기 때문에 단순히 조건 연결만 하는 조인이 아니다.
연관 엔티티인Dept를 실제 조회 결과와 함께 준비하는 것이 목적이다.
또left join을 사용했기 때문에 부서가 없는 사원도 결과에서 빠지지 않을 수 있다.
예를 들어ADAMS처럼deptno가null인 사원이 있어도Emp자체는 조회 결과에 포함될 수 있다.
left join fetch t.deptno.loc_code는 Locations까지 함께 조회한다
두 번째
fetch join은 아래 부분이다.// HelloJPA6.java "left join fetch t.deptno.loc_code" // Dept와 Locations까지 함께 조회이 경로는 길어 보이지만, 나누어 읽으면 어렵지 않다.
t는Emp이다.
t.deptno는Emp가 참조하는Dept이다.
t.deptno.loc_code는 그Dept가 참조하는Locations이다.
따라서 이 코드는Emp → Dept → Locations흐름을 한 번에 따라간다.
Emp를 조회하면서Dept를 함께 가져오고, 그Dept가 참조하는Locations까지 함께 가져오려는 구조이다.
이렇게 작성하면System.out.println(elem)으로Emp를 출력할 때Dept와Locations정보가 함께 보일 수 있다.
이때toString()설정에 따라 출력 문자열이 길어질 수 있다.
left join fetch t.deptno.loc_code는 사원에서 부서를 거쳐 지역까지 이어지는 연관 객체 흐름을 처음 조회 쿼리에서 함께 준비하는 코드이다.
일반 Join과 Fetch Join은 목적이 다르다
join과fetch join은 모양이 비슷해서 헷갈리기 쉽다.
하지만 목적이 다르다.
일반join은 주로 조건을 걸거나 연결된 엔티티의 값을 기준으로 조회 범위를 정할 때 사용한다.
예를 들어 부서명이 특정 값인 사원만 찾고 싶다면join을 조건 조회 목적으로 사용할 수 있다.
반면fetch join은 연관 엔티티를 실제로 함께 가져오는 것이 목적이다.
즉, 조회 결과의 중심 엔티티를 가져오면서, 나중에 사용할 연관 엔티티도 함께 채워 둔다.
아래 두 흐름을 비교하면 차이가 보인다.// NormalJoinFlow.java Query q = em.createQuery( "select t from Emp t join t.deptno d where d.dname = :dname", Emp.class ); // 부서명을 조건으로 Emp 조회일반
join은 여기서d.dname조건을 걸기 위해 사용된다.
연관 엔티티를 항상 함께 채워 두는 것이 핵심 목적은 아니다.// FetchJoinFlow.java Query q = em.createQuery( "select t from Emp t left join fetch t.deptno", Emp.class ); // Emp를 조회하면서 Dept도 함께 조회
fetch join은Emp를 조회하면서Dept를 함께 가져오는 것이 목적이다.
이후 반복문 안에서Dept를 사용할 때 추가 조회를 줄이는 데 도움이 될 수 있다.
일반 Join과 Fetch Join 비교
- 일반
join: 조건이나 연결을 위해 사용한다.- 일반
join: 연관 엔티티를 함께 채워 두는 것이 항상 목적은 아니다.fetch join: 연관 엔티티를 함께 조회하기 위해 사용한다.fetch join: 목록 조회 후 연관 필드를 사용할 때 추가 조회를 줄이는 데 도움이 된다.이 예제처럼 조회한
Emp객체 안에서Dept까지 바로 사용하려면, 단순join보다 연관 엔티티를 함께 채우는fetch join이 더 적합하다.
Fetch Join 결과는 길게 나와도 보는 기준이 있다
HelloJPA6.java의 결과는 길게 나올 수 있다.
System.out.println(elem)으로Emp전체를 출력하고,Emp의toString()안에Dept,Locations정보가 함께 보일 수 있기 때문이다.
하지만 결과가 길어도 보는 기준은 명확하다.
첫 번째는SQL로그에서Emp,Dept,Locations가 함께 조인되는지이다.
두 번째는 출력 결과에서Emp데이터가 출력되는지이다.
세 번째는Dept와Locations정보가 함께 출력되는지이다.
먼저SQL로그를 보면Emp를 기준으로Dept와Locations가left join으로 함께 조회된다.
JPQL에서는left join fetch t.deptno와left join fetch t.deptno.loc_code를 사용했고, 실제SQL에서는 이 연관 흐름이 테이블 조인으로 바뀐다.
HelloJPA6.java의Fetch Join SQL로그이다.
JPQL에서는left join fetch t.deptno와left join fetch t.deptno.loc_code를 사용했다.
실제SQL에서는Emp를 기준으로Dept가left join되고, 이어서Locations가 다시left join된다.
즉,Emp → Dept → Locations연관 흐름이 한 조회 쿼리 안에서 함께 처리되는 것을 확인할 수 있다.
출력 결과에서는Emp객체 안에Dept정보가 함께 보인다.
그리고Dept안에는 다시Locations정보가 포함되어 보인다.
HelloJPA6.java실행 결과이다.
출력 결과를 보면Emp객체 안에deptno=Dept(...)가 함께 출력되고, 그 안에loc_code=Locations(...)도 포함되어 있다.
이는Fetch Join으로Emp,Dept,Locations가 함께 준비되었음을 보여 준다.
ADAMS는 부서 참조가 없기 때문에deptno=null로 출력되지만,left join을 사용했기 때문에 결과 목록에서 빠지지 않는다.
출력 형태는 아래처럼 볼 수 있다.// 출력결과 // Emp(empno=7369, ename=SMITH, ..., deptno=Dept(... loc_code=Locations(...))) // Emp(empno=7499, ename=ALLEN, ..., deptno=Dept(... loc_code=Locations(...))) // ... // Emp(empno=7876, ename=ADAMS, ..., deptno=null) // 데이터 갯수 : 14
ADAMS처럼deptno가null인 사원은left join덕분에 조회 결과에서 제외되지 않는다.
대신 출력 결과에서deptno=null로 보인다.
결과를 볼 때는 출력 문자열만 보면 부족하다.
반드시SQL로그도 함께 봐야 한다.
fetch join이 적용되면Emp조회 쿼리 안에서Dept,Locations가 함께 조인되는 흐름을 확인할 수 있다.
HelloJPA6.java의 결과는 길어도Emp,Dept,Locations가 한 조회 흐름에서 함께 준비되는지 확인하면 된다.
앞 예제와 HelloJPA6의 차이
앞 예제들은 먼저
Emp목록을 조회한 뒤, 반복문에서Dept정보를 사용했다.
이때 연관 엔티티가 아직 준비되지 않았다면 추가 조회가 발생할 수 있었다.
반면HelloJPA6.java는 조회 쿼리 자체에서 연관 엔티티를 함께 가져오라고 명시한다.
그래서 결과를 출력할 때 이미 연관 데이터가 준비되어 있을 수 있다.
차이를 코드 기준으로 보면 아래와 같다.// NormalRelationAccessFlow.java Query q = em.createQuery("select t from Emp t", Emp.class); // Emp만 조회 List<Emp> empList = q.getResultList(); // Emp 목록 조회 for (Emp elem : empList) { // 사원 목록 반복 if (elem.getDeptno() != null) { // 부서 참조가 있으면 System.out.println(elem.getDeptno().getDname()); // 연관 필드 사용 시 추가 조회 가능 } }이 흐름은
Emp를 먼저 조회한다.
그다음 반복문 안에서Dept의 부서명을 사용한다.
FetchType.LAZY상태에서Dept가 아직 준비되지 않았다면 이때 추가 조회가 발생할 수 있다.// FetchJoinRelationAccessFlow.java Query q = em.createQuery( "select t from Emp t left join fetch t.deptno left join fetch t.deptno.loc_code", Emp.class ); // Emp, Dept, Locations 함께 조회 List<Emp> empList = q.getResultList(); // 함께 조회된 결과 사용이 흐름은 처음 조회할 때부터
Dept와Locations까지 함께 가져오도록 지정한다.
그래서 반복문에서 연관 필드를 사용할 때 추가 조회가 반복되는 상황을 줄일 수 있다.
앞 예제와의 차이
- 앞 예제:
Emp를 먼저 조회하고, 반복문에서Dept필드를 사용한다.- 앞 예제:
FetchType.LAZY상태에서는 연관 필드 사용 시점에 추가 조회가 발생할 수 있다.HelloJPA6:fetch join으로Dept,Locations를 처음부터 함께 조회한다.HelloJPA6: 반복문에서 연관 필드를 사용할 때 추가 조회가 반복되는 상황을 줄일 수 있다.
Fetch Join은 연관 필드를 나중에 반복해서 조회하지 않도록, 필요한 연관 엔티티를 처음부터 함께 조회하는 방식이다.
Fetch Join은 필요한 곳에만 사용해야 한다
Fetch Join은N+1문제를 줄이는 데 효과적이다.
하지만 모든 조회에 무조건 붙이면 좋은 것은 아니다.
연관 데이터를 함께 가져온다는 말은 한 번에 가져오는 데이터 양이 늘어날 수 있다는 뜻이다.
사원 이름만 필요한 화면인데 부서와 지역 정보까지 항상 함께 가져오면 오히려 불필요한 조회가 될 수 있다.
그래서fetch join은 연관 데이터가 실제로 필요한 조회에서 사용해야 한다.
예를 들어 사원 목록 화면에서 부서명과 지역명도 함께 보여 줘야 한다면fetch join을 사용할 수 있다.
하지만 사원 번호와 이름만 보여 주는 화면이라면 부서와 지역까지 가져올 필요가 없을 수 있다.
좋은 기준은 아래와 같다.
- 연관 데이터가 화면이나 로직에서 바로 필요하면
fetch join을 고려한다.- 연관 데이터가 필요하지 않으면 기본 조회를 가볍게 유지한다.
- 목록 조회 후 반복문에서 연관 데이터를 계속 사용한다면
N+1여부를 확인한다.- 모든 조회에 무조건
fetch join을 붙이지 않는다.
Fetch Join은 성능 문제를 줄이는 도구이지만, 필요한 연관 데이터가 명확할 때 사용하는 것이 좋다.
핵심 정리
HelloJPA6.java는Fetch Join으로Emp,Dept,Locations를 함께 조회하는 예제이다.
앞 예제에서 반복문 안의 연관 필드 접근으로 추가 조회가 발생할 수 있는 흐름을 봤다면, 이번 예제는 처음 조회 쿼리에서 필요한 연관 엔티티를 함께 가져오는 흐름을 확인한다.
정리하면 아래와 같다.
Fetch Join은 연관 엔티티를 함께 조회하는JPQL기능이다.select t from Emp t에서 조회 결과의 중심은Emp이다.left join fetch t.deptno는Emp를 조회하면서Dept도 함께 조회한다.left join fetch t.deptno.loc_code는Dept가 참조하는Locations까지 함께 조회한다.t는Emp,t.deptno는Dept,t.deptno.loc_code는Locations로 나누어 읽으면 된다.left join을 사용하면 연관 데이터가 없어도 주 엔티티가 결과에서 빠지지 않을 수 있다.- 일반
join은 조건이나 연결 목적이 강하다.fetch join은 연관 엔티티를 함께 조회하는 목적이 강하다.Fetch Join을 사용하면 반복문에서 연관 필드를 사용할 때 추가 조회가 반복되는 상황을 줄일 수 있다.- 모든 조회에 무조건
Fetch Join을 붙이면 안 된다.- 연관 데이터가 실제로 필요한 조회에서만
Fetch Join을 사용하는 것이 좋다.기본은 필요한 데이터만 가볍게 조회하고, 연관 데이터를 반복해서 사용할 조회에서는
Fetch Join으로 함께 가져오는 흐름을 고려해야 한다.
EmpDAO.java는Emp데이터를 조회하는 기능을 메서드 단위로 모아 둔 클래스이다.
앞 예제들에서는 실행 파일 안에서 직접JPQL을 작성하고 바로 실행했다.
이번 예제에서는 조회 기능을DAO메서드로 분리해서 관리한다.
DAO는Data Access Object의 줄임말이다.
데이터 접근 객체라는 뜻이며, 데이터베이스 조회나 저장 같은 작업을 한곳에 모아 두는 역할을 한다.
즉, 실행 파일이 직접 모든JPQL을 작성하지 않고,EmpDAO의 메서드를 호출해서 필요한 조회 기능을 사용할 수 있게 만든다.
이 예제의 핵심은EmpDAO의 모든 메서드를 외우는 것이 아니라, 조회 목적에 따라JPQL, 반환 타입, 메서드 역할이 어떻게 나뉘는지 구조로 이해하는 것이다.
이전 기본개념과 연결하기
앞에서
JPQL은 테이블이 아니라Entity를 기준으로 조회한다고 정리했다.
예를 들어select t from Emp t는 실제 테이블 이름을 직접 쓰는 것이 아니라,Emp엔티티를 조회 대상으로 사용하는 문장이다.
또 앞에서EntityManager는Entity를 저장, 조회, 수정, 삭제할 때 사용하는 핵심 객체라고 정리했다.
EmpDAO.java도 내부에서EntityManager를 사용한다.
다만 실행 파일처럼 한 번만 조회하고 끝나는 것이 아니라, 여러 조회 기능을 메서드로 나누어 제공한다.
이번 예제에서 중요한 점은JPQL문법이 여러 형태로 등장한다는 것이다.
전체 개수 조회에는count()를 사용하고, 기본키 조회에는find()를 사용한다.
조건 조회에는where,=,>=,and,like를 사용한다.
필요한 값만 담을 때는select new로DTO를 만들고, 일부 목록만 가져올 때는setFirstResult()와setMaxResults()를 사용한다.
즉,EmpDAO.java는 새로운 개념 하나만 보는 예제가 아니다.
앞에서 배운 조회 문법을 실제 조회 기능별 메서드로 정리한 예제이다.
기본개념에서 다시 잡아야 할 흐름
DAO는 데이터베이스 접근 코드를 모아 둔 객체이다.EmpDAO는Emp엔티티 조회 기능을 메서드로 나눈다.EntityManager는 실제 조회 작업을 실행한다.JPQL은 테이블이 아니라Entity와 필드를 기준으로 작성한다.- 메서드마다 조회 목적이 다르다.
- 조회 목적이 다르면
JPQL문법과 반환 타입도 달라진다.EmpDAO를 볼 때는 메서드 이름,JPQL, 반환 타입을 함께 봐야 한다.
EmpDAO는 긴 코드 파일로 보기보다, 조회 기능을 목적별로 나눈 지도처럼 읽어야 한다.
예제의 목표
이 예제의 목표는
EmpDAO.java안에 들어 있는 여러 조회 메서드의 역할을 구분하는 것이다.
각 메서드를 깊게 실행 결과까지 분석하는 것이 아니라, 다음 예제에서EmpApp1.java,EmpApp2.java가 어떤 메서드를 호출하는지 이해할 수 있도록 기반 구조를 잡는 것이 중요하다.
어떤 메서드는 전체 데이터 개수를 반환한다.
어떤 메서드는 기본키로 사원 이름을 조회한다.
어떤 메서드는 직무, 급여, 이름 일부 검색처럼 조건을 사용한다.
또 어떤 메서드는DTO, 페이징, 집계 함수처럼 조회 결과를 다른 형태로 받는다.
즉,EmpDAO.java는 단순히 코드가 긴 파일이 아니다.
JPQL조회 기능을 실제 프로그램에서 재사용할 수 있는 메서드로 나눈 파일이다.
이 예제에서 확인할 내용
EmpDAO가EntityManagerFactory와EntityManager를 어떻게 준비하는지 확인한다.- 전체 개수 조회 메서드가 어떤 역할인지 확인한다.
- 기본키 조회와 조건 조회의 차이를 확인한다.
where,like,and같은 조건 문법을 메서드 역할과 연결한다.select new로DTO를 만드는 흐름을 확인한다.setFirstResult()와setMaxResults()로 페이징을 처리하는 흐름을 확인한다.sum(),max(),min()같은 집계 결과를Object[]로 받는 흐름을 확인한다.- 다음 예제에서
EmpApp1.java,EmpApp2.java가 이 메서드들을 어떻게 호출하는지 이어서 볼 준비를 한다.13번은 실행 결과를 보는 파트가 아니라, 14번 실행 결과를 이해하기 위한
EmpDAO메서드 구조를 먼저 잡는 파트이다.
EmpDAO는 EntityManager를 내부에서 사용한다
EmpDAO.java는 내부에서EntityManagerFactory와EntityManager를 생성한다.
그리고 여러 조회 메서드가 같은em을 사용한다.
핵심 구조는 아래와 같다.// EmpDAO.java public class EmpDAO { EntityManagerFactory factory = Persistence.createEntityManagerFactory("emptest"); // emptest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 public void close() { // 자원 정리 메서드 em.close(); // EntityManager 닫기 factory.close(); // EntityManagerFactory 닫기 } }여기서는
"entitytest"가 아니라"emptest"설정 묶음을 사용한다.
즉, 앞의HelloJPA실행 예제와 다른 설정 묶음으로 실행될 수 있다.
close()는 사용이 끝난 뒤EntityManager와EntityManagerFactory를 닫는 메서드이다.
다음 예제의EmpApp1.java,EmpApp2.java에서는 마지막에dao.close()를 호출해 사용한 자원을 정리한다.
이 구조를 먼저 봐야 뒤의 조회 메서드들이 이해된다.
각 메서드는 새로EntityManager를 만드는 것이 아니라,EmpDAO안에 준비된em을 사용해서 조회를 실행한다.
EmpDAO는 조회 기능을 메서드로 나누고, 그 메서드들은 내부의EntityManager를 사용해서 실제JPQL을 실행한다.
EmpDAO 메서드는 조회 목적별로 먼저 나누어 읽는다
EmpDAO.java는 메서드가 많기 때문에 위에서 아래로 무작정 읽으면 헷갈릴 수 있다.
그래서 먼저 조회 목적별로 나누어 보는 것이 좋다.
목적별로 정리하면 아래와 같다.
- 전체 개수 조회:
getAllDataNum()- 기본키 조회:
getEmpName()- 정확한 조건 조회:
findByJob()- 숫자 비교 조건 조회:
findByGreaterThanSal()- 일부 문자열 검색:
findByPartEname()- 여러 조건 조회:
findByEnameAndJob1(),findByEnameAndJob2()DTO조회:findByEmpFreqDTO()- 페이징 조회:
listPart()- 집계 함수 조회:
getGroupFunc()각 메서드는 모두
Emp를 기준으로 조회하지만, 조회 목적과 반환 타입이 다르다.
그래서 메서드를 볼 때는 메서드 이름만 보지 말고JPQL,setParameter(), 반환 타입을 함께 확인해야 한다.
메서드를 읽는 기준
- 메서드 이름은 조회 목적을 알려 준다.
JPQL은 어떤 조건으로 조회하는지 알려 준다.- 반환 타입은 결과를 어떤 형태로 받을지 알려 준다.
getSingleResult()는 결과가 하나일 때 사용한다.getResultList()는 결과가 여러 개일 수 있을 때 사용한다.
EmpDAO에서 가장 먼저 봐야 할 것은 코드 줄 수가 아니라, 메서드별 조회 목적과 반환 타입이다.
전체 개수 조회와 기본키 조회
전체 개수 조회와 기본키 조회는 가장 기본적인 조회 흐름이다.
전체 개수는count()를 사용하고, 기본키로 한 건을 찾을 때는find()를 사용할 수 있다.
전체 데이터 개수 조회
getAllDataNum()은Emp데이터 개수를 구하는 메서드이다.
count()는 개수를 세는 집계 함수이다.
집계 함수는 여러 데이터에서 하나의 결과값을 계산하는 함수라고 이해하면 된다.// EmpDAO.java public Long getAllDataNum() { // 전체 데이터 개수 조회 TypedQuery<Long> q = em.createQuery( "select count(t.empno) from Emp t", Long.class ); // count 결과는 Long return q.getSingleResult(); // 단일 결과 반환 }
select count(t.empno) from Emp t는Emp엔티티의empno값을 기준으로 데이터 개수를 센다.
결과는 목록이 아니라 하나의 숫자이다.
그래서getResultList()가 아니라getSingleResult()를 사용한다.
count()결과는 정수처럼 보이지만,JPQL에서는 보통Long으로 받는다.
따라서 반환 타입도Long이다.
기본키로 사원 이름 조회
getEmpName()은 사원 번호를 받아 해당 사원의 이름을 반환한다.
여기서는JPQL을 직접 작성하지 않고find()를 사용한다.// EmpDAO.java public String getEmpName(Integer numOfEmp) { // 사원 번호로 이름 조회 Emp e = em.find(Emp.class, numOfEmp); // 기본키로 Emp 조회 if (e != null) { // 조회 결과가 있으면 return e.getEname(); // 사원 이름 반환 } else { return "없음"; // 조회 결과가 없으면 없음 반환 } }
find(Emp.class, numOfEmp)는Emp엔티티에서 기본키 값이numOfEmp인 데이터를 찾는다.
기본키는 한 데이터를 식별하는 값이다.
따라서 기본키로 조회하면 결과가 있더라도 보통 하나의 엔티티만 나온다.
조회 결과가 없으면e가null이 될 수 있다.
그래서e.getEname()을 바로 호출하지 않고, 먼저e != null인지 확인한다.
이 구간에서 봐야 할 기준
count()는 전체 개수를 구한다.- 전체 개수는 하나의 결과이므로
getSingleResult()를 사용한다.find()는 기본키로 엔티티 하나를 조회한다.find()결과가 없으면null이 될 수 있다.null여부를 확인한 뒤 필드를 사용해야 한다.개수 조회는
count(), 기본키 조회는find()처럼 조회 목적에 맞는 도구를 선택해야 한다.
조건 조회는 JPQL과 파라미터 바인딩을 사용한다
조건 조회는 특정 기준에 맞는 데이터만 찾는 조회이다.
EmpDAO에서는 직무가 같은 사원, 급여가 기준 이상인 사원, 이름과 직무가 모두 같은 사원 등을 조건으로 조회한다.
이때 중요한 문법이 파라미터 바인딩이다.
파라미터는 쿼리 안에서 나중에 값을 넣을 자리이다.
바인딩은 그 자리에 실제 값을 연결하는 것이다.
즉, 파라미터 바인딩은JPQL안의 빈자리에 메서드로 받은 값을 연결하는 과정이다.
직무와 급여 조건 조회
findByJob()은 직무가 특정 값과 같은 사원을 조회한다.
findByGreaterThanSal()은 급여가 기준값 이상인 사원을 조회한다.// EmpDAO.java public List<Emp> findByJob(String job) { // 직무 조건 조회 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t WHERE t.job = :job", Emp.class ); // job 필드 조건 q.setParameter("job", job); // job 파라미터에 값 연결 return q.getResultList(); // 결과 목록 반환 } public List<Emp> findByGreaterThanSal(int sal) { // 급여 조건 조회 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t WHERE t.sal >= :sal", Emp.class ); // sal 이상 조건 q.setParameter("sal", sal); // sal 파라미터에 기준값 연결 return q.getResultList(); // 결과 목록 반환 }
:job과:sal은 값이 들어갈 자리이다.
setParameter("job", job)은:job자리에 메서드로 받은job값을 연결한다.
setParameter("sal", sal)은:sal자리에 메서드로 받은sal값을 연결한다.
결과는 여러 명일 수 있으므로List<Emp>로 받는다.
그리고 여러 결과를 받을 수 있기 때문에getResultList()를 사용한다.
일부 문자열 검색
findByPartEname()은 사원 이름에 특정 문자열이 포함된 데이터를 찾는다.
이때like조건을 사용한다.// EmpDAO.java public List<Emp> findByPartEname(String partEname) { // 이름 일부 검색 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t WHERE t.ename like :pe", Emp.class ); // like 조건 q.setParameter("pe", "%" + partEname + "%"); // 앞뒤에 어떤 글자가 와도 됨 return q.getResultList(); // 결과 목록 반환 }
like는 정확히 같은 값이 아니라, 특정 글자가 포함된 값을 찾을 때 사용한다.
"%"+partEname+"%"는 앞뒤에 어떤 글자가 와도 된다는 뜻이다.
정확히 같은 값을 찾을 때는=를 사용한다.
일부 글자가 포함된 값을 찾을 때는like와%를 사용한다.
여러 조건 조회
findByEnameAndJob1()과findByEnameAndJob2()는 이름과 직무를 모두 만족하는 사원을 조회한다.
and는 두 조건을 모두 만족해야 한다는 뜻이다.// EmpDAO.java public List<Emp> findByEnameAndJob1(String ename, String job) { // 이름과 직무 조건 조회 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t WHERE t.ename = :ename and t.job = :job", Emp.class ); // 이름 기반 파라미터 사용 q.setParameter("ename", ename); // ename 파라미터 연결 q.setParameter("job", job); // job 파라미터 연결 return q.getResultList(); // 결과 목록 반환 } public Emp findByEnameAndJob2(String ename, String job) { // 이름과 직무로 단건 조회 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t WHERE t.ename = ?1 and t.job = ?2", Emp.class ); // 위치 기반 파라미터 사용 q.setParameter(1, ename); // 첫 번째 파라미터에 ename 연결 q.setParameter(2, job); // 두 번째 파라미터에 job 연결 return q.getSingleResult(); // 단일 결과 반환 }
:ename,:job처럼 이름으로 값을 연결하는 방식을 이름 기반 파라미터라고 한다.
?1,?2처럼 순서로 값을 연결하는 방식을 위치 기반 파라미터라고 한다.
두 방식 모두 가능하다.
다만 처음 배울 때는 이름 기반 파라미터가 읽기 쉽다.
위치 기반은 순서를 잘못 맞추면 다른 값이 잘못 연결될 수 있다.
조건 조회에서 봐야 할 기준
where는 조건을 지정한다.:job,:sal처럼 이름이 붙은 값 자리를 이름 기반 파라미터라고 한다.?1,?2처럼 순서로 값을 넣는 방식을 위치 기반 파라미터라고 한다.setParameter()는 파라미터 자리에 실제 값을 연결한다.- 정확한 값 비교는
=, 숫자 비교는>=, 일부 문자열 검색은like를 사용한다.- 조건이 여러 개이면
and로 연결할 수 있다.조건 조회에서는
JPQL의 조건식과setParameter()에 연결하는 값이 서로 정확히 맞아야 한다.
DTO 조회는 select new로 처리한다
findByEmpFreqDTO()는Emp엔티티 전체가 아니라EmpFreqDTO객체로 결과를 받는다.
이때select new문법을 사용한다.
DTO는Data Transfer Object의 줄임말이다.
데이터 전달 객체라는 뜻이다.
엔티티 전체가 아니라 화면이나 기능에 필요한 값만 따로 담아 전달할 때 사용한다.
핵심 코드는 아래와 같다.// EmpDAO.java public List<EmpFreqDTO> findByEmpFreqDTO() { // DTO 조회 TypedQuery<EmpFreqDTO> q = em.createQuery( "SELECT new EmpFreqDTO(t.empno, t.ename, t.hiredate, t.sal, t.deptno) FROM Emp t", EmpFreqDTO.class ); // DTO 생성자 호출 형태의 JPQL return q.getResultList(); // DTO 목록 반환 }
select new EmpFreqDTO(...)는 조회 결과를 바로EmpFreqDTO객체로 만들어 받겠다는 뜻이다.
이때EmpFreqDTO에는 해당 인자들을 받을 수 있는 생성자가 있어야 한다.
실제 코드에서EmpFreqDTO앞에 패키지 경로가 붙어 있다면,select new는 그 전체 클래스명을 기준으로 객체를 만든다.
즉, 실행 환경과 패키지 구조에 따라SELECT new 패키지명.EmpFreqDTO(...)형태로 작성될 수 있다.
EmpFreqDTO.java에는@AllArgsConstructor가 있다.
@AllArgsConstructor는 모든 필드를 인자로 받는 생성자를 자동으로 만들어 주는Lombok어노테이션이다.
그래서empno,ename,hiredate,sal,deptno를 받는 생성자가 만들어질 수 있다.
// EmpFreqDTO.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 public class EmpFreqDTO { private int empno; // 사원 번호 private String ename; // 사원 이름 private java.sql.Date hiredate; // 입사일 private int sal; // 급여 private Dept deptno; // 부서 }여기서 중요한 점은
select new의 인자 순서와DTO생성자의 인자 순서가 맞아야 한다는 것이다.
JPQL에서t.empno,t.ename,t.hiredate,t.sal,t.deptno순서로 값을 넘기면,EmpFreqDTO생성자도 그 순서대로 받을 수 있어야 한다.
DTO 조회에서 봐야 할 기준
DTO는 필요한 값만 담아 전달하는 객체이다.select new는 조회 결과로DTO객체를 바로 만든다.DTO에는 해당 값을 받을 생성자가 있어야 한다.- 패키지 구조에 따라
select new에 전체 클래스명이 필요할 수 있다.select new의 인자 순서와 생성자 인자 순서가 맞아야 한다.
select new는 엔티티 전체가 아니라 필요한 값을 담은DTO객체를 바로 만들 때 사용하는 조회 방식이다.
페이징과 집계 함수는 결과 형태가 다르다
listPart()는 사원 목록을 급여 내림차순으로 정렬한 뒤 일부만 가져온다.
getGroupFunc()는 급여 총액, 최대 급여, 최소 급여를 한 번에 조회한다.
두 메서드는 모두 조회 기능이지만 결과 형태가 다르다.
페이징 조회
페이징은 전체 데이터 중 일부 구간만 가져오는 방식이다.
setFirstResult()와setMaxResults()를 사용한다.// EmpDAO.java public List<Emp> listPart(int start, int num) { // 일부 목록 조회 TypedQuery<Emp> q = em.createQuery( "SELECT t FROM Emp t ORDER BY t.sal DESC", Emp.class ); // 급여 내림차순 정렬 q.setFirstResult(start); // 조회 시작 위치 q.setMaxResults(num); // 조회할 데이터 수 return q.getResultList(); // 결과 목록 반환 }
ORDER BY t.sal DESC는 급여를 기준으로 내림차순 정렬한다는 뜻이다.
DESC는 큰 값에서 작은 값으로 정렬하는 방식이다.
setFirstResult(start)는 몇 번째 결과부터 가져올지 정한다.
start가0이면 첫 번째 결과부터 가져온다.
start가3이면 앞의3개를 건너뛰고 그다음 결과부터 가져온다.
setMaxResults(num)은 몇 개를 가져올지 정한다.
예를 들어num이3이면 최대3개만 가져온다.
집계 함수 조회
getGroupFunc()는 급여 총액, 최대 급여, 최소 급여를 한 번에 조회한다.
여러 집계 결과를 함께 조회하므로 결과를Object[]로 받는다.// EmpDAO.java public Object[] getGroupFunc() { // 집계 함수 조회 Query query = em.createQuery( "SELECT sum(t.sal), max(t.sal), min(t.sal) FROM Emp t" ); // 급여 합계, 최대, 최소 조회 Object[] result = (Object[]) query.getSingleResult(); // 단일 행 결과를 Object 배열로 받음 return result; // 결과 반환 }
sum(t.sal)은 급여 합계이다.
max(t.sal)은 가장 큰 급여이다.
min(t.sal)은 가장 작은 급여이다.
조회 결과는 한 행으로 나오지만, 그 안에 값이 여러 개 있다.
그래서Object[]로 받은 뒤result[0],result[1],result[2]처럼 꺼내 사용할 수 있다.
이때result[0]은sum(t.sal)결과이고,result[1]은max(t.sal)결과이며,result[2]는min(t.sal)결과이다.
배열은 순서로 값을 꺼내기 때문에,select에 적은 순서와 배열 인덱스를 함께 봐야 한다.
페이징과 집계 함수에서 봐야 할 기준
- 페이징은 정렬 기준, 시작 위치, 조회 개수를 함께 봐야 한다.
setFirstResult()는 조회 시작 위치를 정한다.setMaxResults()는 가져올 데이터 수를 정한다.- 집계 함수는 여러 데이터를 계산해 하나의 결과를 만든다.
- 여러 집계 값을 한 행으로 조회하면
Object[]로 받을 수 있다.Object[]는select에 적은 순서대로 값이 들어간다.페이징은 목록 일부를 가져오는 조회이고, 집계 함수는 여러 데이터를 계산한 결과를 가져오는 조회이다.
EmpDAO와 다음 EmpApp 예제는 함께 봐야 한다
EmpDAO.java만 보면 메서드가 많아 복잡해 보일 수 있다.
하지만 다음 예제의EmpApp1.java,EmpApp2.java에서는 이 메서드들을 순서대로 호출해서 결과를 확인한다.
그래서 13번에서는 메서드의 역할을 먼저 잡고, 14번에서는 실제 실행 결과를 구간별로 읽으면 된다.
예를 들어getAllDataNum()이 어떤JPQL을 실행하는지 13번에서 확인했다면, 14번에서는 그 메서드를 호출했을 때 콘솔에 어떤 결과가 나오는지 확인하면 된다.
이렇게 나누어 보면EmpDAO와EmpApp의 역할도 분명해진다.
EmpDAO는 조회 기능을 제공하는 클래스이고,EmpApp은 그 조회 기능을 호출해서 결과를 확인하는 실행 클래스이다.
EmpDAO와 EmpApp의 역할 차이
EmpDAO: 조회 기능을 메서드로 제공한다.EmpDAO: 내부에서EntityManager와JPQL을 사용한다.EmpApp:EmpDAO메서드를 호출한다.EmpApp: 호출 결과를 콘솔에 출력하며 확인한다.13번에서는
EmpDAO메서드의 역할을 먼저 잡고, 14번에서는EmpApp실행 결과로 그 역할이 어떻게 드러나는지 확인하면 된다.
핵심 정리
EmpDAO는JPQL조회 기능을 한곳에 모아 둔 예제이다.
이 파일을 보면 기본 개념에서 배운 조회 문법이 실제 메서드로 어떻게 분리되는지 확인할 수 있다.
정리하면 아래와 같다.
EmpDAO는 데이터베이스 조회 기능을 모아 둔DAO클래스이다.EntityManager를 내부에서 사용해 실제 조회를 실행한다.- 기본키 조회는
find()를 사용한다.- 전체 개수는
count()로 조회한다.- 정확한 조건은
=를 사용한다.- 일부 문자열 검색은
like를 사용한다.- 숫자 비교는
>=같은 비교 연산자를 사용한다.- 여러 조건은
and로 연결한다.- 파라미터 바인딩은
JPQL의 빈자리에 실제 값을 연결하는 과정이다.- 이름 기반 파라미터는 의미가 잘 보이고, 위치 기반 파라미터는 순서가 중요하다.
- 필요한 값만 담으려면
select new로DTO조회를 할 수 있다.select new는 패키지 구조에 따라 전체 클래스명이 필요할 수 있다.- 페이징은
setFirstResult()와setMaxResults()를 사용한다.- 집계 결과 여러 개는
Object[]로 받을 수 있다.- 다음 예제에서는
EmpApp1.java,EmpApp2.java가 이 메서드들을 호출해 실제 결과를 보여 준다.
EmpDAO는JPQL문법을 실제 데이터 조회 기능으로 나누어 구현한 대표 응용예제이며, 다음EmpApp실행 예제를 이해하기 위한 기반 코드이다.
EmpApp1.java와EmpApp2.java는EmpDAO의 메서드를 실제로 호출해서 결과를 출력하는 실행 예제이다.
앞의EmpDAO.java가 조회 기능을 메서드로 나눈 클래스라면, 이번 예제의EmpApp1.java와EmpApp2.java는 그 메서드를 호출해서 결과를 확인하는 클래스이다.
두 파일은 거의 같은 조회 흐름을 가진다.
차이는 결과를 출력하는 방식이다.
EmpApp1.java는 주로for반복문을 사용하고,EmpApp2.java는stream()을 사용한다.
결과가 매우 길게 나오기 때문에 모든 출력 줄을 이미지로 하나씩 나누어 볼 필요는 없다.
이 예제에서는 실행 흐름을GIF로 확인하고, 본문에서는 각 출력 구간이 어떤EmpDAO메서드 호출 결과인지 연결해서 본다.
이 예제의 핵심은EmpDAO메서드를 호출했을 때 어떤 결과 구간이 만들어지는지 확인하고,for반복문 출력과stream()출력 방식의 차이를 이해하는 것이다.
이전 기본개념과 연결하기
앞 예제에서
EmpDAO는Emp데이터를 조회하는 기능을 메서드 단위로 나누어 둔 클래스라고 정리했다.
getAllDataNum()은 전체 데이터 개수를 조회하고,getEmpName()은 기본키로 사원 이름을 조회한다.
findByJob(),findByPartEname(),findByGreaterThanSal()은 조건에 맞는 사원 목록을 조회한다.
이번 예제에서는 그 메서드들이 실제로 호출된다.
즉,EmpApp1.java와EmpApp2.java는JPQL을 직접 작성하는 파일이 아니라,EmpDAO가 제공하는 메서드를 호출하는 실행 파일이다.
이 흐름을 구분해야 한다.
EmpDAO는 데이터베이스 조회 기능을 제공하는 쪽이고,EmpApp은 그 기능을 호출해서 결과를 출력하는 쪽이다.
기본개념에서 다시 잡아야 할 흐름
EmpDAO는 조회 기능을 메서드로 제공한다.EmpApp1.java와EmpApp2.java는EmpDAO메서드를 호출한다.EmpDAO내부에서는EntityManager와JPQL이 사용된다.EmpApp에서는 조회 결과를 콘솔에 출력한다.EmpApp1.java는for반복문 중심으로 출력한다.EmpApp2.java는stream()중심으로 출력한다.- 결과가 길게 나오므로 구분선과 출력 메시지를 기준으로 조회 구간을 나누어 봐야 한다.
EmpApp을 볼 때는 직접JPQL을 찾기보다, 어떤EmpDAO메서드를 호출하고 그 결과를 어떻게 출력하는지 봐야 한다.
예제의 목표
이 예제의 목표는
EmpDAO의 여러 조회 메서드가 실제 실행 결과로 어떻게 나타나는지 확인하는 것이다.
13번에서는 메서드의 역할과JPQL구조를 먼저 잡았다.
이번에는 그 메서드들을 호출했을 때 콘솔에 어떤 결과 구간이 만들어지는지 본다.
EmpApp1.java와EmpApp2.java는 같은 조회 기능을 다르게 출력하는 예제라고 볼 수 있다.
EmpApp1.java는 전통적인for반복문으로 목록을 하나씩 출력한다.
EmpApp2.java는stream().forEach()와System.out::println같은 방식으로 목록을 출력한다.
즉, 이 예제는 새로운 조회 문법을 배우는 것이 아니다.
DAO메서드를 실행 파일에서 어떻게 호출하고, 긴 콘솔 결과를 어떤 기준으로 읽어야 하는지 확인하는 예제이다.
이 예제에서 확인할 내용
EmpDAO객체를 생성한다.EmpDAO의 조회 메서드를 순서대로 호출한다.getAllDataNum()으로 전체 데이터 개수를 확인한다.getEmpName()으로 특정 사번의 이름을 확인한다.- 조건 조회 메서드들이 어떤 목록을 출력하는지 확인한다.
DTO조회 결과가 어떻게 출력되는지 확인한다.- 페이징 조회 결과가 정렬과 개수 기준에 맞게 나오는지 확인한다.
- 집계 함수 결과가 어떤 순서로 출력되는지 확인한다.
EmpApp1.java와EmpApp2.java의 출력 방식 차이를 확인한다.이번 예제는 결과를 한 줄씩 외우는 것이 아니라, 출력 구간이 어떤
DAO메서드 호출 결과인지 연결해서 보는 것이 중요하다.
EmpApp1은 for 반복문으로 결과를 출력한다
EmpApp1.java는EmpDAO객체를 만들고, 여러 조회 메서드를 순서대로 호출한다.
조회 결과가 목록이면for반복문으로 하나씩 꺼내 출력한다.
전체 코드를 모두 외울 필요는 없다.
아래처럼 어떤DAO메서드를 어떤 순서로 호출하는지 먼저 보면 된다.// EmpApp1.java EmpDAO dao = new EmpDAO(); // DAO 객체 생성 dao.getAllDataNum(); // 전체 데이터 개수 조회 dao.getEmpName(7499); // 사번으로 이름 조회 dao.findByJob("SALESMAN"); // 직무 조건 조회 dao.findByPartEname("T"); // 이름 일부 검색 dao.findByGreaterThanSal(2000); // 급여 조건 조회 dao.findByEnameAndJob1("MARTIN", "SALESMAN"); // 이름과 직무 조건 조회 dao.findByEnameAndJob2("MARTIN", "SALESMAN"); // 이름과 직무 조건 단건 조회 dao.findByEmpFreqDTO(); // DTO 조회 dao.listPart(0, 3); // 페이징 조회 dao.getGroupFunc(); // 집계 함수 조회 dao.close(); // DAO 자원 정리실제 코드에서는 각 메서드 호출 결과를 출력 메시지와 함께 보여 준다.
목록 결과는for반복문으로 하나씩 꺼내 출력한다.
예를 들어findByJob("SALESMAN")의 결과가 여러 명이면,for반복문이 각 사원을 하나씩 꺼내System.out.println(elem)으로 출력한다.
이때elem은 현재 반복에서 꺼낸Emp객체 하나를 의미한다.
// EmpApp1.java List<Emp> r1 = dao.findByJob("SALESMAN"); // 직무 조건 조회 for (Emp elem : r1) { // 목록에서 Emp 객체를 하나씩 꺼냄 System.out.println(elem); // 현재 Emp 객체 출력 }
for반복문은 목록에 들어 있는 데이터를 하나씩 꺼내 처리하는 기본 반복 방식이다.
그래서 초보자가 실행 순서를 따라가기 쉽다.
EmpApp1에서 봐야 할 핵심
EmpDAO dao = new EmpDAO()로 조회 기능을 사용할 준비를 한다.dao.getAllDataNum()처럼DAO메서드를 호출한다.- 목록 결과는
List<Emp>로 받는다.for반복문으로 목록의 요소를 하나씩 출력한다.- 실행이 끝나면
dao.close()로 자원을 정리한다.
EmpApp1.java는EmpDAO메서드 호출 결과를for반복문으로 하나씩 출력하는 실행 예제이다.
EmpApp1 결과는 메서드 호출 순서대로 읽는다
EmpApp1.java는 여러DAO메서드를 한 번에 실행한다.
그래서 결과가 길게 나온다.
이때 전체 결과를 한 덩어리로 보면 헷갈리기 쉽다.
결과는 구분선이나 출력 메시지를 기준으로 나누어 읽어야 한다.
중요한 것은 모든 출력 줄을 외우는 것이 아니라, 각 구간이 어떤 메서드 호출 결과인지 연결하는 것이다.
EmpApp1.java실행 결과이다.
출력 결과가 길기 때문에 한 줄씩 모두 외우지 않는다.
구분선과 출력 메시지를 기준으로 나누어 보고, 각 구간이 어떤EmpDAO메서드 호출 결과인지 연결해서 확인한다.
EmpApp1.java결과는 아래 흐름으로 읽으면 된다.
getAllDataNum()구간에서는 전체 데이터 개수가 출력된다.getEmpName(7499)구간에서는 사번7499직원의 이름이 출력된다.findByJob("SALESMAN")구간에서는 직무가SALESMAN인 사원 목록이 출력된다.findByPartEname("T")구간에서는 이름에T가 포함된 사원 목록이 출력된다.findByGreaterThanSal(2000)구간에서는 급여가2000이상인 사원 목록이 출력된다.findByEnameAndJob1("MARTIN", "SALESMAN")구간에서는 이름과 직무 조건을 모두 만족하는 결과가 출력된다.findByEnameAndJob2("MARTIN", "SALESMAN")구간에서는 위치 기반 파라미터 조건 조회 결과가 출력된다.findByEmpFreqDTO()구간에서는DTO형태의 결과가 출력된다.listPart(0, 3)구간에서는 정렬된 결과 중 일부 목록이 출력된다.getGroupFunc()구간에서는 급여 합계, 최대 급여, 최소 급여가 출력된다.예를 들어
findByPartEname("T")구간에 이름에T가 포함된 사원들이 출력된다면, 그 결과는like조건 조회의 결과로 이해해야 한다.
getGroupFunc()구간은 목록 출력이 아니라 집계 결과 출력이다.
따라서sum,max,min결과가 어떤 순서로 출력되는지 함께 봐야 한다.
EmpApp1 결과를 확인할 때 봐야 할 기준
- 전체 개수 조회 결과가 먼저 출력되는지 확인한다.
- 특정 사번의 이름이 출력되는지 확인한다.
- 직무 조건 조회 결과가 목록으로 출력되는지 확인한다.
- 이름 일부 검색 결과가
like조건에 맞게 출력되는지 확인한다.- 급여 조건 조회 결과가 기준 급여 이상으로 출력되는지 확인한다.
DTO조회 결과가EmpFreqDTO형태로 출력되는지 확인한다.- 페이징 조회 결과가 정렬과 개수 기준에 맞게 출력되는지 확인한다.
- 집계 함수 결과가 합계, 최댓값, 최솟값 순서로 출력되는지 확인한다.
EmpApp1.java결과는 출력 줄 수보다 각 구간이 어떤DAO메서드의 결과인지 연결해서 보는 것이 중요하다.
EmpApp2는 stream으로 결과를 출력한다
EmpApp2.java도EmpDAO메서드를 호출해서 조회 결과를 출력한다.
다만EmpApp1.java와 달리 목록 출력에stream()을 사용한다.
EmpApp2.java는 새로운 조회 기능을 추가한 파일이 아니다.
같은EmpDAO메서드 호출 결과를for반복문 대신stream()방식으로 출력하는 예제이다.
핵심 흐름은 아래와 같다.// EmpApp2.java EmpDAO dao = new EmpDAO(); // DAO 객체 생성 List<Emp> r1 = dao.findByJob("SALESMAN"); // 직무 조건 조회 r1.stream().forEach(elem -> System.out.println(elem)); // stream과 lambda로 결과 출력 List<Emp> r2 = dao.findByPartEname("T"); // 이름 일부 검색 r2.stream().forEach(System.out::println); // 메서드 참조로 결과 출력 dao.close(); // DAO 자원 정리
stream()은 컬렉션 데이터를 순서대로 처리할 수 있게 해 주는 흐름이다.
여기서는List<Emp>에 들어 있는 사원들을 하나씩 출력하는 데 사용한다.
forEach()는 각 요소를 하나씩 처리하는 메서드이다.
elem -> System.out.println(elem)은lambda표현식이다.
lambda는 짧은 함수처럼 동작하는 표현식이라고 이해하면 된다.
여기서는 각elem을 받아 출력한다.
System.out::println은 메서드 참조이다.
메서드 참조는 이미 존재하는 메서드를 간단히 가리키는 표현이다.
여기서는 각 요소를System.out.println()으로 출력하겠다는 뜻이다.
stream 출력에서 봐야 할 핵심
stream()은 목록 데이터를 흐름처럼 처리한다.forEach()는 각 요소를 하나씩 처리한다.elem -> System.out.println(elem)은 각 요소를 출력하는lambda표현식이다.System.out::println은 각 요소를 출력하는 메서드 참조이다.- 출력 결과 자체는
for반복문으로 출력한 것과 비슷할 수 있다.
EmpApp2.java는 조회 기능이 달라진 예제가 아니라, 같은 조회 결과를stream()방식으로 출력하는 예제이다.
EmpApp2 결과도 DAO 메서드 호출 순서대로 읽는다
EmpApp2.java의 결과도EmpApp1.java처럼 구간별로 읽어야 한다.
stream()을 사용한다고 해서 조회 결과의 의미가 달라지는 것은 아니다.
예를 들어findByJob("SALESMAN")을 호출했다면 직무가SALESMAN인 사원들이 출력된다.
findByPartEname("T")를 호출했다면 이름에T가 포함된 사원들이 출력된다.
출력 방식이stream()일 뿐, 조회 조건은EmpDAO메서드에 의해 결정된다.
EmpApp2.java실행 결과이다.
조회 조건은EmpDAO메서드에 의해 결정되고, 출력 방식만stream()과forEach()로 바뀐다.
따라서 결과 해석 기준은EmpApp1.java와 같다.
구분선과 출력 메시지를 기준으로 어떤DAO메서드 호출 결과인지 확인하면 된다.
EmpApp2.java결과에서 중요한 것은 출력 문법이다.
stream().forEach()는 목록의 각 요소를 하나씩 처리한다.
System.out::println은 각 요소를 출력하는 메서드 참조이다.
따라서 결과가 출력되는 의미는for반복문과 크게 다르지 않고, 표현 방식이 달라진 것이다.
EmpApp2 결과를 확인할 때 봐야 할 기준
EmpApp1.java와 같은 조회 메서드가 호출되는지 확인한다.- 결과 목록이
stream().forEach()로 출력되는지 확인한다.System.out::println이 각 요소를 출력하는 역할을 하는지 확인한다.- 출력 결과의 의미는
EmpDAO메서드 기준으로 해석한다.
stream()을 사용해도 결과 해석 기준은 동일하며, 어떤EmpDAO메서드를 호출했는지가 가장 중요하다.
EmpApp1과 EmpApp2는 출력 방식이 다르다
EmpApp1.java와EmpApp2.java는 완전히 다른 조회 예제가 아니다.
두 파일 모두EmpDAO메서드를 호출한다.
차이는 결과 목록을 어떻게 출력하느냐이다.
EmpApp1.java는for반복문을 사용한다.
for반복문은 초보자가 읽기 쉽다.
목록에서 하나씩 꺼내서 출력한다는 흐름이 눈에 잘 보인다.
EmpApp2.java는stream()을 사용한다.
stream()은 코드가 짧고, 목록 처리 흐름을 함수형 스타일로 표현할 수 있다.
하지만 처음 보면lambda나 메서드 참조 문법이 낯설 수 있다.
둘을 비교하면 아래와 같다.// ForLoopOutput.java for (Emp elem : r1) { // 목록에서 하나씩 꺼냄 System.out.println(elem); // 하나씩 출력 }// StreamOutput.java r1.stream().forEach(elem -> System.out.println(elem)); // stream으로 하나씩 출력두 코드는 출력 목적이 같다.
둘 다r1목록의 사원 객체를 하나씩 출력한다.
차이는 반복을 표현하는 문법이다.
for 반복문과 stream 비교
for반복문: 하나씩 꺼내는 흐름이 직접 보인다.for반복문: 초보자가 실행 순서를 따라가기 쉽다.stream(): 목록 처리 코드를 짧게 표현할 수 있다.stream():lambda, 메서드 참조 문법을 함께 이해해야 한다.- 두 방식 모두
DAO메서드의 조회 결과를 출력하는 역할을 한다.
EmpApp1과EmpApp2의 차이는 조회 조건이 아니라, 조회 결과를 출력하는 방식이다.
긴 출력 결과는 메서드 호출 순서와 연결해서 읽는다
EmpApp1.java와EmpApp2.java는 여러 조회 메서드를 순서대로 실행한다.
그래서 콘솔 결과가 길게 나온다.
결과가 길 때는 전체를 외우려고 하면 안 된다.
구분선, 출력 메시지, 메서드 호출 순서를 기준으로 나누어 읽어야 한다.
이렇게 보면 긴 결과도 어렵지 않다.
결과를 읽는 흐름은 아래와 같다.
- 먼저 어떤
DAO메서드가 호출되었는지 본다.- 그 메서드가 어떤 조회 조건을 가지는지 떠올린다.
- 콘솔 출력이 그 조건과 맞는지 확인한다.
- 목록 결과인지, 단일 결과인지, 집계 결과인지 구분한다.
- 결과가 길면 구분선 기준으로 구간을 나누어 읽는다.
예를 들어
findByGreaterThanSal(2000)이 실행된 구간이라면, 급여가2000이상인 사원들이 출력되어야 한다.
listPart(0, 3)이 실행된 구간이라면, 정렬된 결과 중 처음3개가 출력되는지 확인하면 된다.
getGroupFunc()가 실행된 구간이라면, 급여 합계, 최대 급여, 최소 급여가 어떤 순서로 출력되는지 확인하면 된다.
긴 실행 결과는 출력 내용을 통째로 외우지 말고,DAO메서드 호출 순서와 결과 구간을 연결해서 읽어야 한다.
핵심 정리
EmpApp1.java와EmpApp2.java는EmpDAO메서드를 실제로 호출해서 결과를 확인하는 실행 예제이다.
13번에서EmpDAO메서드의 역할을 정리했다면, 이번 예제에서는 그 메서드들이 실제 콘솔 결과로 어떻게 나타나는지 확인한다.
정리하면 아래와 같다.
EmpApp1.java와EmpApp2.java는EmpDAO메서드를 호출하는 실행 클래스이다.EmpDAO는 조회 기능을 제공하고,EmpApp은 그 기능을 호출해 결과를 출력한다.EmpApp1.java는 주로for반복문으로 목록을 출력한다.EmpApp2.java는stream()과forEach()로 목록을 출력한다.lambda표현식은 각 요소를 처리하는 짧은 함수처럼 볼 수 있다.System.out::println은 각 요소를 출력하는 메서드 참조이다.- 두 파일은 조회 조건이 완전히 다른 것이 아니라 출력 방식이 다르다.
- 결과가 길면 구분선과 출력 메시지를 기준으로 나누어 읽어야 한다.
- 각 출력 구간이 어떤
EmpDAO메서드 호출 결과인지 연결해서 봐야 한다.
EmpApp1과EmpApp2를 이해하면DAO메서드가 실제 실행 클래스에서 어떻게 호출되고, 그 결과가 콘솔에 어떻게 출력되는지 연결해서 볼 수 있다.
Team.java,Locker.java,Member.java는 회원과 연결될 팀, 락커, 회원 정보를 표현하는Entity이다.
앞 예제까지는Emp,Dept,Locations를 중심으로 사원, 부서, 지역의 연관관계를 확인했다.
이번 예제부터는Member,Team,Locker를 중심으로 새로운 연관관계 구조를 확인한다.
이 예제는 실행 결과를 보는 예제가 아니다.
뒤에서 나오는MemberTeamTest계열 예제를 이해하기 전에, 먼저 세Entity가 어떤 테이블과 연결되고 서로 어떤 관계를 가지는지 확인하는 단계이다.
이 예제의 핵심은Member가Team과Locker를 객체 참조로 가지고 있고, 데이터베이스에서는TEAM_ID,LOCKER_ID외래키로 연결된다는 점이다.
이전 기본개념과 연결하기
앞에서
JPA의 연관관계는 객체 참조와 외래키를 연결하는 방식이라고 정리했다.
객체 코드에서는 다른Entity객체를 필드로 가진다.
데이터베이스에서는 그 관계를 외래키 컬럼으로 저장한다.
예를 들어 회원이 팀에 속한다면 객체 코드에서는Member안에Team필드가 있을 수 있다.
데이터베이스에서는membertbl테이블 안에TEAM_ID외래키 컬럼이 생길 수 있다.
즉, 객체는Team객체를 참조하고, 테이블은TEAM_ID값으로 관계를 저장한다.
이번 예제에서는 관계가 두 개 나온다.
하나는Member와Team의 관계이고, 다른 하나는Member와Locker의 관계이다.
Member는 팀에 속할 수 있고, 락커 하나를 사용할 수 있다.
기본개념에서 다시 잡아야 할 흐름
Entity는 데이터베이스 테이블과 연결되는Java클래스이다.Member는 회원 정보를 표현한다.Team은 팀 정보를 표현한다.Locker는 락커 정보를 표현한다.Member는Team을 참조한다.Member는Locker를 참조한다.- 객체에서는
team,locker필드로 관계를 표현한다.- 데이터베이스에서는
TEAM_ID,LOCKER_ID외래키 컬럼으로 관계를 표현한다.연관관계 실행 예제를 보기 전에는 먼저 어떤
Entity가 어떤 객체를 참조하는지 확인해야 한다.
예제의 목표
이 예제의 목표는
Team,Locker,Member의 역할과 관계를 구분하는 것이다.
Team과Locker는 회원이 참조할 대상이다.
Member는 회원 정보를 가지면서Team과Locker를 함께 참조한다.
뒤에서MemberTeamTest1.java는Team,Locker,Member데이터를 저장한다.
그때Member객체를 만들면서 팀 객체와 락커 객체를 함께 전달한다.
이 흐름을 이해하려면 먼저Member생성자가 왜Team,Locker객체를 받는지 알아야 한다.
즉, 이번 예제는 저장 실행보다 먼저 보는 관계 구조 확인 단계이다.
코드에서@ManyToOne,@OneToOne,@JoinColumn이 어떤 역할을 하는지 확인하면 뒤의 저장, 조회 예제가 훨씬 쉽게 이어진다.
이 예제에서 확인할 내용
Team은 팀 정보를 담는Entity이다.Locker는 락커 정보를 담는Entity이다.Member는 회원 정보를 담는Entity이다.Member는Team과 다대일 관계를 가진다.Member는Locker와 일대일 관계를 가진다.@ManyToOne은 여러 회원이 하나의 팀을 참조할 수 있음을 나타낸다.@OneToOne은 회원 하나가 락커 하나와 연결될 수 있음을 나타낸다.@JoinColumn은 관계를 저장할 외래키 컬럼을 지정한다.15번은 실행 결과를 확인하는 파트가 아니라, 뒤의
MemberTeamTest실행 예제를 이해하기 위한Entity관계 구조를 잡는 파트이다.
Team은 팀 정보를 담는 Entity이다
Team.java는 팀 정보를 표현하는Entity이다.
팀 기본키와 팀 이름을 가진다.
핵심 구조는 아래와 같다.// Team.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 @Entity // JPA 관리 대상 Entity public class Team { @Id // 팀 기본키 @Column(name = "TEAM_ID") // TEAM_ID 컬럼과 매핑 private String id; private String name; // 팀 이름 }
@Entity는 이 클래스가JPA의 관리 대상이라는 뜻이다.
즉,Team객체는 데이터베이스 테이블과 연결될 수 있는Entity이다.
@Id가 붙은id는 팀을 구분하는 기본키이다.
기본키는 테이블에서 각 행을 구분하는 값이다.
@Column(name = "TEAM_ID")가 있으므로id필드는 데이터베이스의TEAM_ID컬럼과 연결된다.
Team의 기본키 타입은String이다.
그래서 뒤에서new Team("1팀", "아기공룡둘리")처럼 문자열을 기본키 값으로 직접 넣을 수 있다.
Team에서 봐야 할 기준
Team은 팀 정보를 표현하는Entity이다.id는 팀의 기본키이다.id는TEAM_ID컬럼과 연결된다.name은 팀 이름이다.Team은 뒤에서Member가 참조할 대상이다.
Team은 회원이 소속될 팀 정보를 담는 기준Entity이다.
Locker는 락커 정보를 담는 Entity이다
Locker.java는 락커 정보를 표현하는Entity이다.
락커 기본키와 락커 이름을 가진다.
핵심 구조는 아래와 같다.// Locker.java @Entity // JPA 관리 대상 Entity @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 public class Locker { @Id // 락커 기본키 @Column(name = "LOCKER_ID") // LOCKER_ID 컬럼과 매핑 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private Long id; private String name; // 락커 이름 public Locker(String name) { // 락커 이름을 받는 생성자 this.name = name; // 이름 저장 } }
Locker도@Entity가 붙어 있으므로JPA관리 대상이다.
id는 락커의 기본키이고,LOCKER_ID컬럼과 연결된다.
Locker의 기본키는Long타입이고,@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있다.
IDENTITY전략은 기본키 생성을 데이터베이스에 맡기는 방식이다.
즉, 락커를 저장하면 데이터베이스가 기본키 값을 자동으로 만들어 준다.
그래서new Locker("A101")처럼 생성할 때는 락커 이름만 전달한다.
기본키 값은 직접 넣지 않는다.
저장 과정에서 데이터베이스가LOCKER_ID값을 생성한다.
Locker에서 봐야 할 기준
Locker는 락커 정보를 표현하는Entity이다.id는 락커의 기본키이다.id는LOCKER_ID컬럼과 연결된다.IDENTITY전략으로 기본키가 자동 생성된다.name은 락커 이름이다.Locker는 뒤에서Member가 참조할 대상이다.
Locker는IDENTITY전략을 사용하므로 저장 시 데이터베이스가 기본키를 자동 생성한다.
Member는 Team과 Locker를 함께 참조한다
Member.java는 회원 정보를 표현하는Entity이다.
회원 이름을 가지고 있고, 팀과 락커를 객체 참조로 가진다.
핵심 구조는 아래와 같다.// Member.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 @Entity // JPA 관리 대상 Entity @Table(name = "membertbl") // membertbl 테이블과 매핑 public class Member { @Id // 회원 기본키 @Column(name = "MEMBER_ID") // MEMBER_ID 컬럼과 매핑 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; private String username; // 회원 이름 @ManyToOne // 여러 회원이 하나의 팀과 연결 @JoinColumn(name = "TEAM_ID") // TEAM_ID 외래키 컬럼과 연결 private Team team; @OneToOne // 회원 하나가 락커 하나와 연결 @JoinColumn(name = "LOCKER_ID") // LOCKER_ID 외래키 컬럼과 연결 private Locker locker; public Member(String username, Team team, Locker locker) { // 회원 생성 시 관계 객체도 함께 받음 this.username = username; // 회원 이름 저장 this.team = team; // 팀 참조 저장 this.locker = locker; // 락커 참조 저장 } }
@Table(name = "membertbl")은Member엔티티가membertbl테이블과 연결된다는 뜻이다.
즉, 클래스 이름은Member이지만 실제 테이블명은membertbl로 지정되어 있다.
id는 회원 기본키이고,MEMBER_ID컬럼과 연결된다.
@GeneratedValue(strategy = GenerationType.IDENTITY)가 있으므로 회원 기본키도 데이터베이스가 자동 생성한다.
username은 회원 이름이다.
이 필드는 일반 값이다.
반면team과locker는 단순 값이 아니라 다른Entity객체를 참조하는 필드이다.
Member에서 가장 중요한 부분은 일반 필드인username과 연관 필드인team,locker를 구분하는 것이다.
Member와 Team은 다대일 관계이다
Member안의team필드는 아래처럼 작성되어 있다.// Member.java @ManyToOne // 여러 회원이 하나의 팀과 연결 @JoinColumn(name = "TEAM_ID") // TEAM_ID 외래키 컬럼과 연결 private Team team;
@ManyToOne은 다대일 관계를 나타낸다.
다대일은 여러 개의 현재 엔티티가 하나의 대상 엔티티와 연결될 수 있다는 뜻이다.
여기서는 여러 명의Member가 하나의Team에 속할 수 있다.
예를 들어 여러 회원이 같은1팀에 속할 수 있다.
그래서Member → Team관계는 다대일이다.
객체 코드에서는team필드가Team객체를 참조한다.
데이터베이스에서는 이 관계가TEAM_ID외래키 컬럼으로 저장된다.
@JoinColumn(name = "TEAM_ID")는 바로 그 외래키 컬럼명을 지정한다.
Member와 Team 관계에서 봐야 할 기준
Member는Team을 객체로 참조한다.- 여러
Member가 하나의Team을 참조할 수 있다.- 그래서 관계는
@ManyToOne이다.- 데이터베이스에는
TEAM_ID외래키 컬럼으로 저장된다.
@ManyToOne은 객체 참조를 만들고,@JoinColumn(name = "TEAM_ID")은 그 참조를 저장할 외래키 컬럼을 지정한다.
Member와 Locker는 일대일 관계이다
Member안의locker필드는 아래처럼 작성되어 있다.// Member.java @OneToOne // 회원 하나가 락커 하나와 연결 @JoinColumn(name = "LOCKER_ID") // LOCKER_ID 외래키 컬럼과 연결 private Locker locker;
@OneToOne은 일대일 관계를 나타낸다.
일대일은 현재 엔티티 하나가 대상 엔티티 하나와 연결되는 관계이다.
여기서는 회원 한 명이 락커 하나를 사용할 수 있는 구조로 본다.
그래서Member → Locker관계는 일대일이다.
객체 코드에서는locker필드가Locker객체를 참조한다.
데이터베이스에서는 이 관계가LOCKER_ID외래키 컬럼으로 저장된다.
@JoinColumn(name = "LOCKER_ID")는 락커와의 관계를 저장할 외래키 컬럼명을 지정한다.
Member와 Locker 관계에서 봐야 할 기준
Member는Locker를 객체로 참조한다.- 회원 하나가 락커 하나와 연결될 수 있다.
- 그래서 관계는
@OneToOne이다.- 데이터베이스에는
LOCKER_ID외래키 컬럼으로 저장된다.
@OneToOne은 하나의Entity가 다른 하나의Entity와 연결되는 관계를 표현한다.
JoinColumn은 관계를 저장할 외래키 컬럼을 지정한다
@JoinColumn은 연관관계에서 사용할 외래키 컬럼을 지정한다.
컬럼이라는 말이 들어가지만, 일반 필드 매핑에 쓰는@Column과 역할이 다르다.
@Column은 일반 값 필드와 테이블 컬럼을 연결한다.
예를 들어username같은 문자열 값 필드를 테이블 컬럼과 연결할 때 사용할 수 있다.
@JoinColumn은 다른Entity와의 관계를 저장하는 외래키 컬럼을 지정한다.
예를 들어 회원이 팀을 참조할 때, 그 관계를TEAM_ID외래키 컬럼에 저장한다고 알려 준다.
비교하면 아래처럼 볼 수 있다.// ColumnAndJoinColumnCompare.java @Column(name = "MEMBER_ID") // 일반 값 필드와 컬럼 연결 private int id; @ManyToOne // 다른 Entity와의 관계 @JoinColumn(name = "TEAM_ID") // 외래키 컬럼 지정 private Team team;
id는 회원의 기본키 값이다.
그래서@Column으로 컬럼과 연결한다.
team은 단순 값이 아니라Team객체 참조이다.
그래서 관계 어노테이션인@ManyToOne과 외래키 컬럼을 지정하는@JoinColumn을 함께 사용한다.
초보자가 헷갈리기 쉬운 부분은TEAM_ID필드를 따로 만들어야 하는지이다.
객체 중심으로 설계할 때는 보통 외래키 값 자체보다 참조할 객체를 필드로 둔다.
즉,Member안에private int teamId;처럼 외래키 값만 따로 두는 것이 아니라,private Team team;처럼 참조할Entity객체를 둔다.
그러면JPA가team참조와TEAM_ID외래키 컬럼을 연결한다.
객체 코드에서는Team과Locker를 참조하고, 데이터베이스에서는TEAM_ID,LOCKER_ID외래키 값으로 관계를 저장한다.
Member 관계 구조를 한 번에 정리하기
Member,Team,Locker의 관계는 아래처럼 정리할 수 있다.
Team: 팀 정보를 가진다.Locker: 락커 정보를 가진다.Member: 회원 정보를 가지며Team과Locker를 참조한다.Member와Team: 다대일 관계이다.Member와Locker: 일대일 관계이다.Member테이블에는TEAM_ID,LOCKER_ID외래키가 생긴다.객체 흐름으로 보면 회원에서 팀으로 이동할 수 있다.
또 회원에서 락커로 이동할 수 있다.// MemberRelationObjectFlow.java Member member = entityManager.find(Member.class, 1); // 회원 조회 Team team = member.getTeam(); // 회원에서 팀으로 이동 Locker locker = member.getLocker(); // 회원에서 락커로 이동이 흐름은 뒤에서
MemberTeamTest3.java의dto.getTeam().getName(),dto.getLocker().getName()과 직접 연결된다.
Member를 조회한 뒤getTeam()을 호출하면 회원이 참조하는 팀 객체로 이동할 수 있다.
getLocker()를 호출하면 회원이 참조하는 락커 객체로 이동할 수 있다.
다만 관계가 항상 존재한다고 단정하면 안 된다.
뒤 예제에서는 팀과 락커가 없는 회원도 저장한다.
그 경우team과locker가null일 수 있다.
그래서 실제 조회 코드에서는 객체 참조를 따라가기 전에null여부를 확인해야 한다.
뒤의MemberTeamTest예제는 모두Member가Team과Locker를 객체 참조로 가지고 있다는 구조를 바탕으로 실행된다.
핵심 정리
Team,Locker,Member는 뒤의MemberTeamTest계열 예제를 이해하기 위한 기본Entity구조이다.
Team과Locker는 회원이 참조할 대상이고,Member는 이 두Entity를 함께 참조한다.
정리하면 아래와 같다.
Team은 팀 정보를 표현하는Entity이다.Team의 기본키는String타입의TEAM_ID이다.Locker는 락커 정보를 표현하는Entity이다.Locker의 기본키는IDENTITY전략으로 자동 생성된다.Member는membertbl테이블과 매핑된다.Member의 기본키는IDENTITY전략으로 자동 생성된다.Member는Team과 다대일 관계를 가진다.Member는Locker와 일대일 관계를 가진다.TEAM_ID는Member와Team의 관계를 저장하는 외래키이다.LOCKER_ID는Member와Locker의 관계를 저장하는 외래키이다.- 객체 코드에서는
team,locker필드로 관계를 따라간다.- 데이터베이스에서는
TEAM_ID,LOCKER_ID외래키 컬럼으로 관계가 저장된다.이 예제를 이해하면 뒤에서
MemberTeamTest계열 파일을 볼 때 회원, 팀, 락커가 어떤 관계로 저장되고 조회되는지 흐름을 잡을 수 있다.
MemberTeamTest1.java는Team,Locker,Member객체를 만들고 데이터베이스에 저장하는 예제이다.
앞 예제에서는Member가Team과Locker를 객체 참조로 가진다는 구조를 확인했다.
이번 예제에서는 그 구조를 실제 객체 생성과 저장 흐름으로 확인한다.
이 예제에서 중요한 점은Member를 저장할 때 단순히 회원 이름만 저장하는 것이 아니라, 회원이 참조하는Team객체와Locker객체도 함께 연결한다는 점이다.
객체 코드에서는new Member("둘리", t1, list.get(0))처럼 관계 객체를 함께 넘긴다.
데이터베이스에서는 이 관계가TEAM_ID,LOCKER_ID외래키 값으로 저장된다.
이 예제의 핵심은Team3개,Locker8개,Member8명을 저장하면서 객체 참조가 외래키 값으로 기록되는 흐름을 확인하는 것이다.
이전 기본개념과 연결하기
앞 예제에서
Member는Team과 다대일 관계를 가진다고 정리했다.
여러 회원이 하나의 팀에 속할 수 있기 때문에Member안에는@ManyToOne으로Team참조가 들어간다.
그리고 이 관계는 데이터베이스에서TEAM_ID외래키 컬럼으로 저장된다.
또Member는Locker와 일대일 관계를 가진다고 정리했다.
회원 하나가 락커 하나와 연결되는 구조이기 때문에Member안에는@OneToOne으로Locker참조가 들어간다.
그리고 이 관계는 데이터베이스에서LOCKER_ID외래키 컬럼으로 저장된다.
이번 예제는 그 관계를 실제로 저장한다.
먼저 팀 객체3개를 만들고, 락커 객체8개를 리스트에 담는다.
그다음 회원 객체8명을 만들 때 각 회원에게 팀과 락커를 함께 연결한다.
마지막으로persist()와commit()을 통해 데이터베이스에 반영한다.
기본개념에서 다시 잡아야 할 흐름
Team은 회원이 속할 팀이다.Locker는 회원이 사용할 락커이다.Member는 회원이며,Team과Locker를 참조한다.- 객체 생성 시
Member생성자에Team,Locker객체를 함께 넘길 수 있다.persist()는 객체를JPA가 관리하는 상태로 만든다.commit()이 실행되어야 데이터베이스에 실제로 반영된다.- 저장 결과에서는
Member테이블에TEAM_ID,LOCKER_ID외래키가 들어간다.연관관계 저장은 객체 참조를 먼저 연결하고, 그 참조가 데이터베이스 외래키 값으로 저장되는 흐름이다.
예제의 목표
이 예제의 목표는
Member,Team,Locker객체를 저장할 때 관계가 어떻게 연결되는지 확인하는 것이다.
단순히 객체를 각각 따로 저장하는 것이 아니라,Member객체 안에Team과Locker참조를 넣어서 저장한다.
예를 들어t1이라는 팀 객체와list.get(0)이라는 락커 객체가 있다고 하자.
Member객체를 만들 때new Member("둘리", t1, list.get(0))처럼 작성하면, 회원 객체 안에 팀 참조와 락커 참조가 함께 들어간다.
이렇게 저장하면 객체 코드에서는member.getTeam()으로 팀에 접근할 수 있고,member.getLocker()로 락커에 접근할 수 있다.
데이터베이스에서는membertbl테이블에TEAM_ID,LOCKER_ID값이 저장된다.
이 예제에서 확인할 내용
EntityManagerFactory와EntityManager를 만든다.Transaction을 시작한다.Team객체3개를 만든다.Locker객체8개를List에 담는다.Member객체8명을 만들 때Team,Locker를 함께 넘긴다.Team,Locker,Member를persist()로 저장 대상으로 등록한다.commit()으로 데이터베이스에 반영한다.- 저장 후
membertbl테이블에 외래키 값이 들어가는 흐름을 확인한다.이 예제는 저장 코드 자체보다, 객체 참조가 외래키 값으로 바뀌어 저장되는 흐름을 보는 예제이다.
코드 흐름
MemberTeamTest1.java는 먼저JPA실행에 필요한 객체를 만든다.
그다음Transaction을 시작하고, 팀과 락커와 회원 객체를 생성한다.
마지막으로 객체를 저장하고Transaction을commit()한다.
핵심 코드는 아래와 같다.// MemberTeamTest1.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // entitytest 설정으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 em.getTransaction().begin(); // Transaction 시작 Team t1 = new Team("1팀", "아기공룡둘리"); // 1팀 생성 Team t2 = new Team("2팀", "겨울왕국"); // 2팀 생성 Team t3 = new Team("3팀", "짱구는못말려"); // 3팀 생성 em.persist(t1); // 1팀 저장 em.persist(t2); // 2팀 저장 em.persist(t3); // 3팀 저장 List<Locker> list = new ArrayList<Locker>(); // Locker 목록 생성 list.add(new Locker("A101")); // 락커 추가 list.add(new Locker("A102")); // 락커 추가 list.add(new Locker("A103")); // 락커 추가 list.add(new Locker("A104")); // 락커 추가 list.add(new Locker("B101")); // 락커 추가 list.add(new Locker("B102")); // 락커 추가 list.add(new Locker("B103")); // 락커 추가 list.add(new Locker("B104")); // 락커 추가 for (Locker e : list) { // Locker 목록 반복 em.persist(e); // Locker 저장 } Member m1 = new Member("둘리", t1, list.get(0)); // 1팀, A101 락커 연결 Member m2 = new Member("또치", t1, list.get(1)); // 1팀, A102 락커 연결 Member m3 = new Member("도우너", t1, list.get(2)); // 1팀, A103 락커 연결 Member m4 = new Member("올라프", t2, list.get(3)); // 2팀, A104 락커 연결 Member m5 = new Member("안나", t2, list.get(4)); // 2팀, B101 락커 연결 Member m6 = new Member("짱구", t3, list.get(5)); // 3팀, B102 락커 연결 Member m7 = new Member("흰둥이", t3, list.get(6)); // 3팀, B103 락커 연결 Member m8 = new Member("짱아", t3, list.get(7)); // 3팀, B104 락커 연결 em.persist(m1); // 회원 저장 em.persist(m2); // 회원 저장 em.persist(m3); // 회원 저장 em.persist(m4); // 회원 저장 em.persist(m5); // 회원 저장 em.persist(m6); // 회원 저장 em.persist(m7); // 회원 저장 em.persist(m8); // 회원 저장 System.out.println("team, locker 그리고 membertbl 테이블에 데이터 저장~~ "); // 저장 안내 출력 em.getTransaction().commit(); // 데이터베이스 반영 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기이 코드에서는
"entitytest"설정 묶음을 사용한다.
그리고Team3개,Locker8개,Member8명을 저장한다.
여기서 중요한 부분은Member객체를 만들 때t1,t2,t3같은Team객체와list.get(0)부터list.get(7)까지의Locker객체를 함께 넘긴다는 점이다.
TEAM_ID나LOCKER_ID값을 직접 넘기는 것이 아니다.
객체 코드에서는 관계를 외래키 숫자나 문자열이 아니라, 참조 객체로 표현한다.
Member생성자에 외래키 값을 직접 넣는 것이 아니라, 이미 만든Team객체와Locker객체를 넣어 관계를 연결한다.
Transaction은 저장 작업을 하나의 작업 단위로 묶는다
JPA에서 저장, 수정, 삭제 같은 변경 작업은Transaction안에서 실행해야 한다.
Transaction은 하나의 작업 단위이다.
여러 저장 작업을 하나로 묶고, 마지막에 확정할 수 있게 해 준다.
핵심 코드는 아래와 같다.// MemberTeamTest1.java em.getTransaction().begin(); // Transaction 시작 em.persist(t1); // Team 저장 em.persist(list.get(0)); // Locker 저장 흐름 em.persist(m1); // Member 저장 em.getTransaction().commit(); // 변경 내용 확정
em.getTransaction().begin()은Transaction을 시작한다.
이후에 실행되는persist()작업들은Transaction안에서 관리된다.
em.getTransaction().commit()은Transaction안에서 수행한 변경 내용을 데이터베이스에 반영한다.
persist()를 호출했다고 해서 바로 최종 반영이 끝나는 것으로 이해하면 안 된다.
최종 반영은commit()시점에 이루어진다.
이 예제에는 별도의try-catch와rollback()흐름이 들어 있지 않다.
따라서 이 예제에서는begin()으로 시작하고, 저장 작업을 수행한 뒤, 마지막에commit()으로 확정하는 흐름만 확인하면 된다.
변경 작업에서는persist()만 보는 것이 아니라,begin()부터commit()까지의Transaction흐름을 함께 봐야 한다.
persist는 객체를 저장 대상으로 만든다
persist()는 엔티티 객체를JPA가 관리하는 상태로 만드는 메서드이다.
이 상태를 영속 상태라고 한다.
영속 상태는JPA가 객체를 관리하고,Transaction커밋 시점에 데이터베이스와 맞춰 줄 수 있는 상태이다.
이 예제에서는Team,Locker,Member를 모두 저장 대상으로 만든다.// MemberTeamTest1.java em.persist(t1); // Team 저장 대상으로 등록 em.persist(t2); // Team 저장 대상으로 등록 em.persist(t3); // Team 저장 대상으로 등록 for (Locker e : list) { // Locker 목록 반복 em.persist(e); // Locker 저장 대상으로 등록 } em.persist(m1); // Member 저장 대상으로 등록 em.persist(m2); // Member 저장 대상으로 등록 em.persist(m3); // Member 저장 대상으로 등록 em.persist(m4); // Member 저장 대상으로 등록 em.persist(m5); // Member 저장 대상으로 등록 em.persist(m6); // Member 저장 대상으로 등록 em.persist(m7); // Member 저장 대상으로 등록 em.persist(m8); // Member 저장 대상으로 등록
persist(t1)은t1객체를 저장 대상으로 등록한다.
persist(e)는 반복문에서 꺼낸 각Locker객체를 저장 대상으로 등록한다.
persist(m1)부터persist(m8)까지는 각Member객체를 저장 대상으로 등록한다.
중요한 점은Member가Team과Locker를 참조하고 있다는 것이다.
회원 객체를 저장할 때m1안에는 이미t1,list.get(0)참조가 들어 있다.
그래서membertbl에는 회원 정보와 함께TEAM_ID,LOCKER_ID외래키 값이 저장될 수 있다.
다만 이 예제에서는 관계 대상인Team,Locker도 직접persist()한다.
즉,Member만 저장하는 것이 아니라, 참조 대상 객체도 먼저 저장 대상으로 등록한다.
이 흐름을 보면 저장 순서가 더 명확하게 보인다.
연관관계가 있는 객체를 저장할 때는 참조 대상 객체가 함께 저장되어 있어야 외래키 관계가 안전하게 만들어진다.
Member 생성자에서 관계가 연결된다
Member생성자는 회원 이름, 팀 객체, 락커 객체를 받는다.
이 생성자에서username,team,locker필드가 채워진다.
핵심 구조는 아래와 같다.// Member.java public Member(String username, Team team, Locker locker) { // 회원 생성자 super(); // 부모 생성자 호출 this.username = username; // 회원 이름 저장 this.team = team; // 팀 객체 참조 저장 this.locker = locker; // 락커 객체 참조 저장 }그리고 실행 코드에서는 아래처럼 사용한다.
// MemberTeamTest1.java Team t1 = new Team("1팀", "아기공룡둘리"); // 팀 객체 생성 Locker locker = list.get(0); // 첫 번째 Locker 가져오기 Member m1 = new Member("둘리", t1, locker); // 회원과 팀, 락커 연결
m1은 회원 이름으로"둘리"를 가진다.
그리고team필드에는t1객체가 들어간다.
locker필드에는list.get(0)으로 꺼낸 락커 객체가 들어간다.
이 상태에서m1을 저장하면JPA는m1이 어떤 팀과 락커를 참조하는지 알 수 있다.
그 결과 데이터베이스에는membertbl행이 저장되면서TEAM_ID,LOCKER_ID외래키 값도 함께 들어갈 수 있다.
연관관계는 저장할 때 갑자기 생기는 것이 아니라, 객체를 만들 때 참조 필드에 관계 객체를 넣으면서 먼저 만들어진다.
같은 Team을 여러 Member가 참조할 수 있다
Member와Team은 다대일 관계이다.
즉, 여러 회원이 하나의 팀을 참조할 수 있다.
예를 들어 실제 코드에서는m1,m2,m3가 모두t1을 참조한다.// MemberTeamTest1.java Team t1 = new Team("1팀", "아기공룡둘리"); // 팀 객체 생성 Member m1 = new Member("둘리", t1, list.get(0)); // m1이 t1 참조 Member m2 = new Member("또치", t1, list.get(1)); // m2도 t1 참조 Member m3 = new Member("도우너", t1, list.get(2)); // m3도 t1 참조
m1,m2,m3는 서로 다른 회원 객체이다.
하지만 모두 같은t1을 참조한다.
객체 관점에서는 세 회원의team필드가 같은Team객체를 가리킨다.
데이터베이스 관점에서는 세 회원 행의TEAM_ID값이 같은"1팀"으로 저장될 수 있다.
이것이@ManyToOne관계이다.
여러Member가 하나의Team과 연결될 수 있다.
다대일 관계에서는 여러 회원 행이 같은TEAM_ID외래키 값을 가질 수 있다.
모든 Member는 Locker를 하나씩 참조한다
이 예제에서는
Member8명이 모두Locker를 하나씩 참조한다.
즉,Member생성자에 세 번째 인자로null을 넘기는 흐름이 없다.
// MemberTeamTest1.java Member m1 = new Member("둘리", t1, list.get(0)); // A101 락커 연결 Member m2 = new Member("또치", t1, list.get(1)); // A102 락커 연결 Member m3 = new Member("도우너", t1, list.get(2)); // A103 락커 연결 Member m4 = new Member("올라프", t2, list.get(3)); // A104 락커 연결 Member m5 = new Member("안나", t2, list.get(4)); // B101 락커 연결 Member m6 = new Member("짱구", t3, list.get(5)); // B102 락커 연결 Member m7 = new Member("흰둥이", t3, list.get(6)); // B103 락커 연결 Member m8 = new Member("짱아", t3, list.get(7)); // B104 락커 연결
Member와Locker는 일대일 관계로 설정되어 있다.
실제 코드에서는 회원마다 서로 다른 락커 객체를 연결한다.
객체 관점에서는 각 회원의locker필드가 하나의Locker객체를 참조한다.
데이터베이스 관점에서는 각 회원 행의LOCKER_ID값이 들어간다.
이후MemberTeamTest4.java에서는Team과Locker가null인 회원을 따로 추가한다.
따라서MemberTeamTest1.java와MemberTeamTest4.java의 저장 조건을 섞어서 보면 안 된다.
MemberTeamTest1.java에서는 모든 회원이Team과Locker를 가진 상태로 저장된다.
다시 실행할 때는 기본키 중복을 주의한다
MemberTeamTest1.java는Team의 기본키 값을 직접 지정해서 저장한다.
예를 들어new Team("1팀", "아기공룡둘리")처럼 만들면"1팀"이TEAM_ID기본키 값으로 저장된다.
이 상태에서 같은 예제를 다시 실행하면 이미Team테이블에"1팀"데이터가 있는데, 또"1팀"을 저장하려고 할 수 있다.
그러면 기본키 중복 오류가 발생한다.
대표적인 오류 흐름은 아래와 같다.// 오류 흐름 // Team 테이블에 TEAM_ID = "1팀" 데이터가 이미 있음 // 다시 MemberTeamTest1.java 실행 // 또 new Team("1팀", ...) 저장 시도 // Duplicate entry "1팀" for key "team.PRIMARY" 오류 발생이 오류는 코드 문법 문제가 아니다.
이미 저장된 데이터와 새로 저장하려는 기본키 값이 겹쳐서 생기는 문제이다.
다시 실행해서 결과를 확인하려면 기존 데이터를 지우고 실행하면 된다.
외래키 관계가 있으므로 보통 자식 테이블부터 지우는 방식으로 정리한다.// 실행 전 데이터 정리 예시 delete from membertbl; delete from locker; delete from team;데이터를 지운 뒤 다시 실행하면 같은 기본키 값을 다시 저장할 수 있다.
다만 실제 환경에서는 삭제 순서나 제약 조건 설정에 따라 방법이 달라질 수 있다.
기본키 값을 직접 지정하는 저장 예제는 같은 데이터를 다시 저장할 때 중복 오류가 날 수 있으므로, 재실행 전에 기존 데이터를 정리해야 한다.
저장 결과는 테이블의 외래키 값으로 확인한다
이 예제를 실행하면 데이터베이스 테이블에 저장 결과가 반영된다.
Team은 팀 테이블에 저장되고,Locker는 락커 테이블에 저장된다.
Member는membertbl테이블에 저장된다.
중요한 확인 대상은membertbl이다.
membertbl에는 회원 기본 정보와 함께TEAM_ID,LOCKER_ID가 저장된다.
이 두 컬럼을 보면 회원이 어떤 팀과 락커를 참조하는지 확인할 수 있다.
확인 기준은 아래와 같다.
TEAM_ID값이 있으면 해당 회원이 특정 팀을 참조한다는 뜻이다.- 여러 회원의
TEAM_ID가 같으면 같은 팀을 참조한다는 뜻이다.LOCKER_ID값이 있으면 해당 회원이 특정 락커를 참조한다는 뜻이다.- 이 예제에서는
Member8명모두 락커를 연결해서 저장한다.
MemberTeamTest1.java실행 후 저장 결과를 확인하는 화면이다.
Team,Locker,Member객체를 저장하면 각각의 테이블에 데이터가 들어간다.
이때 가장 중요하게 봐야 할 테이블은membertbl이다.
membertbl에는 회원 기본 정보와 함께TEAM_ID,LOCKER_ID외래키 값이 저장된다.
객체 코드에서는Member가Team,Locker객체를 참조한다.
하지만 데이터베이스에는 객체가 그대로 들어가는 것이 아니라, 참조 관계가TEAM_ID,LOCKER_ID값으로 저장된다.
따라서 저장 결과를 볼 때는Member행마다 어떤TEAM_ID,LOCKER_ID가 들어갔는지 확인해야 한다.
여러 회원이 같은TEAM_ID를 가지고 있다면 같은 팀을 참조하는 것이다.
LOCKER_ID가 있으면 락커와 연결된 회원이다.
이 예제에서는Member8명을 모두 서로 다른 락커와 연결해서 저장한다.
연관관계 저장 결과는 객체 코드만 보는 것이 아니라, 테이블의 외래키 컬럼에 어떤 값이 들어갔는지 함께 확인해야 한다.
핵심 정리
MemberTeamTest1.java는Team,Locker,Member객체를 만들고 저장하는 예제이다.
앞에서 확인한Member → Team,Member → Locker관계가 실제 저장 과정에서 어떻게 외래키 값으로 반영되는지 확인할 수 있다.
정리하면 아래와 같다.
"entitytest"설정 묶음으로EntityManagerFactory를 만든다.Team은 회원이 속할 팀이다.Locker는 회원이 사용할 락커이다.Member는 회원 정보와 함께Team,Locker참조를 가진다.- 실제 코드에서는
Team3개,Locker8개,Member8명을 저장한다.Member생성자에Team,Locker객체를 넘기면 객체 관계가 연결된다.persist()는 엔티티 객체를 저장 대상으로 만든다.commit()이 실행되어야 변경 내용이 데이터베이스에 반영된다.- 여러
Member가 같은Team을 참조할 수 있다.- 이 예제에서는 모든
Member가Locker를 하나씩 참조한다.- 같은 기본키 데이터를 다시 저장하면 중복 오류가 날 수 있다.
- 저장 결과는
membertbl의TEAM_ID,LOCKER_ID외래키 컬럼으로 확인한다.이 예제를 이해하면 객체 참조로 만든 연관관계가 데이터베이스 외래키 값으로 저장되는 전체 흐름을 잡을 수 있다.
MemberTeamTest2.java는 사용자가 입력한 팀 이름을 기준으로 해당 팀에 속한 회원 목록을 조회하는 예제이다.
앞 예제에서는MemberTeamTest1.java로Team,Locker,Member데이터를 저장했다.
이번 예제에서는 저장된 데이터에서 특정Team의name값과 연결된 회원을 조회한다.
이 예제에서 중요한 점은 조회 조건이Member자신의 일반 필드가 아니라는 점이다.
회원 이름이나 회원 번호를 조건으로 조회하는 것이 아니라,Member가 참조하는Team의name값을 조건으로 조회한다.
즉, 객체 관계를 따라가서m.team.name을 조건으로 사용하는 예제이다.
이 예제의 핵심은Member → Team연관관계를 따라가서Team의name값을 조회 조건으로 사용할 수 있다는 점이다.
이전 기본개념과 연결하기
앞 예제에서
Member는Team을 참조한다고 정리했다.
객체 코드에서는Member안에Team team필드가 있고, 데이터베이스에서는membertbl테이블의TEAM_ID외래키로 관계가 저장된다.
MemberTeamTest1.java에서는 이 관계를 저장했다.
예를 들어 여러 회원이 같은team1을 참조하도록 만들면, 데이터베이스에서는 여러 회원 행이 같은TEAM_ID값을 가지게 된다.
이번 예제는 저장된 관계를 조회에 사용한다.
Member를 기준으로 조회하되, 조건은Member가 참조하는Team의name값이다.
그래서JPQL에서는m.team.name처럼 객체 참조를 따라가는 표현이 등장한다.
여기서1팀과아기공룡둘리를 반드시 구분해야 한다.
앞 예제에서new Team("1팀", "아기공룡둘리")처럼 생성했다면,1팀은Team의id이고아기공룡둘리는Team의name이다.
이번 조회 조건은m.team.name이므로 입력값은id인1팀이 아니라name인아기공룡둘리와 비교된다.
기본개념에서 다시 잡아야 할 흐름
Member는Team을 참조한다.- 여러
Member가 같은Team을 참조할 수 있다.- 데이터베이스에서는
TEAM_ID외래키로 관계가 저장된다.Team의id값과name값은 다르다.new Team("1팀", "아기공룡둘리")에서1팀은id이고,아기공룡둘리는name이다.JPQL에서는 객체 참조를 따라가서 조건을 작성할 수 있다.m.team.name은 회원이 참조하는 팀의 이름을 의미한다.- 조건에 맞는 회원이 여러 명일 수 있으므로 결과는 목록으로 받는다.
객체 관계는 저장할 때만 쓰는 것이 아니라, 조회 조건을 만들 때도 사용할 수 있다.
예제의 목표
이 예제의 목표는 입력한 팀 이름에 해당하는 회원 목록을 조회하는 것이다.
사용자가 팀 이름을 입력하면,MemberTeamTest2.java는 그 팀 이름과 연결된 회원들을 찾아 출력한다.
예를 들어아기공룡둘리를 입력하면Team의name값이아기공룡둘리인 팀을 참조하는 회원들이 출력될 수 있다.
존재하지 않는 팀 이름을 입력하면 조회 결과가 비어 있을 수 있다.
이때는 팀원이 없다는 안내 메시지를 출력한다.
즉, 이 예제는 단순히 전체 회원을 조회하는 예제가 아니다.
사용자가 입력한 값이JPQL파라미터로 들어가고, 그 값이m.team.name조건과 비교되어 결과 목록이 만들어지는 흐름이다.
이 예제에서 확인할 내용
- 사용자에게 팀 이름을 입력받는다.
- 입력받은 팀 이름을
JPQL파라미터로 연결한다.m.team.name조건으로 회원을 조회한다.- 조회 결과는
List<Member>로 받는다.- 결과 목록이 비어 있으면 안내 메시지를 출력한다.
- 결과 목록이 있으면 회원 정보를 반복해서 출력한다.
Team.id가 아니라Team.name기준으로 조회한다는 점을 확인한다.이 예제는 입력값, 파라미터 바인딩, 연관 필드 조건 조회, 결과 목록 처리가 함께 들어 있는 조회 예제이다.
코드 흐름
MemberTeamTest2.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음 사용자에게 팀 이름을 입력받고, 입력값을JPQL조건에 연결한다.
조회 결과는 목록으로 받은 뒤, 결과가 있는지 없는지에 따라 출력 내용을 나눈다.
핵심 코드는 아래와 같다.// MemberTeamTest2.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager entityManager = factory.createEntityManager(); // EntityManager 생성 Scanner scan = new Scanner(System.in); // 입력 도구 생성 System.out.print("팀명을 입력하세요 : "); // 입력 안내 출력 String teamName = scan.nextLine(); // 팀 이름 입력 String jpql = "select m from Member m where m.team.name = :tn"; // 팀 이름 조건으로 Member 조회 TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class); // Member 목록 조회 쿼리 생성 query.setParameter("tn", teamName); // tn 파라미터에 입력한 팀 이름 연결 List<Member> memberList = query.getResultList(); // 조건에 맞는 회원 목록 조회 if (memberList.isEmpty()) { // 조회 결과가 없으면 System.out.println(teamName + "에는 팀원이 없습니다."); // 안내 메시지 출력 } else { for (Member member : memberList) { // 조회 결과가 있으면 반복 System.out.println(member); // 회원 정보 출력 } } entityManager.close(); // EntityManager 닫기 factory.close(); // Factory 닫기 scan.close(); // 입력 도구 닫기이 코드는 사용자가 입력한 팀 이름을 기준으로 회원 목록을 조회한다.
teamName에는 사용자가 입력한 문자열이 들어간다.
그리고setParameter("tn", teamName)으로JPQL의:tn자리에 그 값이 연결된다.
이후getResultList()로 결과 목록을 가져온다.
결과가 없을 수도 있기 때문에isEmpty()로 목록이 비어 있는지 확인한다.
결과가 있으면for반복문으로 회원 정보를 하나씩 출력한다.
입력값은 바로 문자열 비교에 쓰이는 것이 아니라,setParameter()를 통해JPQL의 파라미터 자리에 안전하게 연결된다.
m.team.name은 연관 객체의 필드를 조건으로 사용한다
이 예제에서 가장 중요한
JPQL은 아래 문장이다.// MemberTeamTest2.java String jpql = "select m from Member m where m.team.name = :tn"; // 팀 이름 조건 조회
select m from Member m은Member엔티티를 조회하겠다는 뜻이다.
여기서m은Member를 가리키는 별칭이다.
where m.team.name = :tn은 조회 조건이다.
이 조건은Member자신의username을 비교하는 것이 아니다.
Member가 참조하는Team객체로 이동한 뒤, 그Team의name값을 비교한다.
나누어 읽으면 아래와 같다.
m은Member이다.m.team은Member가 참조하는Team이다.m.team.name은 그Team의 팀 이름이다.:tn은 사용자가 입력한 팀 이름이 들어갈 파라미터이다.그래서 이 조건은 “회원이 속한 팀의 이름이 입력한 팀 이름과 같은 회원을 조회하라”는 뜻이다.
예를 들어 사용자가아기공룡둘리를 입력했다면,Team의name이아기공룡둘리인 팀을 참조하는 회원들이 조회될 수 있다.
반대로1팀을 입력하면 결과가 없을 수 있다.
왜냐하면 이 코드에서는Team의id가 아니라Team의name과 비교하기 때문이다.
m.team.name은Member에서Team으로 이동한 뒤, 그 팀의name필드를 조회 조건으로 사용하는 표현이다.
Team의 id와 name을 헷갈리면 결과가 달라진다
앞 예제에서
Team객체를 아래처럼 만들었다고 생각해 보자.// MemberTeamTest1.java Team team1 = new Team("1팀", "아기공룡둘리"); // id는 1팀, name은 아기공룡둘리 Team team2 = new Team("2팀", "겨울왕국"); // id는 2팀, name은 겨울왕국이 코드에서 첫 번째 값은
Team의id이다.
두 번째 값은Team의name이다.
따라서team1의id는1팀이고,name은아기공룡둘리이다.
team2의id는2팀이고,name은겨울왕국이다.
이번JPQL은m.team.name = :tn이다.
즉, 입력값은Team의name과 비교된다.
그래서아기공룡둘리또는겨울왕국처럼name값을 입력해야 조건과 맞는다.
만약1팀,2팀처럼id값을 기준으로 조회하고 싶다면 조건을 바꿔야 한다.
이때는m.team.name이 아니라m.team.id를 사용해야 한다.// TeamIdConditionExample.java String jpql = "select m from Member m where m.team.id = :tn"; // 팀 id 기준 조회두 조건은 비슷해 보이지만 비교 대상이 다르다.
m.team.name은 팀 이름을 비교하고,m.team.id는 팀 기본키를 비교한다.
id와 name 구분 기준
m.team.id:Team의 기본키 값을 기준으로 조회한다.m.team.name:Team의 이름 값을 기준으로 조회한다.new Team("1팀", "아기공룡둘리")에서1팀은id이다.new Team("1팀", "아기공룡둘리")에서아기공룡둘리는name이다.- 현재 예제는
m.team.name기준이므로아기공룡둘리같은name값을 입력해야 한다.입력값이 맞는데 결과가 없으면, 먼저
JPQL조건이id를 비교하는지name을 비교하는지 확인해야 한다.
파라미터 바인딩으로 입력값을 조건에 연결한다
사용자가 입력한 팀 이름은 바로
JPQL문자열에 붙이지 않는다.
대신:tn이라는 파라미터 자리를 만들고,setParameter()로 값을 연결한다.
핵심 코드는 아래와 같다.// MemberTeamTest2.java String jpql = "select m from Member m where m.team.name = :tn"; // tn 파라미터 사용 TypedQuery<Member> query = entityManager.createQuery(jpql, Member.class); // 쿼리 생성 query.setParameter("tn", teamName); // 입력받은 팀 이름을 tn에 연결
:tn은 나중에 값을 넣을 자리이다.
setParameter("tn", teamName)은 그 자리에teamName값을 넣는다.
이 방식을 파라미터 바인딩이라고 한다.
파라미터 바인딩은 쿼리 안의 값 자리에 실제 값을 연결하는 과정이다.
이 방식은 코드가 읽기 쉽다.
또 입력값을 문자열로 직접 이어 붙이는 방식보다 안전하게 조건 값을 전달할 수 있다.
파라미터 바인딩에서 봐야 할 기준
:tn은 값이 들어갈 자리이다.setParameter("tn", teamName)은:tn에 입력값을 연결한다.- 파라미터 이름
tn은JPQL과setParameter()에서 같아야 한다.- 입력값은
m.team.name과 비교된다.파라미터 이름이
JPQL의:tn과setParameter("tn", ...)에서 정확히 맞아야 한다.
getResultList는 결과 목록을 반환한다
이 예제는 팀 이름에 해당하는 회원이 여러 명일 수 있다.
그래서 결과를 하나의Member로 받지 않고List<Member>로 받는다.
핵심 코드는 아래와 같다.// MemberTeamTest2.java List<Member> memberList = query.getResultList(); // 조건에 맞는 회원 목록 조회
getResultList()는 조회 결과를 목록으로 반환한다.
결과가 여러 개일 수 있을 때 사용한다.
예를 들어 같은 팀에 여러 회원이 있으면memberList안에 여러Member객체가 들어간다.
반대로 조건에 맞는 회원이 없으면 빈 목록이 반환될 수 있다.
여기서 중요한 점은 결과가 없다고 해서 바로 오류로 처리되는 흐름이 아니라는 점이다.
getResultList()는 조건에 맞는 데이터가 없으면 빈 목록을 줄 수 있다.
그래서isEmpty()로 결과가 비었는지 확인한 뒤 메시지를 출력한다.
// MemberTeamTest2.java if (memberList.isEmpty()) { // 결과 목록이 비어 있으면 System.out.println(teamName + "에는 팀원이 없습니다."); // 안내 메시지 출력 }
isEmpty()는 목록이 비어 있는지 확인하는 메서드이다.
조회 결과가 없을 때 아무것도 출력하지 않으면 사용자가 헷갈릴 수 있다.
그래서 안내 메시지를 따로 출력한다.
여러 결과 조회에서는getResultList()를 사용하고, 결과가 없을 수 있으므로 빈 목록 처리까지 함께 해야 한다.
결과가 있으면 회원 목록을 반복해서 출력한다
조건에 맞는 회원이 있으면
for반복문으로 목록을 하나씩 출력한다.
핵심 코드는 아래와 같다.// MemberTeamTest2.java for (Member member : memberList) { // 회원 목록 반복 System.out.println(member); // 회원 정보 출력 }
memberList에는 조건에 맞는Member객체들이 들어 있다.
for반복문은 그 목록에서Member객체를 하나씩 꺼낸다.
꺼낸 객체는member변수에 들어간다.
System.out.println(member)는 내부적으로member.toString()을 호출한다.
Member클래스에@ToString이 붙어 있으면 회원 정보가 문자열로 출력된다.
이때Member안에Team,Locker참조가 포함되어 있다면 출력 결과가 길게 보일 수 있다.
결과가 길더라도 핵심은 입력한 팀 이름과 연결된 회원들이 출력되는지이다.
회원 목록 출력에서 봐야 할 기준
- 조건에 맞는 회원이 여러 명이면 여러 줄이 출력된다.
System.out.println(member)는toString()결과를 출력한다.- 출력 결과에
Team,Locker정보가 함께 보일 수 있다.- 결과가 길어도 입력한 팀 이름에 해당하는 회원인지 확인하면 된다.
회원 목록 출력 결과가 길게 보여도, 핵심은 입력한 팀 조건과 맞는 회원들이 조회되었는지 확인하는 것이다.
실행 결과는 입력값에 따라 달라진다
이 예제는 사용자가 어떤 팀 이름을 입력하느냐에 따라 결과가 달라진다.
이미MemberTeamTest1.java로 데이터를 저장했다면, 저장된Team.name과 맞는 값을 입력했을 때 회원 목록이 출력된다.
예상 결과 흐름은 아래처럼 볼 수 있다.// 출력결과 // 팀명을 입력하세요 : 아기공룡둘리 // Member(id=..., username=둘리, team=Team(id=1팀, name=아기공룡둘리), locker=Locker(...)) // Member(id=..., username=또치, team=Team(id=1팀, name=아기공룡둘리), locker=Locker(...))존재하지 않는 팀 이름을 입력하면 아래처럼 안내 메시지가 출력될 수 있다.
// 출력결과 // 팀명을 입력하세요 : 없는팀 // 없는팀에는 팀원이 없습니다.여기서 입력값은 반드시
JPQL조건과 맞아야 한다.
현재JPQL은m.team.name을 비교하므로 팀의name값을 입력해야 한다.
따라서아기공룡둘리,겨울왕국같은 값을 입력해야 조건에 맞는다.
만약1팀,2팀같은 팀의id값을 입력하고 싶다면, 현재 코드에서는 결과가 나오지 않을 수 있다.
그 경우에는 조회 조건을m.team.id로 바꿔야 한다.
MemberTeamTest2.java실행 결과이다.
먼저아기공룡둘리를 입력하면Team.name값이 일치하는 팀을 참조하는 회원 목록이 출력된다.
이 결과는 현재JPQL조건이m.team.name = :tn이기 때문에 나온다.
반대로1팀을 입력하면 팀원이 없다는 메시지가 출력된다.
1팀은 저장된Team의id값이고, 현재 조회 조건에서 비교하는 값은Team.name이기 때문이다.
따라서 이 예제에서는 입력값이Team.id인지Team.name인지 구분해서 확인해야 한다.
결과가 다르게 나올 때는 입력값이Team.id인지Team.name인지, 그리고JPQL조건이 어느 필드를 비교하는지 먼저 확인해야 한다.
MemberTeamTest1과 MemberTeamTest2의 차이
MemberTeamTest1.java와MemberTeamTest2.java는 서로 이어지는 예제이다.
하지만 역할은 다르다.
MemberTeamTest1.java는 데이터를 저장한다.
Team,Locker,Member객체를 만들고,Member가Team,Locker를 참조하도록 연결한다.
그 결과가 데이터베이스 테이블에 저장된다.
MemberTeamTest2.java는 저장된 데이터를 조회한다.
특히Member가 참조하는Team의name값을 조건으로 사용해서 회원 목록을 찾는다.
차이를 정리하면 아래와 같다.
MemberTeamTest1.java: 팀, 락커, 회원 데이터를 저장한다.MemberTeamTest1.java: 객체 참조가 외래키 값으로 저장되는지 확인한다.MemberTeamTest2.java: 저장된 회원 데이터를 조회한다.MemberTeamTest2.java:Member가 참조하는Team의name값을 조건으로 사용한다.저장은 관계를 만드는 과정이고, 조회는 만들어진 관계를 조건으로 활용하는 과정이다.
핵심 정리
MemberTeamTest2.java는 팀 이름을 입력받아 해당 팀에 속한 회원 목록을 조회하는 예제이다.
Member와Team의 연관관계를 저장한 뒤, 그 관계를 조회 조건으로 사용하는 흐름을 보여 준다.
정리하면 아래와 같다.
MemberTeamTest2.java는 팀 이름을 입력받는다.- 입력값은
setParameter()로JPQL파라미터에 연결된다.select m from Member m은Member를 조회한다는 뜻이다.m.team.name은Member가 참조하는Team의name필드이다.where m.team.name = :tn은 입력한 팀 이름과 같은 팀을 참조하는 회원을 찾는 조건이다.1팀,2팀은Team.id값이고,아기공룡둘리,겨울왕국은Team.name값이다.- 현재 예제는
Team.name기준 조회이므로아기공룡둘리같은 이름 값을 입력해야 한다.- 결과가 여러 명일 수 있으므로
getResultList()를 사용한다.- 결과가 없을 수 있으므로
isEmpty()로 빈 목록을 확인한다.- 결과가 있으면
for반복문으로 회원 목록을 출력한다.- 입력값이
Team의name과 맞지 않으면 결과가 없을 수 있다.이 예제는 객체 연관관계가 저장뿐 아니라 조회 조건에서도 사용될 수 있음을 보여 준다.
MemberTeamTest3.java는 회원명을 입력받아 해당 회원 한 명을 조회하고, 그 회원이 속한 팀명과 사용하는 락커명을 출력하는 예제이다.
앞 예제에서는Team.name을 기준으로 여러Member를 조회했다.
이번 예제에서는 반대로Member.username을 기준으로 회원 한 명을 조회한다.
이 예제에서 중요한 점은 조회 결과가Member객체 하나라는 것이다.
조회된Member객체에서getTeam().getName()으로 팀 이름을 꺼내고,getLocker().getName()으로 락커 이름을 꺼낸다.
즉,Member → Team,Member → Locker객체 참조를 따라가서 필요한 값을 출력한다.
이 예제의 핵심은 회원 한 명을 조회한 뒤, 조회된Member객체에서 연관된Team과Locker정보를 객체 참조로 꺼내는 흐름이다.
이전 기본개념과 연결하기
앞 예제에서
Member는Team과Locker를 참조한다고 정리했다.
객체 코드에서는Member안에Team team,Locker locker필드가 있다.
데이터베이스에서는membertbl테이블의TEAM_ID,LOCKER_ID외래키로 관계가 저장된다.
MemberTeamTest2.java에서는Member가 참조하는Team의name값을 조회 조건으로 사용했다.
즉,m.team.name처럼Member에서Team으로 이동한 뒤 팀 이름을 비교했다.
이번 예제에서는 조회 조건이 더 단순하다.
Member자신의 필드인username을 조건으로 사용한다.
하지만 출력할 때는 조회된Member에서 다시Team과Locker로 이동한다.
그래서 이 예제는 조건 조회와 연관 객체 탐색이 함께 들어 있다.
조회 조건은m.username = :mn이고, 출력 값은dto.getTeam().getName(),dto.getLocker().getName()으로 꺼낸다.
기본개념에서 다시 잡아야 할 흐름
Member는 회원 정보를 가진다.Member는Team을 참조한다.Member는Locker를 참조한다.username은Member자신의 일반 필드이다.team과locker는 다른Entity를 가리키는 연관 필드이다.- 회원명으로 조회할 때는
m.username을 조건으로 사용한다.- 팀명과 락커명은 조회된
Member객체에서 참조를 따라가서 꺼낸다.조회 조건이
Member자신의 필드여도, 출력할 값은 연관 객체를 따라가서 가져올 수 있다.
예제의 목표
이 예제의 목표는 입력한 회원명에 해당하는 회원 한 명을 조회하고, 그 회원의 소속 팀과 락커 정보를 함께 출력하는 것이다.
예를 들어둘리를 입력하면둘리라는 회원을 조회한다.
조회된Member객체에서 회원 이름, 팀 이름, 락커 이름을 꺼내서 한 문장으로 출력한다.
존재하지 않는 회원명을 입력하면 조회 결과가 없다.
이때는NoResultException을 처리해서 정보가 없다는 메시지를 출력한다.
이 예제에서 확인할 내용
- 사용자에게 회원명을 입력받는다.
- 입력받은 회원명을
JPQL파라미터로 연결한다.m.username조건으로 회원 한 명을 조회한다.- 결과가 한 명이라고 예상하므로
getSingleResult()를 사용한다.- 조회 결과가 없으면
NoResultException이 발생할 수 있다.- 조회된
Member에서Team과Locker정보를 꺼낸다.getTeam().getName()으로 팀명을 출력한다.getLocker().getName()으로 락커명을 출력한다.이 예제는 단건 조회, 예외 처리, 연관 객체 탐색을 한 번에 확인하는 예제이다.
코드 흐름
MemberTeamTest3.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음 사용자에게 회원명을 입력받고, 입력값을JPQL조건에 연결한다.
조회 결과가 있으면 회원명, 팀명, 락커명을 출력하고, 결과가 없으면 안내 메시지를 출력한다.
핵심 코드는 아래와 같다.// MemberTeamTest3.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Scanner scan = new Scanner(System.in); // 입력 도구 생성 System.out.print("회원명을 입력하세요 : "); // 입력 안내 출력 String inputName = scan.nextLine(); // 회원명 입력 scan.close(); // 입력 도구 닫기 String jpql = "select m from Member m where m.username = :mn"; // 회원명 조건 JPQL TypedQuery<Member> q = em.createQuery(jpql, Member.class); // Member 단건 조회 쿼리 생성 q.setParameter("mn", inputName); // mn 파라미터에 입력값 연결 try { Member dto = q.getSingleResult(); // 결과 한 건 조회 System.out.printf( "%s님은 %s팀 소속이고 %s 락커를 사용중입니다.%n", dto.getUsername(), // 회원명 dto.getTeam().getName(), // 팀명 dto.getLocker().getName() // 락커명 ); } catch (NoResultException e) { System.out.printf("%s님은 정보가 없습니다.%n", inputName); // 결과가 없을 때 } em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기이 코드는 회원명으로
Member하나를 조회한다.
inputName에는 사용자가 입력한 회원명이 들어간다.
그리고setParameter("mn", inputName)으로JPQL의:mn자리에 입력값을 연결한다.
조회 결과가 있으면getSingleResult()로Member객체 하나를 받는다.
그다음dto.getUsername(),dto.getTeam().getName(),dto.getLocker().getName()을 사용해 출력 문장을 만든다.
MemberTeamTest3.java는Member를 조회한 뒤, 조회된 객체에서 연관 객체의 값을 꺼내 출력한다.
m.username은 Member 자신의 필드를 조건으로 사용한다
이 예제의
JPQL은 아래와 같다.// MemberTeamTest3.java String jpql = "select m from Member m where m.username = :mn"; // 회원명 조건 조회
select m from Member m은Member엔티티를 조회한다는 뜻이다.
여기서m은Member를 가리키는 별칭이다.
where m.username = :mn은 조회 조건이다.
m.username은Member자신의 회원명 필드이다.
즉, 이번 예제는 팀 이름이나 락커 이름을 조건으로 조회하는 것이 아니라, 회원 이름을 조건으로 조회한다.
나누어 읽으면 아래와 같다.
m은Member이다.m.username은Member의 회원명이다.:mn은 사용자가 입력한 회원명이 들어갈 파라미터이다.그래서 이 조건은 “회원 이름이 입력값과 같은
Member를 조회하라”는 뜻이다.
앞 예제의m.team.name과 비교하면 차이가 분명하다.
m.team.name은Member에서Team으로 이동한 뒤 팀 이름을 조건으로 사용한다.
반면m.username은Member자신의 필드를 바로 조건으로 사용한다.
m.username은 연관 객체로 이동하지 않고,Member자신의 필드를 조건으로 사용하는 표현이다.
getSingleResult는 결과 한 건을 기대할 때 사용한다
이 예제에서는
getSingleResult()를 사용한다.
getSingleResult()는 조회 결과가 한 건이라고 기대할 때 사용하는 메서드이다.
핵심 코드는 아래와 같다.// MemberTeamTest3.java Member dto = q.getSingleResult(); // 결과 한 건 조회회원명으로 한 명을 찾는 흐름이므로 결과를
List<Member>가 아니라Member객체 하나로 받는다.
그래서getResultList()가 아니라getSingleResult()를 사용한다.
하지만getSingleResult()는 조심해서 써야 한다.
조건에 맞는 결과가 없으면NoResultException이 발생할 수 있다.
조건에 맞는 결과가 여러 건이면 다른 예외가 발생할 수도 있다.
이 예제에서는 결과가 없는 경우를 처리하기 위해try-catch를 사용한다.// MemberTeamTest3.java try { Member dto = q.getSingleResult(); // 결과 한 건 조회 System.out.println(dto); // 조회 결과 출력 } catch (NoResultException e) { System.out.printf("%s님은 정보가 없습니다.%n", inputName); // 결과 없음 처리 }
try안에는 정상 실행 코드를 넣는다.
catch안에는 예외가 발생했을 때 실행할 코드를 넣는다.
여기서는 회원이 조회되면 정상 출력하고, 회원이 없으면 정보가 없다는 메시지를 출력한다.
getSingleResult에서 봐야 할 기준
- 결과 한 건을 기대할 때
getSingleResult()를 사용한다.- 결과가 여러 건일 수 있으면
getResultList()가 더 적합하다.- 결과가 없으면
NoResultException이 발생할 수 있다.- 결과 없음 상황을 처리하려면
try-catch가 필요하다.
getSingleResult()는 결과 한 건을 기대하는 메서드이므로, 결과가 없을 때의 예외 처리까지 함께 봐야 한다.
getTeam과 getLocker로 연관 객체의 값을 꺼낸다
조회 결과는
Member객체이다.
하지만 출력에는 팀명과 락커명도 필요하다.
이때Member가 가진 객체 참조를 사용한다.
핵심 출력 코드는 아래와 같다.// MemberTeamTest3.java System.out.printf( "%s님은 %s팀 소속이고 %s 락커를 사용중입니다.%n", dto.getUsername(), // Member 자신의 username dto.getTeam().getName(), // Team의 name dto.getLocker().getName() // Locker의 name );
dto.getUsername()은Member자신의 필드 값을 가져온다.
dto.getTeam().getName()은Member에서Team으로 이동한 뒤 팀 이름을 가져온다.
dto.getLocker().getName()은Member에서Locker로 이동한 뒤 락커 이름을 가져온다.
이 흐름은 테이블의 외래키 값을 직접 꺼내는 방식이 아니다.
객체 참조를 따라가서 필요한 값을 가져오는 방식이다.
객체 이동 흐름은 아래처럼 볼 수 있다.// MemberRelationAccessFlow.java Member dto = q.getSingleResult(); // 회원 조회 String username = dto.getUsername(); // 회원 이름 String teamName = dto.getTeam().getName(); // 회원이 참조하는 팀 이름 String lockerName = dto.getLocker().getName(); // 회원이 참조하는 락커 이름
dto.getTeam()의 결과는Team객체이다.
그 객체에서 다시getName()을 호출하면 팀 이름을 얻을 수 있다.
dto.getLocker()의 결과는Locker객체이다.
그 객체에서 다시getName()을 호출하면 락커 이름을 얻을 수 있다.
연관관계가 매핑되어 있으면 외래키 값을 직접 다루지 않고 객체 참조로 연결된 데이터를 사용할 수 있다.
Locker가 null이면 바로 getName을 호출할 수 없다
앞 예제에서 락커가 없는 회원도 저장할 수 있다고 정리했다.
예를 들어new Member("도우너", team2, null)처럼 저장했다면, 해당 회원의locker필드는null이다.
이 상태에서 아래 코드가 실행되면 문제가 생길 수 있다.// NullLockerProblem.java dto.getLocker().getName(); // locker가 null이면 오류 발생 가능
dto.getLocker()가null이면 그 뒤에.getName()을 호출할 수 없다.
null은 참조할 객체가 없다는 뜻이기 때문이다.
이 상태에서 메서드를 호출하면NullPointerException이 발생할 수 있다.
따라서 이 예제의 출력 코드는 락커가 있는 회원을 조회할 때는 정상 동작한다.
하지만 락커가 없는 회원을 입력하면 별도의null처리가 필요할 수 있다.
안전하게 작성하려면 아래처럼 먼저 확인할 수 있다.// NullLockerSafeExample.java String lockerName; if (dto.getLocker() != null) { // 락커가 있으면 lockerName = dto.getLocker().getName(); // 락커 이름 사용 } else { lockerName = "사용중인 락커 없음"; // 락커가 없을 때 문구 }이렇게 하면 락커가 없는 회원도 오류 없이 처리할 수 있다.
다만 현재 예제의 핵심은null처리 자체보다, 연관 객체를 따라가서 값을 꺼내는 흐름을 확인하는 것이다.
null관계 처리는 뒤의 예제에서 더 중요하게 다룰 수 있다.
연관 객체가 없을 수 있는 필드는 바로 메서드를 이어서 호출하지 말고, 먼저null여부를 확인해야 한다.
실행 결과는 존재하는 회원과 없는 회원으로 나누어 확인한다
이 예제는 입력한 회원명이 실제 데이터에 있는지에 따라 결과가 달라진다.
이미MemberTeamTest1.java로 데이터를 저장했다면, 저장된 회원 이름을 입력했을 때 회원 정보가 출력된다.
예상 결과 흐름은 아래처럼 볼 수 있다.// 출력결과 // 회원명을 입력하세요 : 둘리 // 둘리님은 아기공룡둘리팀 소속이고 A101 락커를 사용중입니다.존재하지 않는 회원명을 입력하면 아래처럼 안내 메시지가 출력될 수 있다.
// 출력결과 // 회원명을 입력하세요 : 마이콜 // 마이콜님은 정보가 없습니다.존재하는 회원을 입력했을 때는
getSingleResult()가Member객체를 반환한다.
존재하지 않는 회원을 입력했을 때는NoResultException이 발생하고,catch구문에서 안내 메시지를 출력한다.
MemberTeamTest3.java실행 결과이다.
먼저둘리를 입력하면Member.username이 일치하는 회원 한 명이 조회된다.
조회된Member객체에서getTeam().getName()으로 팀명을 꺼내고,getLocker().getName()으로 락커명을 꺼내 함께 출력한다.
반대로마이콜을 입력하면 조건에 맞는 회원이 없기 때문에 정보가 없다는 메시지가 출력된다.
이 흐름은getSingleResult()에서 결과가 없을 때NoResultException을 처리한 결과이다.
결과가 있을 때는 연관 객체의 값이 출력되고, 결과가 없을 때는NoResultException처리 결과가 출력된다.
MemberTeamTest2와 MemberTeamTest3의 차이
MemberTeamTest2.java와MemberTeamTest3.java는 모두 저장된Member데이터를 조회한다.
하지만 조회 기준과 결과 형태가 다르다.
MemberTeamTest2.java는 팀 이름으로 회원 목록을 조회한다.
조건은m.team.name = :tn이고, 결과는 여러 명일 수 있으므로List<Member>로 받는다.
MemberTeamTest3.java는 회원 이름으로 한 명을 조회한다.
조건은m.username = :mn이고, 결과는 한 명이라고 기대하므로getSingleResult()를 사용한다.
차이를 정리하면 아래와 같다.
MemberTeamTest2.java:Team.name으로 회원 목록을 조회한다.MemberTeamTest2.java: 결과가 여러 명일 수 있으므로getResultList()를 사용한다.MemberTeamTest3.java:Member.username으로 회원 한 명을 조회한다.MemberTeamTest3.java: 결과 한 건을 기대하므로getSingleResult()를 사용한다.MemberTeamTest2.java: 결과가 없으면 빈 목록을 처리한다.MemberTeamTest3.java: 결과가 없으면NoResultException을 처리한다.목록 조회와 단건 조회는 결과를 받는 메서드와 결과 없음 처리 방식이 다르다.
핵심 정리
MemberTeamTest3.java는 회원명을 입력받아 회원 한 명을 조회하고, 그 회원의 팀명과 락커명을 출력하는 예제이다.
조회 조건은Member자신의username이고, 출력할 팀명과 락커명은 연관 객체 참조를 따라가서 가져온다.
정리하면 아래와 같다.
MemberTeamTest3.java는 회원명을 입력받는다.- 입력값은
setParameter()로JPQL파라미터에 연결된다.select m from Member m은Member엔티티 전체를 조회한다는 뜻이다.where m.username = :mn은 회원명이 입력값과 같은 회원을 찾는 조건이다.- 결과 한 건을 기대하므로
getSingleResult()를 사용한다.- 결과가 없으면
NoResultException이 발생할 수 있다.- 조회된
Member에서getUsername()으로 회원명을 꺼낸다.- 조회된
Member에서getTeam().getName()으로 팀명을 꺼낸다.- 조회된
Member에서getLocker().getName()으로 락커명을 꺼낸다.locker가null이면getLocker().getName()호출 시 오류가 발생할 수 있다.- 목록 조회와 단건 조회는 결과 처리 방식이 다르다.
이 예제는 단건으로 조회한
Member객체에서 연관 객체를 따라가 필요한 값을 출력하는 흐름을 보여 준다.
MemberTeamTest3_1.java는Member엔티티 전체를 조회하지 않고, 출력에 필요한 필드만 직접 조회하는 예제이다.
앞 예제인MemberTeamTest3.java에서는select m from Member m으로Member객체 전체를 조회했다.
이번 예제에서는m.username,m.team.name,m.locker.name만 선택해서 조회한다.
이 예제에서 중요한 점은 조회 결과가Member객체가 아니라는 점이다.
여러 필드를 직접 선택하면 결과 한 줄에 여러 값이 들어 있고, 그 결과를Object[]배열로 받는다.
이 예제의 핵심은 엔티티 전체 조회와 필요한 필드만 조회하는 방식의 차이를 이해하고, 여러 필드 조회 결과를Object[]로 다루는 흐름을 잡는 것이다.
이전 기본개념과 연결하기
앞 예제에서
MemberTeamTest3.java는 회원명을 입력받아Member객체 하나를 조회했다.
그다음 조회된Member객체에서getTeam().getName(),getLocker().getName()을 호출해 팀명과 락커명을 꺼냈다.
즉, 앞 예제의 흐름은 먼저Member엔티티 전체를 가져오는 방식이었다.
회원 객체를 가져온 뒤, 그 객체 안의 연관 필드를 따라가 필요한 값을 꺼냈다.
이번 예제는 방식이 다르다.
처음부터Member객체 전체를 가져오지 않고, 출력에 필요한 값만select절에 적는다.
회원명, 팀명, 락커명만 필요하다면m.username,m.team.name,m.locker.name만 조회할 수 있다.
이 차이를 이해해야 한다.
select m은 엔티티 전체 조회이다.
select m.username, m.team.name, m.locker.name은 필요한 필드만 조회하는 방식이다.
결과를 받는 타입도 달라진다.
기본개념에서 다시 잡아야 할 흐름
select m은Member엔티티 전체를 조회한다.select m.username은Member의 회원명 필드만 조회한다.m.team.name은Member가 참조하는Team의name값을 조회한다.m.locker.name은Member가 참조하는Locker의name값을 조회한다.- 여러 필드를 한 번에 선택하면 결과 한 줄에 값이 여러 개 들어간다.
- 여러 값을 한 줄로 받으면
Object[]로 다룰 수 있다.Object[]는select에 적은 순서대로 값이 들어간다.엔티티 전체를 조회하는 것과 필요한 필드만 조회하는 것은 결과 타입부터 다르다.
예제의 목표
이 예제의 목표는 회원명을 입력받아 회원명, 팀명, 락커명만 조회하는 것이다.
결과 문장은 앞 예제와 비슷하게 보일 수 있다.
하지만 내부 조회 방식은 다르다.
앞 예제는Member객체 전체를 조회한 뒤 객체 참조를 따라갔다.
이번 예제는 처음부터 출력에 필요한 세 값만 조회한다.
그래서 결과를Member타입으로 받을 수 없고,Object[]배열로 받아야 한다.
즉, 이 예제는 출력 결과만 보는 예제가 아니다.
같은 문장을 출력하더라도 조회 방식이 어떻게 달라지는지 확인하는 예제이다.
이 예제에서 확인할 내용
- 사용자에게 회원명을 입력받는다.
- 입력값을
JPQL파라미터로 연결한다.select m.username, m.team.name, m.locker.name으로 필요한 필드만 조회한다.- 조회 결과는
Member가 아니라Object[]로 받는다.result[0],result[1],result[2]순서로 값을 꺼낸다.- 결과가 없으면
NoResultException을 처리한다.locker가null인 회원은 회원이 존재해도 결과가 없을 수 있음을 확인한다.- 앞 예제의 엔티티 전체 조회와 이번 예제의 필드 조회 차이를 비교한다.
이번 예제는 필요한 값만 선택해서 조회할 때 결과를 어떻게 받아야 하는지 확인하는 예제이다.
코드 흐름
MemberTeamTest3_1.java도 사용자에게 회원명을 입력받는다.
다만JPQL의select절이 앞 예제와 다르다.
앞 예제는Member전체를 조회했지만, 이번 예제는 필요한 필드 세 개만 조회한다.
핵심 코드는 아래와 같다.// MemberTeamTest3_1.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Scanner scan = new Scanner(System.in); // 입력 도구 생성 System.out.print("회원명을 입력하세요 : "); // 입력 안내 출력 String inputName = scan.nextLine(); // 회원명 입력 scan.close(); // 입력 도구 닫기 String jpql = "select m.username, m.team.name, m.locker.name from Member m where m.username = :mn"; // 필요한 필드만 조회 Query q = em.createQuery(jpql); // 여러 필드 결과이므로 일반 Query 사용 q.setParameter("mn", inputName); // mn 파라미터에 입력값 연결 try { Object[] result = (Object[]) q.getSingleResult(); // 한 행의 여러 값을 Object 배열로 받음 System.out.printf( "%s님은 %s팀 소속이고 %s 락커를 사용중입니다.%n", result[0], // 회원명 result[1], // 팀명 result[2] // 락커명 ); } catch (NoResultException e) { System.out.printf("%s님은 정보가 없습니다.%n", inputName); // 결과가 없을 때 } em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기여기서는
TypedQuery<Member>가 아니라 일반Query를 사용한다.
조회 결과가Member객체 하나가 아니기 때문이다.
select절에서 세 개의 값을 선택했으므로 결과 한 줄에는 세 값이 들어 있다.
그래서getSingleResult()의 결과를Object[]로 형변환해서 받는다.
여러 필드를 직접 선택하면 조회 결과가 엔티티 객체가 아니라 값들의 묶음이 되므로Object[]로 받아야 한다.
select 절에 여러 필드를 쓰면 Object 배열로 받는다
이 예제에서 가장 중요한
JPQL은 아래 문장이다.// MemberTeamTest3_1.java String jpql = "select m.username, m.team.name, m.locker.name from Member m where m.username = :mn"; // 여러 필드 조회
select m.username은 회원명을 조회한다.
m.team.name은 회원이 참조하는 팀의 이름을 조회한다.
m.locker.name은 회원이 참조하는 락커의 이름을 조회한다.
이렇게 여러 값을 선택하면 결과 한 줄에 값이 여러 개 들어 있다.
이때 결과를Member객체로 받을 수 없다.
Member전체를 조회한 것이 아니라,username,team.name,locker.name이라는 값 세 개를 조회했기 때문이다.
그래서 아래처럼Object[]로 받는다.// MemberTeamTest3_1.java Object[] result = (Object[]) q.getSingleResult(); // 한 행의 여러 값을 배열로 받음 System.out.println(result[0]); // 회원명 System.out.println(result[1]); // 팀명 System.out.println(result[2]); // 락커명
result[0],result[1],result[2]는select절에 적은 순서대로 들어간다.
첫 번째로 적은m.username은result[0]이다.
두 번째로 적은m.team.name은result[1]이다.
세 번째로 적은m.locker.name은result[2]이다.
Object 배열에서 봐야 할 기준
- 여러 필드를 선택하면 결과 한 줄에 여러 값이 들어간다.
- 여러 값을 한 줄로 받으면
Object[]를 사용할 수 있다.- 배열 인덱스는
0부터 시작한다.result[0]은 첫 번째 선택값이다.result[1]은 두 번째 선택값이다.result[2]는 세 번째 선택값이다.select순서와 배열 인덱스를 정확히 맞춰야 한다.여러 필드 조회에서는
select절의 순서와Object[]인덱스 순서가 그대로 연결된다.
Entity 전체 조회와 필드 조회는 결과 형태가 다르다
앞 예제와 이번 예제의 가장 큰 차이는 조회 결과의 형태이다.
앞 예제는Member엔티티 전체를 조회했다.
이번 예제는 필요한 필드만 조회한다.
먼저 앞 예제의 흐름은 아래와 같다.// MemberTeamTest3.java String jpql = "select m from Member m where m.username = :mn"; // Member 전체 조회 TypedQuery<Member> q = em.createQuery(jpql, Member.class); // Member 타입 조회 Member dto = q.getSingleResult(); // Member 객체로 받음 String teamName = dto.getTeam().getName(); // 객체 참조로 팀명 접근 String lockerName = dto.getLocker().getName(); // 객체 참조로 락커명 접근이 방식은
Member객체 전체를 가져온다.
그래서dto.getUsername(),dto.getTeam(),dto.getLocker()처럼 객체 메서드로 값을 꺼낼 수 있다.
반면 이번 예제의 흐름은 아래와 같다.// MemberTeamTest3_1.java String jpql = "select m.username, m.team.name, m.locker.name from Member m where m.username = :mn"; // 필드만 조회 Query q = em.createQuery(jpql); // 여러 필드 조회 Object[] result = (Object[]) q.getSingleResult(); // Object 배열로 받음 String username = (String) result[0]; // 회원명 String teamName = (String) result[1]; // 팀명 String lockerName = (String) result[2]; // 락커명이 방식은 처음부터 필요한 값만 조회한다.
그래서 결과를Member객체로 다루지 않는다.
대신 배열 인덱스로 각 값을 꺼낸다.
두 방식의 차이
- 엔티티 전체 조회:
select m을 사용한다.- 엔티티 전체 조회: 결과를
Member객체로 받는다.- 엔티티 전체 조회: 객체 참조를 따라가 값을 꺼낸다.
- 필드 조회:
select m.username, m.team.name, m.locker.name처럼 필요한 필드만 선택한다.- 필드 조회: 결과를
Object[]로 받는다.- 필드 조회: 배열 인덱스로 값을 꺼낸다.
출력 문장이 비슷해도 내부 조회 방식이 엔티티 전체 조회인지 필드 조회인지 반드시 구분해야 한다.
필요한 값만 조회하면 가볍지만 인덱스 관리가 필요하다
필드 조회의 장점은 필요한 값만 가져올 수 있다는 점이다.
예를 들어 화면에 회원명, 팀명, 락커명만 필요하다면Member객체 전체를 가져오지 않고 세 값만 조회할 수 있다.
하지만 단점도 있다.
결과를Object[]로 받으면 어떤 값이 몇 번째 인덱스에 들어 있는지 직접 맞춰야 한다.
select순서를 바꾸면 배열 인덱스의 의미도 바뀐다.
예를 들어 아래처럼select순서를 바꾸면 결과 인덱스도 달라진다.// ObjectArrayOrderExample.java String jpql = "select m.team.name, m.username, m.locker.name from Member m where m.username = :mn"; // 순서 변경 Object[] result = (Object[]) q.getSingleResult(); // 결과 배열 System.out.println(result[0]); // 팀명 System.out.println(result[1]); // 회원명 System.out.println(result[2]); // 락커명이 코드에서는
result[0]이 회원명이 아니라 팀명이다.
왜냐하면select절에서m.team.name을 첫 번째로 적었기 때문이다.
그래서Object[]를 사용할 때는 반드시select순서와 출력 순서를 함께 봐야 한다.
순서를 잘못 맞추면 출력 문장은 정상처럼 보여도 값의 의미가 뒤섞일 수 있다.
Object[]조회는 필요한 값만 가져올 수 있다는 장점이 있지만, 인덱스 순서를 직접 관리해야 한다는 단점이 있다.
null 연관관계가 있으면 결과 없음의 의미가 달라질 수 있다
앞 예제에서 락커가 없는 회원도 저장할 수 있다고 정리했다.
예를 들어도우너처럼locker가null인 회원이 있을 수 있다.
이번 예제의JPQL은m.locker.name을 직접 조회한다.
이 말은Member에서Locker로 이동한 뒤, 그Locker의name값을 조회한다는 뜻이다.
그런데 회원의locker가null이면m.locker.name으로 이동할 대상이 없다.
이 경우Member데이터 자체는 존재해도, 현재 필드 조회 결과에서는 빠질 수 있다.
예를 들어도우너라는 회원이 실제로 저장되어 있더라도locker가null이라면 문제가 생길 수 있다.
이때getSingleResult()에서 결과를 찾지 못하면catch구문이 실행되어"도우너님은 정보가 없습니다."처럼 출력될 수 있다.
하지만 이 메시지를 단순히 “도우너 회원이 존재하지 않는다”는 뜻으로만 보면 안 된다.
회원 데이터는 존재하지만, 현재select절에서m.locker.name까지 직접 조회하고 있기 때문에 연관된Locker가 없는 데이터가 결과에서 제외된 상황일 수 있다.
이 점이 앞 예제와 다르다.
MemberTeamTest3.java처럼select m으로Member전체를 조회하면 먼저 회원 객체를 가져온 뒤locker가null인지 직접 확인할 수 있다.
하지만MemberTeamTest3_1.java는 처음부터m.locker.name값을 조회하려고 하므로,Locker가 없는 회원은 조회 결과 해석이 달라질 수 있다.
따라서 이 예제는 락커가 있는 회원인둘리같은 데이터로 확인하는 것이 안전하다.
락커가 없는 회원까지 안전하게 처리하려면left join을 사용하거나, 엔티티 전체 조회 후null여부를 확인하는 방식이 필요할 수 있다.
이 단계에서는 아래 정도로 이해하면 된다.
m.locker.name은 락커가 있는 회원에게 자연스럽게 사용할 수 있다.locker가null이면m.locker.name으로 값을 꺼낼 수 없다.- 회원이 존재해도 연관된
Locker가 없으면 결과가 없다고 처리될 수 있다.- 이때 정보 없음 메시지를 회원 자체가 없다는 뜻으로만 해석하면 안 된다.
- 연관관계가 없을 수 있는 데이터는 조회 방식과
null처리 방식을 함께 고려해야 한다.연관 필드를 직접 선택하는 조회에서는 회원 자체의 존재 여부와 연관 객체의 존재 여부를 구분해서 해석해야 한다.
실행 결과는 앞 예제와 비슷하지만 조회 방식이 다르다
실행 결과 문장은
MemberTeamTest3.java와 거의 비슷하게 나올 수 있다.
하지만 내부 조회 방식은 다르다.
예상 결과 흐름은 아래와 같다.// 출력결과 // 회원명을 입력하세요 : 둘리 // 둘리님은 아기공룡둘리팀 소속이고 A101 락커를 사용중입니다.출력 문장만 보면 앞 예제와 같아 보일 수 있다.
하지만 앞 예제는Member객체를 조회한 뒤 객체 참조를 따라갔다.
이번 예제는 처음부터 필요한 필드 세 개만 조회했고, 그 결과를Object[]로 받아 출력했다.
존재하지 않는 회원명을 입력하면 아래처럼 안내 메시지가 출력될 수 있다.// 출력결과 // 회원명을 입력하세요 : 마이콜 // 마이콜님은 정보가 없습니다.결과가 없을 때는 앞 예제와 마찬가지로
NoResultException을 처리한다.
다만 이번 예제에서는마이콜처럼 회원이 아예 없는 경우와도우너처럼 회원은 있지만locker가 없는 경우를 구분해서 생각해야 한다.
MemberTeamTest3_1.java실행 결과이다.
먼저둘리를 입력하면 필요한 필드인 회원명, 팀명, 락커명이 조회되어 출력된다.
출력 문장은 앞 예제와 비슷하지만, 이번 예제는Member엔티티 전체를 조회한 것이 아니라m.username,m.team.name,m.locker.name세 값만 조회한 결과이다.
반대로마이콜을 입력하면 조건에 맞는 결과가 없기 때문에 정보가 없다는 메시지가 출력된다.
이 흐름은getSingleResult()에서 결과가 없을 때NoResultException을 처리한 결과이다.
같은 출력 결과라도Entity전체 조회인지, 필요한 필드만 조회한 것인지 구분해야 한다.
MemberTeamTest3와 MemberTeamTest3_1의 차이
MemberTeamTest3.java와MemberTeamTest3_1.java는 출력 결과가 비슷하다.
하지만 조회 대상과 결과 타입이 다르다.
MemberTeamTest3.java는Member엔티티 전체를 조회한다.
조회된 객체에서getTeam()과getLocker()를 호출해 연관 객체의 값을 꺼낸다.
MemberTeamTest3_1.java는 필요한 필드만 조회한다.
m.username,m.team.name,m.locker.name값을 바로 조회하고, 결과를Object[]로 받는다.
차이를 정리하면 아래와 같다.
MemberTeamTest3.java:select m으로Member전체를 조회한다.MemberTeamTest3.java: 결과 타입은Member이다.MemberTeamTest3.java: 조회 후 객체 참조를 따라가 값을 꺼낸다.MemberTeamTest3_1.java: 필요한 필드만select절에 적는다.MemberTeamTest3_1.java: 결과 타입은Object[]이다.MemberTeamTest3_1.java: 배열 인덱스로 회원명, 팀명, 락커명을 꺼낸다.MemberTeamTest3_1.java: 연관 객체가null이면 결과 해석이 달라질 수 있다.
MemberTeamTest3_1.java는 필요한 값만 조회하는 방식이므로 결과 타입과 값 꺼내는 방식이 앞 예제와 달라진다.
핵심 정리
MemberTeamTest3_1.java는Member엔티티 전체를 조회하지 않고, 필요한 필드만 선택해서 조회하는 예제이다.
회원명, 팀명, 락커명만 필요하기 때문에select m.username, m.team.name, m.locker.name을 사용한다.
정리하면 아래와 같다.
MemberTeamTest3_1.java는 회원명을 입력받는다.- 입력값은
setParameter()로JPQL파라미터에 연결된다.select m.username, m.team.name, m.locker.name은 필요한 필드 세 개만 조회한다.- 조회 결과는
Member가 아니라Object[]이다.result[0]은 회원명이다.result[1]은 팀명이다.result[2]는 락커명이다.Object[]는select절에 적은 순서대로 값이 들어간다.- 결과가 없으면
NoResultException을 처리한다.- 출력 결과가 앞 예제와 비슷해도 내부 조회 방식은 다르다.
locker가null인 회원은 회원이 존재해도 결과가 없다고 처리될 수 있다.- 정보 없음 메시지는 회원 자체가 없다는 뜻인지, 연관 객체가 없어 조회 결과에서 빠진 것인지 구분해서 봐야 한다.
필드 조회는 필요한 값만 가져올 수 있지만, 결과를
Object[]로 받기 때문에select순서와 배열 인덱스를 정확히 맞춰야 한다.
MemberTeamTest4.java와MemberTeamTest4_1.java는 연관관계가 없는 회원을 저장하고 전체 회원 목록에서 그 결과를 확인하는 예제이다.
앞 예제들에서는 회원이Team과Locker를 참조하는 흐름을 계속 확인했다.
이번 예제에서는 반대로 팀도 없고 락커도 없는 회원을 저장한다.
이 예제에서 중요한 점은 연관관계 필드가 항상 객체를 참조하는 것은 아니라는 점이다.
Member의team,locker필드에는Team,Locker객체가 들어갈 수도 있지만, 아무 관계가 없으면null이 들어갈 수도 있다.
이 예제의 핵심은 연관관계 필드도 상황에 따라null일 수 있고, 데이터베이스에서는 외래키 컬럼이 비어 있는 값으로 저장될 수 있다는 점이다.
이전 기본개념과 연결하기
앞에서
Member는Team과 다대일 관계를 가진다고 정리했다.
객체 코드에서는Member안의team필드가Team객체를 참조하고, 데이터베이스에서는TEAM_ID외래키 컬럼으로 관계가 저장된다.
또Member는Locker와 일대일 관계를 가진다고 정리했다.
객체 코드에서는Member안의locker필드가Locker객체를 참조하고, 데이터베이스에서는LOCKER_ID외래키 컬럼으로 관계가 저장된다.
하지만 모든 회원이 반드시 팀과 락커를 가져야 하는 것은 아니다.
업무 규칙에 따라 아직 팀이 정해지지 않은 회원이 있을 수 있고, 락커를 사용하지 않는 회원도 있을 수 있다.
이때 객체에서는team과locker에null을 넣어 관계가 없음을 표현할 수 있다.
이 흐름은 앞의MemberTeamTest3_1.java에서 다룬null연관관계 문제와도 이어진다.
연관 필드를 바로 따라가서 값을 조회하거나 출력하려면, 그 연관 객체가 실제로 존재하는지 먼저 생각해야 한다.
기본개념에서 다시 잡아야 할 흐름
Member는Team을 참조할 수 있다.Member는Locker를 참조할 수 있다.team과locker는 연관관계 필드이다.- 연관관계 필드에는 객체가 들어갈 수도 있고
null이 들어갈 수도 있다.team이null이면 회원이 소속 팀을 가지지 않는다는 뜻이다.locker가null이면 회원이 락커를 사용하지 않는다는 뜻이다.- 데이터베이스에서는
TEAM_ID,LOCKER_ID가 비어 있는 값으로 저장될 수 있다.연관관계는 항상 존재한다고 단정하면 안 되고, 관계가 없는 상태도 하나의 데이터 상태로 이해해야 한다.
예제의 목표
이 예제의 목표는 팀과 락커가 없는 회원을 저장하고, 전체 조회 결과에서
team=null,locker=null로 확인하는 것이다.
MemberTeamTest4.java는new Member("토토로", null, null)처럼 회원 객체를 만든다.
첫 번째null은 팀이 없다는 뜻이고, 두 번째null은 락커가 없다는 뜻이다.
그다음MemberTeamTest4_1.java는 전체 회원 목록을 조회한다.
이때 앞에서 저장한토토로,듀크같은 회원이 함께 출력되고, 그 회원들의team,locker가null로 보일 수 있다.
즉, 이 예제는 저장과 조회가 함께 이어지는 구조이다.
먼저null관계를 가진 회원을 저장하고, 다음 실행 파일에서 전체 조회로 그 결과를 확인한다.
이 예제에서 확인할 내용
MemberTeamTest4.java에서 팀과 락커가 없는 회원을 만든다.Member생성자에null,null을 전달한다.persist()로 회원을 저장한다.commit()으로 데이터베이스에 반영한다.MemberTeamTest4_1.java에서 전체 회원 목록을 조회한다.- 조회 결과에서
team=null,locker=null이 출력되는지 확인한다.- 데이터베이스에서는
TEAM_ID,LOCKER_ID가 비어 있는 값으로 저장될 수 있음을 이해한다.이 예제는 연관관계가 없는 회원을 저장하고, 그 상태가 객체 출력과 테이블 외래키 값에 어떻게 나타나는지 확인하는 예제이다.
MemberTeamTest4는 team과 locker가 null인 회원을 저장한다
MemberTeamTest4.java는Team과Locker없이 회원 두 명을 저장한다.
즉, 회원 객체는 만들지만 팀 객체와 락커 객체는 연결하지 않는다.
핵심 코드는 아래와 같다.// MemberTeamTest4.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 em.getTransaction().begin(); // 트랜잭션 시작 Member m1 = new Member("토토로", null, null); // 팀과 락커가 없는 회원 Member m2 = new Member("듀크", null, null); // 팀과 락커가 없는 회원 em.persist(m1); // 토토로 저장 대상으로 등록 em.persist(m2); // 듀크 저장 대상으로 등록 System.out.println("Member에 추가 데이터 저장~~ "); // 저장 안내 출력 em.getTransaction().commit(); // 저장 확정 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기
new Member("토토로", null, null)에서"토토로"는 회원 이름이다.
첫 번째null은team필드에 들어갈 값이다.
즉, 이 회원은 참조하는Team이 없다.
두 번째null은locker필드에 들어갈 값이다.
즉, 이 회원은 참조하는Locker가 없다.
persist(m1)과persist(m2)는 두 회원 객체를 저장 대상으로 만든다.
그리고commit()이 실행되면 데이터베이스에 반영된다.
Member생성자에null을 넘기면 해당 연관관계가 없는 회원 객체를 만들 수 있다.
null 관계는 데이터베이스 외래키 컬럼에 비어 있는 값으로 저장될 수 있다
객체 코드에서
team과locker에null을 넣으면, 데이터베이스에서는 외래키 컬럼이 비어 있는 값으로 저장될 수 있다.
Member의 관계 필드는 아래처럼 외래키 컬럼과 연결되어 있다.// Member.java @ManyToOne // 여러 회원이 하나의 팀과 연결될 수 있음 @JoinColumn(name = "TEAM_ID") // TEAM_ID 외래키 컬럼 private Team team; @OneToOne // 회원 하나가 락커 하나와 연결될 수 있음 @JoinColumn(name = "LOCKER_ID") // LOCKER_ID 외래키 컬럼 private Locker locker;
team에Team객체가 들어 있으면TEAM_ID에 해당 팀의 기본키 값이 저장된다.
하지만team이null이면 연결할 팀이 없으므로TEAM_ID가 비어 있는 값으로 저장될 수 있다.
locker도 마찬가지이다.
locker에Locker객체가 들어 있으면LOCKER_ID에 락커 기본키 값이 저장된다.
하지만locker가null이면 연결할 락커가 없으므로LOCKER_ID가 비어 있는 값으로 저장될 수 있다.
단, 실제로NULL값이 저장되려면 테이블의 외래키 컬럼이NULL을 허용해야 한다.
만약TEAM_ID,LOCKER_ID컬럼이 반드시 값이 있어야 하도록 설정되어 있다면 저장 과정에서 오류가 날 수 있다.
null 관계 저장에서 봐야 할 기준
- 객체의
team이null이면 참조하는 팀이 없다.- 객체의
locker가null이면 참조하는 락커가 없다.- 테이블에서는
TEAM_ID,LOCKER_ID가 비어 있는 값으로 저장될 수 있다.- 외래키 컬럼이
NULL을 허용해야 정상 저장된다.객체의 연관 필드가
null이면 데이터베이스의 외래키 컬럼도 비어 있는 관계 상태를 나타낼 수 있다.
MemberTeamTest4_1은 Member 전체를 조회한다
MemberTeamTest4_1.java는 전체 회원 목록을 조회한다.
앞에서 저장한 팀과 락커가 없는 회원까지 함께 확인할 수 있다.
핵심 코드는 아래와 같다.// MemberTeamTest4_1.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Query query = em.createQuery("select m from Member m", Member.class); // 전체 Member 조회 List<Member> list = query.getResultList(); // 회원 목록 조회 for (Member member : list) { // 회원 목록 반복 System.out.println(member); // 회원 정보 출력 } em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기
select m from Member m은Member엔티티 전체를 조회한다는 뜻이다.
결과는 여러 명일 수 있으므로getResultList()를 사용한다.
조회된 회원 목록은for반복문으로 하나씩 출력한다.
System.out.println(member)는 내부적으로member.toString()을 호출한다.
그래서Member의toString()결과에team,locker필드가 포함되어 있으면 해당 값도 함께 보일 수 있다.
앞에서 저장한토토로,듀크는 팀과 락커를 연결하지 않았기 때문에 출력 결과에서team=null,locker=null로 보일 수 있다.
전체 조회 결과에서team=null,locker=null이 보이면 해당 회원에게 연관관계가 설정되지 않았다는 뜻이다.
toString 출력에서 null 관계를 확인할 수 있다
MemberTeamTest4_1.java는 회원 객체를 그대로 출력한다.
그래서 출력 결과는Member의toString()형식에 따라 보인다.
예상 출력 흐름은 아래와 같다.// 출력결과 // Member(id=..., username=둘리, team=Team(...), locker=Locker(...)) // Member(id=..., username=또치, team=Team(...), locker=Locker(...)) // ... // Member(id=..., username=토토로, team=null, locker=null) // Member(id=..., username=듀크, team=null, locker=null)기존에 저장한
둘리,또치같은 회원은Team,Locker가 연결되어 있을 수 있다.
그래서team=Team(...),locker=Locker(...)처럼 출력될 수 있다.
반면토토로,듀크는new Member("토토로", null, null),new Member("듀크", null, null)로 저장했다.
그래서 출력 결과에서team=null,locker=null로 보일 수 있다.
이 결과는 오류가 아니다.
관계가 없도록 저장했기 때문에 객체 출력에서도 관계가 없다고 보이는 것이다.
출력 결과에서 봐야 할 기준
team=Team(...)이면 팀 객체를 참조한다는 뜻이다.locker=Locker(...)이면 락커 객체를 참조한다는 뜻이다.team=null이면 팀 관계가 없다는 뜻이다.locker=null이면 락커 관계가 없다는 뜻이다.
null출력은 실패가 아니라, 해당 연관관계가 설정되지 않은 상태를 보여 주는 결과일 수 있다.
null 관계는 뒤의 조회와 삭제 예제로 이어진다
팀과 락커가 없는 회원을 저장해 두면, 뒤의 예제에서 이 데이터를 다시 활용할 수 있다.
특히team이null인 회원을 찾거나 삭제하는 예제로 이어진다.
예를 들어 팀이 없는 회원을 찾으려면m.team is null같은 조건을 사용할 수 있다.
is null은 값이 없는 데이터를 찾을 때 사용하는 조건이다.
// NullRelationConditionExample.java String jpql = "select m from Member m where m.team is null"; // 팀이 없는 회원 조회이 조건은
team연관관계가 설정되지 않은 회원을 찾는다.
즉, 앞에서 저장한토토로,듀크같은 회원이 조회 대상이 될 수 있다.
그래서MemberTeamTest4.java는 단순히null데이터를 저장하고 끝나는 예제가 아니다.
뒤에서is null조건 조회와 삭제 흐름을 확인하기 위한 준비 데이터 역할도 한다.
team=null인 회원을 저장해 두면, 뒤에서is null조건으로 해당 회원을 조회하거나 삭제하는 흐름을 확인할 수 있다.
실행 결과는 null 관계가 포함된 회원 목록으로 나온다
MemberTeamTest4.java를 실행하면 저장 완료 메시지가 나온다.
그다음MemberTeamTest4_1.java를 실행하면 전체 회원 목록이 출력된다.
예상 결과 흐름은 아래와 같다.// 출력결과 // Member에 추가 데이터 저장~~// 출력결과 // Member(id=..., username=둘리, team=Team(...), locker=Locker(...)) // ... // Member(id=..., username=토토로, team=null, locker=null) // Member(id=..., username=듀크, team=null, locker=null)실제
id값은 데이터베이스 상태에 따라 달라진다.
중요한 것은토토로,듀크의team,locker가null로 보일 수 있다는 점이다.
MemberTeamTest4.java실행 결과이다.
토토로,듀크회원을 저장하면서membertbl에insert가 실행된다.
이때 두 회원은Team과Locker를 연결하지 않고 생성했기 때문에, 객체 기준으로team=null,locker=null상태로 저장된다.
MemberTeamTest4_1.java실행 결과이다.
전체 회원 목록을 조회하면 기존 회원들은team=Team(...),locker=Locker(...)처럼 연관 객체가 함께 출력된다.
반면토토로,듀크는team=null,locker=null로 출력된다.
이는 두 회원이 팀과 락커를 참조하지 않는 상태로 저장되었음을 보여 준다.
출력 중간에Team,Locker를 추가 조회하는SQL이 보이는 이유는Member를 출력할 때toString()결과 안에서 연관 객체 정보까지 확인하기 때문이다.
여기서 핵심은 추가 조회 자체가 아니라,토토로,듀크의 연관관계가null로 확인된다는 점이다.
이 결과에서 봐야 할 핵심은 팀과 락커 없이 저장한 회원이 전체 조회 결과에서null관계로 확인된다는 점이다.
핵심 정리
MemberTeamTest4.java와MemberTeamTest4_1.java는 팀과 락커가 없는 회원을 저장하고 전체 조회 결과로 확인하는 예제이다.
연관관계가 항상 존재하는 것이 아니라, 필요에 따라null일 수 있음을 보여 준다.
정리하면 아래와 같다.
MemberTeamTest4.java는 팀과 락커가 없는 회원을 저장한다.new Member("토토로", null, null)에서 첫 번째null은 팀이 없다는 뜻이다.new Member("토토로", null, null)에서 두 번째null은 락커가 없다는 뜻이다.persist()로 회원을 저장 대상으로 만든다.commit()으로 데이터베이스에 반영한다.- 객체의
team이null이면TEAM_ID가 비어 있는 값으로 저장될 수 있다.- 객체의
locker가null이면LOCKER_ID가 비어 있는 값으로 저장될 수 있다.MemberTeamTest4_1.java는 전체 회원을 조회한다.- 전체 조회 결과에서
토토로,듀크가team=null,locker=null로 보일 수 있다.- 이 데이터는 뒤에서
is null조건 조회와 삭제 예제의 준비 데이터가 된다.연관관계가 없는 데이터도
JPA에서 다룰 수 있으며, 이때 객체에서는null, 테이블에서는 비어 있는 외래키 값으로 관계 없음이 표현된다.
MemberTeamTest5.java는 사용자가 입력한 멤버명을 기준으로 해당 멤버의 팀명만 조회하는 예제이다.
앞 예제에서는Member전체를 조회하거나, 필요한 여러 필드를Object[]로 조회하는 흐름을 확인했다.
이번 예제에서는Member전체도 아니고 여러 필드도 아니라,Member가 참조하는Team의name값 하나만 조회한다.
이 예제에서 중요한 점은 조회 결과 타입이Member가 아니라String이라는 점이다.
JPQL에서select m.team.name처럼 팀명 하나만 선택하기 때문에, 결과도 팀명 문자열 하나로 받는다.
이 예제의 핵심은Member에서Team으로 연관관계를 따라간 뒤,Team의name값 하나만 조회하는 흐름을 이해하는 것이다.
이전 기본개념과 연결하기
앞에서
Member는Team을 참조할 수 있다고 정리했다.
객체 코드에서는Member안에Team team필드가 있고, 데이터베이스에서는membertbl테이블의TEAM_ID외래키로 관계가 저장된다.
앞 예제들에서는 이 연관관계를 여러 방식으로 사용했다.
MemberTeamTest2.java에서는m.team.name을 조건으로 사용해서 특정 팀에 속한 회원 목록을 조회했다.
MemberTeamTest3.java에서는Member객체 전체를 조회한 뒤dto.getTeam().getName()으로 팀명을 꺼냈다.
MemberTeamTest3_1.java에서는m.username,m.team.name,m.locker.name처럼 필요한 필드 여러 개를 직접 조회했다.
이번 예제는 그보다 더 좁게 본다.
입력한 멤버명에 해당하는Member를 찾고, 그 멤버가 속한 팀 이름 하나만 조회한다.
그래서select절에는m.team.name만 작성한다.
기본개념에서 다시 잡아야 할 흐름
Member는Team을 참조한다.m.username은Member자신의 회원명 필드이다.m.team은Member가 참조하는Team객체이다.m.team.name은 그Team객체의 팀명이다.select m.team.name은 팀명 값 하나만 조회한다.- 조회 결과가 팀명 하나이므로 결과 타입은
String이다.- 결과가 없으면
NoResultException이 발생할 수 있다.이번 예제는
Member객체를 가져오는 예제가 아니라, 연관 객체의 필드 값 하나만 가져오는 예제이다.
예제의 목표
이 예제의 목표는 멤버명을 입력받아 그 멤버가 속한 팀명을 출력하는 것이다.
예를 들어둘리를 입력하면둘리라는 회원을 찾고, 그 회원이 참조하는Team의name값을 출력한다.
조회 조건은m.username = :un이다.
즉, 입력값은Member의username과 비교된다.
하지만 출력되는 값은Member의username이 아니라m.team.name이다.
따라서 이 예제는 조회 조건과 조회 결과가 서로 다르다.
조건은 회원명이고, 결과는 팀명이다.
이 예제에서 확인할 내용
- 사용자에게 멤버명을 입력받는다.
- 입력받은 값을
JPQL파라미터에 연결한다.m.username = :un조건으로 해당 멤버를 찾는다.select m.team.name으로 팀명 하나만 조회한다.- 조회 결과를
TypedQuery<String>으로 받는다.- 결과가 있으면 팀명을 출력한다.
- 결과가 없으면
NoResultException을 처리한다.이 예제는 “회원명을 조건으로 사용하고, 팀명을 결과로 받는다”는 구조를 정확히 구분해야 한다.
코드 흐름
MemberTeamTest5.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음 사용자에게 멤버명을 입력받고, 입력값을JPQL의 파라미터에 연결한다.
조회 결과가 있으면 팀명을 출력하고, 결과가 없으면 팀을 찾을 수 없다는 메시지를 출력한다.
핵심 코드는 아래와 같다.// MemberTeamTest5.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Scanner scan = new Scanner(System.in); // 입력 도구 생성 System.out.print("멤버명을 입력하세요 : "); // 입력 안내 출력 String inputName = scan.nextLine(); // 멤버명 입력 scan.close(); // 입력 도구 닫기 String jpql = "select m.team.name from Member m where m.username = :un"; // 회원명으로 팀명 조회 TypedQuery<String> q = em.createQuery(jpql, String.class); // 팀명 하나를 String으로 조회 q.setParameter("un", inputName); // un 파라미터에 입력값 연결 String teamName; // 조회한 팀명을 담을 변수 try { teamName = q.getSingleResult(); // 팀명 한 건 조회 System.out.printf("%s님의 팀명은 %s입니다...\n", inputName, teamName); // 조회 성공 출력 } catch (NoResultException e) { System.out.printf("%s님의 팀을 찾을 수 없네요..ㅜㅜ \n", inputName); // 조회 결과 없음 출력 e.printStackTrace(); // 예외 내용 출력 } em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기이 코드에서
inputName은 사용자가 입력한 멤버명이다.
setParameter("un", inputName)은JPQL안의:un자리에 입력한 멤버명을 연결한다.
조회 결과는String타입의teamName으로 받는다.
왜냐하면select m.team.name은Team객체 전체를 조회하는 것이 아니라, 팀명 값 하나만 조회하기 때문이다.
select절에서 무엇을 조회하느냐에 따라createQuery()의 결과 타입도 달라진다.
select m.team.name은 팀명 하나만 조회한다
이 예제에서 가장 중요한
JPQL은 아래 문장이다.// MemberTeamTest5.java String jpql = "select m.team.name from Member m where m.username = :un"; // 팀명 하나만 조회
from Member m은Member엔티티를 조회 대상으로 사용한다는 뜻이다.
여기서m은Member를 가리키는 별칭이다.
where m.username = :un은 조건이다.
Member의username값이 입력값과 같은 데이터를 찾는다.
select m.team.name은 조회 결과이다.
조건에 맞는Member를 찾은 뒤, 그Member가 참조하는Team으로 이동하고, 그Team의name값을 가져온다.
나누어 읽으면 아래와 같다.
m은Member이다.m.username은Member의 회원명이다.m.team은Member가 참조하는Team이다.m.team.name은 그Team의 팀명이다.:un은 사용자가 입력한 멤버명이 들어갈 자리이다.즉, 이 문장은 “입력한 멤버명을 가진 회원을 찾고, 그 회원의 팀명을 조회하라”는 뜻이다.
m.team.name은Member에서Team으로 이동한 뒤, 팀 이름 값만 꺼내는 표현이다.
결과 타입은 TypedQuery
<String>이다앞 예제에서
select m from Member m을 사용했을 때는 결과를Member객체로 받았다.
하지만 이번 예제는select m.team.name을 사용한다.
이 결과는 팀명 문자열 하나이다.
그래서 쿼리 타입도 아래처럼 작성한다.// MemberTeamTest5.java TypedQuery<String> q = em.createQuery(jpql, String.class); // String 결과를 받는 TypedQuery
TypedQuery<String>은 조회 결과가String이라고 미리 정해 둔 쿼리이다.
여기서는 팀명 하나가 문자열로 나오기 때문에String.class를 사용한다.
만약select m from Member m이었다면TypedQuery<Member>가 맞다.
하지만 지금은Member객체 전체가 아니라 팀명 하나만 조회하므로TypedQuery<String>이 맞다.
결과 타입 비교
select m:Member객체 전체를 조회하므로TypedQuery<Member>를 사용한다.select m.team:Team객체를 조회하므로TypedQuery<Team>을 사용할 수 있다.select m.team.name: 팀명 문자열을 조회하므로TypedQuery<String>을 사용한다.
JPQL의select대상과TypedQuery의 타입은 서로 맞아야 한다.
파라미터 바인딩으로 입력한 멤버명을 조건에 연결한다
사용자가 입력한 멤버명은
JPQL문자열에 직접 붙이지 않는다.
대신:un이라는 파라미터 자리를 만들고,setParameter()로 입력값을 연결한다.
핵심 코드는 아래와 같다.// MemberTeamTest5.java q.setParameter("un", inputName); // un 파라미터에 입력한 멤버명 연결
:un은 값이 들어갈 자리이다.
setParameter("un", inputName)은:un자리에 사용자가 입력한 멤버명을 넣는다.
파라미터 이름은 반드시 맞아야 한다.
JPQL에서:un을 사용했다면setParameter("un", ...)처럼 같은 이름을 사용해야 한다.
파라미터 바인딩에서 봐야 할 기준
:un은 값이 들어갈 자리이다.setParameter("un", inputName)은:un에 입력값을 연결한다.un이라는 이름은JPQL과setParameter()에서 같아야 한다.- 입력값은
m.username과 비교된다.파라미터 이름이 다르면
JPQL이 어떤 값으로 조건을 비교해야 하는지 알 수 없으므로 오류가 날 수 있다.
getSingleResult는 팀명 한 건을 기대할 때 사용한다
이 예제에서는
getSingleResult()를 사용한다.
입력한 멤버명에 해당하는 팀명 하나를 기대하기 때문이다.
핵심 코드는 아래와 같다.// MemberTeamTest5.java teamName = q.getSingleResult(); // 팀명 한 건 조회
getSingleResult()는 결과가 한 건이라고 기대할 때 사용하는 메서드이다.
여기서는 결과가Member객체 한 건이 아니라, 팀명 문자열 한 건이다.
따라서teamName변수의 타입은String이다.
조회에 성공하면teamName에는 해당 멤버의 팀명이 들어간다.
하지만 조건에 맞는 결과가 없으면NoResultException이 발생할 수 있다.
그래서 이 예제는try-catch로 결과 없음 상황을 처리한다.
// MemberTeamTest5.java try { teamName = q.getSingleResult(); // 정상 조회 System.out.printf("%s님의 팀명은 %s입니다...\n", inputName, teamName); // 성공 메시지 } catch (NoResultException e) { System.out.printf("%s님의 팀을 찾을 수 없네요..ㅜㅜ \n", inputName); // 실패 메시지 }
try안에는 정상 조회 흐름이 들어간다.
catch안에는 조회 결과가 없을 때의 처리 흐름이 들어간다.
getSingleResult()는 결과 한 건을 기대하는 메서드이므로, 결과가 없을 때의 예외 처리까지 함께 봐야 한다.
팀이 없는 멤버도 결과가 없다고 처리될 수 있다
이 예제에서 조심해야 할 부분은 결과 없음의 의미이다.
NoResultException이 발생했다고 해서 항상 “회원 자체가 없다”는 뜻은 아니다.
현재JPQL은select m.team.name이다.
즉, 회원을 찾는 것뿐 아니라 그 회원의 팀명까지 가져오려고 한다.
만약 입력한 멤버가 실제로 존재하더라도team이null이면m.team.name으로 이동할 팀 객체가 없다.
이 경우 조회 결과가 없다고 처리될 수 있다.
예를 들어토토로가new Member("토토로", null, null)로 저장되어 있다면, 회원 데이터는 존재한다.
하지만 팀이 없기 때문에m.team.name값을 가져올 수 없다.
이때토토로님의 팀을 찾을 수 없네요..ㅜㅜ같은 메시지가 나올 수 있다.
따라서 이 예제의 실패 메시지는 두 가지 가능성을 가진다.
- 입력한 멤버명이 실제로 존재하지 않을 수 있다.
- 입력한 멤버는 존재하지만
team연관관계가 없을 수 있다.이 차이를 분명히 이해해야 한다.
회원 존재 여부를 정확히 확인하려면select m from Member m where m.username = :un처럼 먼저Member전체를 조회한 뒤,team이null인지 확인하는 방식이 더 안전할 수 있다.
select m.team.name에서 결과가 없다는 것은 회원이 없다는 뜻일 수도 있고, 회원은 있지만 팀 관계가 없다는 뜻일 수도 있다.
실행 결과는 팀명이 있는 멤버와 없는 멤버로 나누어 확인한다
이 예제는 입력한 멤버명에 따라 결과가 달라진다.
팀이 연결된 멤버를 입력하면 팀명이 출력된다.
팀이 없거나 존재하지 않는 멤버를 입력하면 팀을 찾을 수 없다는 메시지가 출력된다.
예상 결과 흐름은 아래처럼 볼 수 있다.// 출력결과 // 멤버명을 입력하세요 : 둘리 // 둘리님의 팀명은 아기공룡둘리입니다...팀이 없거나 존재하지 않는 멤버를 입력하면 아래처럼 출력될 수 있다.
// 출력결과 // 멤버명을 입력하세요 : 토토로 // 토토로님의 팀을 찾을 수 없네요..ㅜㅜ여기서
토토로가 실제로 저장되어 있다면 이 결과는 회원이 없다는 뜻이 아니다.
팀 연관관계가 없어서m.team.name값을 가져오지 못한 결과로 볼 수 있다.
MemberTeamTest5.java실행 결과이다.
먼저둘리를 입력하면Member.username이 일치하는 회원을 찾고, 그 회원이 참조하는Team의name값만 조회해서 팀명을 출력한다.
이때 조회 결과는Member객체가 아니라 팀명 문자열 하나이므로TypedQuery<String>으로 받는다.
반대로토토로를 입력하면 팀을 찾을 수 없다는 메시지가 출력된다.
토토로는 회원 데이터가 존재하더라도team이null이면m.team.name값을 가져올 수 없기 때문이다.
따라서 이 결과는 단순히 회원이 없다는 뜻이 아니라, 회원은 있어도 팀 연관관계가 없을 수 있다는 뜻으로 해석해야 한다.
아래에 예외 로그가 함께 출력되는 이유는catch구문 안에서e.printStackTrace()를 호출했기 때문이다.
즉, 안내 메시지와 함께 예외 내용을 콘솔에 출력하도록 코드가 작성되어 있기 때문이다.
실행 결과를 볼 때는 “멤버가 없는 것”과 “멤버는 있지만 팀이 없는 것”을 구분해서 해석해야 한다.
MemberTeamTest3과 MemberTeamTest5의 차이
MemberTeamTest3.java와MemberTeamTest5.java는 모두 멤버명을 입력받는다.
하지만 조회 방식과 결과 타입이 다르다.
MemberTeamTest3.java는select m from Member m으로Member객체 전체를 조회한다.
조회된Member객체에서getTeam().getName()과getLocker().getName()으로 연관 객체의 값을 꺼낸다.
MemberTeamTest5.java는select m.team.name으로 팀명 하나만 조회한다.
따라서Member객체를 받지 않고String타입으로 팀명만 받는다.
차이를 정리하면 아래와 같다.
MemberTeamTest3.java:Member전체를 조회한다.MemberTeamTest3.java: 결과 타입은Member이다.MemberTeamTest3.java: 조회 후 객체 참조를 따라가 팀명과 락커명을 꺼낸다.MemberTeamTest5.java: 팀명 하나만 직접 조회한다.MemberTeamTest5.java: 결과 타입은String이다.MemberTeamTest5.java:TypedQuery<String>을 사용한다.MemberTeamTest5.java: 팀이 없는 멤버는 결과가 없다고 처리될 수 있다.
Member전체를 조회할지, 팀명 값 하나만 조회할지에 따라 결과 타입과 예외 해석이 달라진다.
핵심 정리
MemberTeamTest5.java는 멤버명을 입력받아 해당 멤버의 팀명만 조회하는 예제이다.
Member전체를 조회하지 않고m.team.name값 하나만 조회하기 때문에 결과 타입은String이다.
정리하면 아래와 같다.
MemberTeamTest5.java는 멤버명을 입력받는다.- 입력값은
m.username과 비교된다.select m.team.name은 멤버가 참조하는 팀의 이름만 조회한다.- 결과 타입은
String이다.- 그래서
TypedQuery<String>을 사용한다.getSingleResult()로 팀명 한 건을 조회한다.- 결과가 없으면
NoResultException이 발생할 수 있다.- 결과 없음은 회원 자체가 없다는 뜻일 수도 있다.
- 결과 없음은 회원은 있지만
team이null이라는 뜻일 수도 있다.e.printStackTrace()가 있으면 결과 없음 상황에서 예외 로그도 함께 출력될 수 있다.- 회원 존재 여부와 팀 존재 여부를 구분하려면
Member전체 조회 후team을 확인하는 방식이 더 명확할 수 있다.이 예제는 연관 객체 전체가 아니라 연관 객체의 필드 값 하나만 직접 조회할 때 결과 타입이 어떻게 달라지는지 보여 준다.
MemberTeamTest6.java는team연관관계가 없는 회원을 조회하고 삭제하는 예제이다.
앞 예제에서는MemberTeamTest4.java에서토토로,듀크처럼Team과Locker를 연결하지 않은 회원을 저장했다.
이번 예제에서는team이null인 회원을 찾아 삭제한다.
이 예제에서 중요한 점은null도 조회 조건으로 사용할 수 있다는 점이다.
일반 값 비교처럼=를 사용하는 것이 아니라, 값이 없는 상태를 찾을 때는is null을 사용한다.
이 예제의 핵심은 연관관계가 없는 데이터를m.team is null조건으로 조회하고, 조회된 엔티티를remove()로 삭제하는 흐름을 이해하는 것이다.
이전 기본개념과 연결하기
앞 예제에서
Member는Team과Locker를 참조할 수 있다고 정리했다.
하지만 모든 회원이 반드시 팀과 락커를 가지는 것은 아니었다.
new Member("토토로", null, null)처럼 생성하면team과locker가 없는 회원을 저장할 수 있었다.
객체 코드에서는team=null,locker=null로 보이고, 데이터베이스에서는TEAM_ID,LOCKER_ID외래키 컬럼이 비어 있는 값으로 저장될 수 있다.
즉, 연관관계가 없다는 상태도 하나의 데이터 상태이다.
이번 예제는 그 상태를 조회 조건으로 사용한다.
팀이 없는 회원만 찾기 위해m.team is null을 사용한다.
그리고 조회된 회원 객체를remove()로 삭제한다.
기본개념에서 다시 잡아야 할 흐름
team은Member가 참조하는Team연관 필드이다.team=null이면 소속 팀이 없다는 뜻이다.- 값이 없는 데이터를 찾을 때는
is null을 사용한다.m.team is null은 팀이 없는 회원을 찾는 조건이다.- 삭제는 조회된 엔티티 객체를 대상으로
remove()를 호출한다.- 삭제 작업도 데이터 변경이므로 트랜잭션 안에서 실행해야 한다.
commit()이 실행되어야 삭제 결과가 데이터베이스에 반영된다.연관관계가 없는 데이터를 다룰 때는
null상태를 조회 조건과 삭제 기준으로 사용할 수 있다.
예제의 목표
이 예제의 목표는 팀이 없는 회원을 찾아 삭제하는 것이다.
20번에서 저장한토토로,듀크는team=null,locker=null상태였다.
이번 예제에서는team이null인 회원을 조회한다.
다만m.team is null은 이름이토토로,듀크인 회원만 찾는 조건이 아니다.
이 조건은 팀 연관관계가 없는 모든Member를 조회한다.
따라서 현재 데이터에서 팀이 없는 회원이토토로,듀크뿐이면 두 회원이 삭제 대상이 된다.
조회 결과가 있으면 전체 개수를 먼저 출력한다.
그다음stream().forEach()로 조회된 회원을 하나씩 처리하면서remove()를 호출한다.
마지막으로 트랜잭션을commit()하면 삭제 내용이 데이터베이스에 반영된다.
이 예제에서 확인할 내용
- 트랜잭션을 시작한다.
m.team is null조건으로 팀이 없는 회원을 조회한다.- 조회 결과를
List<Member>로 받는다.- 팀이 없는 회원 수를 출력한다.
- 조회된 회원이 있으면
stream().forEach()로 하나씩 삭제한다.remove()로 조회된 회원을 삭제한다.commit()으로 삭제를 확정한다.- 삭제 후 다시 조회하면 해당 회원들이 사라질 수 있음을 이해한다.
이 예제는
null조건으로 대상을 찾고, 찾은 엔티티를 삭제하는 흐름을 확인하는 예제이다.
코드 흐름
MemberTeamTest6.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음 트랜잭션을 시작하고,team이null인 회원을 조회한다.
조회된 회원 수를 출력한 뒤, 조회 결과가 있으면remove()로 삭제한다.
핵심 코드는 아래와 같다.// MemberTeamTest6.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 em.getTransaction().begin(); // 트랜잭션 시작 String jpql = "select m from Member m where m.team is null"; // 팀이 없는 회원 조회 TypedQuery<Member> q = em.createQuery(jpql, Member.class); // Member 타입 쿼리 생성 List<Member> list = q.getResultList(); // 조회 결과 목록으로 받기 System.out.println("팀이 설정되지 않은 회원 명수 : " + list.size() + "명!!"); // 조회된 회원 수 출력 if (list.size() > 0) { // 삭제 대상이 있으면 list.stream().forEach(x -> { // 조회된 회원을 하나씩 처리 em.remove(x); // 현재 회원 삭제 System.out.println("삭제함"); // 삭제 처리 메시지 출력 }); } System.out.println("데이터 삭제~~ "); // 삭제 흐름 안내 출력 em.getTransaction().commit(); // 삭제 확정 em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기이 코드는
team이 없는 회원을 찾아 삭제한다.
select m from Member m where m.team is null은Member중에서team참조가 없는 데이터만 조회한다.
조회 결과는 여러 명일 수 있으므로getResultList()로 받는다.
그리고list.size()로 삭제 대상이 몇 명인지 먼저 출력한다.
삭제 대상이 있으면stream().forEach()가 목록의 회원을 하나씩 꺼내고, 각 회원에 대해em.remove(x)를 실행한다.
삭제할 대상을 먼저 조회하고, 조회된 엔티티 객체를remove()에 전달해야 한다.
is null은 값이 없는 데이터를 찾는 조건이다
null은 값이 없다는 뜻이다.
일반 값처럼= null로 비교하지 않는다.
값이 없는 상태를 조회할 때는is null을 사용한다.
이번 예제의 핵심 조건은 아래와 같다.// MemberTeamTest6.java String jpql = "select m from Member m where m.team is null"; // team이 없는 회원 조회
m은Member를 가리킨다.
m.team은Member가 참조하는Team이다.
m.team is null은Member가 참조하는Team이 없다는 뜻이다.
즉, 이 조건은 “팀이 연결되지 않은 회원을 조회하라”는 의미이다.
앞에서 저장한토토로,듀크처럼new Member("토토로", null, null)로 만든 회원이 조회 대상이 될 수 있다.
하지만 이 조건은 특정 이름을 기준으로 찾는 조건이 아니다.
username이토토로인지듀크인지 비교하지 않는다.
오직team연관관계가 없는지를 기준으로 조회한다.
is null 조건에서 봐야 할 기준
null은 값이 없는 상태이다.= null이 아니라is null을 사용한다.m.team is null은 팀이 없는 회원을 찾는 조건이다.- 연관 필드도
null조건으로 조회할 수 있다.- 이 조건은 팀이 없는 모든 회원을 조회한다.
null여부를 조건으로 조회할 때는=가 아니라is null을 사용해야 한다.
getResultList로 삭제 대상 목록을 받는다
m.team is null조건에 맞는 회원은 한 명일 수도 있고, 여러 명일 수도 있다.
그래서 이 예제는getSingleResult()를 사용하지 않는다.
여러 결과를 받을 수 있는getResultList()를 사용한다.
핵심 코드는 아래와 같다.// MemberTeamTest6.java List<Member> list = q.getResultList(); // 팀이 없는 회원 목록 조회 System.out.println("팀이 설정되지 않은 회원 명수 : " + list.size() + "명!!"); // 조회 개수 출력
getResultList()는 조건에 맞는 결과를 목록으로 반환한다.
조건에 맞는 회원이 없다면 빈 목록이 반환될 수 있다.
list.size()는 목록에 들어 있는 데이터 개수를 반환한다.
따라서 이 출력은 팀이 없는 회원이 몇 명인지 확인하는 역할을 한다.
예를 들어토토로,듀크만 팀이 없다면2명으로 출력될 수 있다.
반대로 이미 삭제해서 대상이 없다면0명으로 출력될 수 있다.
삭제하기 전에list.size()로 삭제 대상이 몇 명인지 확인하면 현재 데이터 상태를 먼저 파악할 수 있다.
remove는 조회된 엔티티를 삭제한다
remove()는 엔티티를 삭제할 때 사용하는 메서드이다.
이 예제에서는team이null인 회원을 먼저 조회한 뒤, 그 회원 객체를remove()에 넘긴다.
핵심 코드는 아래와 같다.// MemberTeamTest6.java if (list.size() > 0) { // 삭제 대상이 있으면 list.stream().forEach(x -> { // 조회된 회원을 하나씩 꺼냄 em.remove(x); // 현재 회원 삭제 System.out.println("삭제함"); // 삭제 처리 확인 }); }
list에는m.team is null조건에 맞는Member객체들이 들어 있다.
stream().forEach()는 그 객체들을 하나씩 처리한다.
x는 현재 처리 중인Member객체 하나이다.
em.remove(x)는 현재 회원을 삭제 대상으로 만든다.
이때x는 방금 조회된 엔티티이므로JPA가 관리하는 상태이다.
관리되는 엔티티를 삭제하는 흐름이므로remove()를 사용할 수 있다.
System.out.println("삭제함")은 실제 삭제 대상마다 한 번씩 출력된다.
따라서 삭제 대상이 두 명이면삭제함도 두 번 출력될 수 있다.
remove()는 삭제할 엔티티 객체를 대상으로 호출하며, 실제 삭제 반영은 트랜잭션commit()시점에 이루어진다.
삭제 작업은 트랜잭션 안에서 실행해야 한다
조회만 하는 작업은 트랜잭션이 없어도 실행될 수 있다.
하지만 삭제는 데이터베이스 내용을 바꾸는 작업이다.
그래서 트랜잭션 안에서 처리해야 한다.
이번 예제도 아래 흐름으로 진행된다.// MemberTeamTest6.java em.getTransaction().begin(); // 트랜잭션 시작 // 삭제 대상 조회 // remove()로 삭제 em.getTransaction().commit(); // 삭제 반영
begin()은 트랜잭션을 시작한다.
이후 실행되는 삭제 작업은 이 트랜잭션 안에서 관리된다.
commit()은 삭제 내용을 데이터베이스에 반영한다.
remove()를 호출했다고 해서 곧바로 최종 삭제가 끝난 것으로 보면 안 된다.
삭제 확정은commit()시점에 이루어진다.
만약 삭제 중 오류가 발생할 수 있는 코드라면try-catch와rollback()으로 되돌리는 흐름을 넣을 수도 있다.
이 예제에서는 핵심 흐름을 보기 위해 삭제 대상 조회, 삭제, 커밋 흐름에 집중한다.
삭제처럼 데이터를 변경하는 작업은begin()과commit()사이에서 실행해야 한다.
실행 결과는 삭제 대상 수와 delete SQL로 확인한다
이 예제를 실행하면 먼저
team이null인 회원 수가 출력된다.
그다음 삭제 대상이 있으면삭제함메시지가 출력되고,Hibernate로그에서delete문이 실행되는 것을 확인할 수 있다.
예상 결과 흐름은 아래처럼 볼 수 있다.// 출력결과 // 팀이 설정되지 않은 회원 명수 : 2명!! // 삭제함 // 삭제함 // 데이터 삭제~~// SQL 로그 예시 // delete from membertbl where MEMBER_ID=? // delete from membertbl where MEMBER_ID=?출력 결과에서 회원 명수가
2명으로 보이면 팀이 없는 회원 두 명이 조회된 것이다.
삭제함이 두 번 출력되면 두 회원에 대해remove()가 실행된 것이다.
delete로그가 보이면 해당 회원들이 삭제 대상으로 처리된 것이다.
이미MemberTeamTest6.java를 실행해 삭제한 뒤 다시 실행하면 조회 결과가0명일 수 있다.
이때deleteSQL이 나오지 않는 것은 오류가 아니다.
삭제할 대상이 이미 사라졌기 때문이다.
다시 확인하려면MemberTeamTest4.java로토토로,듀크를 다시 저장한 뒤 실행해야 한다.
MemberTeamTest6.java실행 전MemberTeamTest4_1.java로 전체 회원 목록을 조회한 결과이다.
이 시점에는토토로,듀크가team=null,locker=null상태로 존재한다.
즉, 두 회원은 팀과 락커 연관관계가 없는 삭제 대상 데이터이다.
MemberTeamTest6.java실행 결과이다.
m.team is null조건으로 팀이 설정되지 않은 회원을 조회하면 삭제 대상 수가2명으로 출력된다.
그다음stream().forEach()안에서remove()가 실행되면서삭제함메시지가 두 번 출력된다.
마지막에delete from membertblSQL이 두 번 실행되는 것을 보면, 조회된 두 회원이 실제 삭제 대상으로 처리되었음을 확인할 수 있다.
MemberTeamTest6.java실행 후MemberTeamTest4_1.java를 다시 실행한 결과이다.
전체 회원 목록을 다시 조회하면 앞에서 삭제된토토로,듀크가 더 이상 출력되지 않는다.
이 결과로remove()와commit()을 통해 삭제가 데이터베이스에 반영되었음을 확인할 수 있다.
실행 결과에서는 삭제 대상 수가 몇 명인지,remove()가 몇 번 실행되는지, 그리고deleteSQL이 실행되는지를 함께 확인해야 한다.
삭제 후에는 전체 조회 결과가 달라질 수 있다
MemberTeamTest6.java를 실행해team이null인 회원을 삭제하면, 이후 전체 조회 결과가 달라진다.
예를 들어MemberTeamTest4_1.java를 다시 실행하면, 이전에 보였던토토로,듀크가 더 이상 보이지 않을 수 있다.
삭제가commit()으로 반영되었기 때문이다.
이 흐름은 데이터 변경 작업을 확인할 때 중요하다.
삭제 전에는 전체 조회에서team=null,locker=null회원이 보인다.
삭제 후에는 같은 전체 조회를 실행해도 그 회원들이 사라질 수 있다.
확인 흐름은 아래처럼 볼 수 있다.
MemberTeamTest4_1.java실행:토토로,듀크가 보인다.MemberTeamTest6.java실행:토토로,듀크를 삭제한다.- 다시
MemberTeamTest4_1.java실행:토토로,듀크가 사라졌는지 확인한다.이렇게 보면 삭제 예제의 결과를 더 정확하게 확인할 수 있다.
단순히delete로그만 보는 것보다, 삭제 전후 전체 조회 결과를 비교하면 데이터가 실제로 사라졌는지 이해하기 쉽다.
삭제 결과는delete로그뿐 아니라, 삭제 후 다시 조회했을 때 대상 데이터가 사라졌는지까지 확인하면 더 명확하다.
재실행할 때는 데이터 상태를 먼저 확인해야 한다
MemberTeamTest6.java는 삭제 예제이다.
따라서 한 번 실행하면 조건에 맞는 데이터가 실제로 삭제된다.
그래서 같은 예제를 다시 실행했을 때 처음과 같은 결과가 나오지 않을 수 있다.
처음 실행에서는 팀이 없는 회원 수가2명처럼 출력되고deleteSQL이 실행될 수 있다.
하지만 두 번째 실행에서는 이미 삭제된 상태라서 조회 결과가0명일 수 있다.
이것은 코드가 잘못된 것이 아니다.
데이터베이스 상태가 바뀌었기 때문에 결과가 달라진 것이다.
다시 같은 결과를 보고 싶다면 아래 흐름으로 진행하면 된다.
MemberTeamTest4.java를 실행해서토토로,듀크를 다시 저장한다.MemberTeamTest4_1.java로team=null,locker=null상태를 확인한다.MemberTeamTest6.java를 실행해서 다시 삭제 흐름을 확인한다.삭제 예제는 실행할 때마다 데이터 상태가 바뀌므로, 재실행 전에는 삭제할 대상 데이터가 실제로 있는지 먼저 확인해야 한다.
앞 예제와의 연결 흐름
MemberTeamTest4.java,MemberTeamTest4_1.java,MemberTeamTest6.java는 하나의 흐름으로 이어진다.
먼저MemberTeamTest4.java에서 팀과 락커가 없는 회원을 저장한다.
그다음MemberTeamTest4_1.java에서 전체 회원 목록을 조회해team=null,locker=null상태를 확인한다.
마지막으로MemberTeamTest6.java에서m.team is null조건으로 해당 회원들을 찾아 삭제한다.
흐름을 정리하면 아래와 같다.
MemberTeamTest4.java:토토로,듀크를team=null,locker=null상태로 저장한다.MemberTeamTest4_1.java: 전체 회원 목록에서토토로,듀크의null관계를 확인한다.MemberTeamTest6.java:m.team is null조건으로 팀이 없는 회원을 조회한다.MemberTeamTest6.java: 조회된 회원을remove()로 삭제한다.단,
MemberTeamTest6.java는 이름으로토토로,듀크만 고르는 예제가 아니다.
팀이 없는 모든 회원을 삭제 대상으로 삼는다.
따라서 실제 데이터에 팀이 없는 다른 회원이 있다면 그 회원도 함께 조회되고 삭제될 수 있다.
22번은 20번에서 만든null관계 데이터를 조건 조회와 삭제에 활용하는 예제이다.
핵심 정리
MemberTeamTest6.java는team이null인 회원을 조회하고 삭제하는 예제이다.
앞 예제에서 만든토토로,듀크같은 데이터를 대상으로is null조건과remove()동작을 확인한다.
정리하면 아래와 같다.
m.team is null은 팀이 없는 회원을 찾는 조건이다.m.team is null은토토로,듀크라는 이름만 찾는 조건이 아니다.- 데이터에 팀이 없는 다른 회원이 있으면 그 회원도 삭제 대상이 될 수 있다.
null값을 비교할 때는= null이 아니라is null을 사용한다.- 조회 결과는 여러 명일 수 있으므로
getResultList()를 사용한다.list.size()로 삭제 대상 수를 확인한다.- 조회된 회원은
stream().forEach()로 하나씩 처리한다.em.remove(x)는 현재 회원을 삭제 대상으로 만든다.- 삭제는 데이터 변경 작업이므로 트랜잭션 안에서 실행한다.
commit()이 실행되어야 삭제가 데이터베이스에 반영된다.- 실행 결과는 삭제 대상 수,
삭제함메시지,deleteSQL로 확인할 수 있다.- 이미 삭제한 뒤 다시 실행하면 결과가
0명일 수 있다.- 삭제 후 전체 조회를 다시 실행하면 대상 회원이 사라졌는지 확인할 수 있다.
is null조건으로 연관관계가 없는 데이터를 찾고, 조회된 엔티티를remove()로 삭제하는 흐름을 이해하면JPA에서 조건 삭제 흐름을 더 쉽게 볼 수 있다.
MemberTeamTest7.java는 하나의 파일 안에서 여러 조회 방식을 비교하는 예제이다.
앞 예제들에서는JPQL을 사용해Member전체를 조회하거나, 특정 필드만 조회하거나, 조건에 맞는 데이터를 삭제했다.
이번 예제에서는find(),Native SQL,JPQL을 한 번에 실행하면서 조회 방식의 차이를 확인한다.
이 예제에서 중요한 점은 모두Member데이터를 조회하지만, 조회하는 방법과 결과 타입이 다르다는 점이다.
find()는 기본키로Entity하나를 조회한다.
Native SQL은 실제 테이블 이름과 컬럼명을 사용해 조회한다.
JPQL은 테이블이 아니라Entity와 필드 이름을 기준으로 조회한다.
이 예제의 핵심은 같은Member데이터를 조회하더라도find(),Native SQL,JPQL은 작성 방식과 결과 처리 방식이 다르다는 점을 구분하는 것이다.
이전 기본개념과 연결하기
앞 예제에서
Member는 데이터베이스의membertbl테이블과 매핑된Entity라고 정리했다.
객체 코드에서는Member라는 클래스 이름을 사용하지만, 실제 데이터베이스 테이블은membertbl이다.
이 차이는 조회 방식을 비교할 때 중요하다.
JPQL은 데이터베이스 테이블명을 직접 쓰지 않고Entity이름인Member를 사용한다.
반대로Native SQL은 데이터베이스에 직접 보내는SQL이기 때문에 실제 테이블명인membertbl을 사용한다.
또 앞에서는getResultList()로 여러 결과를 목록으로 받는 방식도 확인했다.
이번 예제에서도 전체 조회는 여러 결과가 나올 수 있으므로List<Member>또는List<String>으로 받는다.
기본개념에서 다시 잡아야 할 흐름
Member는JPA가 관리하는Entity이다.membertbl은 실제 데이터베이스 테이블명이다.find()는 기본키로Entity하나를 조회한다.Native SQL은 실제 테이블명과 컬럼명을 사용한다.JPQL은Entity이름과 필드 이름을 사용한다.- 전체 조회 결과는 여러 개일 수 있으므로
List로 받는다.
JPQL은 객체 중심 조회이고,Native SQL은 데이터베이스 테이블 중심 조회이다.
예제의 목표
이 예제의 목표는
Member데이터를 여러 방식으로 조회하면서 차이를 비교하는 것이다.
첫 번째는em.find(Member.class, 2)로 기본키가2인 회원을 조회한다.
두 번째는Native SQL로membertbl전체를 조회하고, 결과를Member객체로 받는다.
세 번째는Native SQL로username컬럼만 조회하고, 결과를 문자열 목록으로 받는다.
네 번째는JPQL로Member전체를 조회한다.
즉, 이번 예제는 단순히 결과를 출력하는 예제가 아니다.
조회 방식마다 어떤 이름을 쓰는지, 결과 타입이 어떻게 달라지는지, 어떤 상황에서 사용할 수 있는지 비교하는 예제이다.
이 예제에서 확인할 내용
find()로 기본키 기준 단건 조회를 한다.find()에서 사용하는 기본키 값은 실제 데이터베이스에 존재해야 한다.createNativeQuery()로 실제SQL을 실행한다.select * from membertbl결과를Member객체로 매핑한다.select username from membertbl결과를String목록으로 받는다.createQuery()로JPQL을 실행한다.select m from Member m결과를Member목록으로 받는다.이번 예제는 조회 결과보다 조회 방식의 차이를 비교하는 것이 더 중요하다.
코드 흐름
MemberTeamTest7.java는 먼저EntityManagerFactory와EntityManager를 만든다.
그다음 네 가지 조회를 순서대로 실행한다.
각 조회 결과 사이에는"=".repeat(50)을 출력해서 콘솔에서 결과 구간을 구분한다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java EntityManagerFactory factory = Persistence.createEntityManagerFactory("entitytest"); // 설정 묶음으로 Factory 생성 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Member m = em.find(Member.class, 2); // 기본키가 2인 Member 조회 System.out.println(m); // 조회 결과 출력 System.out.println("=".repeat(50)); // 구분선 출력 Query q = em.createNativeQuery("select * from membertbl order by username", Member.class); // Native SQL로 전체 조회 List<Member> list = q.getResultList(); // Member 목록으로 결과 받기 if (list.size() != 0) { // 조회 결과가 있으면 list.stream().forEach(System.out::println); // 결과를 한 줄씩 출력 } System.out.println("=".repeat(50)); // 구분선 출력 q = em.createNativeQuery("select username from membertbl order by username"); // Native SQL로 username만 조회 List<String> namelist = q.getResultList(); // 이름 목록으로 결과 받기 if (namelist.size() != 0) { // 조회 결과가 있으면 namelist.stream().forEach(System.out::println); // 이름을 한 줄씩 출력 } System.out.println("=".repeat(50)); // 구분선 출력 TypedQuery<Member> tq = em.createQuery("select m from Member m", Member.class); // JPQL로 Member 전체 조회 List<Member> empList = tq.getResultList(); // Member 목록으로 결과 받기 for (Member elem : empList) { // 조회 결과 반복 System.out.println(elem); // Member 출력 } em.close(); // EntityManager 닫기 factory.close(); // Factory 닫기이 코드에서
entitytest를 사용하는 이유는Member,Team,Locker가persistence.xml의entitytest설정에 등록되어 있기 때문이다.
Member계열 예제는entitytest기준으로 실행해야 한다.
이 예제는 조회 방식이 여러 개이므로, 각 코드 구간이 어떤 방식의 조회인지 먼저 나누어 읽어야 한다.
find는 기본키로 Entity 하나를 조회한다
첫 번째 조회는
find()이다.
find()는Entity클래스와 기본키 값을 전달해서 데이터 하나를 조회한다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java Member m = em.find(Member.class, 2); // 기본키가 2인 Member 조회 System.out.println(m); // 조회 결과 출력
Member.class는 조회할Entity타입이다.
2는 기본키 값이다.
즉, 이 코드는 기본키가2인Member를 찾아오라는 뜻이다.
find()는 테이블명이나JPQL문자열을 직접 작성하지 않는다.
JPA가Member엔티티의 매핑 정보를 보고, 기본키가2인 데이터를 조회한다.
다만 주석에id는 알아서 입력이라고 되어 있는 이유를 생각해야 한다.
데이터베이스 상태에 따라 실제 존재하는Member의 기본키 값은 달라질 수 있다.
따라서 실행할 때는 현재 테이블에 존재하는MEMBER_ID값을 넣어야 조회 결과가 나온다.
만약 해당 기본키 값이 존재하지 않으면find()의 결과는null이 될 수 있다.
이때 콘솔에null이 출력되는 것은find()가 실패해서 예외를 던진 것이 아니라, 해당 기본키를 가진Member가 없어서 조회 결과가 없다는 뜻이다.
따라서 실행 전에 현재membertbl에 존재하는MEMBER_ID값을 확인하고 넣어야 한다.
find에서 봐야 할 기준
find()는 기본키로 한 건을 조회한다.- 첫 번째 인자는 조회할
Entity클래스이다.- 두 번째 인자는 기본키 값이다.
- 기본키 값이 실제로 존재해야 결과가 나온다.
- 기본키 값이 없으면 결과가
null일 수 있다.- 결과 타입은 조회한
Entity타입이다.
find()는 조건문을 직접 작성하지 않고, 기본키 값으로Entity하나를 바로 조회할 때 사용한다.
find 결과가 null일 수 있는 상황
find(Member.class, 2)에서2는 예시 기본키 값이다.
현재 데이터베이스에MEMBER_ID가2인 회원이 있으면Member객체가 출력된다.
하지만 해당 값이 없으면null이 출력될 수 있다.
이 상황은 코드 문법 오류가 아니다.
find()는 기본키 조회 메서드이므로, 해당 기본키가 없으면 가져올 엔티티가 없는 것이다.
예를 들어 아래처럼 볼 수 있다.// FindNullExample.java Member m = em.find(Member.class, 9999); // 존재하지 않는 기본키라고 가정 System.out.println(m); // null 출력 가능이 결과를 보고
JPA설정이 잘못됐다고 바로 판단하면 안 된다.
먼저 현재membertbl에 해당 기본키 값이 실제로 존재하는지 확인해야 한다.
기본키 확인 기준
find()는username이 아니라 기본키로 조회한다.Member의 기본키는MEMBER_ID이다.find(Member.class, 2)는MEMBER_ID = 2인 회원을 찾는다.- 해당
MEMBER_ID가 없으면 결과는null일 수 있다.
find()결과가null이면 먼저 코드보다 현재 테이블에 해당 기본키 값이 존재하는지 확인해야 한다.
Native SQL은 실제 테이블명과 컬럼명을 사용한다
두 번째 조회는
Native SQL이다.
Native SQL은 데이터베이스에서 직접 사용하는SQL문법을 그대로 작성하는 방식이다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java Query q = em.createNativeQuery("select * from membertbl order by username", Member.class); // 실제 SQL 실행 List<Member> list = q.getResultList(); // Member 목록으로 결과 받기여기서는
Member가 아니라membertbl을 사용한다.
membertbl은 실제 데이터베이스 테이블명이다.
그래서Native SQL에서는select * from membertbl처럼 작성한다.
뒤에Member.class를 함께 전달했다.
이 말은 조회 결과를Member엔티티 객체로 매핑하겠다는 뜻이다.
select *로membertbl의 컬럼들을 조회하고, 그 결과를Member객체로 만들어 받는다.
결과는 여러 명일 수 있으므로getResultList()를 사용한다.
반환 결과는List<Member>이다.
Native SQL에서는Entity이름이 아니라 실제 데이터베이스 테이블명인membertbl을 사용한다.
select * 결과를 Member 객체로 받을 수 있다
createNativeQuery()에Member.class를 함께 넘기면 조회 결과를Member객체로 받을 수 있다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java Query q = em.createNativeQuery("select * from membertbl order by username", Member.class); // 결과를 Member로 매핑 List<Member> list = q.getResultList(); // Member 목록 if (list.size() != 0) { // 결과가 있으면 list.stream().forEach(System.out::println); // Member 객체 출력 }
select * from membertbl은 테이블의 모든 컬럼을 조회한다.
이 컬럼들이Member엔티티의 매핑 정보와 맞으면,JPA는 결과를Member객체로 만들 수 있다.
System.out::println은 각Member객체를 출력한다.
Member에toString()이 있으면 회원 정보가 문자열로 출력된다.
이때 출력 결과에는team,locker같은 연관 객체 정보도 보일 수 있다.
왜냐하면Member객체를 출력하는 과정에서toString()이 연관 필드까지 포함할 수 있기 때문이다.
Native SQL 전체 조회에서 봐야 할 기준
select * from membertbl은 실제 테이블 전체 컬럼을 조회한다.Member.class를 함께 전달하면 결과를Member객체로 받을 수 있다.- 결과가 여러 개이므로
List<Member>로 받는다.- 출력은
stream().forEach(System.out::println)으로 처리한다.
Native SQL도 결과 매핑 타입을 지정하면 엔티티 객체 목록으로 받을 수 있다.
특정 컬럼 하나만 조회하면 결과 타입도 달라진다
세 번째 조회도
Native SQL이다.
하지만 이번에는 전체 컬럼이 아니라username컬럼 하나만 조회한다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java q = em.createNativeQuery("select username from membertbl order by username"); // username 컬럼만 조회 List<String> namelist = q.getResultList(); // 이름 목록으로 결과 받기 if (namelist.size() != 0) { // 결과가 있으면 namelist.stream().forEach(System.out::println); // 이름만 출력 }이번
SQL은select username from membertbl이다.
즉,Member전체를 조회하는 것이 아니라username값만 조회한다.
그래서 결과를Member객체로 받을 수 없다.
결과는 이름 문자열들이므로List<String>으로 받는다.
이 흐름은 앞의MemberTeamTest5.java와 비슷하다.
앞에서는select m.team.name으로 팀명 문자열 하나를 조회했다.
이번에는 실제SQL로select username을 실행해서 회원명 문자열 목록을 조회한다.
컬럼 하나만 조회할 때 봐야 할 기준
select username은 회원 이름 값만 조회한다.Member전체를 조회하는 것이 아니다.- 결과는 문자열 목록이다.
- 그래서
List<String>으로 받을 수 있다.조회하는 대상이 엔티티 전체인지, 특정 값 하나인지에 따라 결과 타입이 달라진다.
JPQL은 Entity 이름을 기준으로 조회한다
마지막 조회는
JPQL이다.
JPQL은 데이터베이스 테이블명이 아니라Entity이름을 기준으로 작성한다.
핵심 코드는 아래와 같다.// MemberTeamTest7.java TypedQuery<Member> tq = em.createQuery("select m from Member m", Member.class); // JPQL로 Member 조회 List<Member> empList = tq.getResultList(); // Member 목록 조회 for (Member elem : empList) { // 결과 반복 System.out.println(elem); // Member 출력 }
select m from Member m에서Member는 테이블명이 아니라Entity이름이다.
실제 테이블명이membertbl이어도JPQL에서는Member라고 쓴다.
TypedQuery<Member>를 사용한 이유는 조회 결과가Member객체 목록이기 때문이다.
getResultList()는 여러Member객체를 목록으로 반환한다.
이 구간은 두 번째Native SQL전체 조회와 결과가 비슷해 보일 수 있다.
하지만 작성 방식이 다르다.
Native SQL은select * from membertbl이고,JPQL은select m from Member m이다.
JPQL은 테이블 중심이 아니라Entity중심으로 조회문을 작성한다.
Native SQL과 JPQL의 차이
Native SQL과JPQL은 둘 다 데이터를 조회할 수 있다.
하지만 기준이 다르다.
Native SQL은 데이터베이스 중심이다.
실제 테이블명과 컬럼명을 사용한다.
그래서membertbl,username같은 데이터베이스 이름이 그대로 나온다.
JPQL은 객체 중심이다.
Entity이름과 필드 이름을 사용한다.
그래서Member,m.username,m.team처럼 객체 코드에서 사용하는 이름을 기준으로 작성한다.
비교하면 아래와 같다.// NativeSqlAndJpqlCompare.java em.createNativeQuery("select * from membertbl order by username", Member.class); // 실제 테이블명 사용 em.createQuery("select m from Member m", Member.class); // Entity 이름 사용첫 번째는 실제
SQL이다.
membertbl이라는 테이블명을 사용한다.
두 번째는JPQL이다.
Member라는Entity이름을 사용한다.
차이 정리
Native SQL: 실제 테이블명과 컬럼명을 사용한다.Native SQL: 데이터베이스에 가까운 조회 방식이다.JPQL:Entity이름과 필드 이름을 사용한다.JPQL: 객체 코드에 가까운 조회 방식이다.- 같은 데이터를 조회해도 작성 방식이 다르다.
Native SQL은 데이터베이스 이름을 쓰고,JPQL은Entity이름을 쓴다는 차이를 반드시 구분해야 한다.
출력 구분선은 조회 결과를 나누기 위한 장치이다
이 예제에는 아래 코드가 여러 번 나온다.
// MemberTeamTest7.java System.out.println("=".repeat(50)); // 구분선 출력
"=".repeat(50)은"="문자를50번 반복한 문자열을 만든다.
즉, 콘솔에 긴 구분선을 출력한다.
이 구분선은 데이터 처리에 영향을 주는 핵심 문법은 아니다.
여러 조회 결과가 한 화면에 이어서 출력되면 어디까지가 어떤 조회 결과인지 헷갈릴 수 있다.
그래서 출력 구간을 나누기 위해 사용한다.
이 예제에서는find()결과,Native SQL전체 조회 결과,Native SQL컬럼 조회 결과,JPQL전체 조회 결과를 구분하는 역할을 한다.
구분선은 조회 로직이 아니라, 여러 결과를 콘솔에서 보기 쉽게 나누는 출력용 코드이다.
실행 결과는 조회 방식별로 나누어 확인한다
이 예제를 실행하면 여러 조회 결과가 순서대로 출력된다.
구분선을 기준으로 결과를 나누어 보면 이해하기 쉽다.
예상 흐름은 아래처럼 볼 수 있다.// 출력결과 // Member(id=2, username=..., team=..., locker=...) // ================================================== // Member(id=..., username=..., team=..., locker=...) // Member(id=..., username=..., team=..., locker=...) // ... // ================================================== // 둘리 // 또치 // ... // ================================================== // Member(id=..., username=..., team=..., locker=...) // Member(id=..., username=..., team=..., locker=...)첫 번째 구간은
find()결과이다.
기본키로 조회한Member한 명이 출력된다.
만약 기본키 값이 존재하지 않으면 이 구간은null로 출력될 수 있다.
두 번째 구간은Native SQL로membertbl전체를 조회한 결과이다.
Member객체 목록이 출력된다.
세 번째 구간은Native SQL로username컬럼만 조회한 결과이다.
회원 이름만 한 줄씩 출력된다.
네 번째 구간은JPQL로Member전체를 조회한 결과이다.
다시Member객체 목록이 출력된다.
MemberTeamTest7.java실행 결과이다.
첫 번째 구간에서는find(Member.class, 2)로 기본키가2인Member한 명을 조회한다.
이때 현재membertbl에 해당 기본키 값이 존재하면Member객체가 출력되고, 존재하지 않으면null이 출력될 수 있다.
두 번째 구간에서는Native SQL인select * from membertbl order by username으로 실제 테이블을 조회하고, 결과를Member객체 목록으로 받는다.
세 번째 구간에서는select username from membertbl order by username으로username컬럼만 조회해서 이름 목록만 출력한다.
마지막 구간에서는JPQL인select m from Member m으로Member엔티티 전체를 조회한다.
구분선"=".repeat(50)을 기준으로 보면find(),Native SQL, 컬럼 단독 조회,JPQL조회 결과를 나누어 확인할 수 있다.
실행 결과는 한 덩어리로 보지 말고, 구분선을 기준으로 어떤 조회 방식의 결과인지 나누어 읽어야 한다.
핵심 정리
MemberTeamTest7.java는Member데이터를 여러 방식으로 조회하는 예제이다.
find(),Native SQL,JPQL이 한 파일 안에 함께 나오므로 각각의 기준을 비교해야 한다.
정리하면 아래와 같다.
find(Member.class, id)는 기본키로Member한 명을 조회한다.id값은 현재 데이터베이스에 실제로 존재하는 값이어야 한다.id값이 존재하지 않으면find()결과는null일 수 있다.createNativeQuery("select * from membertbl", Member.class)는 실제 테이블을 조회하고 결과를Member로 매핑한다.createNativeQuery("select username from membertbl")는 이름 컬럼만 조회한다.- 이름 컬럼만 조회하면 결과는
List<String>으로 받을 수 있다.createQuery("select m from Member m", Member.class)는JPQL로Member전체를 조회한다.Native SQL은 실제 테이블명과 컬럼명을 사용한다.JPQL은Entity이름과 필드 이름을 사용한다."=".repeat(50)은 결과 구간을 나누기 위한 출력용 코드이다.같은 데이터를 조회해도
find(),Native SQL,JPQL은 기준과 결과 타입이 다르므로 코드에서 어떤 조회 방식을 쓰는지 먼저 구분해야 한다.
Visitor예제는 방명록 데이터를JPA로 저장, 조회, 검색, 수정, 삭제하는 흐름을 보여 주는 예제이다.
앞 예제들에서는 실행 파일 안에서EntityManager를 직접 만들고 바로 조회하거나 삭제했다.
이번 예제에서는 역할을 더 나누어VisitorApp,VisitorController,VisitorDAO,Visitor가 각각 다른 책임을 맡는다.
이 구조를 보면 실제 프로그램에서 사용자 입력, 기능 처리, 데이터베이스 작업이 어떻게 나뉘는지 이해하기 쉽다.
VisitorApp.java는 메뉴 입력을 받는다.
VisitorController.java는 메뉴에서 선택된 기능을 처리하기 위해Visitor객체를 만들거나 출력 흐름을 담당한다.
VisitorDAO.java는 실제 데이터베이스 조회, 저장, 수정, 삭제 작업을 담당한다.
이 예제의 핵심은 실행 앱이 데이터베이스 작업을 직접 처리하지 않고,Controller와DAO를 거쳐CRUD흐름을 분리한다는 점이다.
이전 기본개념과 연결하기
앞에서는
EntityManager를 직접 사용해서persist(),find(),remove(),JPQL,Native SQL같은 흐름을 확인했다.
이 방식은 실습 흐름을 이해하기에는 좋지만, 기능이 많아지면 실행 파일이 너무 복잡해질 수 있다.
실제 프로그램에서는 사용자 입력을 처리하는 코드, 기능 흐름을 조정하는 코드, 데이터베이스 작업을 처리하는 코드를 나누는 편이 좋다.
그래야 메뉴 흐름은 메뉴 흐름대로 읽히고, 데이터 저장과 조회는 데이터 작업 코드 안에서 따로 확인할 수 있다.
이 예제에서는 그 역할이 아래처럼 나뉜다.
VisitorApp.java는 콘솔 메뉴와 사용자 입력을 담당한다.VisitorController.java는 사용자가 선택한 기능을 실행하기 위해DAO를 호출한다.VisitorDAO.java는 실제JPA데이터베이스 작업을 담당한다.Visitor.java는 방명록 한 건의 데이터를 담는Entity이다.이 예제는 단순한
DAO예제가 아니라App → Controller → DAO → Entity흐름을 확인하는 예제이다.
예제의 목표
이 예제의 목표는 방명록 프로그램에서
CRUD가 어떤 파일을 거쳐 실행되는지 확인하는 것이다.
CRUD는 생성, 조회, 수정, 삭제를 의미한다.
방명록 글을 저장하고, 목록을 보고, 검색하고, 수정하고, 삭제하는 기능이 모두 이 흐름에 포함된다.
이 예제는 파일 하나만 보면 흐름이 잘 보이지 않는다.
VisitorApp.java만 보면 메뉴 번호 입력만 보인다.
VisitorController.java만 보면DAO호출과 출력 처리가 보인다.
VisitorDAO.java만 보면 실제JPA코드가 보인다.
Visitor.java만 보면 데이터 구조만 보인다.
따라서 네 파일을 역할 기준으로 연결해서 봐야 한다.
이 예제에서 확인할 내용
Visitor가 방명록 한 건을 표현하는Entity인지 확인한다.VisitorApp이 메뉴 입력을 받고VisitorController메서드를 호출하는지 확인한다.VisitorController가VisitorDAO를 가지고 기능별 메서드를 실행하는지 확인한다.VisitorDAO가EntityManager를 만들고 실제JPQL,find(),persist(),remove()를 실행하는지 확인한다.- 저장, 수정, 삭제가
Transaction안에서 처리되는지 확인한다.- 수정은 조회한 영속 객체의 값을 바꾸고
commit()시점에 반영되는지 확인한다.이 흐름을 이해하면 뒤의
Meeting예제도 더 쉽게 이어진다.
Meeting예제도 앱, 컨트롤러,DAO, 엔티티가 나뉘어 동작하기 때문이다.
Visitor는 방명록 한 건을 표현하는 Entity이다
Visitor.java는 방명록 한 건의 데이터를 담는Entity이다.
id,name,writeDate,memo필드를 가진다.
코드 구조는 아래와 같다.// Visitor.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @Entity // JPA가 관리할 Entity 클래스 public class Visitor { @Id // 기본키 필드 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; // 방명록 글 번호 private String name; // 작성자 이름 @CreationTimestamp // 저장 시점의 날짜 자동 입력 private java.sql.Date writeDate; // 작성일 private String memo; // 방명록 내용 }
@Entity가 붙어 있으므로Visitor는JPA가 관리하는 엔티티이다.
id에는@Id와@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있다.
따라서id는 기본키이고, 데이터베이스가 값을 자동으로 생성할 수 있다.
name은 작성자 이름이다.
memo는 방명록 글 내용이다.
writeDate에는@CreationTimestamp가 붙어 있다.
@CreationTimestamp는 데이터가 처음 저장되는 시점의 날짜를 자동으로 넣는 역할을 한다.
즉, 사용자가 직접 입력하는 값은 작성자 이름과 방명록 내용이다.
글 번호와 작성일은 저장 흐름에서 자동으로 채워지는 값으로 볼 수 있다.
Visitor에서 봐야 할 핵심
Visitor는 방명록 한 건을 표현한다.id는 기본키이다.IDENTITY전략으로id가 자동 생성될 수 있다.name은 작성자 이름이다.memo는 방명록 내용이다.writeDate는@CreationTimestamp로 저장 시점에 자동 입력될 수 있다.
Visitor는 사용자가 입력한 글 내용을 객체로 담고,JPA가 데이터베이스 테이블과 연결할 수 있게 만든 클래스이다.
VisitorApp은 메뉴 입력을 담당한다
VisitorApp.java는 콘솔에서 메뉴를 보여 주고 사용자의 번호 입력을 받는다.
직접EntityManager를 만들지 않는다.
직접JPQL을 작성하지도 않는다.
대신VisitorController객체를 만들고, 메뉴 번호에 따라 컨트롤러 메서드를 호출한다.
핵심 구조는 아래와 같다.// VisitorApp.java Scanner scan = new Scanner(System.in); // 사용자 입력 도구 생성 VisitorController vc = new VisitorController(); // Controller 생성 while (true) { // 메뉴 반복 실행 System.out.println("1. 방명록 리스트 보기"); // 전체 조회 메뉴 System.out.println("2. 방명록 검색"); // 검색 메뉴 System.out.println("3. 방명록 글 저장"); // 저장 메뉴 System.out.println("4. 방명록 글 수정"); // 수정 메뉴 System.out.println("5. 방명록 글 삭제"); // 삭제 메뉴 System.out.println("6. 종료"); // 종료 메뉴 int num = scan.nextInt(); // 메뉴 번호 입력 scan.nextLine(); // 줄바꿈 처리 if (num == 1) { vc.listVisitor(); // 전체 목록 조회 요청 } else if (num == 2) { String keyword = scan.nextLine(); // 검색어 입력 vc.searchVisitor(keyword); // 검색 요청 } else if (num == 3) { String name = scan.nextLine(); // 작성자 입력 String memo = scan.nextLine(); // 방명록 내용 입력 vc.insertVisitor(name, memo); // 저장 요청 } else if (num == 4) { int id = scan.nextInt(); // 수정할 글 번호 입력 scan.nextLine(); // 줄바꿈 처리 if (vc.oneVisitor(id, false)) { // 수정 대상 존재 여부 확인 String name = scan.nextLine(); // 새 작성자 입력 String memo = scan.nextLine(); // 새 내용 입력 vc.updateVisitor(id, name, memo); // 수정 요청 } } else if (num == 5) { int id = scan.nextInt(); // 삭제할 글 번호 입력 vc.deleteVisitor(id); // 삭제 요청 } else { vc.close(); // DAO 자원 정리 break; // 반복 종료 } }
VisitorApp은 메뉴 흐름을 담당한다.
사용자가 어떤 번호를 입력했는지 판단하고, 필요한 값을 추가로 입력받은 뒤VisitorController에 넘긴다.
여기서 중요한 점은VisitorApp이VisitorDAO를 직접 호출하지 않는다는 것이다.
VisitorApp은VisitorController를 호출한다.
그리고VisitorController가 내부에서VisitorDAO를 호출한다.
VisitorApp에서 봐야 할 핵심
Scanner로 메뉴 번호와 입력값을 받는다.while (true)로 메뉴를 반복 실행한다.1번은 전체 목록 조회이다.2번은 검색이다.3번은 저장이다.4번은 수정이다.5번은 삭제이다.6번은 종료이다.- 실제 기능 실행은
VisitorController메서드 호출로 이어진다.
VisitorApp은 데이터베이스 작업을 직접 하지 않고, 사용자 입력을 받아VisitorController로 넘기는 역할을 한다.
VisitorController는 App과 DAO 사이에서 기능 흐름을 조정한다
VisitorController.java는VisitorApp과VisitorDAO사이에 있는 중간 역할이다.
사용자의 메뉴 입력으로 넘어온 값을 받아Visitor객체를 만들거나,DAO에서 받은 결과를 출력한다.
코드 구조를 보면VisitorController안에는VisitorDAO필드가 있다.
생성자에서VisitorDAO객체를 만든다.// VisitorController.java public class VisitorController { private VisitorDAO dao; // DAO 참조 변수 public VisitorController() { dao = new VisitorDAO(); // DAO 객체 생성 } }이 구조 때문에
VisitorController는VisitorDAO의 메서드를 호출할 수 있다.
즉,VisitorApp은 컨트롤러를 호출하고, 컨트롤러는DAO를 호출한다.
흐름은 아래처럼 이어진다.// VisitorFlow.java VisitorApp // 메뉴 입력 담당 VisitorController // 기능 처리와 출력 담당 VisitorDAO // 실제 JPA 데이터 작업 담당 Visitor // 방명록 데이터 Entity이 구조를 잡아야 예제가 정확히 보인다.
기존처럼VisitorApp → VisitorDAO로 바로 설명하면 중간에서VisitorController가 하는 일이 빠진다.
VisitorController는 메뉴 입력과 데이터베이스 작업 사이에서 기능 흐름을 연결하는 중간 계층이다.
VisitorController는 조회 결과를 출력하고 저장할 객체를 만든다
VisitorController의 메서드는 크게 조회 출력, 저장 요청, 수정 요청, 삭제 요청으로 나누어 볼 수 있다.
전체 목록 출력
listVisitor()는dao.listAll()로 전체 목록을 가져온 뒤, 각Visitor객체의 값을 출력한다.// VisitorController.java public void listVisitor() { List<Visitor> list = dao.listAll(); // 전체 목록 조회 for (Visitor vo : list) { // 조회 결과 반복 System.out.print(vo.getId() + "\t"); // 글 번호 출력 System.out.print(vo.getName() + "\t"); // 작성자 출력 System.out.print(vo.getWriteDate() + "\t"); // 작성일 출력 System.out.println(vo.getMemo()); // 글 내용 출력 } }
DAO는 데이터를 가져오는 역할을 하고,Controller는 가져온 데이터를 화면에 보기 좋게 출력하는 역할을 한다.
콘솔 프로그램이므로 여기서 말하는 화면은 콘솔 출력이다.
단건 존재 여부 확인
oneVisitor()는 글 번호로 방명록 한 건을 조회한다.
수정할 때는 먼저 해당 번호의 글이 있는지 확인해야 한다.
그래서VisitorApp의 수정 메뉴에서 이 메서드를 사용한다.// VisitorController.java public boolean oneVisitor(int id, boolean output) { Visitor vo = dao.one(id); // 기본키로 단건 조회 boolean result = false; // 존재 여부 기본값 if (vo != null) { // 조회 결과가 있으면 result = true; // 존재한다고 표시 if (output) { // 출력 옵션이 true이면 System.out.print(vo.getId() + "\t"); // 글 번호 출력 System.out.print(vo.getName() + "\t"); // 작성자 출력 System.out.print(vo.getWriteDate() + "\t"); // 작성일 출력 System.out.println(vo.getMemo()); // 글 내용 출력 } } return result; // 존재 여부 반환 }
output은 조회 결과를 바로 출력할지 정하는 값이다.
수정 메뉴에서는 존재 여부만 확인하면 되기 때문에false로 호출한다.
이 메서드의 핵심은boolean반환값이다.
방명록 글이 있으면true, 없으면false를 반환한다.
그래서 수정 메뉴는 이 값을 보고 수정 입력을 받을지 말지 결정한다.
검색 출력
searchVisitor()는 검색어를 받아memo에 검색어가 포함된 방명록 글을 출력한다.// VisitorController.java public void searchVisitor(String keyword) { List<Visitor> list = dao.search(keyword); // 검색 결과 조회 if (list.size() == 0) { // 검색 결과가 없으면 System.out.println(keyword + "를 포함한 글은 없네요~~"); // 없음 출력 } else { for (Visitor vo : list) { // 검색 결과 반복 System.out.print(vo.getId() + "\t"); // 글 번호 출력 System.out.print(vo.getName() + "\t"); // 작성자 출력 System.out.print(vo.getWriteDate() + "\t"); // 작성일 출력 System.out.println(vo.getMemo()); // 글 내용 출력 } } }검색 결과가 없을 수도 있다.
그래서list.size() == 0으로 결과 개수를 확인한 뒤 메시지를 다르게 출력한다.
저장 요청
insertVisitor()는 작성자 이름과 글 내용을 받아Visitor객체를 만든다.
그다음dao.insert(vo)를 호출해서 저장을 요청한다.// VisitorController.java public void insertVisitor(String name, String memo) { Visitor vo = new Visitor(); // 새 Visitor 객체 생성 vo.setName(name); // 작성자 저장 vo.setMemo(memo); // 글 내용 저장 boolean result = dao.insert(vo); // 저장 요청 if (result) { // 저장 성공이면 System.out.println(name + "님의 글이 성공적으로 저장했어요^^"); // 성공 메시지 } else { System.out.println(name + "님의 글 저장 작업을 실패했어요ㅜㅜ"); // 실패 메시지 } }여기서
VisitorController는 직접persist()를 호출하지 않는다.
저장할Visitor객체만 만들고, 실제 저장은VisitorDAO의insert()에 맡긴다.
Controller는 저장할 객체를 준비하고, 실제 데이터베이스 저장은DAO가 처리한다.
수정 요청
updateVisitor()는 수정할 글 번호, 작성자, 내용을 받아Visitor객체에 담는다.
그다음dao.update(vo)를 호출한다.// VisitorController.java public void updateVisitor(int id, String name, String memo) { Visitor vo = new Visitor(); // 수정 값을 담을 객체 생성 vo.setId(id); // 수정 대상 번호 저장 vo.setName(name); // 새 작성자 저장 vo.setMemo(memo); // 새 글 내용 저장 boolean result = dao.update(vo); // 수정 요청 if (result) { // 수정 성공이면 System.out.println(name + "님의 글이 성공적으로 수정했어요^^"); // 성공 메시지 } else { System.out.println(name + "님의 글 수정 작업을 실패했어요ㅜㅜ"); // 실패 메시지 } }이 객체는 새로 저장할 객체라기보다, 수정할 값들을 묶어서
DAO로 넘기는 객체로 보면 된다.
실제 수정은DAO에서 기존 데이터를 조회한 뒤, 그 객체의 값을 바꾸는 방식으로 처리된다.
삭제 요청
deleteVisitor()는 삭제할 글 번호를 받아dao.delete(id)를 호출한다.// VisitorController.java public void deleteVisitor(int id) { boolean result = dao.delete(id); // 삭제 요청 if (result) { // 삭제 성공이면 System.out.println(id + "번 글을 성공적으로 삭제했어요^^"); // 성공 메시지 } else { System.out.println(id + "번 글이 존재하지 않아요ㅜㅜ"); // 실패 메시지 } }삭제할 때도
Controller는remove()를 직접 호출하지 않는다.
삭제할 번호만DAO에 넘긴다.
실제 삭제 대상 조회와 삭제 확정은DAO에서 처리된다.
VisitorController에서 봐야 할 핵심
VisitorController는 내부에VisitorDAO를 가진다.- 전체 조회와 검색 결과를 콘솔에 출력한다.
- 저장할 때는
Visitor객체를 만들고dao.insert()를 호출한다.- 수정할 때는 수정할 값을
Visitor객체에 담고dao.update()를 호출한다.- 삭제할 때는 글 번호를
dao.delete()로 넘긴다.- 성공 여부에 따라 다른 메시지를 출력한다.
VisitorController가 있어야VisitorApp의 메뉴 입력과VisitorDAO의 데이터 작업이 자연스럽게 연결된다.
VisitorDAO는 실제 JPA 데이터 작업을 담당한다
VisitorDAO.java는 실제 데이터베이스 작업을 처리한다.
EntityManagerFactory를 가지고 있고, 각 메서드 안에서EntityManager를 만들어 조회, 저장, 수정, 삭제를 실행한다.
기본 구조는 아래와 같다.// VisitorDAO.java public class VisitorDAO { private EntityManagerFactory factory; // EntityManagerFactory 보관 public VisitorDAO() { factory = Persistence.createEntityManagerFactory("emptest"); // emptest 설정으로 Factory 생성 } public void close() { if (factory != null) { // Factory가 있으면 factory.close(); // Factory 자원 정리 } } }여기서는
"emptest"설정 묶음을 사용한다.
이 설정을 기준으로EntityManagerFactory가 만들어지고, 각 메서드에서EntityManager를 생성한다.
VisitorDAO의 메서드는 작업이 끝나면em.close()로EntityManager를 닫는다.
프로그램 종료 시에는close()에서EntityManagerFactory를 닫는다.
VisitorDAO는JPA코드가 실제로 들어 있는 위치이며, 데이터베이스 접근 책임을 맡는다.
listAll은 방명록 전체 목록을 조회한다
listAll()은 방명록 전체 데이터를 조회하는 메서드이다.
JPQL의select t from Visitor t를 사용한다.
// VisitorDAO.java public List<Visitor> listAll() { EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Visitor> q = em.createQuery( "select t from Visitor t", Visitor.class ); // Visitor 전체 조회 JPQL 생성 List<Visitor> list = q.getResultList(); // 여러 건 조회 em.close(); // EntityManager 닫기 return list; // 조회 결과 반환 }
Visitor는 테이블명이 아니라 엔티티 이름이다.
JPQL은 실제 테이블명이 아니라 엔티티 이름을 기준으로 작성한다.
getResultList()는 여러 건의 결과를List로 가져온다.
방명록 전체 조회는 결과가 여러 개일 수 있으므로getResultList()가 사용된다.
one은 기본키로 방명록 한 건을 조회한다
one()은 글 번호id를 기준으로 방명록 한 건을 조회한다.
기본키 조회이므로find()를 사용한다.
// VisitorDAO.java public Visitor one(int id) { EntityManager em = factory.createEntityManager(); // EntityManager 생성 Visitor vo = em.find(Visitor.class, id); // 기본키로 Visitor 한 건 조회 em.close(); // EntityManager 닫기 return vo; // 조회 결과 반환 }
find(Visitor.class, id)는Visitor엔티티 중 기본키가id인 데이터를 찾는다.
조회 결과가 있으면Visitor객체를 반환한다.
조회 결과가 없으면null이 될 수 있다.
이 메서드는 수정 전에 글이 존재하는지 확인할 때도 사용된다.
search는 memo에 검색어가 포함된 글을 찾는다
search()는 검색어를 받아 방명록 내용에 해당 검색어가 포함된 글을 찾는다.
여기서는like조건을 사용한다.
// VisitorDAO.java public List<Visitor> search(String keyword) { EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Visitor> q = em.createQuery( "select t from Visitor t WHERE t.memo like :keyword", Visitor.class ); // memo 포함 검색 JPQL 생성 q.setParameter("keyword", "%" + keyword + "%"); // 검색어 앞뒤에 % 붙이기 List<Visitor> list = q.getResultList(); // 검색 결과 조회 em.close(); // EntityManager 닫기 return list; // 검색 결과 반환 }
:keyword는JPQL안에서 값을 나중에 넣을 자리이다.
setParameter()는 그 자리에 실제 값을 연결한다.
"%" + keyword + "%"는 검색어 앞뒤에%를 붙이는 코드이다.
%는 앞뒤에 어떤 글자가 있어도 된다는 의미로 사용된다.
따라서 검색어가memo중간에 들어 있어도 조회될 수 있다.
search에서 봐야 할 핵심
like는 일부 문자열 검색에 사용한다.:keyword는 이름 기반 파라미터이다.setParameter()로 실제 검색어를 연결한다.%검색어%형태로 만들면 검색어가 포함된 데이터를 찾을 수 있다.검색 기능은
like조건과 파라미터 바인딩을 함께 사용해서 만든다.
insert는 새 방명록 글을 저장한다
insert()는 새Visitor객체를 데이터베이스에 저장한다.
저장은 데이터베이스 상태를 바꾸는 작업이므로Transaction안에서 처리한다.
// VisitorDAO.java public boolean insert(Visitor vo) { boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 em.persist(vo); // 새 Visitor 저장 요청 em.getTransaction().commit(); // 저장 확정 } catch (Exception e) { result = false; // 예외 발생 시 실패 처리 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }
persist(vo)는 새Visitor객체를 저장 대상으로 등록한다.
commit()이 실행되면 저장 작업이 확정된다.
Visitor의id는IDENTITY전략이므로 데이터베이스가 자동으로 생성할 수 있다.
writeDate에는@CreationTimestamp가 있으므로 저장 시점의 날짜가 자동으로 들어갈 수 있다.
저장은persist()호출뿐 아니라Transaction의commit()까지 함께 봐야 한다.
update는 기존 방명록 글을 수정한다
update()는 전달받은Visitor객체의id를 기준으로 기존 데이터를 찾고, 조회된 객체의 값을 바꾼다.
// VisitorDAO.java public boolean update(Visitor vo) { boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 Visitor oldVo = em.find(Visitor.class, vo.getId()); // 수정 대상 조회 System.out.println(oldVo.getName()); // 기존 작성자 출력 oldVo.setName(vo.getName()); // 새 작성자 반영 oldVo.setMemo(vo.getMemo()); // 새 글 내용 반영 em.getTransaction().commit(); // 변경 감지 후 수정 확정 } catch (Exception e) { result = false; // 예외 발생 시 실패 처리 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }수정에서 중요한 점은
persist()가 다시 호출되지 않는다는 것이다.
먼저find()로 기존Visitor를 조회한다.
이렇게 조회된 객체는 영속 상태이다.
영속 상태는JPA가 관리하는 상태라는 뜻이다.
그 상태에서oldVo.setName(),oldVo.setMemo()로 값을 바꾸면commit()시점에 변경 내용이 반영될 수 있다.
이 흐름을 변경 감지라고 한다.
변경 감지는 영속 상태의 객체 값이 바뀐 것을JPA가 감지해서 데이터베이스에 반영하는 기능이다.
단,find()결과가 없으면oldVo가null이 될 수 있다.
이 경우oldVo.getName()이나oldVo.setName()을 호출하는 과정에서 예외가 발생할 수 있고,catch에서 실패 처리된다.
update에서 봐야 할 핵심
- 수정 대상은
find()로 먼저 조회한다.- 조회된 영속 객체의 값을 바꾼다.
persist()를 다시 호출하지 않는다.commit()시점에 변경 감지로 수정 내용이 반영될 수 있다.- 수정 대상이 없으면 실패 처리될 수 있다.
수정은 새 객체를 저장하는 흐름이 아니라, 기존 영속 객체를 조회한 뒤 값을 바꾸는 흐름이다.
delete는 기존 방명록 글을 삭제한다
delete()는 삭제할 글 번호를 받아 기존Visitor객체를 조회한 뒤 삭제한다.
삭제도 데이터베이스 상태를 바꾸는 작업이므로Transaction안에서 처리한다.
// VisitorDAO.java public boolean delete(int id) { boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 Visitor vo = em.find(Visitor.class, id); // 삭제 대상 조회 em.remove(vo); // 삭제 요청 em.getTransaction().commit(); // 삭제 확정 } catch (Exception e) { result = false; // 예외 발생 시 실패 처리 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }삭제할 때도 먼저
find()가 필요하다.
remove()는 삭제할Entity객체를 인자로 받기 때문이다.
기본키 숫자만 바로 넘기는 것이 아니다.
find()결과가 없으면vo가null이 될 수 있다.
이 경우remove()흐름이 정상적으로 진행되지 않고 실패 처리될 수 있다.
delete에서 봐야 할 핵심
- 삭제 대상은
find()로 먼저 조회한다.remove()는 삭제할Entity객체를 받는다.remove()는 삭제 요청이다.commit()이 실행되어야 삭제가 확정된다.- 삭제 대상이 없으면 실패 처리될 수 있다.
삭제는
find()로 대상을 찾고,remove()로 삭제 요청한 뒤,commit()으로 확정하는 흐름이다.
전체 실행 흐름은 App에서 Controller를 거쳐 DAO로 내려간다
이 예제를 정확히 이해하려면 기능 하나가 실행될 때 어느 파일을 거치는지 봐야 한다.
예를 들어 방명록 저장 기능은 아래 순서로 실행된다.
// VisitorInsertFlow.java VisitorApp // 3번 메뉴 선택, 작성자와 글 내용 입력 VisitorController.insertVisitor(name, memo) // Visitor 객체 생성 VisitorDAO.insert(vo) // Transaction 시작, persist, commit Visitor // 저장할 방명록 Entity저장 흐름에서
VisitorApp은 입력만 받는다.
VisitorController는 입력값으로Visitor객체를 만든다.
VisitorDAO는 그 객체를 데이터베이스에 저장한다.
수정 흐름도 같은 기준으로 볼 수 있다.// VisitorUpdateFlow.java VisitorApp // 4번 메뉴 선택, 수정할 id 입력 VisitorController.oneVisitor(id, false) // 수정 대상 존재 여부 확인 VisitorApp // 새 작성자와 새 글 내용 입력 VisitorController.updateVisitor(id, name, memo) // 수정 값 전달 VisitorDAO.update(vo) // 기존 Entity 조회 후 값 변경, commit수정은 저장보다 한 단계가 더 있다.
먼저 수정할 글이 있는지 확인한다.
글이 있으면 새 값을 입력받고, 그 값을DAO로 넘겨 기존 데이터를 수정한다.
삭제 흐름은 아래와 같다.// VisitorDeleteFlow.java VisitorApp // 5번 메뉴 선택, 삭제할 id 입력 VisitorController.deleteVisitor(id) // 삭제 요청 전달 VisitorDAO.delete(id) // 삭제 대상 조회, remove, commit이 흐름을 보면
VisitorController의 역할이 분명해진다.
VisitorController는 단순히 파일 이름만 있는 것이 아니라, 앱과DAO사이에서 기능 실행 흐름을 이어 주는 코드이다.
정확한 실행 흐름은VisitorApp → VisitorController → VisitorDAO → Visitor순서로 이해해야 한다.
결과물은 어떻게 나오는가
VisitorApp.java는 메뉴 기반 프로그램이다.
실행하면 먼저 메뉴가 출력되고, 사용자가 입력한 번호에 따라 결과가 달라진다.
이번 결과물은1번부터5번 메뉴까지 각각 나누어 확인한다.
1번은 전체 목록 조회,2번은 검색,3번은 저장,4번은 수정,5번은 삭제 흐름이다.
VisitorApp.java에서1번 메뉴를 실행한 결과이다.
1번은 방명록 전체 목록을 조회하는 기능이다.
콘솔 메뉴에서 번호를 입력하면VisitorController의listVisitor()가 호출되고,listVisitor()는VisitorDAO의listAll()로 전체 목록을 가져온 뒤 출력한다.
VisitorApp.java에서2번 메뉴를 실행한 결과이다.
2번은 검색어를 입력받아 방명록 글 내용에서 해당 검색어가 포함된 데이터를 찾는 기능이다.
이 흐름은VisitorController의searchVisitor()를 거쳐VisitorDAO의search()에서memo like :keyword조건으로 검색하는 방식과 연결된다.
VisitorApp.java에서3번 메뉴를 실행한 결과이다.
3번은 작성자와 방명록 글을 입력받아 새 방명록 데이터를 저장하는 기능이다.
VisitorController의insertVisitor()는 입력값으로Visitor객체를 만들고,VisitorDAO의insert()가persist()와commit()으로 저장을 확정한다.
VisitorApp.java에서4번 메뉴를 실행한 결과이다.
4번은 기존 방명록 글을 수정하는 기능이다.
먼저VisitorController의oneVisitor()로 수정할 글이 있는지 확인한다.
글이 있으면 새 작성자와 새 내용을 입력받고,VisitorController의updateVisitor()를 거쳐VisitorDAO의update()에서 기존 영속 객체 값을 변경한다.
VisitorApp.java에서5번 메뉴를 실행한 결과이다.
5번은 기존 방명록 글을 삭제하는 기능이다.
VisitorController의deleteVisitor()가 삭제할 글 번호를VisitorDAO의delete()로 넘기고,delete()는 삭제 대상을 조회한 뒤remove()와commit()으로 삭제를 확정한다.
이 결과를 통해VisitorApp의 메뉴 입력이VisitorController를 거쳐 방명록 조회, 검색, 저장, 수정, 삭제 기능으로 이어지고, 실제 데이터 작업은VisitorDAO의CRUD메서드에서 처리된다는 점을 확인할 수 있다.
핵심 정리
Visitor예제는 방명록 데이터를JPA로 처리하는Controller,DAO기반CRUD구조이다.
Visitor는 방명록 한 건을 표현하는Entity이다.
VisitorApp은 사용자의 메뉴 입력을 받는다.
VisitorController는 메뉴 입력으로 선택된 기능을 처리하고DAO를 호출한다.
VisitorDAO는 실제 데이터베이스 작업을 담당한다.
정리하면 아래와 같다.
Visitor는 방명록 데이터를 담는Entity이다.id는 기본키이고,IDENTITY전략으로 자동 생성될 수 있다.writeDate는@CreationTimestamp로 저장 시점에 자동 입력될 수 있다.VisitorApp은 메뉴 번호와 입력값을 받는다.VisitorApp은VisitorDAO를 직접 호출하지 않고VisitorController를 호출한다.VisitorController는 내부에VisitorDAO를 가진다.VisitorController는 조회 결과를 출력하고, 저장·수정에 필요한Visitor객체를 만든다.VisitorController는 성공 여부에 따라 사용자에게 메시지를 출력한다.VisitorDAO는 방명록 전체 조회, 단건 조회, 검색, 저장, 수정, 삭제를 담당한다.listAll()은JPQL로 전체 목록을 조회한다.one()은find()로 기본키 기준 단건 조회를 한다.search()는like조건으로memo포함 검색을 한다.insert()는persist()로 새 방명록 글을 저장한다.update()는 조회한 영속 객체의 값을 바꾸고 변경 감지로 수정한다.update()에서 수정 대상id가 없으면 실패 처리될 수 있다.delete()는 삭제할 객체를 조회한 뒤remove()로 삭제한다.delete()에서 삭제 대상id가 없으면 실패 처리될 수 있다.- 저장, 수정, 삭제는
Transaction안에서 처리한다.
Visitor예제는App,Controller,DAO,Entity가 나뉘어 실제 방명록CRUD흐름을 구성하는 구조를 보여 준다.
Meeting예제는 미팅 글을 저장, 조회, 검색, 수정, 삭제하는 흐름을 보여 주는 예제이다.
구조는 앞의Visitor예제와 비슷하다.
다만Meeting은 날짜와 시간을 함께 다루는LocalDateTime필드를 가진다는 차이가 있다.
MeetingApp.java는 사용자의 메뉴 입력을 받는다.
MeetingController.java는 메뉴 입력과DAO호출 사이의 중간 흐름을 담당한다.
MeetingDAO.java는 실제 데이터베이스 작업을 담당한다.
그리고Meeting글은 뒤에서 댓글인Reply가 연결될 기준 데이터가 된다.
즉, 이번 예제는 미팅 글 자체의CRUD를 확인하면서, 다음 댓글 관계 예제로 이어지는 기준 데이터를 만드는 흐름이기도 하다.
이 예제의 핵심은 날짜와 시간 정보가 포함된Meeting데이터를DAO기반CRUD구조로 처리하는 흐름을 이해하는 것이다.
이전 기본개념과 연결하기
앞의
Visitor예제에서는 방명록 데이터를Entity,DAO, 실행 앱으로 나누어 처리했다.
Visitor는 방명록 한 건을 표현했고,VisitorDAO는 조회, 검색, 저장, 수정, 삭제 작업을 담당했다.
Meeting예제도 큰 구조는 비슷하다.
Meeting은 미팅 글 한 건을 표현하는Entity이다.
MeetingDAO는 미팅 글에 대한 데이터베이스 작업을 담당한다.
MeetingApp은 콘솔 메뉴를 통해 사용자가 기능을 선택하게 한다.
다만 이번 예제에서 추가로 봐야 할 부분은 날짜와 시간이다.
Visitor의 작성일은 저장 시점에 자동으로 들어가는 흐름이었다.
반면Meeting은 사용자가 미팅 일시를 입력하고, 그 값을LocalDateTime으로 다루는 흐름과 연결된다.
기본개념에서 다시 잡아야 할 흐름
Meeting은 미팅 글 한 건을 표현하는Entity이다.MeetingDAO는 미팅 글의 조회, 검색, 저장, 수정, 삭제를 담당한다.MeetingController는 앱과DAO사이에서 기능 호출 흐름을 연결한다.MeetingApp은 메뉴 입력을 받아 기능 실행 흐름을 시작한다.meetingDate는 날짜와 시간을 함께 표현하는LocalDateTime타입이다.LocalDateTime은 데이터베이스의TIMESTAMP계열 컬럼과 연결될 수 있다.- 저장, 수정, 삭제는 데이터베이스 상태를 바꾸므로
Transaction안에서 처리한다.- 뒤의
Reply예제에서 댓글은 특정Meeting글에 연결된다.
Meeting은 단순 글 데이터가 아니라, 날짜·시간 필드와 댓글 연결의 기준이 되는 엔티티이다.
예제의 목표
이 예제의 목표는 미팅 글 데이터를
JPA로 처리하는 구조를 확인하는 것이다.
미팅 글 한 건은Meeting객체로 표현한다.
실제 데이터베이스 작업은MeetingDAO에 모아 둔다.
사용자는MeetingApp의 메뉴를 통해 미팅 글 작성, 삭제, 검색, 수정 기능을 실행한다.
즉, 이번 예제는Meeting하나만 보는 예제가 아니다.
Entity가 어떤 데이터를 담는지,DAO가 어떤 작업을 맡는지, 실행 앱이 어떤 메뉴 흐름을 제공하는지를 함께 확인하는 예제이다.
특히Meeting은LocalDateTime을 사용한다.
따라서 문자열로 입력한 날짜와 시간이 어떻게 미팅 일시로 저장되는지 흐름을 함께 봐야 한다.
이 예제에서 확인할 내용
Meeting이 미팅 글 데이터를 담는Entity인지 확인한다.meetingDate가LocalDateTime타입인지 확인한다.TIMESTAMP컬럼과 연결되는 흐름을 확인한다.MeetingDAO가 전체 조회, 단건 조회, 저장, 삭제, 수정, 이름 검색을 담당하는지 확인한다.MeetingApp의 메뉴가 미팅 글 기능 호출로 이어지는지 확인한다.MeetingApp의 댓글 관련 메뉴는 다음Reply예제와 연결된다는 점을 구분한다.이번 예제는
Meeting데이터의 구조와DAO기반CRUD흐름을 함께 보는 것이 중요하다.
Meeting은 미팅 글 데이터를 담는 Entity이다
Meeting.java는 미팅 글 하나를 표현하는Entity이다.
글 번호, 친구 이름, 미팅 목적, 미팅 일시를 필드로 가진다.
핵심 구조는 아래와 같다.// Meeting.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @Entity // JPA 관리 대상 Entity public class Meeting { @Id // 미팅 글 기본키 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; // 글 번호 private String name; // 미팅할 친구 이름 private String title; // 미팅 목적 @Column(columnDefinition = "TIMESTAMP") // TIMESTAMP 컬럼 지정 private LocalDateTime meetingDate; // 미팅 일시 }
@Entity는 이 클래스가JPA관리 대상이라는 뜻이다.
즉,Meeting객체는 데이터베이스 테이블과 연결될 수 있다.
id는 미팅 글을 구분하는 기본키이다.
@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있으므로 기본키 값은 데이터베이스가 자동으로 만들 수 있다.
따라서 새 미팅 글을 저장할 때 사용자가id를 직접 입력하지 않아도 된다.
meetingDate는LocalDateTime타입이다.
LocalDateTime은 날짜와 시간을 함께 표현하는 타입이다.
예를 들어2026-04-30T14:30처럼 날짜와 시간이 한 값 안에 들어간다.
@Column(columnDefinition = "TIMESTAMP")는 이 필드가 데이터베이스에서TIMESTAMP계열 컬럼으로 만들어지도록 지정하는 설정이다.
Meeting은 날짜와 시간이 함께 필요한 데이터를LocalDateTime과TIMESTAMP로 매핑하는 예제이다.
MeetingDAO는 미팅 글 CRUD 작업을 담당한다
MeetingDAO.java는 미팅 글을 조회, 저장, 검색, 수정, 삭제하는 기능을 제공한다.
CRUD는Create,Read,Update,Delete의 줄임말이다.
데이터를 생성하고, 읽고, 수정하고, 삭제하는 기본 작업을 의미한다.
MeetingDAO는 생성자에서EntityManagerFactory를 만든다.
각 메서드는 필요할 때EntityManager를 만들고, 작업이 끝나면 닫는다.
핵심 구조는 아래와 같다.// MeetingDAO.java public class MeetingDAO { private EntityManagerFactory factory; // EntityManagerFactory 보관 public MeetingDAO() { // DAO 생성자 super(); // 부모 생성자 호출 factory = Persistence.createEntityManagerFactory("emptest"); // emptest 설정으로 Factory 생성 } public void close() { // Factory 정리 if (factory != null) { // Factory가 있으면 factory.close(); // Factory 닫기 } } }여기서는
"emptest"설정 묶음을 사용한다.
앞의Member,Team,Locker예제에서 사용한"entitytest"와 다르다.
Meeting은emptest설정에 포함된 예제 흐름으로 보면 된다.
DAO안에EntityManagerFactory가 있고, 각 데이터 작업 메서드에서EntityManager를 만들어 사용한다.
이 구조는 데이터 접근 코드를 한곳에 모으기 위한 방식이다.
MeetingDAO는 미팅 글 데이터 작업을 모아 둔 클래스이고, 실행 앱은 이 기능들을 메뉴 흐름으로 사용한다.
listM과 oneM은 미팅 글 조회 기능이다
listM()은 미팅 글 전체 목록을 조회한다.
oneM()은 기본키id로 미팅 글 한 건을 조회한다.
핵심 코드는 아래와 같다.// MeetingDAO.java public List<Meeting> listM() { // 미팅 글 전체 조회 EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Meeting> q = em.createQuery( "select t from Meeting t", Meeting.class ); // Meeting 전체 조회 JPQL List<Meeting> list = q.getResultList(); // 목록 조회 em.close(); // EntityManager 닫기 return list; // 결과 반환 } public Meeting oneM(int id) { // 미팅 글 한 건 조회 EntityManager em = factory.createEntityManager(); // EntityManager 생성 Meeting vo = em.find(Meeting.class, id); // 기본키로 Meeting 조회 em.close(); // EntityManager 닫기 return vo; // 조회 결과 반환 }
listM()은 여러 건을 조회하므로getResultList()를 사용한다.
조회 결과는List<Meeting>으로 받는다.
oneM()은 기본키를 알고 있을 때 사용한다.
find(Meeting.class, id)는Meeting엔티티에서 기본키가id인 데이터 한 건을 찾는다.
다만id가 실제로 존재하지 않으면oneM()의 결과가null일 수 있다.
따라서 단건 조회 결과를 사용하는 쪽에서는 조회 결과가 있는지 확인하는 흐름이 필요할 수 있다.
전체 조회는JPQL과getResultList()를 사용하고, 기본키 단건 조회는find()를 사용한다.
insertM은 새 미팅 글을 저장한다
insertM()은 새 미팅 글을 저장하는 메서드이다.
저장은 데이터베이스 상태를 바꾸는 작업이므로Transaction안에서 처리해야 한다.
핵심 코드는 아래와 같다.// MeetingDAO.java public boolean insertM(Meeting vo) { // 미팅 글 저장 boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 em.persist(vo); // 저장 요청 em.getTransaction().commit(); // 저장 확정 } catch (Exception e) { result = false; // 실패 처리 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }
persist(vo)는Meeting엔티티를 저장 대상으로 등록한다.
commit()이 실행되면 저장 작업이 확정된다.
Meeting은IDENTITY전략을 사용하므로 기본키id는 데이터베이스가 자동으로 만들 수 있다.
따라서 저장할 때 사용자가 입력해야 하는 핵심 값은 친구 이름, 미팅 목적, 미팅 일시이다.
미팅 글 저장은Meeting객체를 만들고,persist()로 저장 요청한 뒤,commit()으로 확정하는 흐름이다.
deleteM은 기본키로 찾은 미팅 글을 삭제한다
deleteM()은 미팅 글을 삭제하는 메서드이다.
삭제도 데이터베이스 상태를 바꾸는 작업이므로Transaction안에서 처리해야 한다.
핵심 흐름은 아래와 같다.// MeetingDAO.java public boolean deleteM(int id) { // 미팅 글 삭제 boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 Meeting vo = em.find(Meeting.class, id); // 삭제할 미팅 글 조회 em.remove(vo); // 삭제 요청 em.getTransaction().commit(); // 삭제 확정 } catch (Exception e) { result = false; // 실패 처리 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }삭제는 먼저 삭제할 엔티티를 조회하는 것부터 시작한다.
find(Meeting.class, id)로 삭제할 미팅 글을 찾고, 그 결과를remove(vo)에 전달한다.
삭제할id가 실제로 존재하지 않으면vo가null일 수 있다.
그 상태에서 바로remove(vo)를 호출하면 문제가 생길 수 있다.
현재 구조에서는try-catch로 실패 처리될 수 있지만, 실제 프로그램에서는 삭제 대상이 있는지 먼저 확인하는 흐름이 더 안전하다.
삭제는 삭제할 엔티티를 먼저 조회하고, 조회된 엔티티를remove()로 삭제 대상으로 만든 뒤commit()으로 확정한다.
updateM은 조회한 영속 객체의 값을 바꿔 수정한다
updateM()은 기존 미팅 글을 수정하는 메서드이다.
JPA에서는 보통update()라는 메서드를 따로 호출하지 않는다.
먼저 기존 엔티티를 조회한 뒤, 조회된 객체의 값을 바꾸면commit()시점에 변경 감지가 일어날 수 있다.
핵심 코드는 아래와 같다.// MeetingDAO.java public boolean updateM(Meeting vo) { // 미팅 글 수정 boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 Meeting oldVo = em.find(Meeting.class, vo.getId()); // 기존 미팅 글 조회 oldVo.setName(vo.getName()); // 이름 변경 oldVo.setTitle(vo.getTitle()); // 제목 변경 oldVo.setMeetingDate(vo.getMeetingDate()); // 미팅 일시 변경 em.getTransaction().commit(); // 변경 감지 후 수정 확정 } catch (Exception e) { result = false; // 실패 처리 e.printStackTrace(); // 예외 로그 출력 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }
oldVo는find()로 조회된 영속 상태의Meeting객체이다.
영속 상태는JPA가 관리하는 상태를 의미한다.
이 상태에서oldVo.setName(...),oldVo.setTitle(...),oldVo.setMeetingDate(...)처럼 값을 바꾸면JPA가 변경 내용을 감지할 수 있다.
그리고commit()시점에 수정SQL이 실행될 수 있다.
이 흐름을 변경 감지라고 한다.
다만 수정할id가 실제로 존재하지 않으면oldVo가null일 수 있다.
그 상태에서oldVo.setName(),oldVo.setTitle(),oldVo.setMeetingDate()를 호출하면 문제가 생길 수 있다.
현재 코드는try-catch로 감싸져 있으므로 이런 상황에서는result=false로 실패 처리될 수 있다.
MeetingDAO의 수정도 영속 상태 엔티티의 값을 바꾸고commit()하는 변경 감지 흐름으로 처리된다.
searchM은 이름 조건으로 미팅 글을 검색한다
searchM()은 이름이 일치하는 미팅 글을 조회한다.
앞의Visitor검색에서는like를 사용했지만, 여기서는=조건을 사용한다.
핵심 코드는 아래와 같다.// MeetingDAO.java public List<Meeting> searchM(String name) { // 이름으로 미팅 글 검색 EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Meeting> q = em.createQuery( "select t from Meeting t WHERE t.name = :name", Meeting.class ); // 이름 일치 조건 JPQL q.setParameter("name", name); // name 파라미터에 검색값 연결 List<Meeting> list = q.getResultList(); // 결과 목록 조회 em.close(); // EntityManager 닫기 return list; // 검색 결과 반환 }
:name은 파라미터 자리이다.
setParameter("name", name)으로 입력받은 이름을 연결한다.
이 쿼리는 이름이 정확히 일치하는 미팅 글만 찾는다.
일부 문자열 검색이 아니라 정확한 이름 검색이다.
예를 들어 저장된 이름이둘리라면 검색어도둘리처럼 정확히 맞아야 결과가 나올 수 있다.
Visitor 검색과 Meeting 검색의 차이
Visitor검색은memo like :keyword구조였다.Visitor검색은 글 내용에 검색어가 포함되면 조회될 수 있다.Meeting검색은name = :name구조이다.Meeting검색은 이름이 정확히 일치해야 조회된다.
searchM()은 포함 검색이 아니라 이름이 정확히 같은 미팅 글을 찾는 검색이다.
LocalDateTime 입력 형식을 맞춰야 한다
MeetingApp.java에서는 미팅 일시를 입력받는다.
이때 입력값은LocalDateTime으로 변환될 수 있는 형태여야 한다.
입력 예시는 아래처럼 보면 된다.// 입력예시 // 2026-04-30T14:30
2026-04-30은 날짜이다.
14:30은 시간이다.
가운데의T는 날짜와 시간을 구분하는 문자이다.
형식으로 쓰면yyyy-MM-ddTHH:mm형태이다.
여기서MM은 월이고,dd는 일이다.
HH는 24시간 기준 시간이고,mm은 분이다.
만약2026-04-30 14:30처럼 공백으로 입력하거나,2026/04/30처럼 날짜 구분 형식이 다르면 문자열을LocalDateTime으로 바꾸는 과정에서 오류가 날 수 있다.
따라서 미팅 일시는 안내된 형식에 맞춰 입력해야 한다.
LocalDateTime으로 바꿀 날짜·시간 문자열은2026-04-30T14:30처럼 날짜와 시간 사이에T를 넣어야 한다.
MeetingApp은 메뉴로 미팅 글 기능과 댓글 기능을 실행한다
MeetingApp.java는 콘솔 메뉴를 통해 미팅 글 기능과 댓글 기능을 실행한다.
사용자가 번호를 입력하면 기능 호출 흐름이 시작된다.
메뉴 구조는 아래처럼 볼 수 있다.// MeetingApp.java // 1. 미팅글 작성 // 2. 미팅글 삭제 // 3. 이름으로 검색 // 4. 미팅글 수정 // 5. 댓글 작성 // 6. 댓글 읽기 // 7. 댓글 전체 읽기 // 8. 종료
1번부터4번까지는 미팅 글 자체의CRUD와 관련된다.
1번은 작성,2번은 삭제,3번은 이름 검색,4번은 수정이다.
5번부터7번은 댓글과 관련된다.
5번은 댓글 작성,6번은 특정 미팅 글의 댓글 읽기,7번은 댓글 전체 읽기 흐름이다.
댓글은 다음Reply예제에서 더 자세히 다룰 내용이다.
따라서 이번25번에서는 미팅 글CRUD를 중심으로 보고,5번부터7번은 다음 예제로 이어지는 결과 흐름으로만 확인한다.
또 메뉴 번호 입력은 숫자로 해야 한다.
nextInt()로 메뉴 번호를 읽는 구조라면, 숫자가 아닌 값을 입력했을 때InputMismatchException이 발생할 수 있다.
25번에서는1번부터4번까지는Meeting글 자체의CRUD흐름으로 보고,5번부터7번까지는 다음Reply예제로 이어지는 댓글 흐름으로 구분해서 본다.
실행 결과는 1번부터 7번까지 메뉴 흐름으로 확인한다
MeetingApp.java는 메뉴형 프로그램이다.
따라서 실행 결과는 사용자가 어떤 번호를 입력하느냐에 따라 달라진다.
이번 예제의 실행 결과는1번부터7번까지 메뉴 흐름을 기준으로 확인한다.
1번부터4번까지는 미팅 글 작성, 삭제, 검색, 수정 흐름이다.
5번부터7번까지는 댓글 작성, 댓글 읽기, 댓글 전체 읽기 흐름이며, 다음Reply예제와 연결된다.
MeetingApp.java에서1번 미팅글 작성 메뉴를 실행한 결과이다.
친구 이름, 미팅 목적, 미팅 일시를 입력하면 새Meeting데이터가 저장된다.
이때 미팅 일시는2026-04-30T14:30처럼 날짜와 시간 사이에T를 넣어 입력해야 한다.
MeetingApp.java에서2번 미팅글 삭제 메뉴를 실행한 결과이다.
삭제할 미팅 글 번호를 입력하면 해당Meeting데이터를 찾아 삭제한다.
삭제 작업은MeetingDAO의deleteM()에서 삭제할 엔티티를 조회한 뒤remove()로 삭제 대상으로 만들고,commit()으로 반영하는 흐름과 연결된다.
MeetingApp.java에서3번 이름으로 검색 메뉴를 실행한 결과이다.
검색할 이름을 입력하면Meeting.name값이 입력값과 정확히 일치하는 미팅 글을 조회한다.
이 흐름은MeetingDAO의searchM()에서name = :name조건으로 검색하는 방식과 연결된다.
MeetingApp.java에서4번 미팅글 수정 메뉴를 실행한 결과이다.
수정할 글 번호와 새 값을 입력하면 기존Meeting객체의 값이 변경된다.
수정은 영속 상태 객체의 값을 바꾸고,commit()시점에 변경 감지로 데이터베이스에 반영되는 흐름이다.
MeetingApp.java에서5번 댓글 작성과6번 댓글 읽기 메뉴를 실행한 결과이다.
5번은 특정 미팅 글에 댓글을 작성하는 흐름이고,6번은 특정 미팅 글에 연결된 댓글을 조회하는 흐름이다.
이 부분은Meeting글이 댓글인Reply와 연결되는 다음 예제로 이어진다.
MeetingApp.java에서7번 댓글 전체 읽기 메뉴를 실행한 결과이다.
특정 미팅 글 하나만 보는 것이 아니라, 저장된 댓글 전체를 조회하는 흐름이다.
이 결과는 다음Reply예제에서 댓글 엔티티와 미팅 글의 관계를 설명할 때 다시 연결해서 보면 된다.
이 결과를 통해MeetingApp의1번부터4번은 미팅 글CRUD흐름이고,5번부터7번은 다음Reply예제로 이어지는 댓글 처리 흐름임을 확인할 수 있다.
핵심 정리
Meeting예제는 미팅 글 데이터를JPA로 처리하는DAO기반CRUD구조이다.
Meeting은 미팅 글 한 건을 표현하는Entity이고,MeetingDAO는 실제 데이터베이스 작업을 담당한다.
MeetingApp은 사용자의 메뉴 입력을 받아 기능 실행 흐름을 시작한다.
정리하면 아래와 같다.
Meeting은 미팅 글 데이터를 담는Entity이다.id는 기본키이고,IDENTITY전략으로 자동 생성될 수 있다.meetingDate는 날짜와 시간을 함께 표현하는LocalDateTime타입이다.meetingDate는TIMESTAMP컬럼과 연결될 수 있다.MeetingDAO는 미팅 글 전체 조회, 단건 조회, 검색, 저장, 수정, 삭제를 담당한다.listM()은JPQL로 전체 목록을 조회한다.oneM()은find()로 기본키 기준 단건 조회를 한다.insertM()은persist()로 새 미팅 글을 저장한다.deleteM()은 삭제할 객체를 조회한 뒤remove()로 삭제한다.updateM()은 조회한 영속 객체의 값을 바꾸고 변경 감지로 수정한다.searchM()은name = :name조건으로 이름이 정확히 일치하는 미팅 글을 검색한다.- 저장, 수정, 삭제는
Transaction안에서 처리한다.MeetingApp의1번부터4번은 미팅 글CRUD메뉴이다.MeetingApp의5번부터7번은 다음Reply예제로 이어지는 댓글 메뉴이다.
Meeting예제는 날짜·시간 데이터를 포함한 게시글형Entity를DAO기반으로 처리하고, 다음 댓글 관계 예제로 이어지는 기준 흐름을 만든다.
Reply는 댓글을 표현하는Entity이고,Meeting글에 연결된다.
미팅 글 하나에는 여러 댓글이 달릴 수 있다.
댓글 입장에서는 하나의 미팅 글을 참조하므로Reply와Meeting은 다대일 관계가 된다.
이 예제는 단순히 댓글을 저장하는 예제가 아니다.
댓글이 어떤 미팅 글에 속하는지JPA연관관계로 연결하고, 특정 미팅 글에 달린 댓글만 조회하는 흐름을 확인하는 예제이다.
이 예제의 핵심은Reply가Meeting을 참조하고, 데이터베이스에서는refid외래키로 댓글과 미팅 글이 연결된다는 점이다.
이전 기본개념과 연결하기
앞 예제에서는
Meeting글 자체의 작성, 삭제, 검색, 수정 흐름을 확인했다.
Meeting은 미팅 글 한 건을 표현하는Entity였고,MeetingDAO는 미팅 글 데이터를 처리했다.
이번 예제에서는 그Meeting글에 댓글을 연결한다.
댓글은 혼자 의미가 완성되는 데이터가 아니다.
어떤 미팅 글에 달린 댓글인지 알아야 한다.
그래서Reply는Meeting을 참조한다.
객체 코드에서는Reply안에Meeting refid필드가 있고, 데이터베이스에서는 댓글 테이블의refid외래키가 미팅 글의 기본키와 연결된다.
기본개념에서 다시 잡아야 할 흐름
Meeting은 미팅 글 한 건을 표현한다.Reply는 댓글 한 건을 표현한다.- 미팅 글 하나에는 여러 댓글이 달릴 수 있다.
- 댓글 하나는 하나의 미팅 글에 속한다.
- 그래서
Reply에서Meeting으로 가는 관계는 다대일이다.- 객체에서는
Reply가Meeting객체를 참조한다.- 데이터베이스에서는
refid외래키로 연결된다.댓글 데이터는 댓글 내용만 보는 것이 아니라, 어떤 부모 글에 연결되는지까지 함께 봐야 한다.
예제의 목표
이 예제의 목표는
Reply와Meeting의 관계를 이해하는 것이다.
댓글을 저장할 때는 작성자와 내용만 저장하는 것이 아니라, 어떤 미팅 글에 달린 댓글인지도 함께 저장해야 한다.
또 댓글을 조회할 때도 전체 댓글을 조회할 수 있고, 특정 미팅 글에 달린 댓글만 조회할 수도 있다.
이때 특정 미팅 글의 댓글을 찾는 조건이t.refid.id = :refid이다.
즉, 이번 예제는 댓글 저장, 특정 글 댓글 조회, 전체 댓글 조회를 통해 다대일 관계를 확인하는 예제이다.
이 예제에서 확인할 내용
Reply가 댓글 데이터를 담는Entity인지 확인한다.Reply가Meeting을@ManyToOne으로 참조하는지 확인한다.@JoinColumn(name = "refid")가 외래키 컬럼과 연결되는지 확인한다.- 특정 미팅 글의 댓글을
t.refid.id = :refid조건으로 조회하는지 확인한다.- 댓글 저장 시 어떤 미팅 글에 속한 댓글인지 함께 연결되어야 함을 이해한다.
- 전체 댓글 조회와 특정 글 댓글 조회의 차이를 구분한다.
이번 예제는 댓글
Entity자체보다 댓글이Meeting글과 어떻게 연결되는지를 보는 것이 중요하다.
Reply는 Meeting을 참조하는 댓글 Entity이다
Reply.java는 댓글 데이터를 표현하는Entity이다.
댓글 번호, 댓글 작성자, 댓글 내용, 댓글이 속한 미팅 글을 필드로 가진다.
핵심 구조는 아래와 같다.// Reply.java @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 @Entity // JPA 관리 대상 Entity @AllArgsConstructor // 모든 필드를 받는 생성자 자동 생성 @NoArgsConstructor // 기본 생성자 자동 생성 public class Reply { @Id // 댓글 기본키 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; // 댓글 번호 private String name; // 댓글 작성자 private String content; // 댓글 내용 @ManyToOne(optional = false) // 여러 댓글이 하나의 Meeting에 연결됨 @JoinColumn(name = "refid") // refid 외래키 컬럼과 연결 private Meeting refid; // 댓글이 속한 미팅 글 }
@Entity는Reply가JPA관리 대상이라는 뜻이다.
즉,Reply객체는 댓글 테이블과 연결될 수 있다.
id는 댓글 한 건을 구분하는 기본키이다.
@GeneratedValue(strategy = GenerationType.IDENTITY)가 붙어 있으므로 기본키 값은 데이터베이스가 자동으로 만들 수 있다.
name은 댓글 작성자이고,content는 댓글 내용이다.
여기까지는 일반 댓글 데이터이다.
가장 중요한 필드는refid이다.
이 필드는 숫자처럼 보이는 이름을 가지고 있지만, 실제 타입은Meeting이다.
즉, 댓글이 속한 미팅 글 객체를 참조한다.
Reply의refid는 단순 숫자 필드가 아니라,Meeting엔티티를 참조하는 연관관계 필드이다.
ManyToOne은 여러 댓글이 하나의 미팅 글에 연결된다는 뜻이다
Reply에서 가장 중요한 매핑은 아래 부분이다.// Reply.java @ManyToOne(optional = false) // 여러 댓글이 하나의 Meeting에 연결됨 @JoinColumn(name = "refid") // refid 외래키 컬럼 사용 private Meeting refid; // 댓글이 속한 미팅 글
@ManyToOne은 다대일 관계를 의미한다.
여기서 다는Reply이고, 일은Meeting이다.
즉, 여러 댓글이 하나의 미팅 글에 연결될 수 있다는 뜻이다.
예를 들어 미팅 글1번에 댓글이 세 개 달릴 수 있다.
그 댓글 세 개는 모두 같은Meeting글을 참조한다.
댓글 입장에서는 자신이 속한 미팅 글이 하나이다.
optional = false는 이 관계가 필수라는 뜻이다.
즉, 댓글은 연결된 미팅 글 없이 존재하기 어렵다는 구조이다.
댓글을 저장하려면 어떤Meeting글에 달린 댓글인지 함께 정해야 한다.
@JoinColumn(name = "refid")는 댓글 테이블에서 외래키로 사용할 컬럼 이름을 지정한다.
데이터베이스에서는refid컬럼이Meeting의 기본키를 가리키는 역할을 한다.
관계를 나누어 보면
Reply여러 개가 하나의Meeting에 연결될 수 있다.- 댓글 하나는 특정
Meeting하나를 참조한다.- 객체에서는
Reply.refid가Meeting객체를 가진다.- 데이터베이스에서는
refid외래키가Meeting의 기본키와 연결된다.객체에서는
Meeting객체 참조로 연결되고, 테이블에서는refid외래키 값으로 연결된다.
listReplyByMettingId는 특정 미팅 글의 댓글을 조회한다
MeetingDAO.java에는 특정 미팅 글 번호에 해당하는 댓글 목록을 조회하는 메서드가 있다.
메서드 이름에는Metting이라고 되어 있지만, 의미는 미팅 글 번호로 댓글을 찾는 기능이다.
핵심 코드는 아래와 같다.// MeetingDAO.java public List<Reply> listReplyByMettingId(int refid) { // 특정 미팅 글의 댓글 조회 EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Reply> q = em.createQuery( "select t from Reply t WHERE t.refid.id = :refid", Reply.class ); // Reply가 참조하는 Meeting의 id 조건 q.setParameter("refid", refid); // refid 파라미터에 미팅 글 번호 연결 List<Reply> list = q.getResultList(); // 댓글 목록 조회 em.close(); // EntityManager 닫기 return list; // 결과 반환 }이 메서드는 특정 미팅 글에 달린 댓글만 조회한다.
refid매개변수로 미팅 글 번호를 받는다.
select t from Reply t는Reply엔티티를 조회한다는 뜻이다.
여기서t는Reply를 가리키는 별칭이다.
WHERE t.refid.id = :refid가 핵심 조건이다.
t.refid는 댓글이 참조하는Meeting객체이다.
t.refid.id는 그Meeting객체의 기본키이다.
따라서 이 조건은 댓글 자신의 번호를 비교하는 것이 아니다.
댓글이 연결된 미팅 글의 번호를 비교한다.
t.refid.id는Reply에서Meeting으로 객체 참조를 따라간 뒤,Meeting의 기본키를 조건으로 사용하는 표현이다.
t.refid.id는 댓글이 연결된 미팅 글 번호를 의미한다
초보자가 가장 헷갈리기 쉬운 부분은
refid라는 이름이다.
refid라는 이름만 보면 숫자 값처럼 보일 수 있다.
하지만Reply클래스에서refid의 타입은Meeting이다.
다시 구조를 보면 아래와 같다.// Reply.java private Meeting refid; // 댓글이 속한 미팅 글그래서
t.refid는 숫자가 아니라Meeting객체이다.
그리고t.refid.id는 그Meeting객체의id값이다.
즉, 아래 표현은 이렇게 읽어야 한다.// ReplyConditionRead.java t.refid.id // 댓글이 참조하는 Meeting의 id이 조건을 사용하면 특정 미팅 글에 달린 댓글만 찾을 수 있다.
예를 들어refid에3을 넣으면,Meeting.id가3인 글에 연결된 댓글 목록을 찾는다.
refid 해석 기준
Reply.id는 댓글 자신의 기본키이다.Reply.refid는 댓글이 참조하는Meeting객체이다.Reply.refid.id는 댓글이 참조하는Meeting의 기본키이다.t.refid.id = :refid는 특정 미팅 글에 달린 댓글을 찾는 조건이다.
refid라는 이름 때문에 숫자 필드처럼 보이지만, 객체 코드에서는Meeting객체 참조라는 점을 놓치면 안 된다.
insertReply는 댓글을 저장한다
insertReply()는 댓글 엔티티를 저장한다.
댓글도 데이터베이스에 새 행을 추가하는 작업이므로Transaction안에서 처리된다.
핵심 코드는 아래와 같다.// MeetingDAO.java public boolean insertReply(Reply vo) { // 댓글 저장 boolean result = true; // 성공 여부 기본값 EntityManager em = factory.createEntityManager(); // EntityManager 생성 try { em.getTransaction().begin(); // Transaction 시작 System.out.println(vo); // 저장할 댓글 정보 출력 em.persist(vo); // 댓글 저장 요청 em.getTransaction().commit(); // 저장 확정 } catch (Exception e) { result = false; // 실패 처리 e.printStackTrace(); // 예외 로그 출력 } em.close(); // EntityManager 닫기 return result; // 성공 여부 반환 }댓글 저장은
persist(vo)로 진행된다.
vo는 저장할Reply객체이다.
하지만 댓글은 작성자와 내용만 있어서는 충분하지 않다.
Reply에는Meeting refid가 필수로 들어가야 한다.
즉, 댓글을 만들 때 어떤 미팅 글에 달린 댓글인지 함께 설정되어 있어야 한다.
MeetingApp.java에서는 댓글 작성 메뉴에서 댓글을 작성할 미팅 글 번호를 입력받는다.
그 번호를 기준으로Meeting글을 연결한 뒤 댓글을 저장하는 흐름으로 이해하면 된다.
댓글 저장의 핵심은Reply객체에 작성자, 내용뿐 아니라 연결할Meeting객체도 들어 있어야 한다는 점이다.
listR은 전체 댓글을 조회한다
MeetingDAO.java에는 전체 댓글을 조회하는listR()도 있다.
특정 미팅 글의 댓글만 보는 것이 아니라, 저장된 모든 댓글을 조회하는 기능이다.
핵심 코드는 아래와 같다.// MeetingDAO.java public List<Reply> listR() { // 전체 댓글 조회 EntityManager em = factory.createEntityManager(); // EntityManager 생성 TypedQuery<Reply> q = em.createQuery( "select t from Reply t", Reply.class ); // Reply 전체 조회 JPQL List<Reply> list = q.getResultList(); // 댓글 목록 조회 em.close(); // EntityManager 닫기 return list; // 결과 반환 }
select t from Reply t는 모든Reply를 조회한다는 뜻이다.
조건이 없기 때문에 특정 미팅 글의 댓글만 조회하지 않는다.
listReplyByMettingId()와 비교하면 차이가 분명하다.
listReplyByMettingId()는WHERE t.refid.id = :refid조건이 있다.
그래서 특정 미팅 글의 댓글만 조회한다.
반면listR()은 조건이 없다.
그래서 전체 댓글을 조회한다.
두 조회 메서드의 차이
listReplyByMettingId()는 특정 미팅 글의 댓글만 조회한다.listReplyByMettingId()는t.refid.id = :refid조건을 사용한다.listR()은 전체 댓글을 조회한다.listR()은 별도의 조건이 없다.댓글 조회는 특정 미팅 글 기준 조회와 전체 댓글 조회를 구분해야 한다.
MeetingApp의 댓글 메뉴와 연결된다
MeetingApp.java에는 댓글 관련 메뉴가 있다.
5번은 댓글 작성,6번은 특정 미팅 글의 댓글 읽기,7번은 댓글 전체 읽기이다.
핵심 흐름은 아래와 같다.// MeetingApp.java if (num == 5) { // 댓글 작성 메뉴 System.out.print("댓글 작성할 미팅글 번호 : "); // 미팅 글 번호 입력 안내 int refid = Integer.parseInt(scan.nextLine()); // 미팅 글 번호 입력 System.out.print("댓글 작성자 이름 : "); // 댓글 작성자 입력 안내 String name = scan.nextLine(); // 작성자 입력 System.out.print("댓글 내용 : "); // 댓글 내용 입력 안내 String content = scan.nextLine(); // 내용 입력 con.replyCreate(refid, name, content); // 댓글 작성 요청 } else if (num == 6) { // 특정 미팅 글 댓글 읽기 System.out.print("읽고자 하는 댓글의 미팅글 번호 : "); // 미팅 글 번호 입력 안내 int refid = Integer.parseInt(scan.nextLine()); // 미팅 글 번호 입력 con.replyRead(refid); // 특정 미팅 글 댓글 읽기 } else if (num == 7) { // 댓글 전체 읽기 con.replyAllRead(); // 전체 댓글 읽기 }
5번 댓글 작성에서는 미팅 글 번호, 댓글 작성자, 댓글 내용을 입력받는다.
여기서 미팅 글 번호가 댓글의 부모 글을 정하는 기준이 된다.
6번 댓글 읽기에서는 미팅 글 번호를 입력받는다.
이 번호를 기준으로 특정 미팅 글에 달린 댓글만 조회한다.
7번 댓글 전체 읽기는 특정 미팅 글 번호를 입력받지 않는다.
저장된 전체 댓글을 조회한다.
5번과6번은 미팅 글 번호가 필요하고,7번은 전체 댓글 조회이므로 미팅 글 번호가 필요하지 않다.
댓글 저장은 Meeting 글이 먼저 있어야 자연스럽다
댓글은 어떤 미팅 글에 달린 데이터이다.
따라서 댓글을 저장하려면 먼저 댓글을 달 미팅 글이 있어야 한다.
예를 들어 미팅 글 번호3번에 댓글을 달려면, 먼저Meeting.id가3인 데이터가 존재해야 한다.
그다음 댓글 작성 메뉴에서3을 입력해야 댓글이 그 미팅 글과 연결될 수 있다.
만약 존재하지 않는 미팅 글 번호를 입력하면 댓글을 연결할 대상이 없어서 저장 흐름에서 문제가 생길 수 있다.
Reply의@ManyToOne(optional = false)도 댓글이 반드시 미팅 글에 연결되어야 한다는 구조를 보여 준다.
댓글 작성 전 확인할 내용
- 미팅 글이 먼저 저장되어 있어야 한다.
- 댓글 작성 시 입력하는 번호는
Meeting글 번호이다.- 이 번호는
Reply자신의 댓글 번호가 아니다.- 존재하지 않는 미팅 글 번호를 입력하면 저장이 실패할 수 있다.
댓글 작성 메뉴에서 입력하는
refid는 댓글 번호가 아니라 댓글이 달릴 미팅 글 번호이다.
실행 결과는 미팅 글 번호 기준으로 댓글이 연결되는지 확인한다
이 예제의 결과는
MeetingApp.java의 댓글 메뉴를 통해 확인한다.
댓글은 미팅 글이 먼저 있어야 자연스럽게 테스트할 수 있다.
따라서 먼저 미팅 글을 확인하고, 해당 미팅 글 번호를 기준으로 댓글을 작성한 뒤, 같은 번호로 댓글을 조회하는 흐름으로 보면 된다.
예상 결과 흐름은 아래와 같다.// 출력결과 흐름 // 5 입력: 댓글 작성할 미팅 글 번호 입력 후 댓글 저장 // 6 입력: 같은 미팅 글 번호를 입력해 해당 글의 댓글 조회 // 7 입력: 전체 댓글 조회결과를 볼 때는 댓글 내용만 보면 부족하다.
댓글이 어떤 미팅 글 번호와 연결되었는지를 함께 봐야 한다.
MeetingApp.java에서5번 댓글 작성과6번 댓글 읽기 메뉴를 실행한 결과이다.
5번에서는 댓글을 작성할 미팅 글 번호, 댓글 작성자, 댓글 내용을 입력한다.
이때 입력한 미팅 글 번호가Reply의refid관계와 연결된다.
그다음6번에서 같은 미팅 글 번호를 입력하면, 해당 미팅 글에 달린 댓글만 조회된다.
이 흐름은MeetingDAO의listReplyByMettingId()에서t.refid.id = :refid조건으로 조회하는 방식과 연결된다.
MeetingApp.java에서7번 댓글 전체 읽기 메뉴를 실행한 결과이다.
특정 미팅 글 하나만 기준으로 보는 것이 아니라, 저장된 전체 댓글을 조회한다.
이 흐름은MeetingDAO의listR()에서select t from Reply t로 전체 댓글을 조회하는 방식과 연결된다.
실행 결과에서는 댓글 작성 시 입력한 미팅 글 번호와 댓글 조회 시 입력한 미팅 글 번호가 같은지 확인해야 한다.
핵심 정리
Reply예제는 댓글이Meeting글에 연결되는 다대일 관계를 확인하는 예제이다.
댓글은 작성자와 내용만 가진 독립 데이터처럼 보일 수 있지만, 실제로는 어떤 미팅 글에 달린 댓글인지 함께 저장되어야 한다.
정리하면 아래와 같다.
Reply는 댓글 데이터를 담는Entity이다.Reply에는 댓글 작성자name과 댓글 내용content가 있다.Reply는Meeting을@ManyToOne으로 참조한다.- 미팅 글 하나에는 여러 댓글이 달릴 수 있다.
- 댓글 하나는 하나의 미팅 글에 속한다.
@JoinColumn(name = "refid")는 댓글 테이블의refid외래키 컬럼을 의미한다.Reply.refid는 숫자가 아니라Meeting객체 참조이다.t.refid.id = :refid는 댓글이 연결된 미팅 글 번호를 기준으로 댓글을 조회하는 조건이다.insertReply()는 댓글을 저장한다.- 댓글 저장 시 연결할
Meeting이 함께 설정되어야 한다.listReplyByMettingId()는 특정 미팅 글의 댓글만 조회한다.listR()은 전체 댓글을 조회한다.MeetingApp의5번은 댓글 작성,6번은 특정 글 댓글 읽기,7번은 전체 댓글 읽기이다.
Reply예제의 핵심은 댓글 데이터가 독립적으로만 존재하는 것이 아니라,Meeting엔티티를 참조하는 다대일 관계로 저장된다는 점이다.
Book.java는 실행 앱과 직접 연결된 예제는 아니지만, 단순한Entity매핑 구조를 확인하기 좋은 파일이다.
id,title,price,kind처럼 책 정보를 표현하는 기본 필드만 가지고 있다.
앞의Visitor,Meeting,Reply처럼 메뉴 실행 흐름이나 연관관계를 함께 확인하는 예제는 아니다.
따라서 이 파일은Entity기본 구조와IDENTITY기본키 자동 생성 전략을 확인하는 보조 예제로 보면 된다.
이 예제의 핵심은 연관관계 없이 기본키와 일반 필드만 가진 단순Entity구조를 확인하는 것이다.
이전 기본개념과 연결하기
앞의
Visitor,Meeting,Reply예제에서는 실제 프로그램 구조에 가까운 흐름을 확인했다.
Visitor는 방명록 데이터를 처리했고,Meeting은 미팅 글 데이터를 처리했으며,Reply는Meeting글에 연결되는 댓글 데이터를 처리했다.
이 예제들은DAO,Controller, 실행 앱, 연관관계까지 함께 봐야 했다.
그래서 파일 간 연결 흐름이 길었다.
반면Book.java는 구조가 훨씬 단순하다.
다른Entity를 참조하지 않고, 책 한 권의 기본 정보만 필드로 가진다.
그래서 복잡한 흐름보다Entity의 기본 매핑 구조를 다시 확인하는 데 적합하다.
기본개념에서 다시 잡아야 할 흐름
Book은 책 한 권의 정보를 표현하는Entity이다.@Entity가 붙으면JPA가 관리할 수 있는 클래스가 된다.id는Book의 기본키이다.@GeneratedValue(strategy = GenerationType.IDENTITY)는 기본키 값을 데이터베이스가 자동 생성하게 한다.title,price,kind는 일반 필드이다.Book은 다른Entity와 연관관계를 가지지 않는다.
Book은 복잡한 관계보다Entity의 가장 기본적인 형태를 확인하는 예제이다.
예제의 목표
이 예제의 목표는
Book클래스가 어떤 방식으로 데이터베이스 테이블과 연결될 수 있는지 확인하는 것이다.
실행 파일을 통해 저장하거나 조회하는 흐름을 보는 것이 아니라,Entity클래스 자체의 매핑 구조를 읽는 데 초점을 둔다.
Book은 책 번호, 책 제목, 가격, 종류를 가진다.
기본키는id이고, 책의 일반 정보는title,price,kind필드에 저장된다.
연관관계가 없기 때문에@ManyToOne,@OneToOne,@OneToMany같은 어노테이션은 나오지 않는다.
즉, 이 예제는 다른 테이블과 연결하는 흐름이 아니라, 한 테이블 안에서 기본키와 일반 컬럼이 어떻게 구성되는지 확인하는 예제이다.
이 예제에서 확인할 내용
Book에@Entity가 붙어 있는지 확인한다.id에@Id가 붙어 기본키로 사용되는지 확인한다.id가IDENTITY전략으로 자동 생성되는지 확인한다.title,price,kind가 일반 필드인지 확인한다.- 다른
Entity를 참조하는 연관관계 필드가 없는지 확인한다.이 예제는 실행 결과보다
Entity클래스의 기본 매핑 구조를 읽는 것이 중요하다.
Book은 책 정보를 담는 단순 Entity이다
Book.java는 책 한 권의 정보를 표현하는Entity이다.
책 번호, 제목, 가격, 종류를 필드로 가진다.
핵심 구조는 아래와 같다.// Book.java @Entity // JPA 관리 대상 Entity @Getter // getter 자동 생성 @Setter // setter 자동 생성 @ToString // 객체 출력 문자열 자동 생성 public class Book { @Id // 책 기본키 @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 기본키 자동 생성 private int id; // 책 번호 private String title; // 책 제목 private int price; // 책 가격 private String kind; // 책 종류 }
@Entity는 이 클래스가JPA관리 대상이라는 뜻이다.
즉,Book객체는 데이터베이스 테이블과 연결될 수 있다.
@Id는 기본키 필드를 지정한다.
기본키는 테이블에서 각 행을 구분하는 값이다.
여기서는id가 책 한 권을 구분하는 기준이다.
@GeneratedValue(strategy = GenerationType.IDENTITY)는 기본키 자동 생성 전략이다.
IDENTITY전략은 기본키 값을 데이터베이스가 자동으로 생성하는 방식이다.
따라서 새 책 데이터를 저장할 때id값을 직접 넣지 않아도 될 수 있다.
Book의id는 기본키이고,IDENTITY전략으로 데이터베이스에서 자동 생성될 수 있다.
title, price, kind는 일반 필드이다
Book에서title,price,kind는 일반 필드이다.
기본키도 아니고, 다른Entity와 연결되는 연관관계 필드도 아니다.
핵심 필드는 아래와 같다.// Book.java private String title; // 책 제목 private int price; // 책 가격 private String kind; // 책 종류
title은 책 제목을 저장한다.
문자열 값이므로 타입은String이다.
price는 책 가격을 저장한다.
숫자 값이므로 타입은int이다.
kind는 책 종류를 저장한다.
예를 들어 책의 분류나 유형을 담는 값으로 볼 수 있다.
타입은String이다.
이 필드들에는 별도의@Column설정이 없다.
따라서 기본 매핑 규칙에 따라 필드 이름과 같은 컬럼으로 연결될 수 있다.
다만 실제 테이블명과 컬럼명은 프로젝트의 이름 변환 설정이나 데이터베이스 상태에 따라 달라질 수 있다.
일반 필드에서 봐야 할 기준
title은 책 제목이다.price는 책 가격이다.kind는 책 종류이다.- 별도의 연관관계 어노테이션이 없다.
- 일반 필드는 기본 매핑 규칙에 따라 컬럼으로 연결될 수 있다.
일반 필드는 객체의 값을 담는 기본 속성이며, 연관관계 필드처럼 다른
Entity를 참조하지 않는다.
Book은 연관관계 없이 기본 매핑만 확인한다
Book에는@ManyToOne,@OneToOne,@OneToMany같은 연관관계 어노테이션이 없다.
즉, 다른Entity를 참조하지 않는다.
앞에서 본Reply는Meeting을 참조했다.
그래서@ManyToOne과@JoinColumn을 함께 확인해야 했다.
또Member는Team,Locker를 참조했기 때문에 객체 관계와 외래키 흐름을 함께 봐야 했다.
반면Book은 그런 관계가 없다.
책 한 권의 정보가 한 객체 안에 단순하게 들어 있다.
그래서 기본키와 일반 필드만 확인하면 구조를 이해할 수 있다.
연관관계가 없는 Entity의 특징
- 다른
Entity타입 필드가 없다.- 외래키 역할을 하는 연관 필드가 없다.
@JoinColumn이 없다.@ManyToOne,@OneToOne같은 관계 어노테이션이 없다.- 기본키와 일반 필드 중심으로 구조를 읽으면 된다.
연관관계가 없는
Entity는 다른 객체로 이동하는 흐름이 없으므로, 기본키와 일반 필드 매핑을 중심으로 읽으면 된다.
Book과 Reply 구조를 비교하면 차이가 분명하다
Book과Reply를 비교하면 단순Entity와 연관관계가 있는Entity의 차이를 쉽게 볼 수 있다.
Book은 책 자체의 정보만 가진다.
다른 객체를 참조하지 않는다.// Book.java private String title; // 책 제목 private int price; // 책 가격 private String kind; // 책 종류반면
Reply는 댓글 내용만 가지는 것이 아니라, 어떤 미팅 글에 달린 댓글인지도 함께 가진다.// Reply.java @ManyToOne(optional = false) // 여러 댓글이 하나의 Meeting에 연결됨 @JoinColumn(name = "refid") // refid 외래키 컬럼과 연결 private Meeting refid; // 댓글이 속한 미팅 글
Book은 일반 필드만 보면 된다.
하지만Reply는Meeting을 참조하므로 객체 관계와 외래키까지 같이 봐야 한다.
이 차이를 알면 파일을 볼 때 먼저 무엇을 확인해야 하는지 정리된다.
연관관계가 없는Entity는 기본 필드를 먼저 보고, 연관관계가 있는Entity는 어떤 객체를 참조하는지 먼저 확인해야 한다.
Book은 단순 필드 중심의Entity이고,Reply는 다른Entity를 참조하는 관계 중심의Entity이다.
실행 앱은 제공되지 않았으므로 매핑 구조만 확인한다
현재
Book은 실행 앱과 직접 연결된 결과 흐름을 확인하는 예제가 아니다.
따라서 저장, 조회, 수정, 삭제 결과까지 억지로 확장해서 설명하지 않는다.
제공된 코드 기준으로는Book.java의 매핑 구조를 확인하는 정도가 가장 안전하다.
즉, 이 예제는 콘솔 결과를 확인하는 예제라기보다,Entity클래스의 기본 형태를 정리하는 예제이다.
만약 나중에Book을 저장하는 실행 파일이 추가된다면,persist()로 저장하고find()나JPQL로 조회하는 흐름까지 연결할 수 있다.
하지만 현재 단계에서는 실행 흐름이 아니라 매핑 구조를 확인하는 데 집중한다.
제공된 실행 앱이 없을 때는 결과를 억지로 만들지 말고, 현재 코드가 보여 주는 매핑 구조까지만 정리해야 한다.
핵심 정리
Book.java는 책 정보를 담는 단순Entity이다.
복잡한 실행 흐름이나 연관관계 없이 기본키와 일반 필드 매핑만 확인하면 된다.
정리하면 아래와 같다.
Book은 책 한 권의 정보를 표현한다.@Entity가 있으므로JPA관리 대상이다.id는 기본키이다.id는IDENTITY전략으로 자동 생성될 수 있다.title은 책 제목이다.price는 책 가격이다.kind는 책 종류이다.Book에는 다른Entity를 참조하는 연관관계 필드가 없다.Book에는@ManyToOne,@OneToOne,@JoinColumn이 없다.- 현재 제공된 코드 기준으로는 실행 결과보다 매핑 구조를 확인하는 예제로 보면 된다.
Book예제는 복잡한 관계 없이Entity기본 매핑을 다시 확인하는 짧은 보조 예제이다.