티스토리 뷰
아래 내용은 스프링 부트와 JPA 활용 1,2 - 김영한 강의 영상을 보고 실무에서 필요할 때
찾아보려고 정리한 내용입니다
1. 연관 관계 주인 설정 |
2. 양방향 연관 관계 편의 메서드 |
3. 다대다 연관 관계(사용 금지) |
4. 엔티티 상속관계 |
5. 공통 필드를 쉽게 구성하는 방법, @MappedSuperclass |
6. JPA 타입 비교 |
7. 프록시 |
8. 즉시 로딩, 지연 로딩 |
9. cascade |
10. 고아 객체, orphanRemoval = true, cascade 같이 적용 |
11. 임베디드 @Embeddable, @Embedded |
12. 값 타입 Collection, Enum |
13. 리스트 타입 필드 선언과 동시에 초기화 |
14. @Transactional |
15. 도메인 모델 패턴 |
16. 변경 감지와 병합 |
17. Entity 주의 사항 |
18. 조회 성능 최적화 |
19. Distinct |
20. 페치 조인과 페이징 |
21. 캐시를 사용하는 경우 |
22. OSIV(OpenSessionInView, Spring Boot 설정 파일에서는 OpenInView 명칭 사용) |
1. 연관 관계 주인 설정
외래 키를 관리하는 테이블을 기준(주인)으로 @JoinColumn 설정
주인이 아닌 테이블에는 mappedBy설정으로 주인에 대해 설정
물론 모든 설정은 엔티티에 적용합니다
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
2. 양방향 연관 관계 편의 메서드
외래 키를 관리하는 주인 엔티티에서 연관 관계 엔티티를 대입할 때(Setter) 대입 메서드에서
상대 엔티티로 부터 자신(this)을 넣어주는 로직 추가
clean, flush 메서드 실행 없이 주인이 아닌 엔티티에서 조회하려 할 때 문제가 발생할 수 있음
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
3. N:M(다대다) 연관 관계
우선 이 기능은 사용하지 않은 것을 권장합니다
예시로 A, B 엔티티가 있고 이 둘이 N:M 연관 관계를 사용하면 A : 중간 테이블(새로 생김) : B 구조가 됩니다
운영 단계에서는 저런 테이블을 사용할 때 중간 테이블에 필드가 추가적으로 필요한 경우가 잦은데
필드를 넣고 사용하면 문제가 발생할 수 있고 N:M 사용하여 조회할 경우 데이터를 알아보기 어려운 상황도 생깁니다
그래서 해결 방법으로는 N:M 연관 관계를 1:N & N:1 로 프록시 엔티티를 생성하여 해결합니다
4. 엔티티 상속관계
@Entity
// 구현 클래스 마다 테이블 전략(사용을 권장하지 않음)
// 조인이 없고 각 각의 테이블로 관리되어 하위 테이블에 대한 통합 데이터나 추가적인 기능들이 제한이 많음
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
// 단순한 테이블 또는 데이터가 많지 않을 경우 적용하기 좋음
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
// 조인 가능한 구조
@Inheritance(strategy = InheritanceType.JOINED)
public class Item { }
5. 공통 필드를 쉽게 구성하는 방법, @MappedSuperclass
생성일, 생성자, 수정일, 수정자와 같이 테이블 마다 공통적으로 필요한 필드를 쉽게 구성하는 방법
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedBy
private String modifiedBy;
@LastModifiedDate
private LocalDateTime updatedDate;
}
@Entity 설정은 필요 없으며 @Column 사용할 수 있습니다
적용하는 방법은 자바 상속과 동일하며 필요한 엔티티에 extends 클래스명 적용하면 됩니다
그리고 해당 클래스를 직접 생성해서 사용할 일은 없으므로 추상(abstract) 선언을 권장합니다
스프링 부트를 사용하는 경우 아래와 같이 메인 클래스에 @EnableJpaAuditing 추가해 주어야합니다
@EnableJpaAuditing
@SpringBootApplication
public class SpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(JpaboilerplateApplication.class, args);
}
}
6. JPA 타입 비교
우선 JPA 를 사용하기로 하셨다면 equals, hash 메서드는 꼭 만들어서 사용하시기 바랍니다
이유는 프록시 때문입니다 필요하시다면 잠시 바로 아래 프록시를 보고 오시면 좋습니다
결국 정확한 타입 비교가 어렵고 상속 클래스 또한 비교에 사용할 수 없습니다
그리고 비지니스 로직에서 메서드 매개변수에 선언된 내용을 보고서는 실제 또는 프록시 객체인지
더욱 확인이 어렵습니다
꼭 타입을 비교해야겠다면 instanceOf 사용을 권장하기도 합니다
7. 프록시
위 JPA 타입 비교에서 살짝 언급하였는데 A, B 엔티티가 연관 관계 설정되어 있고 Lazy 로딩이 설정되어 있으면
A가 B를 호출하기 전까지 실제 엔티티 타입을 가지는게 아닌 프록시 객체를 만들고 리턴합니다
JPA 가 영속성 컨텍스트로 엔티티를 관리하는 메커니즘에 의해 이러한 구조를 가지게 됩니다
8. 즉시 로딩, 지연 로딩
즉시 로딩은 A, B 엔티티가 연관 관계와 EAGER 로딩인 경우 A, B 조인으로 한번에 조회
지연 로딩은 A, B 엔티티가 연관 관계와 LAZY 로딩인 경우 A 만 조회 B는 사용 시점에 조회
@OneToMany, @ManyToMany 는 기본 설정값이 지연 로딩입니다
@ManyToOne, @OneToOne 은 기본 설정이 즉시 로딩입니다
N+1 문제
A, B 엔티티에서 연관 관계를 즉시로딩으로 설정한 후 호출하는 경우
A를 조회하였고 데이터가 100개가 나왔다 그러면 100개의 A에 연관된 B를 즉시로딩 하기 위해
1번에 쿼리 + 100개의 데이터에 대한 B 확인 쿼리 = 총 101 개의 쿼리가 호출됩니다
이는 첫 번째의 쿼리에서 얻은 수 만큼의 쿼리가 호출되므로 N+1 또는 1+N 이라고 부릅니다
해결 방법
먼저 @ManyToOne, @OneToOne 에 로딩 설정을 지연 로딩으로 설정합니다
그리고 성능적으로 이득을 보고 싶은 경우 join fetch 를 설정하여 1개의 쿼리로 모든 데이터를 얻을 수 있습니다
9. cascade
계층 구조로 엔티티를 관리하고 싶을 때 사용하는 방법으로 부모의 엔티티를 변경하면 자식도 같이 반영됩니다
사용할 때 주의할 점은 자식을 관리하는 부모 엔티티가 하나인 경우에만 사용하셔야 합니다
부모가 여럿인 경우 사용할 수는 있으나 자칫하면 A 라는 부모가 사용하다가 삭제하여 없어진 자식을
B 라는 부모가 찾으려고 시도할 때 없어서 에러가 발생할 수 있고 추적하기도 어려울 수 있습니다
참고로 ALL = PERSIST, MERGE, REMOVE, REFRESH, DETACH 모두 포함
설정은 cascade = CascadeType.ALL 넣어주면 됩니다
자식 엔티티에 부모 키가 포함되지 않았다면 양방향 연관 관계 편의 메서드를 설정하셨는지 확인해 보시기바랍니다
@Entity
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItemList = new ArrayList<>();
public Order(OrderItem... orderItems) {
for (OrderItem orderItem : orderItems) {
orderItemList.add(orderItem);
}
}
}
@Entity
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
private String itemName;
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
public OrderItem(String itemName) {
this.itemName = itemName;
}
}
@Test
void cascade() throws Exception {
OrderItem iphone = new OrderItem("iphone15");
OrderItem galaxy = new OrderItem("galaxyS23");
Order order = new Order();
order.addChild(iphone);
order.addChild(galaxy);
em.persist(order);
}
10. 고아 객체, orphanRemoval = true, cascade 같이 적용
우선 해당 기능은 특정 자식이 부모와의 관계가 끊어진 (remove) 경우 삭제되고
부모가 삭제되는 경우에는 모든 자식이 같이 제거되는 기능입니다
이 기능 또한 특정 엔티티가 개인 소유할 때만 사용해야됩니다
장점으론 cascade + 고아 객체를 사용할 경우 자식 라이프싸이클을 부모가 관리할 수 있습니다
적용 방법은 9번의 예제에서 orphanRemoval = true를 추가합니다
@Entity
public class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItemList = new ArrayList<>();
}
11.임베디드 @Embeddable, @Embedded
단순 값 타입을 공통으로 사용할 경우 아래와 같이 편하게 적용할 수 있습니다, 기본 생성자 필수
@Entity
public class Delivery {
@Embedded
private Address address;
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
}
하나의 임베디드 객체를 2개 만들어서 적용할 때 사용하는 방법
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "home_city")),
@AttributeOverride(name = "street", column = @Column(name = "home_street")),
@AttributeOverride(name = "zipcode.zip", column = @Column(name = "home_zipcode")),
})
private Address home;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "com_city")),
@AttributeOverride(name = "street", column = @Column(name = "com_street")),
@AttributeOverride(name = "zipcode.zip", column = @Column(name = "com_zipcode")),
})
private Address company;
중요 임베디드 값 타입 복사
예시로 2명의 멤버를 선언하고 저장할 때 주소가 같아서 하나의 임베디드 객체를 생성 후 같이 적용하였고
그 상태에서 A의 멤버만 변경하고 싶어서 A.임베디드.set 호출하여 값을 변경하고 수정하면
해당 임베디드 객체를 사용했던 멤버는 모두 같은 주소로 변경됩니다
12. 값 타입 Collection, Enum
엔티티에서 값 타입의 collection, enum 을 사용하기 위한 설정입니다
@ElementCollection
private List<String> strings = new ArrayList<>();
@Enumerated
private OrderStatus orderStatus;
13. 리스트 타입 필드 선언과 동시에 초기화
JPA 메커니즘에 의하면 OneToMany 와 같은 리스트 타입을 생성할 때 JPA 에서 구현한
Wrapper 클래스로 해당 클래스를 감싼다고 합니다
이를 getClass 로 출력해보면 ArrayList 가 아닌 PerststentBag 또는 다른 이름이 출력되는데
이는 JPA 에서 변경사항을 감지 및 추적하기 위한 용도의 랩퍼클래스를 뜻합니다
결과적으로 뒤 늦은 컬렉션 초기화는 JPA 메커니즘에 문제가 발생할 수 있습니다
14. @Transactional
우선 순수 JPA @Transactional 으로 설명하자면 영속성 컨텍스트가 관리되는 시점입니다
즉 해당 어노테이션이 없는 곳에서는 엔티티가 관리되지 않습니다
이점을 유의하시고 개발하셔야 합니다
스프링 부트를 사용하고 OSIV 를 적용하면 설정이 달라지긴 합니다
@Service 클래스에서 주로 데이터 변경이 발생하므로 적용합니다
15. 도메인 모델 패턴
Mybatis를 사용할 때 비지니스 로직을 모두 서비스단에 적용하던 방식은 트랜잭션 스크립트 패턴이며
JPA를 사용할 때는 엔티티 안에 비즈니스 로직을 넣고 서비스단은 단순히 레파지토리를 호출하는 용도
그리고 레파지토리는 DB 와 관련된 로직이 작성됩니다
그리고 엔티티 안에 비즈니스 로직을 포함하는 방식을 도메인 모델 패턴이라 합니다
각 패턴에 대해 좋고 나쁨은 없으며 프로젝트 특성에 따라 선택할 수 있습니다(같이 적용 가능)
16. 변경 감지와 병합
merge는 사용 금지
변경 감지는 보통 엔티티가 관리하는 필드에 값을 대입하여 쿼리를 수정하고
merge는 전달 받은 객체를 merge 하여 수정하는데 객체 안에 값이 null 일 경우
null 업데이트 될 수 있습니다
그리고 변경 감지를 사용할 경우, 같은 필드에 수 차례 수정 로직이 있는 경우
최종 변경 시점에 대한 쿼리만 호출되므로 더티 체킹하여 1 건의 쿼리만 호출할 수 있습니다
17. Entity 사용 주의 사항 ** 중요 **
Setter
엔티티는 Setter를 사용하지 않고 의미있는 이름의 변경 메서드를 생성하여 사용합니다
@JsonIgnore
양방향 연관 관계 엔티티를 컨트롤러의 응답 파라미터로 노출한 경우
객체를 JSON으로 컨버팅하는 과정에서 A > B > A > 무한 참조로 에러가 발생합니다
이 문제를 해결하기 위한 연관 관계 필드에 해당 어노테이션을 붙여서 해결할 수 있습니다
그러나 엔티티를 응답 파라미터로 사용하지 않은 것을 권장합니다
ToString
위 @JsonIgnore와 비슷한 케이스입니다 문자열을 만들기 위해 무한 참조가 발생합니다
Entity 응답 객체
엔티티를 컨트롤러 응답 파라미터에 사용하지 않아야 합니다
위 @JsonIgnore 예시와 엔티티를 직접 리턴하면 필드 변경시 API 명세가 변경될 수 있습니다
18. 조회 성능 최적화
Entity 는 DTO 로 변환하여 응답합니다
1+N(또는 N+1) 문제가 발생할 때는 join fetch 패치 사용하고 모든 연관 관계는 Lazy 로딩 설정합니다
쿼리 방식 선택 권장 순서는 아래와 같습니다
Entity to DTO
필요한 경우 fetch join 추가
DTO 직접 조회
JPA 에서 제공하는 네이티브 SQL, Spring JDBC Template SQL 사용
정리하자면 순서대로 접근하고 안될 경우 아래로 내려갑니다
엔티티 조회 방식 접근
페치 조인으로 쿼리 수 최적화
컬레션 최적화
페이징 필요한 경우 hibernate.default_batch_fetch_size, @BatchSize
페이징 X 경우 페치 조인 사용
엔티티 조회 방식으로 해결이 안될 경우 DTO 조회
DTO 조회로 해결이 안될 경우 NativeSQL, Spring JdbcTemplate
19. Distinct
중요 컬렉션 페치 조인을 사용하는 경우 페이징이 불가능합니다
데이터 베이스 기준 1:N 조인이 있으면 데이터베이스가 조인할 때 당연히 row 수가 증가 되는데
엔티티가 증가되는건 우리가 원하는 결과가 아니며 이를 위해 distinct 를 추가하면
같은 엔티티가 조회될 경우 애플리케이션에서 중복을 제거한 후 결과 값을 리턴합니다
Starting with Hibernate 6, distinct is always passed to the SQL query and the flag
QueryHints#HINT_PASS_DISTINCT_THROUGH has been removed.
강의에서 확인했을 당시 hibernate 버전이 6 아래였고 중복되는 데이터를 막기 위해
JPQL 안에 distinct 를 사용했으나 이 후 버전에 대해서는 항상 자동 적용됩니다
20. 페치 조인과 페이징
join fetch 를 사용하면 DB 조인 메커니즘에 의해 객체 관점으로 데이터가 출력되지 않을 수 있습니다
이는 row 수 증가를 뜻하며 이를 해결하기 위해 distinct 로 중복을 제거하게 되는데 join fetch 를 사용하면
페이징을 사용할 수 없어서 페이징 관련 메서드를 설정할 경우 아래와 같은 메시지가 출력 됩니다
WARN: firstResult/maxResults specified with collection fetch; applying in memory
그러나 실제로 사용할 수 없는건 아니고 데이터를 가져온 후 메모리에서 해당 작업을 수행합니다
여기서 문제는 데이터가 많을 경우 메모리를 많이 사용하여 서버에 장애가 발생할 수 있습니다
결국 1:N 에 fetch 조인을 사용한 경우 페이징 메서드를 설정하지 않은 것을 권장합니다
1:N 페치 조인시 페이징 해결 방안
우선 문제가 발생하는 건 ToMany 에서 발생하므로 ToOne 에 대해서는 동일하게 fetch 조인을 JPQL 로 세팅합니다
그리고 hibernate.default_batch_fetch_size: 숫자 또는 @BatchSize(size = 숫자) 설정합니다
설정 이 후 페치 조인시 in 절을 생성합니다
위 숫자는 하나의 in 절 쿼리에 집어넣을 엔티티 식별 값(id) 개수를 의미합니다
hibernate.default_batch_fetch_size설정은 application.properties(yml) 에 설정하기 위한 내용이고
@BatchSize는 ToMany, ToOne 마다 설정이 다른데
ToMany는 엔티티의 연관 관계 필드에다가 적용해주시고
ToOne은 클래스(엔티티)에 적용하시면 됩니다
숫자 설정 값은 1 ~ 1000(Max) 인데 100 이상으로 설정하면 크게 고민할 필요 없다고 합니다
21. 캐시를 사용하는 경우
성능 이슈를 해결하기 위해 캐시를 적용할 경우 예를 들면 Redis 등
엔티티를 캐시에 바로 저장하면 JPA 메커니즘에 의해 데이터가 꼬일 수 있습니다
이를 해결하기 위한 방법으로는 DTO 에 데이터를 저장하고 관리하도록 해야합니다
22. OSIV(OpenSessionInView, Spring Boot 설정 파일에서는 OpenInView 명칭 사용)
영속성 컨텍스트가 DB 커넥션을 끊고 관리를 멈추는 시점을 말합니다
true 의 경우 요청 시점 부터 클라이언트에게 응답을 하기 직전까지 유지하고
false 의 경우 SpringBoot 기준 @Transaction 선언 지점까지만 유지합니다
- Total
- Today
- Yesterday
- properties 암호화
- AWS 자동 배포
- Certbot
- AWS CodePipeline
- Spring Data JPA
- REST API
- letsencrypt
- CodePipeline
- Spring Boot 3.x
- AWS MSK
- aws codecommit
- jasypt
- querydsl
- aws codebuild
- 네임 서버 변경
- ssl nginx 라우팅
- CodeBuild
- 후이즈에서 AWS Route 53
- certonly
- QueryDSL 사용 방법
- ELK
- aws codedeploy
- logstash
- JPA 벌크성 수정 쿼리
- AWS 로드밸런서 SSL 등록
- Spring boot
- codedeploy
- AWS Opensearch
- 도메인 내부 테스트
- 시스템 환경변수
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |