SHsus 2022. 12. 2. 20:33

새로운 조원들과 시작한 4주차 스프링 숙련 주차입니다.

이번에도 저번의 강사님의 새로운 강의로 시작 ~

 

 

 

 

 

 

입문 주차의 복습

입문 주차동안 JPA에 대해 간단하게 사용법에 대해 알아봤다.

이 주차에서는 JPA 구현 원리와 내부 동작을 더 살펴보고, 더 복잡한 상황을 다루기 위한 관련 내용의 학습을 한다.

 

내부 동작이나 구현 원리까지 알아야 하는 이유가 뭘까 ?

 

라이브러리, 프레임워크를 잘 배우려면 이게 왜 우리를 편하게 해주는지 에 대해서 고민하면 좋다.

잘 사용하기 위해서는 어떻게 우리를 편하게 해주는지에 대한 생각을 하는것이 좋다.

 

이번에 제공되는 강의에 대해서는 전부 다 알고 이해하는것이 좋다고 한다(흐음...?)

다만 한번에 다 이해하려 하지 말고 나중에 다시 보게 되면 그 때 도움이 될 수 있는 수준까지를 목표로 하자.

 

 

 

 

 

이번 주차의 목표

1. 내부 동작에 대한 이해를 위한 영속성 컨텍스트 에 대해 이해하기
2. 더 구체적으로 어노테이션을 배우고 다양한 상황에 응용하기
3. 나중에 JPA 만으로 처리가 어려운 상황을 위한 대응 즉, 준비하기

 

 

 

 

시작하기에 앞서

입문주차에 사용한것은 JPA 가 아니라 Spring Data Jpa 라는 라이브러리를 사용하였다.

JPA 를 도와서(랩핑) 사용하기 편하게 만들어주는 역할을 한다.

이제 일부 코드를 통해 JPA 와 어떤 차이가 있는지 알아보자.

 

Spring Data Jpa 의 코드

// Entity를 생성 및 save
Member minsook = new Member();
member.setId("abcd1234"); 
member.setUsername("민숙");

memberRepository.save(minsook);
memberRepository.find();

 

JPA 를 직접 사용한 코드
위에서 처럼 Entity 를 먼저 생성한 후에 아래의 코드처럼

1. EntityManager를 생성해줄 EntityManagerFactory를 만들어야 한다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa심화주차");

 

이처럼 Entity 를 관리할 목적으로 팩토리를 왜 또 만들어야 하나...?

이는 하나의 큰 작업을 동시에 처리하려 하다 보면 동시성의 문제가 발생할 수 있다.

동시성 개념이 잘 이해가 않가는데 이를 한줄로 요약하자면 아래와 같다.

 

두 개 이상의 세션이 공통된 자원에 대해 모두 읽고 쓰는 작업을 할 때 발생하는 문제

 

운영체제의 동시성 문제

동시성 문제란 무엇이며, 동기와 비동기는 무엇일까?

velog.io

위의 글을 참고했다. 검색해보니 이 JPA 에서 말하는 동시성이랑 의미는 일치하는 것 같다.

 

위 내용을 이어서...

즉, 특정 리소스나 정보는 서로 공유하지 못하게 처리하는 것이 필요하다.

팩토리 따로 만들어지는 이유는 여러 작업들이 하나의 엔티티 매니저만을 사용하는것을 방지하기 위해서다.

팩토리의 역할은 필요할 때 마다 여러개의 엔티티매니저를 생성한다.

 

위에서 엔티티 생성 및 매니저 생성도 끝냈으면 이번에는

 

2. Entity를 관리해줄 EntityManager를 EntityManagerFactory에서 생성!

EntityManager em = emf.createEntityManager();

 

3. 저장 및 조회 예시

// 엔티티를 영속화(저장)
em.persist(minsook);
// 엔티티를 찾기
em.find(Member.class, 100L);

 

위 같은 방식으로 훨씬 더 번거롭게 과정이 진행된다.

이를 이미지로 정리하면 아래와 같다.

 

 

 

 

 

 

 

 

 

영속성 컨텍스트란 ?

엔티티를 영구 저장 하는 환경 이라는 의미이다.
어플리케이션(자바 코드 자체)이 DB 에서 꺼내온 데이터 객체를 보관하는 역할
엔티티 매니저를 통해 엔티티를 조회 하거나 저장할 때 엔티티를 보관하고 관리한다.

 

좀 더 쉽게 이해하자면...

엔티티 매니저마다 개별적으로 부여되는 공간같은 개념

엔티티 객체를 엔티티 매니저마다 가지고 있는 영속성 컨텍스트 라는 공간에다 넣고 빼고 하면서 사용

이를 "영속화 한다" 라고 한다.

 

비영속(New)

영속성 컨텍스트와는 관계가 없는 새로운 상태
실제 DB의 데이터와는 관련이 없고 자바 객체인 상태를 말한다.
아래같이 엔티티를 생성만 했을 뿐인 단계가 그 예시다.

Member minsook = new Member();
member.setId("minsook");
member.setUsername("민숙");

 

영속(Managed)

엔티티 매니저를 통해 엔티티가 영속성 컨택스트에 저장되어 관리되고 있는 상태
이 시점부터 데이터 생성, 벼경 등을 추가하면 JPA 가 추적하면서 필요시 DB에 반영
엔티티 매니저를 통해 영속성 컨텍스트에 엔티티를 저장

em.persist(minsook);

 

준영속(Detached)

영속성 컨텍스트에서 관리되다가 분리된 상태

// 엔티티를 영속성 컨택스트에서 분리
em.detach(minsook);
// 영속성 컨텍스트를 비우기
em.clear();
// 영속성 컨택스트를 종료
em.close();

 

삭제(Removed)

영속성 컨텍스트에서 삭제된 상태

em.remove(minsook)

 

 

 

이러한 설계의 이유는 ?

 

1차 캐시

DB와 함께하는 작업은 상대적으로 부하와 비용이 있는 작업이다. 그래서 이러한 부담을 줄일 필요가 있다.
만약 자바 애플리케이션 에서 데이터를 조회할 때 마다 DB로 셀렉문 같은 SQL 쿼리를 줄이는 것이 좋다.
이를 위해서 영속성 컨텍스트 내에 1차 캐시를 만든다.

 

1. find("member") 와 같은 로직이 있을 때 먼저 1차 캐시를 조회

2. 있을 경우 해당 데이터를 반환

3. 없을 경우 실제 DB 로 쿼리문을 보낸다.

4. 이제 반환하기 전에 1차 캐시에 자장하고 반환

 

위 처럼 DB 에 직접 다녀가지 않고 해결이 가능하다.

 

 

쓰기 지연 SQL 저장소

Member A, Member B 라는 엔티티가 있다고 가정
이를 생성할 때 마다 DB 에 다녀오는 것은 매우 비효율 적이다.
그래서 여러번 DB 를 방문하지 않기 위해서 내부에 "쓰기 지연 SQL 저장소" 가 존재한다.

 

1. member A, member B 를 영속화

2. entityManger.commit() 메서드 호출

3. 내부적으로 쓰기 지연 SQL 저장소에서 Flush가 발생

4. INSERT A , INSERT B 와 같은 쓰기 전용 쿼리들이 DB 로 흘러들어 간다.

 

위에서 말하는 FLUSH 쌓인 것을 밀어낸다는 의미로 해석하면 될 것 같다.

즉 코드를 밀어내서 DB 에 넣어주는 역할 정도로 해석하자

 

 

DirtyChecking

데이터의 변경을 감지해서 자동으로 수정하는 역할
JPA가 1차 캐시와 쓰기지 SQL 저장소를  이용해서 변경과 수정을 감지해준다.

1차 캐시에서 DB의 엔티티 정보 외에도 조회한 시점의 데이터도 같이 저장한다.

즉, 이 때 엔티티객체와 조회 시점의 데이터가 다르면 변경이 발생한 것을 알 수 있다.

이를 위해 변경 부문을 반영 할 수 있는 UPDATE 쿼리를 작성해둔다.

 

 

동일성 보장

동일성 비교를 예로 인스턴스의 참조 값 비교
즉, 인스턴스의 참조 값 비교를 통해 데이터와 애플리케이션 단의 동일성을 보장한다는 의미
이는 동등성 비교와 헷갈릴 수 있는데 이를 이전에 사용했던 함수를 생각하면 이해가 쉽다.

대입연산자인 " == " 과 " equlas() " 를 통한 비교를 생각하면 된다.
equlas() 의 경우 인스턴스 내부에 있는 값 비교 를 의미하니 == 과는 전혀 다르다.

 

아래의 코드가 바로 이에대한 이해하기 쉬운 예시가된다.

Member member1 = em.find(Member.class, "minsook");
Member member2 = em.find(Member.class, "minsook");
System.out.println(member1 == member2) => true

 

1차 캐시는 Map으로 인스턴스를 캐싱하고 있기 때문에,
같은 식별자(@ID 값)에 대해 매번 같은 인스턴스에 접근하게 되므로 동일성이 보장이 된다. 

 

 

 

 

 

 

 

엔티티 매핑 심화

지난 입문주차에 다뤘던 내용들에 대해서 조금 더 디테일한 설명과 설정들에 메인 주제
지난주 내용과 교차하고 대조하면서 보면 좋을 것 같다고 하지만 모니터가 부족하다 !!!!

 

코드를 예시로 바로바로 기록 !!

 

 

@Entity 
// JPA 가 내부적으로 구분하는 이름
// DB 의 테이블과 일대일로 매칭되는 객체단위이다.
// 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미한다.
// 필수인 기본 생성자이다.  final 클래스, enum, interface 등에는 사용이 불가능하다.
// 설정을 따로 하지 않을 시 기본값으로 클래스 이름을 그대로 사용한다.

@Table (name="USER") 
// 엔티티와 매핑할 테이블의 이름이다.
// 클래스 이름과 테이블 이름이 다를 경우 JPA 에게 테이블 이름을 알려준다.
// 생략할 경우 DB의 테이블명에 클래스 이름을 그대로 사용한다.
public class Member { 

@Id 
// DB의 테이블이 기본적으로 가지는 유일값 PK(Primary Key) 가 있는데,
// 이것을 기준으로 질의한 데이터를 추출해 결과셋으로 반환해 준다. 대부분의 테이블에는 반드시 PK 가 존재한다.
// JPA 에도 엔티티 클래스 상에 해당 PK 를 명시적으로 표기하는데 이 때 이 어노테이션을 사용한다.

@Column (name = "user_id") 
// 객체 필드를 테이블 컬럼에 매핑하는데 사용한다. 생략도  가능하다.
// 속성들은 자주 쓸 일이 없고, 특정 속성중 하나로 effect 라는 속성이 있다.

private String id; 
 
private String username; 

private Integer age; 

@Enumerated (EnumType. STRING) 
// 자바의 Enum 을 테이블에서 사용한다고 생각하면 된다.
// Enum : 관련이 있는 상수들의 집합. 즉, 고정된 값을 의미하는 상수를 말한다.
// 속성에는 Ordinal, String 이 있는데, String은 해당 문자열을 그대로 저장하기 때문에 코스트가 높다.
// 하지만 나중에 Enum 으로 변경되어 위험할 일이 없기 때문에 일반적으로는 String 을 많이 사용한다.
private RoleType userRole;

// @Enumerated (EnumType. ORDINAL) 
// private RoleType userRole;

@Temporal (TemporalType. TIMESTAMP) 
private Date createdDate;

@Temporal (TemporalType. TIMESTAMP)  
private Date modifiedDate;
 }

 

기본 어노테이션 참고 링크

 

JPA 기본 Annotation 정리

@Entity @Entity 어노테이션은 데이타베이스의 테이블과 일대일로 매칭되는 객체 단위이며 Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미합니다. 그래서 객체의 인스턴스를 구

www.icatpark.com

 

 

 

 

 

 

 

연관관계 관련 심화

단방향 연관관계

@Entity
@Getter
@Setter
public class Member {
     @Id
     @Column(name = "member_id")
     private String id;
     private String username;

     @ManyToOne
     // 이름 그대로 다대일(N:1) 관계라는 매핑 정보이다.
     // 한명의 유저가 여러개의 주문을 한다는 가정을 생각하고 보자.
     // 주요 속성에는 optional, fetch, cascade 가 있으며, 이 중 하난 fetch 로 설정하면
     // 항상 연관된 엔티티가 있어야 생성이 가능하다는 의미

     @JoinColumn(name="team_id")
     // 외래 키를 매핑할 때 사용한다. 기본적으로 @Column 이 가지고 있는 필드 매핑관련
     // 옵션 설정들과, 외래키 관련 몇가지 옵션이 추가되어 있는 옵션이다.,
     private Team team;

     public void setTeam(Team team) {
         this.team = team;
     }
}

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

     private String name;
}

 

양방향 연관관계

 

Member Entity

여러가지 주문을 할 수 있기 때문에 @OneToMany 를 사용

@Getter
@Entity
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	@Column(nullable = false)
    private String memberName;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Orders> orders = new ArrayList<>();

    public Member(String memberName) {
        this.memberName = memberName;
    }
}

 

Orders Entity

이 하나에 음식 하나, 주문자 한명을 가지고 있기 때문에 @ManyToOne 를 사용

@Getter
@Entity
@NoArgsConstructor
public class Orders {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "food_id")
    private Food food;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    public Orders(Food food, Member member) {
        this.food = food;
        this.member = member;
    }
}

 

Food Entity

여러가지 주문을 가질 수 있기 때문에 @OneToMany 사용

@Getter
@Entity
@NoArgsConstructor
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String foodName;
    @Column(nullable = false)
    private int price;

    @OneToMany(mappedBy = "food",fetch = FetchType.EAGER)
    private List<Orders> orders = new ArrayList<>();

    public Food(String foodName, int price) {
        this.foodName = foodName;
        this.price = price;
    }
}

 

위의 예제코드를 보면서 설명하자면...

Member 와 Food 는 서로간에 이렇다할 차이가 존재하지는 않는다.

 

하지만 양방향의 예시인 만큼 Orders Entity 는 단방향에 없었던 @OneToMany 어노테이션이 존재한다.

이를 통해 양방형을 표기해준다고 생각해도 무방하다.

 

객체에는 사실 양방향 연관관계 라는 것이 존재하지 않는다.
서로 다른 단방향으로 조회하는 로직 2개를 잘 묶어서 양방향인 것처럼 보인게 한 것이다.
저 상세하게 풀이하면 맴버객체에 주문객체의 주소값을, 주문객체에는 맴버객체의 주소값을 가진다.

 

DB 테이블에서의 모양

 

반면 위와 같이 DB 테이블에서의 관계를 보자면

외래키는 연관관계각 있는 두개의 테이블 중에서 하나의 테이블에만 있으면 된다.

그래서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는데, 이것을 연관관계의 주인이라 한다.

여기서 결정된 연관관계의 주인만이 DB 연관관계와 매핑되고 외래키를 관리한다.

 

 

 

 

양방향 연관관계의 주의점

연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력해야 한다.
DB에 외래키 값이 정상적으로 저장되지 않으면 이부분을 의심해야 한다.

 

위에서 한 말이 잘 이해가 가지 않는데 아래의 코드처럼 해본 결과를 보면...

// 연관관계의 주인을 2개 생성
Order order = new Order ("order", "order”);
em.persist(order);

Order order2 = new Order (”order2", "order2”);
em.persist(order2);

// 멤버 생성
Member member = new Member("member", ”member”);

// 여기가 실수 포인트!!!
// member 에 들어있는 Orders 에 위에서 만든 Order 2개를 넣는다.
member.getOrders().add(order);
member.getOrders().add(order2);

// 이후에 member 를 저장
em.persist(member);

null 값이 들어간 것을 볼 수 있다.

 

null 값이 들어간 이유는 저장하기 전에  order.member 에는 아무 값도 입력하지 않았기 때문이다.

그래서 memberId 의 외래 키의 값이 Null 이 저장된다.

 

해결법 1.

객체 관점에서 양쪽 방향에 모두 값을 입력해 주는것으로 안정적이게 해결

양방향 모두 값을 입력하지 않으면 JPA 를 사용하지 않는 순수한 객체 상태에서는 문제가 커질 수 있다.

 

order.setMember(member)
member.getOrders().add(order);

 

해결법 2.

위의 방식을 매번 할때마다 하면 불편하고 비효율적이다.

그래서 여기서는 반복문을 사용해서 좀 더 쉽게 해결하도록 하자.

 

// Order 안에서 Memeber 를 넣어줄 때,
// 그때 그 member의 order 의 값도 같이 넣어준다.
private Order order;
  public void setMember(Member member) {
    this.member = member;
    member.getOrders().add(this);
    // 그래서 getOrders.add 의 this 는 Order Entity 를 의미한다.
  }
  ...
}

 

 

 

 

 

 

프록시

연관된 객체들을 DB 에서 조회하기 위해서 사용된다.
프록시를 사용하면 연관된 객체들을 처음부터 DB 에서 조회하는 것이 아니라 실제로 사용하는 시점에 DB 에서 조회가 가능하다.
다만, 자주 함께 사용되는 객체들은 조인을 사용해서 함께 조회하는 것이 더 효과적이다.

 

 

지연로딩

연관된 엔티티를 실제 사용할 때 조회한다.
JPA 는 필요없는 DB 조회를 줄이는 것으로 성능을 최적화 한다.
이 과정에서 엔티티가 실제 사용될 때까지 DB 조회를 지연하는 방법이 있는데 이를 지연로딩 이라고 한다.
이 방법에서는 실제 엔티티 객체 대상에 DB 조회를 지연시키는 가짜 객체인 프록시 객체가 필요하다.
설정 방법 : @ManyToOne(getch = FetchType.LAZY)

 

프록시 객체는 실제 객체에 대한 참조(target) 를 보관한다.
프록시 객체의 메소드를 호출할 시 실제 객체의 메소드를 호출한다.

 

즉시로딩

엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
처음부터 모든 테이블에 조인을 걸고 별도로 쿼리가 나가는 경우가 생긴다.
그래서 연관관계가 많고 복잡할수록 코스트가 기하급수적으로 늘어나기에 정확하게 이해하고 사용해야 한다.
가급적으로 모두 지연로딩을 걸어두는게 일반적이다.
설정 방법 : @ManyToOne(fetch = FetchType.EAGER)

 

위의 코드랑 다른 원래의 기본 설정방법은 사실 아래의 코드이다.
허나 위처럼 쓰는게 더 편하고 깔끔하다.
@ManyToOne, @OneToOne: 즉시 로딩(FetchType.EAGER)

@OneToMany, @ManyToMany: 지연 로딩(FetchType.LAZY)

 

 

즉, 이 둘의 차이를 짧게 요약하면

”지연로딩”은 실제로 가짜 객체를 이용하면, 그때 별도의 쿼리가 나가는 방식이며,
“즉시로딩”은 연관된 엔티티를 조인해서 다 긁어와버리는 것이다.

 

 

연관관계 이미지 설명
연관관계 이미지 설명

 

 

 

 

 

 

영속성 전이

CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶으면 이 기능을 사용
지금 당장은 이런게 있다는 정도로만 이해하고 넘어가자

 

설정방법

@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Address> addresses;