
ORM은 자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법이다.
ORM이 있다면 데이터베이스 값을 객체처럼 사용할 수 있다. = SQL을 몰라도 자바 언어로만 데이터베이스에 접근해서 원하는 데이터를 받아올 수 있다.
ORM에도 여러 종류가 있다. 자바에서는 JPA를 표준으로 사용한다.
- JPA: 자바에서 관계형 DB를 사용하는 방식을 정의한 인터페이스
- 하이버네이트: JPA인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크
- 하이버네이트의 목표: 자바 객체를 통해 DB 종류에 상관없이 DB를 자유자재로 사용
DB테이블과 매핑되는 객체(쿼리를 실행하는 객체)
엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 한다.

회원 2명이 동시에 가입하면 엔티티 매니저 팩토리는
엔티티 매니저1과 엔티티 매니저2를 생성하여 각각의 커넥션을 통해 DB에 접근한다.
그러나 스프링 부트는 엔티티 매니저 팩토리를 하나만 생성해서 관리한다.
또한 @PersistenceContext 또는 @Autowired 어노테이션을 사용해 엔티티 매니저를 사용한다.@PersistenceContext EntityManager em;또한 스프링부트는 기본적으로 빈을 하나만 생성해서 공유하므로 동시성문제가 발생할 수 있다. 그래서 실제로는 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용한다.
필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출한다.
엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다.
영속성 컨텍스트는 엔티티를 관리하는 가상의 공간이다.
영속성 컨텍스트 덕분에 DB에서 데이터를 효과적으로 가져오고, 엔티티를 편하게 사용할 수 있다.
다음은 영속성 컨텍스트의 4가지 특징이다.
이 특징들을 통해 DB접근을 최소화하고 성능을 높일 수 있다.
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다.
캐시의 프라이머리키는 엔티티의 @Id 어노테이션이 달린 값이다.
본격적으로 엔티티를 조회하면
1) 1차 캐시에서 데이터를 조회하고 값이 있으면 반환한다.
2) 값이 없으면 DB에 조회하여 1차캐시에 저장한 다음 반환한다.
즉 DB를 거치지 않고 캐시를 통해 빠르게 조회할 수 있다.
트랜잭션을 커밋하기 전까지는 DB에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것이다.
ex> 데이터추가쿼리:3 으로 설정하면, 트랜잭션 커밋시 3개 쿼리를 한꺼번에 전송한다.
엔티티 매니저는 엔티티가 처음 로드될 때의 상태(1차 캐시에 저장된 상태)와 트랜잭션이 커밋될 때의 현재 상태(변경)를 비교하여 변경된 값을 DB에 자동으로 반영한다.
데이터베이스에서 데이터를 즉시 로드하지 않고 실제로 사용할 때까지 로딩을 지연시키는 방법이다. 이를 통해 불필요한 데이터베이스 쿼리를 피하고, 성능을 최적화할 수 있다.
public class EntityManagerTest{
@Autowired
EntityManager em;
public class example(){
// 1) 엔티티 매니저가 엔티티를 관리하지 않는 상태(비영속 상태)
Member member = new Member(1L, "홍길동");
// 엔티티가 관리되는 상태
em.persist(member);
// 엔티티 객체가 분리된 상태
em.detach(member);
// 엔티티 객체가 삭제된 상태
em.remove(memeber);
}
}
1) 엔티티를 처음만들면 비영속 상태기 된다.
2) persist() 메소드로 관리 상태로 만들수 있다.(Member 객체가 영속성 컨텍스트에서 상태가 관리됨)
3) detach() 메소드로 영속성 컨텍스트로 부터 관리를 포기(분리)할 수 있다.
4) remove() 메소드로 엔티티(객체)를 영속성 컨텍스트와 데이터베이스에서 삭제할 수 있다.
스프링 데이터는 비즈니스 로직에 더 집중할 수 있도록 DB 사용 기능을 클래스 레벨에서 추상화 하였다.
스프링 데이터에서 제공하는 인터페이스를 통해 스프링 데이터를 사용할 수 있다.
이 인터페이스에서는 CRUD를 포함하여 여러 메소드가 포함되어 있고 알아서 쿼리를 만들어준다.
스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가됨
-> PagingAndSortingRepository(스프링 데이터의 인터페이스)를 상속받아 JpaRepository(인터페이스)를 만들었다.
이는 JPA를 더 편리하게 사용하는 메소드를 제공한다.
@PersisteContext
EntityManager em;
public void join(){
// 기존에 엔티티 상태를 바꾸는 방법(메소드를 호출해서 상태 변경)
Member member = new Member(1L, "홍길동");
em.persist(member);
}
하지만 스프링 데이터 JPA를 사용하면 레포지토리 역할을 하는 인터페이스를 만들어 DB 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long>{
}
JpaRepository를 상속해 인터페이스를 만들고, 제네릭에는 관리할 <엔티티 이름, 엔티티 기본키의 타입>을 입력하면 기본 CRUD 메소드를 사용할 수 있다.
MembersRepository.java 파일에서
Alt+Enter로 [Create Test]를 한다.
INSERT INTO member (id, name) VALUES(1, 'A')
INSERT INTO member (id, name) VALUES(2, 'B')
INSERT INTO member (id, name) VALUES(3, 'C')
spring:
sql:
init:
mode: never
이는 src/main/resources 폴더내에 있는 data.sql 파일을 자동으로 실행하지 않도록 하기 위함이다.
MemberRepositoryTest.java 파일에 코드를 다음과 같이 작성한다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Sql("/insert-members.sql")
@Test
void getAllMembers(){
List<Member> members = memberRepository.findAll();
assertThat(members.size()).isEqualTo(3);
}
}
1) 테스트 설정: @DataJpaTest 애노테이션을 통해 JPA 관련 설정이 로드된다.
2) 의존성 주입: @Autowired를 통해 MemberRepository 빈이 주입된다.
3) SQL 스크립트 실행: @Sql("/insert-members.sql") 애노테이션을 통해 테스트 전에 insert-members.sql 스크립트가 실행된다. 이 스크립트는 테스트 데이터베이스에 멤버 데이터를 삽입한다.
4) 테스트 메서드 실행: getAllMembers 메서드가 실행되며, memberRepository.findAll()을 호출하여 데이터베이스에서 모든 멤버를 조회한다.
5) 단언: 조회된 멤버의 개수가 3개인지 assertThat 메서드를 사용하여 검증한다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Sql("/insert-members.sql")
@Test
void getMemberById(){
Member member = memberRepository.findById(2L).get();
assertThat(member.getName()).isEqualTo("B");
}
}
id로 멤버를 찾을 수 있다.
id는 모든 테이블에서 기본키로 사용하므로 값이 없을 수가 없다.
그러나 name은 값이 있을수도 있고 없을수도 있다.
하지만 JPA는 메소드 이름으로 쿼리를 작성하는 기능을 제공한다.
따라서 MemberRepsitory 인터페이스에 findByName()이라는 메소드를 추가한다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByName(String name);
}
Spring Data JPA는 리포지토리 인터페이스에서 메서드 이름을 분석하여, 해당 메서드 이름에 따라 적절한 쿼리를 자동으로 생성한다. 이 기능을 통해 복잡한 쿼리를 직접 작성하지 않고도 데이터를 조회할 수 있다.
- 쿼리 메소드 프로세스
1) 메소드 이름 파싱(Name)
2) 파싱결과로 JPQL 쿼리 생성
3) JPA 엔티티 매니저에 의해 쿼리 실행
이제 테스트코드를 작성해본다.
@DataJpaTest class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @Sql("/insert-members.sql") @Test void getMemberByName(){ Member member = memberRepository.findByName("C").get(); assertThat(member.getId()).isEqualTo(3); } }
@Query 메소드를 쓴다.
@Repository public interface MemberRepository extends JpaRepository<Member, Long> { @Query("select m from Member m where m.name = ?1") Optional<Member> findByNameQuery(String name); }
@Query: 이 애노테이션은 리포지토리 메서드에 JPQL 쿼리를 직접 정의한다.
JPA는 save()를 통해 데이터를 추가할 수 있다.
@Test void saveMember(){ Member member = new Member(1L, "A"); memberRepository.save(member); assertThat(memberRepository.findById(1L).get().getName()).isEqualTo("A"); }
saveAll() 메소드를 통해 여러 엔티티를 한꺼번에 저장할 수 있다.
@Test void saveMembers(){ List<Member> members = List.of(new Member(2L, "B"), new Member(3L, "C")); memberRepository.saveAll(members); assertThat(memberRepository.findAll().size()).isEqualTo(2); }6> deleteById() 메소드 사용
deleteById()를 사용하면 Id로 레코드를 삭제할 수 있다.
@Sql("/insert-members.sql") @Test void deleteMemberById(){ memberRepository.deleteById(2L); assertThat(memberRepository.findById(2L).isEmpty()).isTrue(); }
deleteAll()을 사용하면 모든 데이터를 삭제할 수 있다.
따라서 실제 서비스 코드에서는 거의 사용하지 않는다.
이런 원리를 사용하는 곳이 하나 있다.
바로 테스트간의 격리를 보장하기 위한곳에 쓰인다.@Sql("/insert-members.sql") @Test void deleteAll(){ memberRepository.deleteAll(); assertThat(memberRepository.findAll().size()).isZero(); }따라서 @AfterEach 어노테이션을 붙여 cleanUp()메소드와 같은 형태로 사용한다.
@DataJpaTest class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @AfterEach public void cleanUp(){ memberRepository.deleteAll(); } .... }
JPA에서 데이터를 수정하려면 Member.java(엔티티)에 수정하는 메소드를 자체적으로 추가해야한다.
다음은 name의 필드값을 바꾸는 단순한 메소드이다.public class Member{ ... public void changeName(String name){ this.name = name; } }만약 위의 changeName메소드가 @Transactional 어노테이션이 포함된 메소드에서 호출되면 JPA는 Dirty Checking(변경감지) 기능을 통해 엔티티의 필드값이 변경됨에 따라 그 변경사항을 DB에 자동으로 반영한다.
이제 테스트코드를 작성해보자.
@DataJpaTest class MemberRepositoryTest { ... @Sql("/insert-members.sql") @Test void update(){ Member member = memberRepository.findById(2L).get(); member.changeName("BC"); assertThat(memberRepository.findById(2L).get().getName()).isEqualTo("BC"); } ... }위 코드에서 @Transactional 어노테이션은 존재하지 않는다.
어떻게 업데이트가 반영이 된것일까? 그것은 @DataJpaTest 어노테이션에 @Transactional이 포함되어 있기 때문이다.
따라서 서비스 메소드에서 이 기능을 사용하려면 반드시 @Transactional을 붙여야 한다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "name", nullable = false)
private String name;
public void changeName(String name){
this.name = name;
}
}
1) @Entiity 어노테이션은 Member 객체를 JPA가 관리하는 엔티티로 지정한다.
= Member 클래스와 실제 DB의 테이블을 매핑시킨다.
여기서는 테이블 이름을 지정하지 않았으므로 클래스 이름과 같은 테이블인 member테이블과 매칭된다.
만약 특정 테이블을 지정하고 싶다면 다음과 같이 작성한다.@Entity(name="member_list") public class Article{ ... }2) @NoArgsConstructor는 엔티티의 기본 생성자다.(파라미터가 없음)
접근 제어자로 protected를 선택하였다(public도 가능)
@AllArgsConstructor는 파라미터가 있는 생성자이다.
3) @Id 는 long타입의 id필드를 PK로 지정하겠다는 뜻이다.
4) @GeneratedValue는 기본키의 생성 방식을 결정한다.
여기서는 strategy = GenerationType.IDENTITY를 통해 자동으로 기본키가 증가하도록 설계하였다.
5) @Column 어노테이션은 데이터베이스의 Column과 필드를 매핑해준다.
- model: 모델은 애플리케이션의 데이터를 나타내며, 일반적으로 데이터 전송 객체(Data Transfer Object, DTO), 폼 객체(Form Object), 뷰 모델(View Model) 등을 포함한다. 모델 객체는 주로 데이터의 표현 및 전송에 중점을 둡니다.
- domain: 도메인은 비즈니스 로직과 관련된 개념이다. 도메인 객체 또는 도메인 모델은 애플리케이션이 해결하고자 하는 문제 영역의 핵심 개념과 규칙을 나타낸다. 이는 주로 비즈니스 규칙, 정책, 엔터티(Entity) 및 밸류 오브젝트(Value Object)를 포함한다.
- DTO (Data Transfer Object): 계층 간 데이터 전송을 위한 객체로, 주로 컨트롤러와 서비스 간의 데이터 전송에 사용됩니다.
- DAO (Data Access Object): 데이터베이스에 대한 CRUD 작업을 캡슐화하는 객체로, 데이터 접근을 처리합니다.(ex> 스프링 데이터 JPA를 사용하면 리포지토리가 DAO의 역할을 대신함)
1> 컨트롤러에서 DTO를 서비스로 전달한다.
2> 서비스에서는 DTO를 엔티티로 변환하여 리포지토리로 전달한다.
3> 리포지토리는 데이터베이스와 상호작용하여 엔티티를 저장하거나 조회한다.
4> 서비스에서 필요한 경우, 엔티티를 다시 DTO로 변환하여 컨트롤러로 반환한다.

1) 레포지토리는 엔티티 매핑정보(DB를 자바객체로 표현)를 바탕으로 DB에 쿼리를 보낸다.
= JPA는 리포지토리 메서드를 호출하면 적절한 JPQL (Java Persistence Query Language) 또는 SQL 쿼리를 생성하여 데이터베이스에서 데이터를 조회한다.
2) 데이터베이스는 요청된 데이터를 조회하여 응답한다. 이 데이터는 엔티티 객체로 변환되어 레포지토리로 반환된다.
3) 레포지토리는 데이터베이스로부터 받은 엔티티 객체를 사용자에게 반환한다.
반환된 데이터는 일반적으로 서비스 계층이나 컨트롤러를 통해 사용자에게 전달된다.