진짜 개발자
본문 바로가기

FrameWork/Spring JPA

Spring JPA - JPA를 이용해 Commerce App 만들기 - 3 (엔티티 개발)

728x90
Spring JPA - JPA를 이용해 Commerce App 만들기 - 3 (entity 작성)

 

이번 시간에는 지난 포스팅에서 다룬 설계를 토대로하여, Entity 클래스를 작성해보도록 하겠습니다.

 

 

우선 Entity를 개발하기 앞서 패키지 구조를 살펴보도록 하겠습니다.



위와같은 구조로 프로젝트를 진행해본결과 장점은 아래와 같습니다.

  • 첫째로, commondomains를 분리함으로써, 도메인로직들은 domains 디렉토리 하위에 존재함을 직관적으로 볼수 있기때문에, 도메인로직에 좀더 집중할 수 있습니다.
  • 둘째로, domains디렉토리에서 다시 어플리케이션에서 사용되는 domain들이 나뉘고, 그 안에 layerd architecture의 형태로 디렉토리 구조를 잡음으로써, ui 계층로직, service 계층로직, domain 계층 로직, infra 계층 로직을 다시 분리하여, 각각의 domain들의 도메인로직에 집중할 수 있기 때문에, 매우 효율적인 구조로 느껴졌습니다.

 

 

 

 

BaseEntity 작성 (DB 추적을위한 : JpaAudit)

도메인 설계시 존재하지 않았던, BaseEntity 작성이라는 것을 보고 의아하셨을 것입니다.

개발을 하다보면 각각의 Entity들에서 공통적으로 필요한 필드들이 존재하는데요, 대표적으로 id, 생성일자, 수정일자... 등이 존재합니다. 특히 서버를 운영중에, DB의 각 row들의 생성일자, 수정일자는 매우 중요한 역할을 합니다.

이를 위해 JPA에서는 JpaAudit이란것을 제공합니다. JpaAudit은 각각의 Entity의 생성시간과, 수정시간을 자동으로 생성 및 갱신해줍니다.

 

@MappedSuperclass

- 이 엔티티를 상속받는 클래스에게 단순히 매핑정보만을 상속합니다.

@EntityListeners

- db의 특정 동작을 하기 전 또는 후에 커스텀 콜백을 요청할수 있도록하는 어노테이션입니다. AuditinEntityListener.class 를 속성으로 전달하면, 엔티티의 생성 또는 수정시간을, 자동으로 업데이트를 할 수 있습니다.

 

이제 BaseEntity를 상속받는 Entity들은 createdDate, lastModifiedDate가 자동으로 기록됩니다.

 

 

 

 

Entity

이제 지난 포스팅에서 설계한 것을 토대로 각 Entity들을 생성하도록 하겠습니다. 설계한 것이 거의 그대로 코드로 옮겨지기 때문에, 특별한 내용이 아니라면 생략하도록 하겠습니다.

 

https://gitlab.com/galid1/jpa-commerce

완성된 코드는 gitlab을 참조해주세요.

 

 

 

공통적 어노테이션 설명

우선 모든 Entity에서 거의 공통적으로 들어가는 어노테이션들에 대한 설명 및 팁을 알려드리겠습니다.

 

@Table

@Table(name = "member) 어노테이션의 경우, MemberEntity 클래스가, jpa Auto ddl 옵션에 의해 생성될때, member_entity가 아닌 members라는 이름으로 생성되도록 하기 위해 부여했습니다. 애초에 클래스 이름을 Members로 한다면, @Table 어노테이션이 필요하지 않지만, 해당 클래스가 Entity임을 직관적으로 알아볼 수 있도록 하기 위해 저는 이런식으로 코딩을 합니다.

 

 

@NoArgsConstructor(access = AccessLevel.PROTECTED)

@NoArgsConstructor 어노테이션은 Lombok의 어노테이션인데요, 기본 생성자를 생성해주는 어노테이션입니다. Jpa의 경우, Repository에서 Entity를 조회하는 경우, Entity를 생성할때, 기본생성자를 이용하기 때문에, 꼭! 기본생성자를 Entity클래스에 포함해야합니다. (Intellij에서는 위 그림과 같이 Entity 클래스에 기본생성자가 없는 경우 경고메시지를 보여주기도 합니다.)

Java에서는 별도의 생성자가 존재하지 않는경우 기본 생성자를 자동으로 생성해주는데요, 그런데도 왜 @NoArgsConstructor를 별도로 부여할까요? 저희는 @Builder 어노테이션이 부여되어 있으며, 도메인 개념을 포함하는 생성자를 별도로 생성할 것이기 때문입니다.

*하지만 @NoArgsConstructor를 그대로 사용하면 안됩니다. 도메인 개념을 포함하는 별도의 생성자가 존재 하더라도, @NoArgsConstructor로 인해 생성되는 기본생성자를 그대로 둔다면, 별도의 생성자를 사용해야하는지를 망각한 개발자가 기본생성자를 이용해 도메인 개념에 해를 가하는 객체를 생성할 수도 있기 때문입니다. 따라서, JPA에서 허용하는 수준의 access = AccessLevel.PROTECTED속성을 이용해, 최대한 기본 생성자를 감추도록 합니다. (PROTECTED 수준의 기본생성자까지는 jpa에서 찾을수 있다고합니다.)

 

 

@Builder

생성자위에 @Builder어노테이션이 보이는데요, Lombok에서 제공하는 어노테이션입니다. 생성자에 부여된 파라미터들을 포함하는 Builder를 만들어주는 어노테이션입니다. 생성자의 파라미터가 많은 경우, 실수로 파라미터를 잘못전달할 수도 있는데요, Builder를 이용해 생성하면 이런 실수의 우려를 덜어줍니다. 또한 Builder를 이용해 객체를 생성하는 경우, 생성자의 파라미터를 위치를 변경하더라도 객체를 생성하는 코드를 변경하지 않아도 됩니다.

 

 

@Getter

Lombok의 어노테이션으로, FieldLevel에 부여한다면, 해당 필드의 Getter 메소드를 자동으로 생성하며, ClassLevel에 부여하면, 모든 필드의 getter 메소드를 생성합니다.

 

 

@Id, @GeneratedValue

@Id는 엔티티의 식별자로 지정하는 어노테이션입니다. @GeneratedValue는 식별자의 생성 전략을 지정하는 어노테이션으로, 기본값은 AUTO이며, Database의 auto_increment전략과 거의 동일한 기능을 합니다.

 

 

*@XToOne(fetch = FetchType.LAZY)

이 어노테이션은 X 대 1 관계를 나타내기 위한 어노테이션입니다. 하지만 중요히 봐야할 것은 (fetch = FetchType.LAZY) 속성입니다.

 

기본적으로 JPA로 프로그래밍시, 모든 연관관계의 fetchLAZY로 진행합니다.

 

xToOne 관계는 기본값이 EAGER이기 때문에, 별도로 FetchType.LAZY를 지정해야하며, xToMany 관계는 기본값이 LAZY이기 때문에 생략합니다.

 

 

 

 

1. MemberEntity

 

 

 

2 OrderEntity

setter를 사용하지 말라고했었는데요? (setOrderItemList(OrderItemEntity... orderItemEntityList) )

- setter를 사용할때에 도메인 개념을해하기 때문에 set보다는 조금 더 해당메소드가 하는 기능을 잘 설명할수 있는 이름으로 메소드를 작명하라고 했었습니다. 하지만, 이 메소드의 공개범위는 private으로 해당 Entity안에서만 사용되는 메소드입니다. 따라서, 해당 setter가 외부에서 호출될 일이 없기 때문에 상관이 없습니다.

- OrderItemEntity들을 추가할 때 setOrderItemList() 메소드를 이용함으로써, 자동으로 OrderItemEntityList에 따라 주문한 상품 가격(각각의 상품 가격 * 수량)이 계산되도록합니다.

 

OrderDate는 어디에?

설계할 때에 존재한 orderDate는 JPA Auditing 기능을 통해 database에 자동으로 추가되는 created_date를 orderDate로 사용할 예정이기 때문에 제외하였습니다.

 

도메인 로직?

cancel() 은 도메인로직입니다. 도메인 로직을 entity에 두는 이유는, 도메인의 제약사항에 해당하는 데이터들이 entity에 모여있기 때문입니다. (OOP에서는 메소드 호출에 필요한 데이터가 존재하는 곳에 메소드가 위치하는것이 가장 응집력있는 프로그래밍이라고 합니다.) 또한, cancel()의 메소드는 어떠한 상황에서 cancel이 가능 또는 불가능한지를 표현할 수 있어야합니다.

마지막으로, cancel()시 연결된 item의 재고량을 복구하기 위해, orderItemList에 존재하는 모든 orderItemEntity의 cancel()을 호출함으로써, 재고량을 복구합니다.

 

 

2.1 OrderStatus

 

 

 

3. DeliveryEntity

 

 

3.1 DeliveryStatus

 

 

 

4. OrderItemEntity

order(ItemEntity item, int orderQuantity)

OrderEntity 생성시 OrderItemEntity가 필요하며, 이는 결국 주문이 발생했다고 볼 수있습니다. 또한, 주문이 발생한다면, item의 재고량이 주문 수량만큼 감소하는 로직이 필요합니다. 따라서, 전달받은 itemEntity와 연관관계를 맺음과 동시에, item의 재고량을 감소하도록 했습니다.

 

cancel()

OrderEntity에서 주문 취소시, 각 OrderItemEntity의 cancel()를 호출했는데요, 이는 앞서 설명드렸던 것처럼, 각각의 주문 아이템의 재고량을 복구시키기 위함이었습니다. 따라서, cancel()에서는 주문수량 만큼 다시 재고량을 복구시키는 로직을 구현했습니다.

 

 

 

5. ItemEntity

 

 

5.1 Album

 

 

5.2 Book

 

 

5.3 Movie

 

 

 

6. CategoryEntity

 

 

 

다음 편에서는, 서비스 및 리포지토리 개발에대해서 살펴보도록 하겠습니다.