카테고리 없음

spring-boot(5)

rjqnrdl83 2025. 2. 11. 18:09

5. 데이터베이스 조작이 편해지는 ORM

 

5.2 ORM이란?

ORM(Object Realtional Mapping)자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법입니다.

 

ORM 장점과 단점

1: SQL을 직접 작성하지 않고 사용하는 언어로 데이터베이스에 접근할 수 있습니다.

2: 객체지향적으로 코드를 작성할 수 있기 때문에 비즈니스 로직에만 집중할 수 있습니다.

3: 데이터베이스 시스템이 추상화되어 있기 때문에 MySQL에서 PostgerSQL로 전환한다고 해도 추가로 드는 작업이 거의 없습니다.

4: 매핑하는 정보가 명확하기 때문에 ERD에 대한 의존도를 낮출 수 있습니다.

 

1. 프로젝트의 복잡성이 커질수록 사용 난이도도 올라갑니다.

2. 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능한 경우가 있습니다.

 

5.3 JPA와 하이버네이트

ORM에도 여러 종류가 있습니다. 자바에서는 JPA(Java Persistence API)를 표준으로 사용합니다. JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다. 인터페이스이므로 실제 사용을 위해서는 ORM 프레임워크를 선택해야 합니다. 대표적으로 하이버네이트가 있습니다. 하이버네이트JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크입니다. 하이버네이트의 목표는 자바 객체를 통해 데이터베이스 종류에 상관없이 데이터베이스를 자유자재로 사용할 수 있게 하는 데 있습니다.

 

JPA와 하이버네이트의 역할

JPA: 자바 객체와 데이터베이스를 연결해 데이터를 관리합니다. 객체 지향 도메인 모델과 데이터베이스의 다리 역할을 합니다.

하이버네이트: JPA의 인터페이스를 구현합니다. 내부적으로는 JDBC API를 사용합니다.

 

영속성 컨텍스트란?

영속성 컨텍스트는 JPA의 중요한 특징 중 하나로 엔티티를 관리하는 가상의 공간입니다. 영속성 컨텍스트는 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩이라는 특징이 있습니다.

 

1차 캐시

영속성 컨텍스트는 내부에 1차 캐시를 가지고 있습니다. 캐시의 키는 엔티티의 @Id 어노테이션이 달린 기본 키 역할을 하는 식별자이며 값은 엔티티입니다. 엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환합니다. 값이 없으면 데이터베이스에서 조회해 1차 캐시에 저장한 다음 반환합니다. 이를 통해 캐시된 데이터를 조회할 때에는 데이터베이스를 거치지 않아도 되므로 빠르게 데이터를 조회할 수 있습니다.

 

쓰기 지연

트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미합니다. 적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스의 부담을 줄일 수 있습니다.

 

변경 감지

트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경 사항을 감지해 변경된 값을 데이터베이스에 자동으로 반영합니다. 이를 통해 적당한 묶음으로 쿼리를 요청할 수 있고 데이터베이스 시스템의 부담을 줄일 수 있습니다.

 

지연 로딩

쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미합니다.

 

이 특징들이 갖는 공통점은 데이터베이스의 접근을 최소화해 성능을 높일 수 있다는 겁니다.

 

엔티티의 상태

엔티티는 4가지 상태를 가집니다.

분리 상태: 영속성 컨텍스트가 관리하고 있지 않은 상태

관리 상태: 영속성 컨텍스트가 관리하는 상태

비영속 상태: 영속성 컨텍스트와 전혀 관계가 없는 상태

삭제된 상태: 영속성 컨텍스트에서 삭제한 상태

 

5.4 스프링 데이터와 스프링 데이터 JPA

스프링 데이터 JPA는 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술입니다. 스프링 데이터 JPA를 사용하면 리포지터리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있습니다.

아래와 같이 JpaRepository 인터페이스를 만든 인터페이스에서 상속받고 제네릭에는 관리할 <엔티티 이름, 엔테테 기본키의 타입>을 입력하면 기본 CRUD 메서드를 사용할 수 있습니다.

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

학습테스트: 기능 구현을 위한 테스트라기보다는 사용하는 라이브러리, 프레임워크에서 지원하는 기능을 검증하며 어떻게 동작하는지 파악하는 테스트.

 

조회 메서드

member 테이블에 있는 모든 데이터를 가져오려면 'SELECT * FROM member' 라는 쿼리문을 작성해야 했습니다. JPA에서 데이터를 가져올 때는 findAll()메서드를 사용합니다.

@DataJpaTest
class MemberRepositoryTest {
    @Autowired
    MemberRepository memberRepository;

    @Sql("/insert-members.sql")
    @Test
    void getAllMembers() {
        //when
        List<Member> members = memberRepository.findAll();
        //then
        assertThat(members.size()).isEqualTo(3);
    }
}

@Sql이라는 어노테이션은 테스트를 실행하기전에 SQL스크립트를 실행시킬 수 있습니다.

 

id가 2인 멤버를 찾고 싶으면 'SELECT * FROM member WHERE id=2;' 라는 쿼리문을 작성하면 됩니다. JPA로는 아래와 같이 작성하면 됩니다.

@Sql("/insert-members.sql")
@Test
void getMemberById() {
    //when
    Member member = memberRepository.findById(2L).get();
    //then
    assertThat(member.getName()).isEqualTo("B");
}

 

만약 id가 아닌 name으로 찾고 싶을 때는 어떻게 할까요? id는 모든 테이블에서 기본키로 사용하므로 값이 없지 않지만 name은 값이 있거나 없을 수 있어 JPA에서는 name을 찾아주는 메서드를 지원하지는 않습니다.

name의 값이 'C'인 멤버를 찾아야 하는 경우 'SELECT * FROM member WHERE name = 'C'; 라는 쿼리문을 작성해야 합니다. 이런 쿼리를 동적 메서드로 만들어보겠습니다. MemberRepository파일에 findByName() 메서드를 추가합니다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByName(String name);
}

이 메서드는 상속받아 구현할 필요도 없이 메서드로 정의한 후 가져다 쓰면 됩니다.

 

@Sql("/insert-members.sql")
@Test
void getMemberByName() {
    //when
    Member member = memberRepository.findByName("C").get();
    //then
    assertThat(member.getId()).isEqualTo(3);
}

테스트를 실행하면 성공적으로 동작하는 것을 확인할 수 있습니다. 이런 기능을 쿼리 메서드라고 합니다. JPA가 정해준 메서드 이름 규칙을 따르면 쿼리문을 특별히 구현하지 않아도 메서드처럼 사용할 수 있습니다.

 

@Query 메서드로 SQL 쿼리문 실행해보기

JPA를 이용하여 표현하기 너무 복잡한 쿼리이거나 성능이 너무 중요해서 SQL 쿼리문을 직접 사용해야 하는 경우도 있습니다. 그럴 때는 @Query 메서드를 쓰기도 합니다. @Query는 다음과 같이 사용합니다.

@Query("select m from Member m where m.name=?1")
Optional<Member> findByNameQuery(String name);

 

추가 메서드 사용해보기

새로운 1번 멤버 'A'를 추가하려면 'INSERT INTO member (id, name) VALUES (1, 'A');' 라는 쿼리문을 입력하면 됩니다.

JPA에서는 save()라는 메서드를 사용합니다.

@Test
void saveMember() {
    //given
    Member member = new Member(1L, "A");
    //when
    memberRepository.save(member);
    //then
    assertThat(memberRepository.findById(1L).get().getName()).isEqualTo("A");
}

given 절에 새로운 A 멤버 객체를 준비하고 when 절에 실제로 저장한 뒤에 then 절에서는 1번 아이디에 해당하는 멤버의 이름을 가져오고 있습니다. 조회와는 다르게 이미 추가된 데이터가 있으면 안되므로 @Sql 에너테이션을 사용하지 않았습니다.

여러 엔티티를 한꺼번에 저장하고 싶다면 saveAll() 메서드를 사용하면 됩니다. 추가할 멤버 객체들을 리스트로 만들고 saveAll() 메서드로 한꺼번에 추가한 후 추가한 멤버 객체 수만큼 데이터에 있는지 확인하는 테스트입니다.

@Test
void saveMembers() {
    //given
    List<Member> members = List.of(new Member(2L, "B"), new Member(3L, "C"));
    //when
    memberRepository.saveAll(members);
    //then
    assertThat(memberRepository.findAll().size()).isEqualTo(3);
}

 

삭제 메서드 사용해보기

member 테이블에 있는 id가 2인 멤버를 삭제할 때는 'DELETE FROM member WHERE id=2;'라는 쿼리문을 작성하면 됩니다. JPA에서는 deleteById()를 사용하면 아이디로 레코드를 삭제할 수 있습니다.

@Sql("/insert-members.sql")
@Test
void deleteMemberById() {
    //when
    memberRepository.deleteById(2L);
    //then
    assertThat(memberRepository.findById(2L).isEmpty()).isTrue();
}

2번 멤버를 삭제한 뒤 2번 아이디를 가진 레코드가 있는지 조회했습니다.

 

만약 모든 데이터를 삭제하고 싶다면 deleteAll() 메서드를 사용할 수 있습니다.

@Sql("/insert-members.sql")
@Test
void deleteAll() {
    //when
    memberRepository.deleteAll();
    //then
    assertThat(memberRepository.findAll().size()).isZero();
}

3명의 멤버를 추가하고 deleteAll() 메서드를 사용해 모든 멤버를 삭제한 뒤 크기가 0인지 검증했습니다.

 

수정 메서드 사용해보기

id가 2인 멤버의 이름을 'BC'로 바꾸려면 'UPDATE member SET name= 'BC' WHERE id =2;라는 쿼리문을 작성해야 합니다. JPA에서 데이터를 수정할 때는 조금 다른 방법을 사용합니다. 트랜잭션 내에서 데이터를 수정해야 하기 때문에 메서드만 사용하면 안되고 @Transactional 어노테이션을 메서드에 추가해야 합니다.

public void changeName(String Name) {
    this.name = Name;
}

이 메서드가 @Transactional 어노테이션이 포함된 메서드에서 호출되면 JPA는 변경 감지 기능을 통해 엔티티의 필드값이 변경될 때 그 변경사항을 데이터베이스에 자동으로 반영합니다. 만약 엔티티가 영속 상태일 때 필드값을 변경하고 트랜잭션이 커밋되면 JPA는 변경사항을 데이터베이스에 자동으로 적용합니다.

@Sql("/insert-members.sql")
@Test
void update() {
    //given
    Member member = memberRepository.findById(2L).get();
    //when
    member.changeName("BC");
    //then
    assertThat(memberRepository.findById(2L).get().getName()).isEqualTo("BC");
}

@Transactional 어노테이션이 없는데도 잘 실행된 이유는 @DataJpaTest 어노테이션을 사용했기 때문입니다. 테스트를 위한 설정을 제공하며 자동으로 데이터베이스에 대한 트랜잭션 관리를 설정합니다.

 

어노테이션들의 역할을 알아보겠습니다.

@NoArgsConstructor(access = AccessLevel.PROTECTED) //(1)
@AllArgsConstructor
@Getter
@Entity //(2)
public class Member {
    @Id  //(3)
    @GeneratedValue(strategy= GenerationType.IDENTITY)  //(4)
    @Column(name="id", updatable=false)
    private Long id;

    @Column (name="name", nullable=false)  //(5)
    private String name;

    public void changeName(String Name) {
        this.name = Name;
    }
}

(1)  protected 기본 생성자입니다.

(2) Entity 어노테이션은 Member 객체를 JPA가 관리하는 엔티티로 지정합니다. 즉 Member 클래스와 실제 데이터베이스의 테이블을 매핑시킵니다. Entity의 속성중에 name을 사용하면 name의 값을 가진 테이블 이름과 매핑되고 테이블 이름을 지정하지 않으면 클래스 이름과 같은 이름의 테이블과 매핑됩니다.

(3) @Id는 Long 타입의 id 필드를 테이블의 기본키로 지정합니다.

(4) @GeneratedValue 기본키의 생성 방식을 결정합니다. 

  • AUTO: 선택한 데이터베이스 방언에 따라 방식을 자동으로 선택
  • IDENTITY: 기본키 생성을 데이터베이스에 위임(=AUTO_INCREMENT)
  • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본키를 할당하는 방법
  • TABLE: 키 생성 테이블 사용

(5) @Column 어노테이션은 데이터베이스의 칼럼과 필드를 매핑해줍니다.

  • name: 필드와 매핑할 칼럼 이름. 설정하지 않으면 필드 이름으로 지정.
  • nullable: 컬럼의 null 허용 여부. 설정하지 않으면 true
  • unique: 컬럼의 유일한 값 여부. 설정하지 않으면 false
  • columnDefinition: 컬럼 정보 설정. default 값을 줄수 있습니다.

요약

1. ORM 객체와 데이터베이스를 연결하는 프로그래밍 기법입니다.

2. JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다.

3. 하이버네이트는 JPA의 대표적인 구현체로 자바 언어를 위한 ORM 프레임워크입니다.

4. 스프링 데이터JPAJPA를 쓰기 편하게 만들어 놓은 모듈입니다.