본문 바로가기
Blog/TIL

[240604] 값 타입, 페치 조인 배우기

by 코젼 2024. 6. 4.
728x90
반응형

🔶코딩테스트

🔶JPA

🔶값 타입

🔶객체지향 쿼리 언어


목차

    / 오늘의 TIL /


    코딩테스트

    https://school.programmers.co.kr/learn/courses/30/lessons/120902?language=java#

     

    프로그래머스

    코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

    programmers.co.kr

    숏코딩

    import java.util.Arrays;
    
    class Solution {
        public int solution(String my_string) {
            
            return Arrays.stream(
                    my_string.replaceAll("- ", "-")
                            .replaceAll("[+] ", "")
                            .split(" "))
                    .mapToInt(Integer::parseInt)
                    .sum();
        }
    }

    풀이

    class Solution {
        public int solution(String my_string) {
            
            String[] str = my_string.split(" ");
            int result = Integer.parseInt(str[0]);
            
            boolean oper = true;
            
            for (int i = 1; i < str.length; i++) {
                
                if (str[i].equals("-")) oper = false;
                else if (str[i].equals("+")) oper = true;
                else {
                    if (oper) { //+
                        result += Integer.parseInt(str[i]);
                    } else { //-
                        result -= Integer.parseInt(str[i]);
                    }
                }
            }
            
            return result;
        }
    }

    JPA

    프록시

    • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
      • 최초 1회 초기화 요청 시, 영속성 컨텍스트에서 DB를 조회하고 실제 엔티티를 생성해준다.
      • 초기화가 되지 않은 상태에서 실제 데이터를 요청하면 프록시 내부에 있는 target 필드를 통해 실제 엔티티 내부에 있는 필드 값을 조회한다.
      • 초기화가 된 후는 프록시 내부에 있는 필드 값을 가져온다.

    JPA의 동일성 보장: proxy 조회 후 find를 하면 둘다 proxy가 된다.

    프록시 초기화 불가: em.detach(), em.close()로 영속성 컨텍스트를 제거 후 실제 데이터를 불러오려고 하면 세션이 없다고 표시된다.

    org.hibernate.LazyInitializationException

    이미 생성된 객체에서 find를 통해 가져온 객체끼리는 ==로 비교 가능하다.

    getReference로 객체를 가져와서 타입 체크 시 프록시기 때문에 ==비교 대신, instance of를 사용해야 한다.

    m1 instanceof Member

    즉시 로딩과 지연 로딩

    • 지연 로딩(LAZY)를 이용하면 프록시로 조회한다. [권장]
      • @ManyToOne, @OneToOne은 default가 즉시 로딩이므로 LAZY로 설정한다.
    @ManyToOne(fetch = FetchType.LAZY)
    • 즉시 로딩(EAGER)를 이용하면 join으로 한 번에 조회하고, proxy가 필요 없어진다. 초기화도 필요 없다.
      • 예상하지 못한 SQL이 발생할 가능성이 있다.
      • JPQL에서 N+1 문제를 일으킨다.

    영속성 전이, 고아 객체

    영속성 전이 + 고아 객체를 둘다 사용하는 경우

    • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화하고 em.remove()로 제거할 수 있다.
    • 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
    • DDD의 Aggregate Root 개념을 구현할 때 유용

    cascade

    Parent가 persist되면 cascade(영속성 전이)를 통해 함께 persist 된다.

    연관 관계 매핑과 관련이 없다.

    단일 엔티티에 완전히 종속적일 때만 사용할 것을 권장한다.

    • cascade 종류
      • ALL: 모두 적용
      • PERSIST: 영속
      • REMOVE: 삭제
    //Child
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    
    //Parent
    @OneToMay(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> childList = new ArrayList<>();
    
    public void addChild(Child child) {
    	childList.add(child);
        child.setParent(this);
    }
    Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    parent.addChild(child1);
    parent.addChild(child2);
    
    em.persist(parent);

     

    고아 객체

    부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 제거한다.

    orphanRemoval이 true로 되어있으면, 컬렉션에서 빠진 것은 삭제된다.

    참조하는 곳이 하나일 때 사용해야 한다.

    특정 엔티티가 개인 소유할 때 사용한다.

    cascade all과 동일한 영향을 미친다.

    //Parent
    @OneToMay(mappedBy = "parent", orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();

    기본 값 타입

    엔티티 타입

    @Entity로 정의하는 객체

    데이터 변경 시 식별자로 추적 가능

     

    값 타입

    기본값 타입

    int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체

    데이터 변경 시 식별자가 없으므로 추적 불가

    생명주기를 엔티티에 의존한다.

    공유하면 안된다.

    • primitive type 기본 타입은 항상 값을 복사하기 때문에 절대 값을 공유하지 않는다.
    • 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경하면 안된다.
    //primitive type
    int c = 10;
    int d = c;
    
    c = 20;
    
    System.out.println(c); //20
    System.out.println(d); //10
    
    //============================
    
    //Wrapper class
    Integer a = new Integer(10);
    Integer b = a;
    
    a.setValue(20);
    
    System.out.println(a); //20
    System.out.println(a); //20

     

    임베디드 타입(복합 값 타입)

    새로운 값 타입을 직접 정의할 수 있다.

    JPA는 임베디드 타입이라고 한다.

    기본 생성자 필수

    재사용이 가능하고 높은 응집도를 가진다.

    의미있는 비즈니스 메서드를 만드는 데 도움된다.

     

    - @Embeddable: 값 타입을 정의하는 곳에 표시

    - @Embedded: 값 타입을 사용하는 곳에 표시

     

    Member 내에 필드로 존재하던

    등록 날짜, 수정 날짜 -> Period 클래스로 생성

    @Embeddable
    public class Period {
    	private LocalDateTime startDate;
        private LocalDateTime endDate;
    }
    
    public class Member {
    	@Embedded
    	private Period workPeriod;
    }

    길 번호, 주소명 -> Address 클래스로 생성

    @Embeddable
    public class Address {
    	private String city;
        private String street;
        private String zipcode;
    }
    
    public class Member {
    	@Embedded
    	private Address homeAddress;
    }

     

    @AttributeOverride 를 통해 컬럼명 속성을 재정의 할 수 있다.

    값 타입과 불변 객체

    임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. (side effect 가능성)

    Address address = new Address("city", "street", "10000");
    
    Member member = new Member();
    member.setUsername("memeber1");
    member.setHomeAddress(address);
    em.persist(member);
    
    Member member2 = new Member();
    member2.setUsername("memeber2");
    member2.setHomeAddress(address);
    em.persist(member2);
    
    //member, member2 객체 둘다 city가 "newCity"로 변경된다.
    member.getHomeAddress().setCity("newcity");

    따라서 값을 복사해서 사용하는 것이 안전하다.

    Address address = new Address("city", "street", "10000");
    
    Member member = new Member();
    member.setUsername("memeber1");
    member.setHomeAddress(address);
    em.persist(member);
    
    //값 복사 객체 생성
    Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
    
    Member member2 = new Member();
    member2.setUsername("memeber2");
    member2.setHomeAddress(copyAddress); //복사한 객체 삽입
    em.persist(member2);
    
    //member 객체만 city가 "newCity"로 변경된다.
    member.getHomeAddress().setCity("newcity");

     

    직접 정의한 값 타입은 객체 타입이다.

    따라서 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없고, 객체의 공유 참조는 피할 수 없다.

    객체 타입은 참조를 전달한다. (깊은 복사)

     

    객체 타입 참조 해결 방안 (불변 객체 설계)

    객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다.

    참조로 인한 사이드 이펙트를 없앨 수 있다.

    값 타입은 불변 객체(immutable object)로 설계할 것

     

    생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.

    Integer, String은 자바가 제공하는 대표적인 불변 객체이다.

     

    값 타입의 비교

    • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
    • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
      • 값 타입은 a.equlas(b)로 비교해주어야 한다.
      • equlas를 재정의하면, 객체의 필드가 동일한 객체는 논리적으로 같은 객체로 판단된다.
      • 오버라이드 시, 해시코드도 같이 구현해주어야 함(자바 컬렉션 사용) - 해시 값을 사용하는 컬렉션을 사용할 때 문제 발생 가능성
    int a = 10;
    int b = 10;
    System.out.println((a == b)); //true
    
    Address address1 = new Address("city", "street", "10000");
    Address address2 = new Address("city", "street", "10000");
    System.out.println((address1.equals(address2))); //true
    System.out.println((address1 == address2)); //false
    
    //Address
    @Override
    public boolean equlas(Object o) {
    	if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.getCity()) && //getter로 호출해야 proxy가 계산할 수 있음
        		Objects.equals(street, address.getStreet()) &&
                Objects.equals(zipcode, address.getZipcode());
    }
    
    @Override
    public int hashCode() {
    	return Objects.hash(city, street, zipcode);
    }

     

    값 타입 컬렉션

    식별자 id 같은 개념을 넣어서 pk로 사용하면 값 타입이 아니고 엔티티가 되어버린다.

    그래서 값 타입은 테이블에 값들만 저장이 되고 이걸 묶어서 pk로 구성하게 된다.

    -> 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

    수정할 때 생성자를 통해 통째로 갈아끼워야 한다. setter 금지!

     

    값 타입이기 때문에 상위 클래스를 persist하면 같은 라이프 사이클이 돌아가서 모두 persist 된다.

    값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다.

    값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 그래서 권장X

    대신에 일대다[1:N] 관계를 고려한다.

     

    값 타입을 하나 이상 저장할 때 @ElementCollection을 사용한다.

    컬렉션들은 기본적으로 지연(Lazy)로딩이다.

    @Embedded
    private Address homeAddress;
    
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", 
    	joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME") //값이 1개이므로 예외적으로 @Column 세팅
    private Set<String> favoriteFoods = new HashSet<>();
    
    @ElementCollection
    @CollectionTable(name = "ADDREDSS"
    		joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();


    객체지향 쿼리 언어

    JPQL

    객체 지향 SQL, 추상화 SQL(특정 SQL에 종속되지 않는다)

    엔티티 객체를 대상으로 쿼리를 진행한다.

    <->SQL: 데이터베이스 테이블을 대상으로 쿼리를 진행한다.

     

    기본 문법과 쿼리 API

    TypeQuery, Query

    - TypeQuery: 반환 타입이 명확할 때 사용

    - Query: 반환 타입이 명확하지 않을 때 사용

    TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
    TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);
    Query query3 = em.createQuery("select m.username, m.age from Member m");

     

    결과 조회 API

    - query.getResultList(): 결과가 하나 이상일 때. 결과 없는 경우 빈 리스트 반환

    - query.getSingleResult(): 결과가 하나일 때 단일 객체 반환. 결과 없는 경우 NoResultException, 둘 이상이면 NonUniqueResultException

     

    파라미터 바인딩

    TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class);
    query.setParameter("username", "member1");

     

    프로젝션

    //엔티티 타입
    select m from Member m
    select t from Member m join m.team t
    
    //임베디드 타입 (값 타입의 한계는 address는 m에 종속되어 있다.)
    select m.address from Member m
    
    //스칼라 타입 가능
    select m.username, m.age from Member m

     

    • 엔티티 프로젝션: 영속성 컨텍스트에서 관리된다.
    • 스칼라 타입 프로젝션
      • Query
      • Object[]
      • new (생성자처럼 사용 가능)
    List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
            .getResultList();
    
    MemberDTO memberDTO = result.get(0);
    System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());

     

    페이징 API

    dialect에 따라 변화해서 작성된다.

    • setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)
    • setMaxResults(int maxResult): 조회할 데이터 수
    List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
            .setFirstResult(1)
            .setMaxResults(10)
            .getResultList();

     

    조인

    //내부 조인
    select m from Member m [INNER] join m.team t
    
    //외부 조인
    select m from Member m left [OUTER] join m.team t
    
    //세타 조인
    select count(m) from Member m, Team t where m.username = t.name

    ON 절

    - 조인 대상 필터링

    //jpql
    select m, t from Member m left join m.team t on t.name = 'a'
    
    //sql
    select m.*, t.* from Member m left join Team t on m.TEAM_ID=t.id and t.name='a'

    - 연관관계 없는 엔티티 외부 조인

    //jpql
    select m, t from Member m left join team t on m.username = t.name
    
    //sql
    select m.*, t.* from Member m left join Team t on m.username = t.name

     

    서브 쿼리

    jpa는 from 절의 서브쿼리를 제공하지 않는다. -> 조인으로 풀 수 있으면 풀어서 해결. 안되면 native sql

    select 서브쿼리는 가능하다. hibernate에서 지원한다. 

    where, having 절에서 사용 가능하다.

     

    조건식

    case 식

    //기본 case식
    select
    	case when m.age <= 10 then '학생요금'
        	when m.age >= 60 then '경로요금'
            else '일반요금'
        end
    from Member m
    
    //단순 case식
    select
    	case t.name
        	when '팀A' then '110%'
            when '팀B' then '120%'
            else '105%'
        end
    from Team t

    coalesce: 하나씩 조회해서 null이 아니면 반환

    nullif: 두 값이 같으면 null 반환, 다르면 첫 번째 값 반환

    //사용자 이름이 없으면 이름 없는 회원을 반환
    select coalesce(m.username, '이름 없는 회원') from Member m
    
    //사용자 이름이 관리자면 Null을 반환하고 나머지는 본인의 이름을 반환
    select nullif(m.username, '관리자') from Member m

     

    기본 함수

    trim
    lower, upper
    length

    abs, sort, mod
    index (권장X)

    //concat
    select concat('a', 'b') from Member m
    
    //substring
    select substring(m.username, 2, 3) from Member m
    
    //locate
    select locate('de', 'abcdefg') from Member m //4번째 표시
    
    //size
    select size(t.members) from Team t //컬렉션 크기

    사용자 정의 함수도 호출할 수 있으나, 하이버네이트는 사용 전 방언에 추가해주어야 한다.

    사용하는 db 방언 상속받아서 등록

    select group_concat(m.username) from Member m
    select function('group_concat', m.username) from Member m

     

    경로 표현식

    묵시적 내부 조인 사용 X

    • 상태 필드: 경로 탐색 끝. 탐색 X
      • m.username
    • 단일 값 연관 경로: 묵시적 내부 조인 발생. 탐색 O
      • m.team(.name...)
    • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생. 탐색 X
      • t.members (List는 그대로 탐색 종료)
      • 명시적인 조인 작성 `select m from Team t join t.members m`

    페치 조인(fetch join)

    즉시 로딩과 비슷함~! 사용 안 하면 N+1 문제 발생된다.

    별칭을 줄 수 없다. 하이버네이트는 가능하지만 가급적 사용하지 않는다.

    둘 이상의 컬렉션은 페치 조인 할 수 없다.

    페이징 API 사용 불가(일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능)

    @BatchSize로 미리 가져올 크기 제한 가능

    persistence.xml로도 미리 설정 가능

    <property name="hibernate.default_batch_fetch_size" value="100"/>

    1. 페치 조인을 사용해서 엔티티를 조회한다.

    2. 페치 조인 후 애플리케이션에서 dto로 바꿔서 화면을 반환한다.

    3. new operation으로 dto로 스위칭해서 가져온다.

     

    지연 로딩 < 페치 조인 우선순위

     

    일대다 조인은 중복해서 출력되기 때문에 유의할 것(데이터 뻥튀기 이슈)

    jpql distinct 기능

    - sql에 distinct를 추가

    - 애플리케이션에서 엔티티 중복 제거 (같은 식별자를 가진 엔티티 제거)

     

    sql 조인 종류가 아니고, jpql에서 성능 최적화를 위해 제공하는 기능이다.

    연관된 엔티티나 컬렉션을 sql 한 번에 함께 조회하는 기능이다.

    //jpql
    select m from Member m join fetch m.team
    
    //sql
    select M.*, T.* from member M inner join team T on M.TEAM_ID = T.ID

     

    엔티티 직접 사용

    sql에서 해당 엔티티의 기본 키 값을 사용한다.

    -> 엔티티를 파라미터로 전달하거나 식별자를 직접 전달하는 것 모두 동일한 쿼리.

    (중요!) 외래 키 값에서 m.team = :team 을 조건으로 가져올 때, m.team은 @JoinColumn(name = "TEAM_ID")의 TEAM_ID를 뜻한다.

    //jpql
    select count(m.id) from Member m
    select count(m) from Member m
    
    //sql
    select count(m.id) as cnt from Member m

     

    Named 쿼리

    정적 쿼리. 어노테이션, XML에 정의

    애플리케이션 로딩 시점에 초기화 한 후 재사용한다.

    애플리케이션 로딩 시점에 쿼리를 검증한다.

     

    나중에 Spring Data JPA에서 interface에서 @Query로 사용할 수 있다!!

    @NamedQuery(
    	name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"
    )
    
    em.createNamedQuery("Member.findByUsername", Member.class)
    	.setParameter("username", "회원1")
        .getResultList();

     

    벌크 연산

    너무 많은 sql을 실행해야 할 때 유용하다.

    쿼리 한 번으로 여러 테이블의 로우를 변경할 수 있다.(엔티티)

    결과는 영향받은 엔티티 수를 반환한다.

     

    스프링 데이터 JPA에서 나중에 @Modifying 사용하면 된다.

    //flush 자동 호출
    //영속성 컨텍스트 값 업데이트 안됨 -> clear 필요!
    int resultCount = em.createQuery("update Member m set m.age = 20")
    	.executeUpdate();

     

    (중요!)

    영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날리기 때문에,

    벌크 연산을 먼저 실행하거나

    벌크 연산 수행 후 영속성 컨텍스트를 초기화해야 한다.


    Criteria

    String sql이 아니기 때문에 컴파일 시점에 문법 오류를 찾을 수 있고, 동적 쿼리를 작성하는 데 용이하다.

    그런데 잘 안 쓰긴 함... 이런게 있다 정도 알아두면 될듯.

    너무 복잡하고 실용성이 없다.

    -> QuerDSL 사용 권장!

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Member> query = cb.createQuery(Member.class);
    
    Root<Member> m = query.from(Member.class);
    CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
    
    List<Member> resultList = em.createQuery(cq).getResultList();

     

    QueryDSL

    동적쿼리 작성 편리

    단순하고 쉬움

    컴파일 시점에 문법 오류를 찾을 수 있음

    private final JPAQueryFactory queryFactory;
    
    public void hello() {
    	QMember m = QMember.member;
        List<Member> result = queryFactory
                      	  .select(m)
                            .from(m)
                            .where(m.name.like("kim"))
                            .fetch();
    }

     

    Native Query

    sql query를 작성한다.

    em.createNativeQuery("sql").getResultList();

     

    JDBC, SpringJdbcTemplate

    JDBC 커넥션을 직접 사용하거나 JdbcTemplate, Mybatis 등을 함게 사용할 수 있다.

    영속성 컨텍스트를 적절한 시점에 강제로 flush 할 필요가 있다.

    -> jpa를 우회해서 sql을 실행하기 직전에 영속성 컨텍스트 수동 플러쉬

    em.flush();
    
    dbconn.executeQuery("select * from member");

    * flush는 commit, create query가 실행되면 동작한다.

     

    728x90
    반응형

    'Blog > TIL' 카테고리의 다른 글

    [240606] queue 복습1  (0) 2024.06.07
    [240605] 문제를 잘 파악하자  (0) 2024.06.05
    [240603] JPA의 핵심, 연관관계의 주인 설정  (0) 2024.06.03
    [240602] JPA 영속성 컨텍스트  (0) 2024.06.02
    [240601] 코딩 테스트 문제 풀기  (0) 2024.06.01

    댓글