본문 바로가기
Back-end/JPA 개념

JPA 개발 꿀팁

by devraphy 2022. 5. 25.

개요

- 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가 아닌 다른 디렉토리에 할당하는 것이 올바르다.  

댓글