no image
[JPA] 즉시 로딩, 지연 로딩, 영속성 전이, 고아 객체
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.즉시 로딩과 지연 로딩public void printUserAndTeam(String memberId) { Member member = em.find(Member.class, memberId); Team team = member.getTeam(); System.out.println("회원 이름: " + member.getUsername()); System.out.println("소속팀: " + team.getName());}회원과 팀을 함게 출력해야하는 로직에서는 상관 없지만, 단순히 회원만 출력하는 로직에서는 굳이 팀까지 DB에서 조회해서 가져올 필요가 없다.public void print..
2024.07.27
no image
[JPA] 프록시(Proxy)
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.em.find() vs em.getReference()em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회import javax.persistence.EntityManager;import javax.persistence.EntityManagerFactory;import javax.persistence.Persistence;public class Example { public static void main(String[] args) { EntityManagerFactory emf = Persist..
2024.07.26
no image
[인프런 알고리즘] Chpater 3, 5번 문제(연속된 자연수의 합)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드첫 번째 코드(투 포인터 + 슬라이딩 윈도우)import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class sec03_05 { public static int solution(int N) { int count = 0, sum = 0; int lPtr = 0, halfPlusOne = (N / 2) + 1; int[] arr = new int[halfPlusOne]; for(int i = 0; i N) sum..
2024.07.26
no image
[JPA] 상속관계 매핑
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.상속관계 매핑관계형 데이터베이스는 상속관계라는 개념이 존재하지 않는다.RDB에서는 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사한 개념이다.상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것을 뜻한다.상속관계 매핑은 총 2가지 전략이 있다.조인 전략단일 테이블 전략기본적으로 조인 전략을 선택하되, 성능이 우선시된다면 단일 테이블 전략 선택 @Inheritance상속 매핑 전략을 설정하여 상위 클래스와 이를 상속받는 하위 클래스 간의 데이터 저장 방식을 지정할 수 있다. @Inheritance 애노테이션은 상위 클래스에 사용되며, 상속 매핑 전략으로 단일 테이블 전략, 조..
2024.07.25
no image
[인프런 알고리즘] Chpater 4, 4번 문제(연속 부분수열)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec03_04 { public static int solution(int N, int M, int[] arr) { int count = 0, sum = 0; int lptr = 0; for (int rptr = 0; rptr M) sum -= arr[lptr++]; if (..
2024.07.25
no image
[JPA] 다양한 연관관계 매핑
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.연관관계 매핑시 고려사항 3가지다중성단방향, 양방향연관관계의 주인 다중성다대일: @ManyToOne일대다: @OneToMany일대일: @OneToOne다대다: @ManyToMany 단방향, 양방향테이블객체외래 키 하나로 양쪽 조인 가능참조용 필드가 있는 쪽으로만 참조 가능방향이라는 개념이 없다.한쪽만 참조하면 단방향 양쪽이 서로 참조하면 양방향  연관 관계의 주인테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음객체 양방향 관계는 A->B, B->A 처럼 참조가 두 방향객체 양방향 관계는 참조가 두 방향이 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야 함연관관계의 주인: 외래 키를 관리하는 참조주인의 반..
2024.07.24
no image
[인프런 알고리즘] Chpater 3, 3번 문제(최대 매출)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드첫 번째 코드(중첩 for 문)import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.StringTokenizer;public class sec03_03 { public static int solution(int N, int M, int[] arr) { int max = Integer.MIN_VALUE; for(int i = 0; i max) max = tempSum; } return max;..
2024.07.24
no image
[인프런 알고리즘] Chapter 3, 2번 문제(공통원소 구하기)
이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.문제 설명 코드첫 번째 코드import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.Collections;import java.util.HashMap;import java.util.StringTokenizer;public class sec03_02 { public static ArrayList solution(int[] arr1, int[] arr2) { ArrayList integers = new ..
2024.07.23

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


즉시 로딩과 지연 로딩

public void printUserAndTeam(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 이름: " + member.getUsername());
    System.out.println("소속팀: " + team.getName());
}

회원과 팀을 함게 출력해야하는 로직에서는 상관 없지만, 단순히 회원만 출력하는 로직에서는 굳이 팀까지 DB에서 조회해서 가져올 필요가 없다.

public void printUser(String memberId) {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 이름: " + member.getUsername());
}

 

지연 로딩

@ManyToOne(fetch = FetchType.LAZY) 어노테이션은 JPA에서 연관 관계를 정의할 때 사용되며, fetch = FetchType.LAZY 옵션을 통해 지연 로딩(Lazy Loading)을 지정할 수 있다. 

이는 연관된 엔티티를 실제로 사용할 때까지 데이터베이스 조회를 지연시키는 방식이다.

  • @ManyToOne, @OneToOne은 기본(default)이 즉시 로딩
  • @OneToMany, @ManyToMany는 기본(default)이 지연 로딩

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY) //지연 로딩 설정
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    //getter and setter...
}

 

@Entity
public class Team {
    @Id  @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();

    //getter and setter...
}

 

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getID());

System.out.println("m = " + m.getTeam().getClass()); //프록시
System.out.println("=======");
m.getTeam().getName(); //프록시 초기화

 

즉시 로딩

@ManyToOne(fetch = FetchType.EAGER) 어노테이션은 JPA에서 연관된 엔티티를 즉시 로딩하도록 설정하는 방법이다. 

즉, 해당 엔티티를 조회할 때 연관된 엔티티도 함께 로드된다. 이는 기본적으로 사용되는 로딩 방식이기도 하다.

하지만 실무에서는 즉시 로딩을 사용해선 안된다!

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
    
    @ManyToOne(fetch = FetchType.EAGER) //즉시 로딩 설정
    
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}

 

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getID());

System.out.println("m = " + m.getTeam().getClass());
System.out.println("=======");
m.getTeam().getName();

 

즉시 로딩의 문제점

  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정을 변경해서 사용
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • 가급적 지연 로딩을 사용해야 한다.

 

영속성 전이(CASCADE)

JPA에서 영속성 전이(Cascade)는 엔티티의 상태 변화가 연관된 엔티티에게도 전이되는 것을 의미한다.

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 사용한다.

예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장해야할 때 사용한다.

참조하는 곳이 하나일 때 사용해야한다. 여러 곳에 연관관계가 있을 때 사용하면 안 된다.

이를 통해 개발자는 연관된 엔티티의 상태를 관리하기 쉽게 할 수 있다. @ManyToOne, @OneToMany, @OneToOne, @ManyToMany와 같은 관계에서 cascade 속성을 설정할 수 있다.

 

@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

주의

영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

 

Cascade 유형

  • CascadeType.PERSIST: 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화한다.
  • CascadeType.MERGE: 엔티티를 병합할 때 연관된 엔티티도 함께 병합한다.
  • CascadeType.REMOVE: 엔티티를 삭제할 때 연관된 엔티티도 함께 삭제한다.
  • CascadeType.REFRESH: 엔티티를 새로고침할 때 연관된 엔티티도 함께 새로고침한다.
  • CascadeType.DETACH: 엔티티를 준영속 상태로 만들 때 연관된 엔티티도 함께 준영속 상태로 만든다.
  • CascadeType.ALL: 모든 Cascade 유형을 포함한다.

 

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //영속성 전이 설정
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }

    //getter and setter...
}

 

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    //getter and setter...
}

 

Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

//em.persist(parent);
//em.persist(child1);
//em.persist(child2);

em.persist(parent);

 

 

고아 객체

orphanRemoval = true

JPA에서 고아 객체(orphan entity)란 부모 엔티티와의 연관 관계가 제거되어 더 이상 참조되지 않는 자식 엔티티를 의미한다. 이러한 고아 객체는 데이터베이스에서 자동으로 삭제되도록 설정할 수 있다. 이를 위해 JPA에서는 orphanRemoval 속성을 제공한다

고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

orphanRemoval = true 설정을 통해 고아 객체가 발생할 경우 자동으로 삭제되도록 할 수 있다. 

이 설정은 @OneToMany 및 @OneToOne 관계에서만 사용 가능하다.

 

Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);//자식 엔티티를 컬렉션에서 제거
//DELETE FROM CHILD WHERE ID=?

 

참조하는 곳이 하나일 때 사용해야 한다.
특정 엔티티가 개인 소유할 때 사용해야 한다.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) //영속성 전이 설정, 고아객체 삭제 설정
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
    
    //getter and setter...
}

 

@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    
    //getter and setter...
}

 

Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);

em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);

개념적으로 부모를 제거하면 자식은 고아가 된다.
따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다.
이것은 CascadeType.REMOVE처럼 동작한다.

 

CascadeType.ALL + orphanRemoval=true

두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
쉽게 설명하면, 부모 엔티티는 영속성 컨텍스트를 통해 관리되지만, 자식 엔티티는 영속성 컨텍스트가 아닌 부모 엔티티를 통해서 관리되는 것 처럼 보이는 것이다

'Java Category > JPA' 카테고리의 다른 글

[JPA] JPQL 기본 문법  (0) 2024.07.29
[JPA] 값 타입(Value Type)  (0) 2024.07.28
[JPA] 프록시(Proxy)  (0) 2024.07.26
[JPA] 상속관계 매핑  (0) 2024.07.25
[JPA] 다양한 연관관계 매핑  (5) 2024.07.24

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class Example {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("example-unit");
        EntityManager em = emf.createEntityManager();

        // 엔티티의 참조를 가져오는 예시
        em.getTransaction().begin();

        // 예시로 사용할 엔티티 클래스와 기본 키
        Long primaryKey = 1L;
        MyEntity entity = em.getReference(MyEntity.class, primaryKey);

        // entity를 실제로 사용하면 데이터베이스 조회가 발생
        System.out.println(entity.getName());

        em.getTransaction().commit();
        em.close();
        emf.close();
    }
}

 

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

 

프록시 객체의 초기화

  1. 프록시 객체의 메서드를 호출하거나 필드에 접근
  2. 영속성 컨텍스트에 초기화 요청
  3. 영속성 컨텍스트는 DB를 조회하여 데이터를 가져옴
  4. 영속성 컨텍스트는 가져온 데이터를 바탕으로 실제 엔티티를 생성
  5. 프록시 객체가 실제 엔티티의 메서드를 호출하거나 필드에 접근

 

프록시 객체를 사용할 때의 주의사항

1.초기화 시점

  • 프록시 객체는 처음 접근할 때 한 번만 초기화된다.
  • 예를 들어, 프록시 객체의 메서드를 호출하거나 필드에 접근할 때 실제 데이터베이스 조회가 발생하여 초기화된다.

2.프록시 객체와 실제 엔티티의 차이

  • 프록시 객체가 초기화되더라도, 이는 프록시 객체가 실제 엔티티로 변하는 것이 아니다.
  • 초기화된 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.

3.타입 체크 주의사항

  • 프록시 객체는 원본 엔티티 클래스를 상속받는다. 따라서, == 연산자로 타입을 비교하면 실패할 수 있다.
  • 대신, instanceof 연산자를 사용하여 프록시 객체인지 확인할 수 있다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member1.setUsername("member2");
em.persist(member2);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getID());
Member m2 = em.getReference(Member.class, member2.getID());

System.out.println("m1 = " + m1.getClass());
System.out.println("m2 = " + m2.getClass());

System.out.println("m1 == m2 = " + (m1 == m2));

 

4.영속성 컨텍스트의 역할

  • 영속성 컨텍스트에 이미 찾고자 하는 엔티티가 존재하면, em.getReference()를 호출해도 실제 엔티티가 반환된다.
  • 즉, 프록시 객체가 아닌 실제 엔티티 인스턴스가 반환된다.
Member member = new Member();
member.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member.getID());
System.out.println("m1 = " + m1.getClass());

Member m2 = em.getReference(Member.class, member.getID());
System.out.println("m2 = " + m2.getClass());

System.out.println("m1 == m2 = " + (m1 == m2));

 

참고로 JPA에서 영속성 컨텍스트에서 하나의 트랜잭션안에 묶여있다면 진짜 엔티티든, 프록시든 객체 비교시 동일성을 보장한다.

Member member = new Member();
member.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member.getID());
Member m2 = em.find(Member.class, member.getID());

System.out.println("m1 = " + m1.getClass());
System.out.println("m2 = " + m2.getClass());

System.out.println("m1 == m2 = " + (m1 == m2));

 

 

5.준영속 상태에서의 문제

  • 준영속 상태(persistent context의 도움을 받을 수 없는 상태)에서 프록시 객체를 초기화하면 문제가 발생할 수 있다.
  • 이는 LazyInitializationException 등의 예외를 초래할 수 있다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member1.getID());
System.out.println("m1 = " + m1.getClass());

em.detach(m1);

m1.getUsername();

 

 

프록시 관련 메서드

PersistenceUnitUtil.isLoaded(Object entity)

프록시 인스턴스의 초기화 여부를 알려주는 메서드이다.

Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush();
em.clear();

Member m1 = em.getReference(Member.class, member1.getID());

System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(m1));

 

entity.getClass()

프록시 클래스를 확인하는 메서드이다.

Member m1 = em.getReference(Member.class, member1.getID());
System.out.println("m1 = " + m1.getClass());

 

Hibernate.initialize(entity)

프록시 객체를 강제로 초기화하는 메서드이다.

Hibernate.initialize(m1); //강제 초기화

 

참고: JPA 표준은 강제 초기화 없음

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

첫 번째 코드(투 포인터 + 슬라이딩 윈도우)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class sec03_05 {
    public static int solution(int N) {
        int count = 0, sum = 0;
        int lPtr = 0, halfPlusOne = (N / 2) + 1;
        int[] arr = new int[halfPlusOne];

        for(int i = 0; i < halfPlusOne; ++i) arr[i] = i + 1;

        for(int rPtr = 0; rPtr < halfPlusOne; ++rPtr)
        {
            sum += arr[rPtr];
            while (sum > N) sum -= arr[lPtr++];
            if(sum == N) ++count;
        }

        return count;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        System.out.println(solution(N));
    }
}

 

두 번째 코드(수학적 접근)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class sec03_05 {
    public static int solution(int N) {
        int count = 0, temp = 1;
        --N;
        while (N > 0)
        {
            N = N - (++temp);
            if(N % temp == 0) ++count;
        }

        return count;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        System.out.println(solution(N));
    }
}

 

설명

첫 번째 코드 설명

  • 투 포인터와 슬라이딩 윈도우 알고리즘을 사용한다.
  • (N / 2) + 1 까지의 배열을 생성한다.
  • 두 개의 포인터(lPtr, rPtr)와 합(sum)을 사용하여 연속된 수의 합을 구한다.
  • sum이  N 보다 크면 lPtr을 오른쪽으로 이동시켜 sum을 줄인다.
  • sum이  N 과 같아지면 경우의 수를 증가시킨다.
(N / 2) + 1 까지의 배열을 생성하는 이유는, 연속된 자연수의 합으로 숫자 N 을 표현할 때, 최소 두 개의 숫자의 합이 N 이 되어야 하기 때문이다. 이를 통해 불필요한 계산을 줄이고, 효율적으로 답을 찾을 수 있다.
1.어떤 자연수 N 을 연속된 자연수의 합으로 나타낼 때, 최소 두 개 이상의 수가 필요하다.
예를 들어, N = 15 인 경우, 최소 두 개의 연속된 수가 합쳐져서 15가 되어야 한다.

2.만약 N 이 15라면, 15 를 넘지 않으면서 연속된 수의 합으로 나타낼 수 있는 가장 큰 자연수는 15의 절반인 7.5이다. 실제로 N/2 를 넘는 숫자부터는 N 을 넘지 않으면서 연속된 자연수의 합으로 표현할 수 없다.
예를 들어, N = 15 일 때 8 이상의 수를 시작점으로 연속된 합을 만들 수 없다.

3.N/2 까지만 배열을 생성하면, 불필요한 연산을 줄일 수 있다. 예를 들어, N = 15 인 경우 N/2 = 7.5 이고, 여기에 1을 더해 8까지의 배열을 생성하면 충분하다.
따라서 halfPlusOne = (N / 2) + 1은 최적화된 범위이다.

 

두 번째 코드 설명

  • 초기설정
    -count는 연속된 자연수의 합으로  N 을 나타낼 수 있는 방법의 수를 세는 변수이다.
    -temp는 현재 연속된 자연수의 개수를 나타내는 변수로, 초기값은 1이다.
    -N 에서 1을 뺀 값으로 시작한다.
  • 루프
    -N 이 0보다 클 때까지 반복한다.
    -N 에서 temp를 증가시킨 값을 뺀다.
    -현재  N  값이 temp로 나누어 떨어지면, 연속된 자연수의 합으로  N 을 나타낼 수 있는 경우이므로 count를 증가시킨다.
  • 예를 들어,  N = 15 일 때:
    -초기값:  N = 14 , temp = 1
    -첫 번째 루프:  N = 14 - 2 = 12 , temp = 2, 12 % 2 == 0 -> count = 1
    -두 번째 루프:  N = 12 - 3 = 9 , temp = 3, 9 % 3 == 0 -> count = 2
    -세 번째 루프:  N = 9 - 4 = 5 , temp = 4, 5 % 4 != 0
    -네 번째 루프:  N = 5 - 5 = 0 , temp = 5, 0 % 5 == 0 -> count = 3

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


상속관계 매핑

관계형 데이터베이스는 상속관계라는 개념이 존재하지 않는다.

RDB에서는 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사한 개념이다.

상속관계 매핑이란 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것을 뜻한다.

상속관계 매핑은 총 2가지 전략이 있다.

  • 조인 전략
  • 단일 테이블 전략

기본적으로 조인 전략을 선택하되, 성능이 우선시된다면 단일 테이블 전략 선택

 

@Inheritance

상속 매핑 전략을 설정하여 상위 클래스와 이를 상속받는 하위 클래스 간의 데이터 저장 방식을 지정할 수 있다. @Inheritance 애노테이션은 상위 클래스에 사용되며, 상속 매핑 전략으로 단일 테이블 전략, 조인 전략, 테이블 퍼 클래스 전략을 제공한다.

strategy: 상속 매핑 전략을 지정하며, InheritanceType.SINGLE_TABLE, InheritanceType.JOINED 중 하나를 선택할 수 있다.

@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

 

@DiscriminatorColumn

상위 클래스와 이를 상속받은 하위 클래스 간의 데이터 구분을 위해 사용된다. 이 애노테이션은 단일 테이블 전략이나 조인 전략에서 사용되며, 특정 컬럼을 기준으로 엔티티 타입을 구분한다.

 

주요 속성

  • name: 테이블에 저장될 컬럼의 이름을 지정한다.
@DiscriminatorColumn(name = "DTYPE")// 기본이 "DTYPE"

 

 

조인 전략

 

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

	//getter and setter...
}

 

@Entity
public class Movie extends Item{
    private String director;
    private String actor;
    //getter and setter...
}

 

@Entity
@DiscriminatorValue("MyAlbum")//DTYPE 컬럼에 저장될 이름 지정
public class Album extends Item{
    private String artist;
    //getter and setter...
}

 

@Entity
public class Book extends Item{
    private String Author;
    private String isbn;
    //getter and setter...
}

 

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Movie movie = new Movie();
            movie.setDirector("aaaa");
            movie.setActor("bbbb");
            movie.setName("바람과 함께 사라지다.");
            movie.setPrice(10000);

            Album album = new Album();
            album.setArtist("Rebugs");
            album.setPrice(2000);
            album.setName("JPA");

            em.persist(movie);
            em.persist(album);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용가능
  • 저장공간 효율화

 

단점

  • 조회시조인을많이사용,성능저하
  • 조회 쿼리가 복잡함
  • 데이터 저장시 INSERT SQL 2번 호출

 

단일 테이블 전략

 

Item 테이블만 변경, 나머지는 모두 그대로

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)//변경된 부분
@DiscriminatorColumn
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

	//getter and setter...
}

 

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
  • 조회 쿼리가 단순함

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
  • 단일테이블에모든것을저장하므로테이블이커질수있다.상 황에 따라서 조회 성능이 오히려 느려질 수 있다.

 

@MappedSuperclass

  • 상속관계 매핑X
  • 엔티티X, 테이블과 매핑X
  • 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
  • 조회, 검색 불가(em.find(BaseEntity) 불가)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
  • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능
@MappedSuperclass
public abstract class BaseEntity {
    private String createBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
    //getter and setter...
}

 

@Entity
public class Member extends BaseEntity{ //상속
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

 

@Entity
public class Team extends BaseEntity{ //상속
    @Id  @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

 

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec03_04 {
    public static int solution(int N, int M, int[] arr)
    {
        int count = 0, sum = 0;
        int lptr = 0;

        for (int rptr = 0; rptr < N; ++rptr)
        {
            sum += arr[rptr];
            while (sum > M) sum -= arr[lptr++];
            if (sum == M) count++;
        }

        return count;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        int[] arr = new int[N];
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());
        System.out.println(solution(N, M, arr));
    }
}

 

설명

  • 투 포인터, 슬라이딩 윈도우 알고리즘을 복합적으로 사용해야 한다.
  • lptr 포인터와 rptr 포인터를 사용하여 배열을 탐색한다.
  • rptr 포인터는 배열의 처음부터 끝까지 이동하며, sum 변수에 현재 포인터가 가리키는 값을 더한다.
  • sum이 M보다 클 경우, lptr 포인터를 이동시키면서 sum에서 값을 뺀다.
  • sum이 M과 같아지면, count를 증가시킨다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


연관관계 매핑시 고려사항 3가지

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

 

다중성

  • 다대일: @ManyToOne
  • 일대다: @OneToMany
  • 일대일: @OneToOne
  • 다대다: @ManyToMany

 

단방향, 양방향

테이블 객체
외래 키 하나로 양쪽 조인 가능 참조용 필드가 있는 쪽으로만 참조 가능
방향이라는 개념이 없다. 한쪽만 참조하면 단방향
  양쪽이 서로 참조하면 양방향

 

 

연관 관계의 주인

  • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
  • 객체 양방향 관계는 A->B, B->A 처럼 참조가 두 방향
  • 객체 양방향 관계는 참조가 두 방향이 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야 함
  • 연관관계의 주인: 외래 키를 관리하는 참조
  • 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능(Read Only)

 

 

다대일 [N : 1]

다대일은 가장 많이 사용하는 연관관계이다.

다대일의 반대는 일대다[1: N]이다.

단방향

 

@Entity
public class Member {
    @Id
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
 private int age;

    
    @Column(name = "TEAM_ID")
    private Long teamId;

    
    @ManyToOne //Member 입장에서 Team과의 관계는 ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
	
    //getter and setter...
}

이렇게 엔티티를 설정하면 단방향 매핑(Member -> Team)이 된다.

 

양방향

외래 키가 있는 쪽이 연관관계의 주인이 되도록 설정해야 한다.

양쪽을 서로 참조하도록 개발 한 것이다.

@Entity
public class Member {
    @Id
    private Long id;
    
    @Column(name = "USERNAME")
    private String name;
 private int age;

    
    @Column(name = "TEAM_ID")
    private Long teamId;

    
    @ManyToOne //Member 입장에서 Team과의 관계는 ManyToOne
    @JoinColumn(name = "TEAM_ID") //외래키 지정
    private Team team;
	
    //getter and setter...
}

 

@Entity
public class Team {
private String name;
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team") //Team 입장에서는 Member와 OneToMany
    List<Member> members = new ArrayList<Member>();
    
    //getter and setter...
}

Member 테이블은 연관관계의 주인이므로 아무 속성 없이 @ManyToOne 애노테이션을 사용한다.

또한, 외래키를 통해 JPA가 JOIN을 하기 때문에 @JoinColumn를 통해 외래키를 지정해주어야 한다.

연관관계 주인이 아닌 Team 테이블은 MappedBy 속성을 사용한다.(@OneToMany(mappedBy = "team"))

 

일대다 [N : 1]

단방향

  • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

	//getter and setter..
}

 

@Entity
public class Team {
    @Id  @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
	
    //getter and setter...
}

 

public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setUsername("member1");

            em.persist(member);

            Team team = new Team();
            team.setName("teamA");
            team.getMembers().add(member); //UPDATE 쿼리 생성

            em.persist(team);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();

    }
}

일대다 단방향 매핑의 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 좋다.

 

양방향

이런 매핑은 공식적으로 존재하지 않는다.

@JoinColumn(insertable=false, updatable=false)

읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;

}

 

@Entity
public class Team {
    @Id  @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

양쪽 엔티티를 연관관계의 주인으로 설정하고 한쪽을 읽기 전용으로 설정하는 방식이다.

 

결론은 다대일 양방향을 사용해야한다.

 

일대일 [1 : 1]

일대일 관계는 그 반대도 일대일

주 테이블이나 대상 테이블 중에 외래 키 선택 가능

  • 주 테이블에 외래 키
  • 대상 테이블에 외래 키

외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

 

단방향

다대일(@ManyToOne) 단방향 매핑과 유사

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

 

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

    private String name;
}

 

연관관계의 주인이 아닌 테이블 즉, 대상 테이블에 외래키 단방향은 존재하지 않는다.

 

 

양방향

CASE 1

다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인

반대편은 mappedBy 적용

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long ID;

    @Column(name = "USERNAME")
    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

 

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
}
  • 주 객체가 대상 객체의 참조를 가지는 것 처럼
  • 주 테이블에 외래 키를 두고 대상 테이블을 찾음
  • 객체지향 개발자 선호
  • JPA 매핑 편리
  • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
  • 단점: 값이 없으면 외래 키에 null 허용

 

 

CASE 2

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

    private String name;
    
    @OneToOne(mappedBy = "locker")
    private Member member;
}

 

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;

    private String name;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Member member;
}

 

  • 대상 테이블에 외래 키가 존재
  • 전통적인 데이터베이스 개발자 선호
  • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
  • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

 

다대다 [N : M]

다대다 매핑은 사용해선 안된다.

따라서 중간 테이블을 두고 @OneToMany, @ManyToOne 를 활용해서 매핑해야 한다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

첫 번째 코드(중첩 for 문)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec03_03 {
    public static int solution(int N, int M, int[] arr) {
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < N - (M - 1); ++i)
        {
            int tempSum = 0;
            for(int j = i; j < N - (N - M) + i; ++j) tempSum += arr[j];

            if(tempSum > max) max = tempSum;
        }

        return max;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        st = new StringTokenizer(br.readLine());
        int [] arr = new int[N];
        for(int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());
        System.out.println(solution(N, M, arr));
    }
}

 

두 번째 코드(슬라이딩 윈도우)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class sec03_03 {
    public static int solution(int N, int K, int[] arr) {
        int tempSum = 0;
        for(int i = 0; i < K; ++i) tempSum += arr[i];
        int max = tempSum;

        for(int i = K; i < N; ++i)
        {
            tempSum += (arr[i] - arr[i - K]);
            if(tempSum > max) max = tempSum;
        }

        return max;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int K = Integer.parseInt(st.nextToken());
        st = new StringTokenizer(br.readLine());
        int [] arr = new int[N];
        for(int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());
        System.out.println(solution(N, K, arr));
    }
}

 

설명

두 번째 코드(슬라이딩 윈도우) 에대한 설명

  • tempSum을 0으로 초기화하고, 첫 번째 for 루프에서 배열의 첫 번째 K개의 요소의 합을 계산하여 tempSum에 저장한다.
  • 초기 최대값 max를 첫 윈도우의 합인 tempSum으로 설정한다.
  • 두 번째 for 루프에서는 슬라이딩 윈도우 기법을 사용하여 다음 요소를 더하고, 이전 요소를 빼서 새로운 윈도우의 합을 구한다. 이 합이 max보다 크면 max를 갱신한다.

이 알고리즘 문제는 인프런의 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비 (김태원)의 문제입니다.


문제 설명

 

코드

첫 번째 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.StringTokenizer;

public class sec03_02 {
    public static ArrayList<Integer> solution(int[] arr1, int[] arr2) {
        ArrayList<Integer> integers = new ArrayList<>();
        HashMap<Integer, Boolean> map = new HashMap<>();

        for (int i : arr1) map.put(i, false);
        for (int i : arr2) if(map.containsKey(i)) integers.add(i);
        Collections.sort(integers);

        return integers;
    }
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for(int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());

        int M = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        int[] arr2 = new int[M];
        for(int i = 0; i < M; i++) arr2[i] = Integer.parseInt(st.nextToken());

        for (Integer i : solution(arr, arr2)) System.out.print(i + " ");
    }
}

 

두 번째 코드(투 포인터)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.StringTokenizer;

public class sec03_02 {
    public static ArrayList<Integer> solution(int[] arr1, int[] arr2) {
        ArrayList<Integer> integers = new ArrayList<>();
        Arrays.sort(arr1); Arrays.sort(arr2);
        int ptr1 = 0, ptr2 = 0;
        
        while (ptr1 < arr1.length && ptr2 < arr2.length)
        {
            if (arr1[ptr1] == arr2[ptr2])
            {
                integers.add(arr1[ptr1]);
                ++ptr1;
                ++ptr2;
            }
            else if (arr1[ptr1] < arr2[ptr2]) ++ptr1;
            else if (arr1[ptr1] > arr2[ptr2]) ++ptr2;
        }

        return integers;
    }
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] arr = new int[N];
        for(int i = 0; i < N; i++) arr[i] = Integer.parseInt(st.nextToken());

        int M = Integer.parseInt(br.readLine());
        st = new StringTokenizer(br.readLine());
        int[] arr2 = new int[M];
        for(int i = 0; i < M; i++) arr2[i] = Integer.parseInt(st.nextToken());

        for (Integer i : solution(arr, arr2)) System.out.print(i + " ");
    }
}

 

설명

두 번째 코드(투 포인터)에 대한 설명

  • 두 배열 arr1과 arr2를 정렬한다.
  • 두 배열의 포인터 ptr1과 ptr2를 초기화한다.

  • 두 포인터를 이동시키면서 공통된 원소를 찾는다.
    - arr1[ptr1]와 arr2[ptr2]가 같으면 해당 원소를 결과 리스트에 추가하고 두 포인터를 모두 증가시킨다.
    - arr1[ptr1]가 arr2[ptr2]보다 작으면 ptr1을 증가시킨다.
    - arr1[ptr1]가 arr2[ptr2]보다 크면 ptr2를 증가시킨다.

  • 두 포인터 중 하나가 배열의 끝에 도달할 때까지 위 과정을 반복한다.
  • 결과 리스트를 반환한다.