[02.02] 내일배움캠프[Spring] TIL-64

박상훈·2023년 2월 3일
0

내일배움캠프[TIL]

목록 보기
64/72

[02.02] 내일배움캠프[Spring] TIL-64

1. Spring Data Jpa

  • @Repository : MarkerInterface로 특별한 기능은 없음.
  • Repository ~ JpaRepository 까지는 @NotRepositoryBean 이 붙어있는 인터페이스이다.
    • JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.
      • 어떻게? @SpringBootApplication 을 통해 자동으로 붙여지는 @EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 등록된다.
  • JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체이다
  • ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해준다.
  • 기존 Repository
    • @Repository 을 클래스에 붙인다.
    • 앞서배운 RawJPA의 Repository 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
  • 새로운 JpaRepository
    • JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
      • @NotRepositoryBean상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NotRepositoryBean** = 빈생성 막음)
      • SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)

간단하게 보는 JpaRepository

// 변경 전
@Repository
public class UserRepository {

  @PersistenceContext
  EntityManager entityManager;

  public User insertUser(User user) {
    entityManager.persist(user);
    return user;
  }

  public User selectUser(Long id) {
    return entityManager.find(User.class, id);
  }
}

// 변경 후
public interface UserRepository extends JpaRepository<User, Long> {
  
}

Respository기능 제한하기

  • JpaRepository 에서 사용할 메소드 제한하기
    • JpaRepository 는 기본적으로 모든 기능을 제공하기 때문에 리스크가 있을 수 있다.
    • 따라서, 아래와 같은 방법으로 원하는 기능 메소드만 구현하도록 제한할 수 있다.

💼 예시: UserRepository가 있다고 할 때, 기능을 제한해주지 않으면 ,findByPassword()등 민감하거나 불필요한 메소드의 선언과 사용이 가능해진다.
💼 두가지의 방법이 있지만, 둘다 내 머리속에 넣기엔 욕심임을 깨닳고 한가지 방법을 명확하게 포스팅하자!

@RepositoryDefinition

@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class)
public interface CommentRepository {

    Comment save(Comment comment);

    List<Comment> findAll();
    
}

Repository에 기능 추가하기

delete()

  • delete() 호출 시 영속성 상태인지 확인한다.
  • 영속성 컨텍스트에 없다면(!em.contains(entity))엔티티를 조회해서 영속성 상태로 바꾼다.
    -> 삭제할껀데 왜 굳이?
    -> CaseCade,OrphanRemoval 에 의한 자식도 삭제가 누락되지 않도록!
  • JpaRepository 의 delete() 는 해당 엔티티를 바로 삭제하지 않는다.
    • remove() 메소드를 통해 remove 상태로 바꾼다.
public interface MyRepository {
	...여기다가 추가할 메소드 선언...
}
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {

	@Autowired
	EntityManager entityManager;


	@Override
	public void delete(User user) {
		entityManager.remove(user);
  }

}
  • findAll()할 때 이름만 가져오기 커스텀
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {

	@Autowired
	EntityManager entityManager;

	@Override
	public List<String> findNameAll() {
    return entityManager.createQuery("SELECT u.username FROM User AS u", String.class).getResultList();
  }

}
  • 여기까지 해서 JpaRepository에 대한 개념 정리 ( 틀릴 가능성 높음..! )
  • 우리는 보통 JpaRepository<>를 구현해서 Respository를 사용한다.
    -> 근데 어떻게 인터페이스에서 상속 받았다고 해서 모든 작업이 이뤄질 수 있을까? 에 대한 고민
    -> 위에서 언급하긴 했지만, @SpringBootApplication 동작함으로써 상속 받은 Repository인터페이스의 구현체가 DI되는 것!
    -> 그래서 우리가 커스텀한 Repository도 인터페이스로 메소드를 구현해놓고, 구현체를 만들어서 커스텀 해놓으면, Repository에서 extends해서 사용할 수 있는 것!
 , MyRepositoryCreate<User> << 커스텀 한 리파지토리를 쓰고 싶다면..

2. Jpa 페이징,정렬

  • JpaRepository의존성
  • ListPagingAndSortingRepository
    • PagingAndSortingRepository : 여기에서 페이징 & 소팅 기능을 제공한다.

페이징 처리 순서

1) PageRequest를 사용하여 Pageable에 페이징 정보를 담아 객체화 한다.
2) Pageable을 JpaRepository가 상속된 메서드 T(Entity)와 함께 파라미터로 전달한다.
3) 2번의 메서드의 return으로 Page<T>가 응답된다.
4) 응답된 Page<T>에 담겨진 Page정보를 바탕으로 로직을 처리하면 된다.

페이징 요청/응답 클래스

  • 요청 : org.springframework.data.domain.Pageable
    -> 페이징을 제공하는 중요한 인터페이스이다.

Pageable 만드는 케이스 예제

PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들

Pageable 요소

pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()Stream<Entity> 반환
  • 저번 프로젝트에도 Page<>형식으로 반환했던 기억이 있다. ( 많은 요소들이 담겨있었음 )

페이지 반환 타입

Page<>

  • 게시판 형태의 페이징에서 사용된다.
  • 전체 요소 갯수도 함께 조회한다.(totalElements)
  • 응답은 위와 동일

Slice<>

  • 더보기 형태의 페이징에서 사용된다.
  • 전체 요소 갯수 대신 offset필드로 조회할 수 있음
    -> Count쿼리가 따로 발생하지는 않고 limit +1 조회( Offset은 성능이 안좋아서 현업에서 안쓴다고 한다..)

List<>

  • 전체 목록보기 형태의 페이징에서 사용된다.
  • 기본 타입으로 count조회가 발생하지 않는다.

정렬

  • 컬럼 값으로 정렬하기
Sort sort1 = Sort.by("name").descending();     // 내림차순
Sort sort2 = Sort.by("password").ascending();  // 오름차순
Sort sortAll = sort1.and(sort2);      // 2개이상 다중정렬도 가능하다 -> 요거 유용할듯?
Pageable pageable = PageRequest.of(0, 10, sortAll);  // pageable 생성시 추가

컬럼이 아닌 값으로 정렬하기

  • @Query 사용시 Alias(쿼리에서 as 로 지정한 문구) 를 기준으로 정렬할 수 있다.
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);

// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));

페이징/정렬 실무 팁

  • List<>가 필요하면 응답을 Page<>로 받지말고 , List<>로 받아라
    -> 전체 count쿼리가 추가로 발생하는 Page<>보다는 List<>가 대용량 처리할 때 더 안정적이고 빠르다.

3. SpringData 쿼리 / QueryDSL

SpringData쿼리

  • SpringData Common의 CRUDRepository + PagingAndSortRepository쿼리기능 제공
  • 쿼리 실습 예제
// 기본
List<User> findByNameAndPassword(String name, String password);

// distinct (중복제거)
List<User> findDistinctUserByNameOrPassword(String name, String password);
List<User> findUserDistinctByNameOrPassword(String name, String password);

// ignoring case (대소문자 무시)
List<User> findByNameIgnoreCase(String name);
List<User> findByNameAndPasswordAllIgnoreCase(String name, String password);

// 정렬
List<Person> findByNameOrderByNameAsc(String name);
List<Person> findByNameOrderByNameDesc(String name);

// 페이징
Page<User> findByName(String name, Pageable pageable);  // Page 는 카운트쿼리 수행됨
Slice<User> findByName(String name, Pageable pageable); // Slice 는 카운트쿼리 수행안됨
List<User> findByName(String name, Sort sort);
List<User> findByName(String name, Pageable pageable);

// 스트림 (stream 다쓴후 자원 해제 해줘야하므로 try with resource 사용추천)
Stream<User> readAllByNameNotNull();

QueryDSL

  • 기능
  • QueryDSL의 Predicate 인터페이스로 조건문을 여러개 구성하여 따로 관리할 수 있다.
    • findOne(Predicate),findAll(Predicate)가 주로 사용된다.
    • findOne() = Optional<>리턴
    • findALL() = List<> , Page<> , Iteralbe<> , Slice<> 리턴
  • 장점
    • 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
    • 자동완성 등, IDE의 도움을 받을 수 있다.
    • 동적인 쿼리 작성이 편하다.
    • 쿼리 작성시 제약 조건등을 메서드 추출을 통해 재사용 할 수 있다.( 아마 이게 좀 편리할듯? )
  • 원리
    • QueryDSL의존성을 추가하면 SpringData에 의해QueryDslPredicateExecutor인터페이스가 추가된다.
    • QueryDslPredicateExecutorRepository가 QueryDsl 을 실행할 수 있는 인터페이스를 제공하는 역할을 한다.

QueryDSL사용방법

  • Spring 2.X버전 까지는 QueryDSL빌드 Task를 따로 설정해줘야했다.
plugins {
    ...
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
    ...
    querydsl.extendsFrom compileClasspath
}

def querydslSrcDir = 'src/querydsl/generated'

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

project.afterEvaluate {
    project.tasks.compileQuerydsl.options.compilerArgs = [
            "-proc:only",
            "-processor", project.querydsl.processors() +
                    ',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
    ]
}

dependencies {
    implementation("com.querydsl:querydsl-jpa") // querydsl
    implementation("com.querydsl:querydsl-apt") // querydsl
    ...
}
  • Spring 3.X버전은 의존성만 추가해줘도 빌드에 자동으로 포함되서 실행된다.
// application.yml or properties

dependencies {
		....

		// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

QueryDSL실습하기

  • QuerydslPredicateExecutor<Channel> 의존성 추가
public interface ChannelRepository extends JpaRepository<Channel, Long>, QuerydslPredicateExecutor<Channel> {

}
  • QueryDSL TestCode
    @Test
    void queryDslTest() {
        // given
        var newChannel = Channel.builder().name("sanghoon").build();
        channelRepository.save(newChannel);

        Predicate predicate = QChannel.channel.name.equalsIgnoreCase("SANGHOON");

        // when
        Optional<Channel> optional = channelRepository.findOne(predicate);

        // then
        assert optional.get().getName().equals(newChannel.getName());
    }
  • 간단하게 요약하자면, 조건( 어떤 케이스를 찾아온다거나 등.. ) 의 Predicate를 만들어서,
    find(Predicate)를 하면 된다.
  • join이 필요한 쿼리일경우 불가능!!!
  • join쿼리는 없는대신 조건이 많은경우 ( 가능! )
@Service
public class ThreadServiceImpl implements ThreadService {

  @Autowired
  ThreadRepository threadRepository;

  @Override
  public List<Thread> selectNotEmptyThreadList(Channel channel) {
    var thread = QThread.thread;

    // 메세지가 비어있지 않은 해당 채널의 쓰레드 목록
    var predicate = thread
				.channel.eq(channel)
        .and(thread.message.isNotEmpty());
    var threads = threadRepository.findAll(predicate);

    return IteratorAdapter.asList(threads.iterator());
  }

  @Override
  public Thread insert(Thread thread) {
    return threadRepository.save(thread);
  }
}
  • TestCode
@Test
  void getNotEmptyThreadList() {
    // given
    var newChannel = Channel.builder().name("c1").type(Type.PUBLIC).build();
    var savedChannel = channelRepository.save(newChannel);
    var newThread = Thread.builder().message("message").build();
    newThread.setChannel(savedChannel);
    threadService.insert(newThread);

    var newThread2 = Thread.builder().message("").build();
    newThread2.setChannel(savedChannel);
    threadService.insert(newThread2);

    // when
    var notEmptyThreads = threadService.selectNotEmptyThreadList(savedChannel);

    // then 메세지가 비어있는 newThread2 는 조회되지 않는다.
    assert !notEmptyThreads.contains(newThread2);
  }

4. 언제 누가 이런짓을 했는지(Auditing)

  • 저번 프로젝트 때 썼던 TimeStamp를 생각해보자!
@CreatedDate
private Date created;

@LastModifiedDate
private Date updated;

@CreatedBy
@ManyToOne
private Account createdBy;

@LastModifiedBy
@ManyToOne
private Account updatedBy;

Auditing적용 방법

1) 메인 Application에 @EnableJpaAuditing추가

@EnableJpaAuditing
@SpringBootApplication
public class Application {...}

2) 엔티티 클래스위에 @EntityListeners(AuditingEntityListener.class)추가

@Getter
@MappedSuperclass // 다른 엔티티에서 사용할 것임에 필수!
@EntityListeners(AuditingEntityListener.class)
public class TimeStamp {
    @CreatedDate
    private LocalDateTime createdAt;

    @CreatedBy
    @ManyToOne
    private User createdBy;

    @LastModifiedDate
    private LocalDateTime modifiedAt;

    @LastModifiedBy
    @ManyToOne
    private User modifiedBy;
}
  • 3) @CreatedAt,@ModifiedAt은 구현체 없이 잘 동작하지만, @CreatedBy,@ModifiedBy는 구현체가 있어야 값이 들어감 -> 구현체 없으면 해당 필드 null로 들어가는 것 확인.

3-1) 구현체 만들기( AuditorAware )
-> 보통 누구에 의해서 만들어졌냐? , 누구에 의해서 수정됐냐? 는 UserDetails 즉, Authentication에서 빼온 정보를 넣어준다.

@Service
public class UserAuditorAware implements AuditorAware<User> {
    @Override
    public Optional<User> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			
        if (authentication == null || !authentication.isAuthenticated()) {
            return Optional.empty();
        }

        return Optional.of(((UserDetailsImpl) authentication.getPrincipal()).getUser());
    }
}
@EnableJpaAuditing(auditorAwareRef = "userAuditorAware") // auditorAware 의 빈이름을 넣어준다.
@SpringBootApplication
public class Application {

Auditing직접 구현해보기

  • 생성일시, 생성자, 수정일시, 수정자는 결국 엔티티의 영속성이 변경될 때 저장한다.
  • 엔티티의 영속성이 변경되는 생성 > 수정 > 삭제 이 흐름을 엔티티 라이프 사이클 이벤트라 한다.
  • @PostConstruct : 객체가 생성되면 자동으로 실행되는 메소드에 붙이는 이것의 원리와 같다.
  • 엔티티 저장 이벤트
    전 : @PrePersist : EntitiyManager가 엔티티를 영속성 상태로 만들기 직전에 메소드 수행
    후 : @PostPersist : EntitiyManager가 엔티티를 영속성 상태로 만든 직후에 메소드 수행
  • 엔티티 수정 이벤트
    전 : @PreUpdate : EntitiyManager가 엔티티를 갱신상태로 만들기 직전에 메소드 수행
    후 : @PostUpdate : EntitiyManager가 엔티티를 갱신상태로 만든 직후에 메소드 수행
  • 엔티티 삭제 이벤트
    전 : @PreRemove : EntitiyManager가 엔티티를 삭제 상태로 만들기 직전에 메소드 수행
    후 : @PostRemove : EntitiyManager가 엔티티를 삭제 상태로 만든 직후에 메소드 수행
  • 이것들을 활용해서 @CreatedAt,@ModifiedAt를 구현해보자
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamp {

  private LocalDateTime createdAt;

  private LocalDateTime modifiedAt;

  public void updateCreatedAt() {
    this.createdAt = LocalDateTime.now();
  }

  public void updateModifiedAt() {
    this.modifiedAt = LocalDateTime.now();
  }
}
// Entity 내 정의

/**
   * 라이프 사이클 메소드
   */
  @PrePersist
  public void prePersist() {
    super.updateModifiedAt();
    super.updateCreatedAt();
  }

  @PreUpdate
  public void PreUpdate() {
    super.updateModifiedAt();
  }

Auditing직접 구현해보기(과제)

  • 💁‍♂️ 여기서 과제!!!
  • ContextHolder + 엔티티 라이프 사이클 이벤트(@PrePersist, @PreUpdate) 를
  • 사용해서 createdBy, modifiedBy 를 구현해볼까요?
  • TestCode로 유저 저장될 때 인증객체 만들었음.( 사실 로그인이 맞겠지만 그것까지 테스트에서 신경쓸 필요는 없으니.. ! )
    @Test
    void contextHolderLifeCycleTest(){
        var newUser = User.builder().username("new").password("pass").build();
        //var newUser2 = User.builder().username("modified").password("pass").build();
        var savedUser =  userRepository.save(newUser);

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(newUser,null);
        Authentication authentication = token;
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);

        var newChannel = Channel.builder().name("sanghoon").build();
        var newChannel2 = Channel.builder().name("sanghoon modified").build();
        var savedChannel = channelRepository.save(newChannel);

        savedChannel.updateChannel(newChannel2);

    }

@Getter
@MappedSuperclass // 엔티티에 매핑하기 위해서
@EntityListeners(AuditingEntityListener.class)
public class TimeStamp {

 @CreatedBy
    @ManyToOne
    private User createdBy;

	@LastModifiedBy
    @ManyToOne
    private User modifiedBy;

 	 public void updateCreatedBy(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            this.createdBy = null;
        }
            this.createdBy = (User) authentication.getPrincipal();

    }

    public void updateModifiedBy(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            this.modifiedBy = null;
        }
        this.modifiedBy = (User) authentication.getPrincipal();

    }
    
 }
@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Getter
public class Channel extends TimeStamp {

...// channel을 만들거나, 수정할 때를 테스트할 것 임므로 Chaanel Entity를 사용했음.

   @PrePersist
    public void prePersist(){
        super.updateCreatedAt();
        super.updateModifiedAt();
        super.updateCreatedBy();
    }


    @PreUpdate
    public void preUpdate(){

        super.updateModifiedAt();
        super.updateModifiedBy();
    }
}
profile
기록하는 습관

0개의 댓글