2021년 07월 24일 TIL

3 minute read

프로젝트를 진행하면서 팀원들에게 내가 JPA에 대해 공부하고, 이해한 내용들, 그리고 작성 시에 고려해서 작성하면 좋을 부분들에 대해 요약하여 문서로 넘겨주었다. 틀린 부분이 있을 수도 있지만, 추후 공부하면서 더 메꿔나가야겠다.

JPA 작성 시 유의 사항

Entity

  • Getter는 생성, Setter는 필요한 경우에만 생성(가능하면 생성 X)
  • Embeded Embedable은 하나만 있어도 괜찮지만 가독성을 위해 둘 다 붙이기
  • 연관관계의 주인은 FK가 있는 쪽에 주는 것이 정석
  • Time은 LocalDateTime 을 사용
  • 값 타입 엔티티의 경우, 기본 생성자를 필수로 만들어줘야하고, 접근제한자는 Protected 사용(값 타입은 변경 불가능하게 설계해야하므로)
  • XtoOne은 기본 fetch type이 eager이므로, lazy로 변경
  • cascade의 사용은 라이프 사이클이 persist와 같이 돌면 그때 cascade.all 사용
  • @NoArgsConstructor(access = “protected”) 사용하고 파라미터 있는 생성자를 강제하게 만드는 방식으로 리팩토링
    • 이렇게 작성하게되면 당연히, @AllArgsConstructor의 필요성을 느끼게 될 수도 있는데, Entity의 인자가 많은 경우 @AllArgsConstructor는 파라미터의 순서가 바뀌면 원하는 대로 동작하지 않을 수 있으므로 위험하다. 따라서 이 생성자는 @Builder 를 통해 구현해주는 것이 좋다.
  • 연관관계 편의 메서드 설정
public void setMember(Member member){
    this.member = member;
    member.getOrders().add(this);
}

Service

  • @Transactional(javax 버전도 있는데, Spring이 제공하는걸 씁시다)
    • 클래스에 어노테이션 사용시 아래의 메서드들 일괄 적용
    • 클래스의 어노테이션에 readOnly = true를 주고, 조회하는 코드가 아닐시에 따로 @Transactional을 달아서 갱신해주는 식으로 코드 구성
  • 원래는 엔티티매니저를 @PersistentContext 로 넣어야하는데, @Autowired로 되도록 지원해줍니다. (Spring data jpa)

  • 비즈니스 로직은 그 데이터를 가지고 있는 곳에서 정의하는 것이 응집력이 있다
    • 비즈니스 로직을 엔티티에서 만드는 것 -> 도메인 모델 패턴
    • 반대의 경우를 트랜잭션 스크립트 패턴이라고 한다.

번외 LomBok

  • 지양해야할 Annotation

    • @AllArgsConstructor
      • 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성한다.
      • 이와 비슷한 @RequriedArgsConstructor도 있다. 초기화 되지 않은 모든 final 필드, @NonNull과 같이 제약조건이 설정되어있는 모든 필드들에 대한 생성자를 자동으로 생성한다.
      • 발생할 수 있는 문제 상황
        • 예로 들어, 두 개의 같은 타입 인스턴스 멤버를 선언한 상황에서 개발자가 선언된 인스턴스 멤버의 순서를 바꾸면, 개발자도 인식하지 못하는 사이에 lombok이 생성자의 파라미터 순서를 필드 선언 순서에 따라 변경하게 된다. 이때, IDE가 제공해주는 리팩토링은 전혀 동작하지 않고, 두 필드가 동일 타입이기 때문에 기존 소스에서도 오류가 발생하지 않아 아무런 문제없이 동작하는 것으로 보이지만, 실제로 입력된 값이 바뀌어 들어가는 상황이 발생한다.
    • @Data
      • @Data = @RequiredArgsConstructor + @Getter + @Setter + @ToString + @EqualsAndHashCode
        • 각 annotation의 주의사항을 모두 포함한다.
        • @EqualsAndHashCode : equals 메소드와 hashcode 메소드를 생성한다
          • 예를 들어, 객체를 Set에 저장한 뒤 필드 값을 변경하면 hashCode가 변경되면서 이전에 저장한 객체를 찾을 수 없는 문제가 발생한다.
        • @RequriedArgsConstructor의 주의사항은 앞에서 거론했다.
        • @Data 대신, @Getter, @Setter, @ToString으로 명시하는 것을 권장한다.
    • @NoArgsConstructor
      • 파라미터가 없는 생성자를 생성한다.
      • 사용 시 주의 사항
        • 필드들이 final로 생성되어 있는 경우에는 필드를 초기화할 수 없기 때문에 생성자를 만들 수 없고 에러가 발생한다. → @NoArgsConstructor(force=true) 옵션을 이용해 final 필드를 강제 초기화 시켜 생성자를 만들 수 있다.
        • @Nonnull 같이 필드에 제약조건이 설정되어 있는 경우, 추후 초기화를 진행하기 전까지 생성자 내 null-check 로직이 생성되지 않는다.
    • @Builder
      • @Builder를 사용하면 객체 생성이 명확해진다. 가급적 클래스 보다는 직접 만든 생성자 혹은 static 객체 생성 메소드에 붙이는 것을 권장한다.
      • @Builder를 붙이면 파라미터 순서가 아닌 이름으로 값을 설정하기 때문에 리팩토링에 유연하게 대응할 수 있고, 필드 순서를 변경해도 문제가 없다.

단, Dto 사용시에는 크게 문제될 것이 없으므로 @Data, @AllArgsConstructor도 사용해도 괜찮다고는 생각합니다!

spring bean 를 inject 할때 field injection 을 쓰지 말아야 하는 이유는? 그럼 어떤 방식으로 해야하는가?

  • 의존성을 주입하기가 쉬워서 몇 개씩 막 추가하게 된다.
    • 생성자 주입을 사용하면 생성자의 파라미터가 많아짐과 동시에 하나의 클래스가 많은 책임을 떠안는다는 사실을 직관적으로 파악할 수 있다.
  • 클래스의 의존관계가 명확하지 않다.
    • 보다 정확한 의존 관계를 설명하기 위해 생성자 주입 또는 setter 주입을 사용해야 한다.
  • 필드 주입은 final 선언을 할 수 없다. 그래서 객체가 변할 수 있다.
    • 생성자 주입을 사용하면 final 선언을 할 수 있다.
  • 단위 테스트 하기 어렵다.
    • 생성자 주입은 테스트하기 보다 쉽다.
  • 필드 주입은 객체 생성시점에 순환참조가 일어나는지 확인할 수 있는 방법이 없다.
    • 생성자 주입은 순환 의존성을 가질 경우 BeanCurrentlyCreationExeption을 발생하게 되서 순환 의존성을 알 수 있다.

참조 링크 https://velog.io/@rosa/Lombok-%EC%A7%80%EC%96%91%ED%95%B4%EC%95%BC-%ED%95%A0-annotation

API 개발 시 유의사항

  • 절대 엔티티로 입력받거나 리턴하지않도록 해주세요.(모든 에러의 근원)
    • 따라서, RequestDto, ResponseDto를 따로 만들어서 필요한 데이터만 받고, 필요한 데이터만 주는 것이 좋습니다.
  • 연관관계가 있는 엔티티 조회 시 N+1 문제를 방지하기 위해 fetch join을 사용해주세요
    • 컬렉션의 fetch join 시에는, 1번만 해야합니다
      • 컬렉션 A의 내부에 컬렉션 B, C 가 있을 경우, B, C 둘 모두에 fetch join을 시도하는 경우가 있어선 안됩니다.
      • 컬렉션 A와 B를 fetch join 하고, 컬렉션 B 내부의 컬렉션 C를 fetch join 해서도 안됩니다.
      • 이러한 일이 발생하는 경우, 일 대 다 대 다의 조인이 발생하기 때문에, 최악의 경우 1*N^2개의 컬럼이 돌아오게됩니다.
      • 컬렉션의 fetch join 시 페이징이 불가능합니다.(일 대 다이기때문에, 데이터가 예측할 수 없이 증가하므로, 하려고하면 하이버네이트가 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도하기때문에, 메모리가 박살납니다)
    • 해결 방법
      • XToOne 관계를 모두 fetch join 한다 (계속 toOne 관계라면 연속적으로 fetch join을 걸어도 괜찮다)
      • 컬렉션은 lazy로 조회한다
      • 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BatchSize를 적용
      • 이 방식은 퍼올려야하는 데이터가 많을 수록 이득
      • 이걸로 N+1에서 어느정도 해방될 수 있기때문에, 가능하면 세팅하는게 좋은 것 같다.
      • 디테일하게 적용하고싶다면, @BatchSize로 적용하자
  • 엔티티로 받는 대신 Dto로 받는 방법도 있는데, 확장성이나 재활용성이 떨어지므로, 그냥 엔티티로 받아서 가공합시다
  • Dto로 반환할 때에, 단순히 엔티티를 Dto로 래핑하라는 의미가 아닌, 엔티티와의 연결성을 모두 끊어야한다(인자로 List<엔티티>를 갖고 있는 경우, 가진 List의 엔티티도 모두 정리해야한다)

Categories:

Updated: