Загружаем каталог…
Загружаем каталог…
이전에는 JPA가 Entity를 어떻게 관리하는지 살펴봤다. EntityManager ↓ 영속성 컨텍스트 ↓ 1차 캐시 ↓ 쓰기 지연 ↓ 변경 감지 그리고 일반 JPA에서는 다음과 같이 EntityManager 를 직접 사용했다. em.persist(memo); Memo memo = em.find(Memo.class, id); em.remove(memo); 하지만 Spring Boot에서 매번 EntityManager 를 직접 다루면서 CRUD를 작성한다면 코드가 다시 많아질 수 있다. Spring에서는 JPA를 더 편리하게 사용할 수 있도록 Spring Data JPA 를 제공한다. 이번에는 Spring Data JPA ↓ Repository ↓ JpaRepository ↓ SimpleJpaRepository 의 관계와, 왜 Repository 인터페이스만 선언해도 CRUD를 사용할 수 있는지 정리해 보려고 한다. 1. Spring Data JPA란? Spring Data JPA는 JPA를 더 쉽게 사용할 수 있도록 만들어진 Spring의 모듈 이다. 강의에서는 Spring Data JPA가 JPA를 추상화한 Repository 인터페이스를 제공하며, 개발자는 이를 통해 JPA 기능을 간편하게 사용할 수 있다고 설명한다. :chatgpt-content-reference{index="0"} 지금까지 관계를 조금 더 확장하면 다음과 같다. Application ↓ Spring Data JPA ↓ JPA ↓ Hibernate ↓ JDBC ↓ DB 즉, Spring Data JPA가 JPA를 대체하는 것은 아니다. Spring Data JPA = JPA를 편하게 사용하도록 도와주는 계층 이라고 이해하면 된다. 2. JPA와 Spring Data JPA의 차이 이전까지 배운 JPA에서는 직접 EntityManager 를 사용했다. 예를 들어 저장하려면: em.persist(memo); 조회하려면: Memo memo = em.find(Memo.class, id); 삭제하려면: em.remove(memo); 즉, Service ↓ EntityManager ↓ JPA ↓ DB 형태였다. Spring Data JPA를 사용하면: Service ↓ Repository ↓ Spring Data JPA ↓ JPA ↓ DB 형태가 된다. 개발자는 EntityManager 를 직접 다루는 대신 Repository 인터페이스를 통해 JPA 기능을 사용한다. 3. JpaRepository Spring Data JPA에서 가장 자주 사용하는 인터페이스가 JpaRepository 다. 메모장 프로젝트에서는 다음과 같이 작성한다. public interface MemoRepository extends JpaRepository<Memo, Long> { } 강의에서도 JpaRepository 를 상속받는 인터페이스를 만들고, 제네릭에 Entity 클래스와 @Id 타입 을 지정하도록 설명한다. :chatgpt-content-reference{index="1"} 즉, JpaRepository<Memo, Long> 에서 Memo → 관리할 Entity 타입 Long → Memo의 @Id 타입 을 의미한다. 4. 여기서 <Memo, Long> 은 무엇일까? 이 부분은 Java Generics 다. 제네릭은 클래스나 인터페이스를 만들 때 사용할 타입을 미리 고정하지 않고, 실제 사용하는 시점에 타입을 지정하는 기능이다. 예를 들어: List<String> 에서는 List → 여러 타입에 재사용할 수 있는 구조 String → 현재 List에서 사용할 타입 이다. JpaRepository 도 같은 방식이다. JpaRepository<T, ID> 형태로 정의되어 있고, 실제 사용할 때: JpaRepository<Memo, Long> 처럼 타입을 결정한다. 즉, T → Entity 타입 ID → Entity의 식별자 타입 으로 이해하면 된다. 메모장에서는 @Entity public class Memo { @Id private Long id; } 이므로 JpaRepository<Memo, Long> 이 된다. 강의에서도 Memo 를 제네릭의 Entity 위치에 지정하면서 해당 Repository가 memo 테이블의 CRUD를 담당하게 된다고 설명한다. :chatgpt-content-reference{index="2"} 5. 그런데 인터페이스만 만들었는데 어떻게 동작할까? 다음 코드를 보면 조금 이상하다. public interface MemoRepository extends JpaRepository<Memo, Long> { } 우리는 인터페이스만 선언했다. 직접 구현 클래스를 만들지 않았다. public class MemoRepositoryImpl implements MemoRepository { ... } 같은 코드는 작성하지 않는다. 그런데 Service에서는 바로 사용할 수 있다. @Service public class MemoService { private final MemoRepository memoRepository; public MemoService( MemoRepository memoRepository ) { this.memoRepository = memoRepository; } } 어떻게 가능한 걸까? 6. Spring Data JPA가 구현체를 만들어 준다 Spring Data JPA는 애플리케이션이 실행될 때 JpaRepository 를 상속한 Repository 인터페이스를 찾아낸다. 그리고 해당 정보를 바탕으로 구현체를 만들어 Spring Bean으로 등록한다. 강의에서는 이를 SimpleJpaRepository 를 통해 설명한다. Spring Data JPA가 Repository 인터페이스 정보를 이용해 구현 클래스를 만들고 Bean으로 등록하기 때문에 개발자가 구현체를 직접 작성하지 않아도 된다고 설명한다. :chatgpt-content-reference{index="3"} 개념적으로 보면: MemoRepository 인터페이스 ↓ Spring Data JPA가 확인 ↓ SimpleJpaRepository 기반 구현체 ↓ Spring Bean 등록 ↓ Service에 DI 이다. 따라서 private final MemoRepository memoRepository; 에 실제 사용할 객체를 Spring이 주입할 수 있게 된다. 7. 결국 이전에 배운 IoC/DI와 연결된다 여기서 5편에서 배운 내용과 연결된다. Spring Data JPA ↓ Repository 구현체 생성 ↓ Bean 등록 ↓ Spring IoC Container ↓ Service에 DI 즉, Spring Data JPA가 Repository 구현체를 만들어 주고, Spring IoC Container가 그 객체를 관리하며, Service는 생성자 주입으로 받아 사용한다. @Service public class MemoService { private final MemoRepository memoRepository; public MemoService( MemoRepository memoRepository ) { this.memoRepository = memoRepository; } } 이렇게 지금까지 배웠던 개념들이 서로 연결된다. 8. save() Entity를 저장할 때는 save() 를 사용할 수 있다. Memo saveMemo = memoRepository.save(memo); 메모 생성 코드는 다음과 같은 형태다. public MemoResponseDto createMemo( MemoRequestDto requestDto ) { Memo memo = new Memo(requestDto); Memo saveMemo = memoRepository.save(memo); return new MemoResponseDto(saveMemo); } 강의에서도 save() 에 저장할 Entity를 전달해 데이터를 저장한다고 설명하며, 내부적으로 영속성 컨텍스트를 이용한다. :chatgpt-content-reference{index="4"} 즉, memoRepository.save(memo) ↓ Spring Data JPA ↓ JPA ↓ 영속성 컨텍스트 ↓ DB 과정을 거친다. 9. save() 내부에서도 JPA가 동작한다 이전에 일반 JPA에서 저장할 때는 em.persist(memo); 를 사용했다. 그런데 Spring Data JPA에서는 memoRepository.save(memo); 를 사용한다. 그렇다고 EntityManager 가 사라진 것은 아니다. 강의에서 살펴보는 SimpleJpaRepository 의 save() 는 Entity가 새 객체인지 판단한 뒤 persist() 또는 merge() 를 사용한다. :chatgpt-content-reference{index="5"} 개념적으로 보면: save(entity) 내부에서 새로운 Entity → em.persist() 기존 Entity → em.merge() 처럼 JPA가 사용된다. 즉, JpaRepository ↓ SimpleJpaRepository ↓ EntityManager ↓ JPA 구조가 그대로 존재한다. Spring Data JPA가 그 코드를 대신 작성해 준 것이다. 10. findAll() 전체 데이터를 조회할 때는 memoRepository.findAll(); 을 사용할 수 있다. 메모장에서는 다음과 같이 DTO로 변환할 수 있다. public List<MemoResponseDto> getMemos() { return memoRepository.findAll() .stream() .map(MemoResponseDto::new) .toList(); } 강의에서도 findAll() 을 이용해 테이블의 전체 데이터를 조회한다. :chatgpt-content-reference{index="6"} 흐름은: findAll() ↓ List<Memo> ↓ Stream ↓ MemoResponseDto로 변환 ↓ List<MemoResponseDto> 이다. 11. findById() 특정 ID를 가진 Entity를 찾고 싶다면: memoRepository.findById(id); 를 사용한다. 그런데 반환 타입을 보면 단순히 Memo 가 아니다. Optional<Memo> 이다. 강의에서도 SimpleJpaRepository 의 findById() 반환 타입이 Optional 이라고 설명한다. :chatgpt-content-reference{index="7"} 여기서 Optional 이 등장한다. 12. Optional 이란? Optional<T> 는 값이 있을 수도 있고 없을 수도 있다는 상황을 표현하는 컨테이너 라고 이해하면 된다. 예를 들어: Optional<Memo> 는 Memo가 존재할 수도 있음 또는 Memo가 존재하지 않을 수도 있음 을 의미한다. DB에서 findById(1L) 을 실행했는데 ID가 1인 Memo가 존재하지 않을 수도 있기 때문에 Optional<Memo> 를 반환하는 것이다. 13. 왜 그냥 null 을 반환하지 않을까? 만약 단순히 Memo 를 반환한다면 Memo memo = memoRepository.findById(id); 메모가 존재하지 않을 때 null 이 들어올 수 있다. 그 후 실수로 memo.getContents(); 를 호출하면 NullPointerException 이 발생할 수 있다. Optional 을 사용하면 값이 존재할 수도 있음 이라는 사실을 타입 자체에서 명확하게 표현할 수 있다. 따라서 개발자가 값이 없는 경우를 처리하도록 유도한다. 14. orElseThrow() 메모장 프로젝트에서는 다음과 같이 처리한다. private Memo findMemo(Long id) { return memoRepository .findById(id) .orElseThrow(() -> new IllegalArgumentException( "선택한 메모는 존재하지 않습니다." ) ); } 강의에서도 findById() 의 결과가 없으면 orElseThrow() 를 이용해 예외를 발생시키는 방식으로 처리한다. :chatgpt-content-reference{index="8"} 흐름을 보면: findById(id) ↓ Optional<Memo> ↓ 값 있음? ┌────┴────┐ Yes No ↓ ↓ Memo 예외 발생 이다. 15. Optional 과 제네릭을 같이 보면 여기에서도 제네릭이 사용된다. Optional<Memo> 은 Optional<T> T = Memo 라는 뜻이다. 즉, Optional 도 특정 타입 하나에만 사용하는 클래스가 아니라 다양한 타입을 담을 수 있도록 제네릭으로 설계되어 있다. 예를 들어: Optional<String> Optional<User> Optional<Memo> 처럼 사용할 수 있다. 이처럼 제네릭을 사용하면 하나의 클래스 구조를 여러 타입에 재사용하면서도 타입 안정성을 유지할 수 있다. 16. 수정에는 왜 update() 가 없을까? Spring Data JPA를 보면 save() findAll() findById() delete() 등은 있는데 일반적으로 다음과 같은 메서드를 찾지 않는다. update() 왜일까? 이전 편에서 배운 변경 감지(Dirty Checking) 때문이다. 강의에서도 SimpleJpaRepository 에는 별도의 update() 메서드가 없으며, Entity를 수정할 때 영속성 컨텍스트의 변경 감지를 사용한다고 설명한다. :chatgpt-content-reference{index="9"} 예를 들어: @Transactional public Long updateMemo( Long id, MemoRequestDto requestDto ) { Memo memo = findMemo(id); memo.update(requestDto); return id; } 이다. 17. 수정 흐름 다시 보기 먼저 Entity를 조회한다. Memo memo = findMemo(id); 트랜잭션 안에서 조회한 Entity는 영속 상태로 관리된다. findMemo() ↓ Memo 조회 ↓ Managed Entity 그다음 값을 바꾼다. memo.update(requestDto); 그러면: Entity 값 변경 ↓ Dirty Checking ↓ UPDATE SQL 생성 ↓ DB 반영 이 일어난다. 그래서 다음 코드를 다시 호출하지 않아도 된다. memoRepository.save(memo); 즉, JPA 수정에서 중요한 것은 영속 상태 Entity를 변경하면 JPA가 변경을 감지한다 는 점이다. 18. 왜 @Transactional 이 필요할까? 수정 메서드에는 다음과 같이 @Transactional 이 붙는다. @Transactional public Long updateMemo(...) { } 강의에서도 변경 감지가 적용될 수 있도록 수정 메서드에 @Transactional 을 사용한다. :chatgpt-content-reference{index="10"} 흐름을 보면: @Transactional ↓ 트랜잭션 시작 ↓ 영속성 컨텍스트 유지 ↓ Entity 조회 ↓ Entity 수정 ↓ Dirty Checking ↓ flush ↓ commit 이다. 지난 편에서 배운 영속성 컨텍스트가 Spring Data JPA에서도 그대로 사용되는 것이다. 19. delete() Entity를 삭제할 때는 다음과 같이 사용할 수 있다. memoRepository.delete(memo); 예를 들면: public Long deleteMemo(Long id) { Memo memo = findMemo(id); memoRepository.delete(memo); return id; } 강의에서도 delete() 에 삭제할 Entity 객체를 전달하는 방식으로 사용한다. :chatgpt-content-reference{index="11"} 일반 JPA에서는 em.remove(memo); 를 직접 사용했다면, Spring Data JPA에서는 memoRepository.delete(memo); 로 추상화된 것이다. 20. 결국 CRUD가 이렇게 단순해진다 Spring Data JPA를 적용하면 Repository는: public interface MemoRepository extends JpaRepository<Memo, Long> { } 이 정도로 끝날 수 있다. 그리고 Service에서는: save() findAll() findById() delete() 를 바로 사용할 수 있다. 정리하면: CREATE → save() READ 전체 → findAll() READ 하나 → findById() UPDATE → 영속 Entity 변경 + Dirty Checking DELETE → delete() 이다. 강의의 MemoService도 save , findAll , findById , delete , 그리고 변경 감지를 조합해 CRUD를 구현한다. :chatgpt-content-reference{index="12"} 21. 그런데 정렬해서 조회하려면? 기본 findAll() 은 단순히 전체 Entity를 조회한다. 하지만 메모장에서는 최근 수정된 메모부터 조회하고 싶을 수 있다. 예를 들어: modifiedAt DESC 형태다. SQL이라면 직접 다음과 비슷하게 작성할 수 있다. SELECT * FROM memo ORDER BY modified_at DESC; Spring Data JPA에서는 이런 경우에도 SQL을 직접 작성하지 않고 메서드 이름으로 쿼리를 만들 수 있다. 이 기능이 Query Methods 다. 22. Query Methods란? Spring Data JPA에서는 정해진 규칙에 맞춰 Repository의 메서드 이름을 작성하면 해당 이름을 분석해 쿼리를 만들어 준다. 강의에서는 이를 Query Methods 라고 설명한다. :chatgpt-content-reference{index="13"} 예를 들어: List<Memo> findAllByOrderByModifiedAtDesc(); 라는 메서드를 선언할 수 있다. 직접 구현 코드는 작성하지 않는다. public interface MemoRepository extends JpaRepository<Memo, Long> { List<Memo> findAllByOrderByModifiedAtDesc(); } 23. 메서드 이름을 나눠 보면 findAllByOrderByModifiedAtDesc() 를 나눠 보면 이해하기 쉽다. findAll → 전체 조회 By → 조건 표현 시작 OrderBy → 정렬 ModifiedAt → modifiedAt 필드 기준 Desc → 내림차순 따라서 의미는: modifiedAt 을 기준으로 내림차순 정렬하여 전체 Memo 조회 가 된다. 강의에서도 이 메서드가 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 쿼리를 만든다고 설명한다. :chatgpt-content-reference{index="14"} 24. 이제 findAll() 대신 Query Method 사용 기존에는: public List<MemoResponseDto> getMemos() { return memoRepository.findAll() .stream() .map(MemoResponseDto::new) .toList(); } 이었다면, 다음처럼 변경할 수 있다. public List<MemoResponseDto> getMemos() { return memoRepository .findAllByOrderByModifiedAtDesc() .stream() .map(MemoResponseDto::new) .toList(); } 이제 최근 수정된 메모가 먼저 조회된다. 25. 조건이 필요한 Query Method Query Method에는 매개변수도 전달할 수 있다. 예를 들어 특정 username을 가진 Memo를 찾고 싶다면: List<Memo> findAllByUsername( String username ); 처럼 선언할 수 있다. 강의에서도 ByUsername 에 필요한 값을 메서드 파라미터로 전달하는 형태를 설명한다. :chatgpt-content-reference{index="15"} 즉, 메서드 이름 → 어떤 조건으로 검색할 것인지 표현 매개변수 → 검색에 사용할 실제 값 이다. 26. 특정 문자열이 포함된 Memo 찾기 이번
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[내일배움캠프] Spring 8 Spring Data JPA — Repository 구현 없이 CRUD가 가능한 이유. 이전에는 JPA가 Entity를 어떻게 관리하는지 살펴봤다. EntityManager ↓ 영속성 컨텍스트 ↓ 1차 캐시 ↓ 쓰기 지연 ↓ 변경 감지 그리고 일반 JPA에서는 다음과 같이 EntityManager 를 직접 사용했다. em.persist(memo); Memo memo = em.find(Memo.class, id); em.remove(memo); 하지만 Spring Boot에서 매번 EntityManager 를 직접 다루면서 CRUD를 작성한다면 코드가 다시 많아질 수 있다. Spring에서는 JPA를 더 편리하게 사용할 수 있도록 Spring Data JPA 를 제공한다.…
Открыть источник