개요
- JPA를 이용해 서비스를 개발할 때 주의해야 하는 부분을 정리해보자.
PK 매핑 필드의 컬럼명을 지정하자
- 일반적으로 JPA에서 Entity를 작성할 때 PK 값을 id라는 필드로 매핑한다.
- JPA에서는 Entity.id 형식으로 특정 객체의 id 값을 호출할 수 있다.
- 그러나 DB의 관점에서는 각 테이블마다 id라는 칼럼이 존재하기 때문에
단순 쿼리문에서 id가 어떤 테이블의 id인지 구분하기 어려울 수 있다.
- 그러므로 각 Entity의 id를 생성할 때, 반드시 @Column을 이용하여 "테이블명_id" 형식으로 설정하도록 하자.
연관 관계 설정
a) 항상 다(N) 쪽이 FK를 가진다.
- 일대다(1:N) 또는 다대일(N:1) 관계에서는 항상 다 쪽이 FK를 가진다.
- 반대에서 일(1)의 역할을 하는 Entity는 mappedBy를 이용하여 연관 관계의 주인을 설정한다.
b) 다대다(N:N) 관계는 사용하지 않는다.
- 다대다 관계의 조인은 중간 테이블을 반드시 사용해야 한다.
- 이 중간 테이블로 인해 발생하는 문제점 때문에 사용하지 않는다.
→ 중간 테이블에는 매핑된 필드 이외에 다른 필드(= colmun)가 추가될 수 없다.
c) Lazy(지연 로딩) 설정은 필수다.
- 연관 관계 중 OneToMany처럼 복수로 끝나는 관계는 기본 설정이 LAZY로 되어있다.
- 그러나 ManyToOne처럼 단수로 끝나는 관계는 기본 설정이 EAGER로 되어있다.
- 그러므로 반드시 One으로 끝나는 관계는 FetchType을 LAZY로 설정해야 한다.
→ @OneToOne, @ManyToOne
Getter & Setter
a) Setter는 제한적으로 열어둔다.
- Lombok을 사용해서 개발하다 보면 편의상 Getter와 Setter를 모두 열어놓는다.
- 모든 필드에 대해 Getter는 열어 놓아도 문제가 되지 않는다. 단순 읽기 작업을 수행하기 때문이다.
- 반면, Setter는 필요한 필드에만 한해서 열어두는 것을 권장한다.
- Setter는 값을 조작하기 때문에 발생하는 문제의 스케일이 달라진다.
- 그러므로 Setter는 필요에 한해서 부분적으로 열어두는 것을 권장한다.
b) Setter의 호출 범위를 제한하자.
- Setter를 사용하더라도 데이터를 조작할 수 있는 변경 조건 또는 지점을 제한하는 것을 권장한다.
- 이처럼 Setter의 호출 조건을 제한적으로 형성하는 것은 에러 발생 시 범위를 최소화할 수 있다.
- 대표적인 방법으로 비즈니스 메서드 내부에서 Setter를 호출하는 방식으로 제한할 수 있다.
Enum
a) 반드시 EnumType.STRING을 사용한다.
- Enum을 사용할 때에는 반드시 EnumType.STRING을 설정하여 사용한다.
- Enum의 기본 타입은 ORDINAL인데, 이를 사용하면 ENUM의 값이 DB에는 숫자 형식으로 기록된다.
- 숫자 형식(= ORDINAL)을 사용하는 경우, 새로운 ENUM 값이 추가될 때를 생각해보자.
- 기존에 DB에 저장된 ENUM 값이 밀려버리는 경우가 발생할 수 있다.
- 이와 같은 경우, 기존에 DB에 저장된 2번 ENUM 값이 다른 ENUM값으로 변질될 수 있다.
- 그러므로 Enum을 사용할 때에는 반드시 STRING 타입으로 설정하는 것을 권장한다.
값 타입
a) Getter만 사용한다.
- 값 타입은 값을 다루기 위한 객체다.
- 그러므로 값 타입 객체의 값을 자체적으로 변경할 이유가 없다.
- 만약 변경해야 하더라도 JPA를 이용한 DB 측의 값을 변경하는 방식으로 변경할 수 있다.
- 그러므로 값 타입은 Getter만을 열어두고 사용한다.
b) 기본 생성자를 protected로 설정한다.
- 값 타입은 아무 곳에서나 생성해도 되는 객체가 아니다.
- 값 타입이 필드로써 매핑되어 있는 객체가 생성될 때에만 사용하도록 제한해야 한다.
- 그러므로 protected 접근 지정자를 이용하여 값 타입 내부에 기본 생성자를 설정한다.
* protected는 protected가 붙은 변수, 메서드는 동일 패키지의 클래스 또는
해당 클래스를 상속받은 다른 패키지의 클래스에서만 접근이 가능하다.
merge 보다 변경 감지 기능을 사용하자
- 준영속 상태의 객체를 이용하여 DB를 업데이트하는 경우를 생각해보자.
* 준영속 상태 - JPA의 영속성 컨텍스트에 의해 관리되지 않는 객체
a) 변경 감지
- 변경 감지를 사용하는 경우, 준영속 객체를 이용하여 해당 객체와 동일한 id를 가진 영속 객체를 찾는다.
- 그리고 해당 영속 객체의 값을 준 영속 객체의 값으로 변경하는 방법을 사용한다.
- 이 과정은 다음과 같은 코드로 표현할 수 있다.
@Transactional
public void updateItem(Book paramBook) {
Book realBook = em.find(Item.class, paramBook.getId());
realBook.setName(paramBook.getName());
realBook.setQuantity(paramBook.getQuantity());
...
}
- 이처럼 영속성 컨텍스트에서 값을 바꾸려는 영속 객체를 찾아온 뒤, 준영속 객체의 데이터를 이용하여 값을 변경한다.
- 위의 코드에서 realBook은 영속 객체이므로, 따로 persist를 할 필요가 없어진다.
- 영속 객체의 데이터가 변경되면 JPA에서 알아서 update 쿼리를 발행하기 때문이다.
- 이와 같은 방법이 변경 감지이다.
b) Merge
- merge 메서드는 준영속 객체를 이용하여 영속성 콘텍스트에 관리되는 객체를 찾아 값을 변경한다.
- 그러므로 사용되는 준영속 객체는 반드시 식별자 값을 가지고 있어야 한다.
- merge 메서드는 다음 코드와 같은 방식으로 사용할 수 있다.
@Transactional
public void updateItem(Book paramBook) {
Book mergeBook = em.merge(paramBook);
// em.merge()에서 반환하는 객체는 영속 객체다.
}
- merge 메서드의 내부 과정을 살펴보면 변경 감지 기능과 동일하게 작동한다.
- 변경 감지처럼 준영속 객체의 값을 이용하여 영속 객체의 값을 변경한다.
- 다만, 영속 객체의 모든 값이 준영속 객체의 값으로 통째로 변경된다.
c) merge 사용 시 주의사항
- 변경 감지 방식은 영속 객체의 값을 선택적으로 변경할 수 있다.
- merge의 경우, 영속 객체의 모든 값을 준영속 객체의 값으로 변경한다.
- 이는 준영속 객체의 필드 A의 값이 null이라면 영속 객체의 필드 A의 값도 null로 변경된다는 것이다.
- 즉, merge는 영속 객체의 값을 선택적으로 변경할 수 없다. 객체의 값이 통째로 변경되는 것이다.
- 이와 같은 이유로 Entity의 값을 변경할 때에는 merge 보다 변경 감지 방식을 사용하는 것을 권장한다.
Setter 대신 의미있는 메서드를 사용하자.
- 위에서 설명한 변경 감지에서는 다음과 같은 코드를 사용하였다.
@Transactional
public void updateItem(Book paramBook) {
Book realBook = em.find(Item.class, paramBook.getId());
realBook.setName(paramBook.getName());
realBook.setQuantity(paramBook.getQuantity());
...
}
- 이와 같은 방식으로 Setter를 사용하여 변경하는 방법은 유지보수 측면에서 비효율적인 방식이다.
- 즉, 객체 값의 변경을 Controller 또는 Service에서 수행하지 않고 Entity에서 수행하는 것이 올바른 방법이다.
- 다음 예시 코드처럼 의미있는 메서드를 만들어서 사용하는 것을 권장한다.
// updateItem()은 Controller에 속하는 메서드다.
@Controller @RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@PostMapping("/items/.....")
public void updateItem(Book paramBook) {
Book realBook = em.find(Item.class, paramBook.getId());
realBook.updatePQ(paramBook.getPrice(), paramBook.getQuantity());
}
}
// updatePQ() 메서드는 Book Entity에 속하는 메서드다.
@Entity @Getter @Setter
public class Book {
@Id @GeneratedValue @Column(name = "book_id")
private Long id;
private String name;
...
public void updatePQ(int price, int quantity) {
this.setPrice(price);
this.setQuantity(quantity);
}
}
- 단순 Entity 만을 이야기하는 것이 아니다.
- 이처럼 Controller, Service, Entity의 역할과 책임을 확실히 하여 변경 시점을 고정적으로 가져가는 것이
서비스가 확장되어도 효율적인 유지보수를 할 수 있는 좋은 설계의 바탕이 된다는 것을 이해하자.
Repository는 Entity를 다루기 위한 클래스다.
- Repository는 Entity를 직접 다루는 작업을 수행하는 클래스다.
- 그러므로 DTO 직접 조회 방식과 같이 Entity가 아닌 다른 대상을 처리하기 위한 메서드는
Repository가 아닌 다른 디렉토리에 할당하는 것이 올바르다.
'Back-end > JPA 개념' 카테고리의 다른 글
API 조회 성능 최적화 - Entity 직접 노출 방식 (0) | 2022.05.28 |
---|---|
JPA를 이용한 API 개발 꿀팁 - Annotation (0) | 2022.05.27 |
35. JPQL - 벌크 연산 (0) | 2022.05.08 |
34. JPQL - 다형성 쿼리, Entity 직접 사용, Named 쿼리 (0) | 2022.05.07 |
33. JPQL - Fetch Join의 한계 (0) | 2022.05.06 |
댓글