인프런 김영한 의 "실전! 스프링 부트와 JPA 활용1" 강의에 대한 정리를 해본 글입니다.
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
https://github.com/jinsujj/jpa-shop
현재 '부엉이 개발자 블로그' 도 .NET 진영에서 사용하는, Dapper 라는 ORM 을 활용해서 개발을 했습니다.
Dapper 는 실제 SQL 문을 사용해서 DB 로부터 원하는 데이터를 가져와 그대로 객체와 맵핑하는 형태를 띄지만, Hibernate 를 활용한 JPA 는 기본적으로 SQL 을 활용하기 보다는 Java 언어를 통해서 데이터를 가져오는 형태를 뜁니다.
ORM 이 트렌드가 되고, 누구나 다 SQL 보다는 JPA 로 DB 트랜잭션을 처리하기 원하지만, 내 주변, 친구, 선배 개발자 모두 SQL 대비 어떤 장점이 있기에 JPA 를 사용하고 싶은가 라는 질문에는 그럴듯한 답변을 듣지 못했습니다.
그냥... 트렌드라서.. DB의 종류에 구애받지 않으니까.. 개발 생산성을 높일수 있어서?
1. DB 의 종류에 구애받지 않는다는 점. 그저 구색 맞추기 아닌가?. 운영중인 시스템의 DB 를 다른 DB로 마이그레이션 한다는 것은 엄청난 비용이 드는 행위입니다. 삼성의 경우, 거대한 Oracle DB 를 중심으로 여러 시스템이 DB 를 물어 운용되는 형태에서, Oracle Version 을 업데이트 하는 행위도, '데이터 정합성', 'SQL 문법 변화'등, 몇 일에 걸쳐서 많은 인력이 붙어 진행 됐었습니다. 2. SQL 로 데이터를 처리하는게 ORM 을 활용하는 것 보다, 성능이 월등히 떨어집니다 (JPA 내부적으로 SQL 로 변환하는 과정을 거치며, 조인이 많아질수록 성능은 기하급수 적으로 감소합니다)
3. SQL 튜닝이 필요한 시점에서는 결국 다시 SQL 문으로 작성해서 튜닝 처리해야 하는 점. 4. 개발 생산성에 대해서는 ERD 를 작성할 필요가 없다는 점에서는 공감할 수 있지만, 규모가 커진 상태에서는 개발 생산성 효과가 오히려 떨어지는 것 아닌가? 하는 의문. |
이러한 문제에 대해서도 납득할 만한 이유를 찾지 못해, 직접 JPA 도 공부하고 클린 아키텍처에 대해서도 파다가, 최근 프론트 개발자분과의 커피챗을 통해서 새로운 관점에서 납득할 만한 이유를 찾게 되었습니다. 지금까지 정리해본 바를 서술하면 아래와 같습니다.
스타트업에서 일하는 사람으로서 ,우리는 새로운 기술을 활용해 게임 챌린저가 되고 싶은 마음이 있기에,
결국 트레이드 오프 관계인 기술속에서, 해당 기술에 관심있는 사람이 모여서 꾸린 팀에서 우린 이렇게 할거야 라는 팀의 결정.
- 전사적으로 데이터를 관리 하는 ERP나, 공장의 데이터를 추적 관리하는 MES 는 업의 특성상, 원하는 데이터를 조회하기 까지 복잡한 조인을 통하여 데이터를 가져와야 하는점.
- 대부분 그리드와 차트 형태의 화면에서 몇 백~ 몇 십만건의 대용량 데이터를 불러와야 하는점 ,
(*2가지 이유로 화면 로딩속도에 지연이 빈번하고, 해당 이슈로 DB 조회 속도가 중요하기에, SQL을 선호하지 않은가?)
- MSA 마이크로 서비스가 확산 되면서, 기존 거대한 DB 서버 하나로 처리하는게 아닌 각 마이크로 서비스에서 자체적으로 가지고 있는 DB 를 활용하는 추세로 변하고 있는점, 즉 DB 테이블간의 조인이 줄어든다.NoSql 이 뜨는 이유이기도 하다.
- 도메인 주도로 설계를 함에 따라서, DB 에 녹일 수있는 부분을 시스템으로 끌고 온점. 이는 DB 의 의존성을 낮추게 되고, DB 로부터 발생하는 트랜잭션도 줄이게 된다
각설은 여기까지하고, 인프런 JPA 내용을 요약하면 아래와 같다.
회원기능 | 상품 기능 | 주문 기능 | 기타 요구사항 |
회원 등록 | 상품 등록 | 상품 주문 | 상품은 재고 관리가 필요하다 |
회원 조회 | 상품 수정 | 주문 내용 조회 | 상품의 종류는 도서,음반,영화가 있다. |
상품 조회 | 주문 취소 | 상품을 카테고리로 구분할 수 있다 | |
상품 주문시 배송 정보를 구분할 수 있다. |
회원, 주문, 상품의 관계: 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다. 하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.
- 부가적인 얘기지만, DB 테이블 관점이 아닌 클린 아키텍처 관점에서 보면, 주문과 상품이라는 클래스 객체간 의존성을 갖는 구조에서, 주문 상품이라는 인터페이스를 통해서 의존성을 분리하는 설계방식이 좋은 설계로 받아들여 지는데, 비슷한 맥락으로 느껴졌다.
상품 분류: 상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다.
회원(Member) | 이름과 임베디드 타임인 주소(Address), 그리고 주문(orders) 리스트를 가진다. |
주문(Order) | 한번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문 상품(OrderItem) 은 일대다 관계다. 주문은 상품을 주문한 회원과 배송 정보, 주문 날짜, 주문 상태(status) 를 가지고 있다. 주문 상태는 열거형을 사용했는데 주문(ORDER), 취소(CANCEL) 을 표현할 수 있다. |
주문상품(OrderItem) | 주문한 상품 정보와 주문 금액(orderPrice), 주문 수량(count) 정보를 가지고 있다. * 보통 OrderLine, LineItem 으로 많이 표현한다. |
상품 (Item) | 이름, 가격, 재고수량(stockQuantity) 을 가지고 있다. 상품을 주문하면 재고수량이 줄어든다. 상품의 종류로는 도서, 음반 영화가 있는데 각각은 사용하는 속성이 조금씩 다르다. |
배송 (Delivery) | 주문시 하나의 배송 정보를 생성한다. 주문과 배송은 일대일 관계다. |
카테고리(Category) | 상품과 다대다 관계를 맺는다. parent, child 로 부모 자식 카테고리를 연결한다. |
주소(Address) | 값 타입(임베디드 타입)이다, 회원과 배송(Delivery) 에서 사용한다 |
참고: 회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것 같지만, 객체 세상은 실제 세계와는 다르다. 실무에서는 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다. 여기서는 일대다, 다대일의 양방향 연관관계를 설명하기 위해서 추가했다.
MEMBER | 회원 엔티티의 Address 임베디드 타입 정보가 회원 테이블에 그대로 들어갔다. 이것은 DELIVERY 테이블도 마찬가지다. |
ITEM | 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다. DTYPE 컬럼으로 타입을 구분한다. |
연관관계 매핑 분석
회원과 주문: 일대다 , 다대일의 양방향 관계다. 따라서 연관관계의 주인을 정해야 하는데, 왜래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다. 그러므로 Order.member 를 ORDERS.MEMBER_ID 외래키와 매핑한다.
카테고리와 상품: @ManyToMany 를 사용해서 매핑했지만, 실무에서 @ManyToMany 는 사용하지 말자. 여기서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다.
엔티티에는 가급적 Setter 를 사용하지 말자.
- Setter 가 모두 열려있다 -> 변경 포인트가 너무 많아서, 유지보수가 어렵다. 나중에 리팩토링으로 Setter 제거 |
모든 연관관계는 지연로딩으로 설정!
- 즉시로딩(EAGER)은 예측이 어렵고, 어떤 SQL 이 실행될지 추적하기 얼협다. 특히 JPQL을 실행할 때, N+1 문제가 자주 발생. - 실무에서 모든 연관관계는 지연로딩(Lazy) 로 설정해야 한다. - 연관된 엔티티를 함께 DB 에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다. - @XToOne ( OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로, 직접 지연로딩으로 설정해야한다. |
컬렉션은 필드에서 초기화하자.
컬렉션은 필드에서 바로 초기화 하는 것이 안전하다. - null 문제에서 안전하다. - 하이버네이트는 엔티티를 영속화 할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만약 getOrders() 처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다. |
Address.java
@Embeddable
@Getter
public class Address {
public String city;
public String street;
public String zipCode;
protected Address() {
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
}
Member.java
OrderItem.java
OrderStatus.java