ORM (Object-Relational Mapping)

ORM은 Object와 데이터베이스 Relational을 매핑할 때 발생하는 개념적인 불일치를 해결하기 위한 솔루션을 제공하는 프레임워크이다.

객체지향 데이터베이스
프로퍼티 여러 프로퍼티를 가질 수 있음 컬럼만 가질 수 있음
크기 다양 한정적
계층구조 상속, 구현을 이용한 계층구조 표현할 수 없음
식별자 hashCode 혹은 equals 메소드 기본키, 외래키 등

이처럼 ORM은 복잡하고 다양한 객체를 테이블에 어떻게 매핑할 것인가, 상속구조를 테이블로 어떻게 매핑할 것인가, 식별자를 어떻게 매핑할 것인가 등의 문제를 해결하기 위한 솔루션을 제공한다.

JPA(Java Persistance Api)

JPA는 여러가지 ORM 솔루션 중 자바의 표준 스펙을 의미한다. 대부분의 자바 표준 스펙은 하이버네이트 기반으로 만들어져 있다. 하이버네이트를 개발한 사람이 JPA 표준 스펙을 만들 때 동참했기 때문이다. 그러나, 하이버네이트의 모든 기능을 JPA가 다 커버하지는 못하기 때문에 하이버네이트의 설정과 관련된 부분도 알아야 한다.

스프링 데이터 JPA

JPA를 쉽게 사용할 수 있도록 스프링 데이터를 이용해 추상화시켜놓은 라이브러리이다. 구현체는 하이버네이트를 사용하며 JPA의 EntityManager로 감싸서 사용한다.

스프링 데이터 JPA에는 굉장히 많은 추상화가 이루어져있는데, 주로 스프링 데이터 JPA가 제공하는 인터페이스와 애노테이션들을 사용해서 JPA와 하이버네이트를 사용해서 개발을 진행한다. 하이버네이트의 이면에는 당연히 JDBC가 존재한다.

SDJ(스프링 데이터 JPA) -> JPA -> 하이버네이트 -> Datasource

실습

먼저 spring-boot-starter-data-jpa 의존성을 추가한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Account 클래스와 Repository를 생성한다.

@Entity
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return Objects.equals(id, account.id) &&
                Objects.equals(username, account.username) &&
                Objects.equals(password, account.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, password);
    }
}

public interface AccountRepository extends JpaRepository<Account, Long> {
    Account findByUsername(String username);
}

Account는 아이디, 사용자명, 패스워드를 가진 클래스이며 AccountRepository는 Account 클래스에 대한 Repository로, Id의 타입은 Long을 지정한다. AccountRepository를 테스트하기 위한 코드를 작성하자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountRepositoryTest {
	@Autowired
    DataSource dataSource;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Autowired
    AccountRepository repository;
}
  • @SpringBootTest
    • 통합(integration) 테스트
    • 애플리케이션의 모든 bean을 등록
  • @DataJpaTest
    • 슬라이싱 테스트
    • Reposistory와 관련된 bean만 등록
    • DataSource, JdbcTemplate, Repository 등 JPA와 관련된 객체를 주입받을 수 있다.

데이터베이스에 대한 설정이 없기 떄문에 현재는 테스트 코드가 제대로 실행되지 않는다. 테스트에서는 주로 인메모리 데이터베이스를 사용하므로 h2를 추가해보자. 테스트를 돌릴 때는 임베디드 DB가 훨씬 빠르다.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

애플리케이션도 마찬가지로 데이터베이스 정보가 필요한데, 여기서는 PostgreSQL을 사용한다.

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
spring.datasource.url=jdbc:postgresql://192.168.99.100:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=pass

모든 준비가 끝났다. 연결에 대한 테스트 코드를 작성해보자.

try(Connection connection = dataSource.getConnection()) {
    DatabaseMetaData metaData = connection.getMetaData();
    System.out.println(metaData.getURL());
    System.out.println(metaData.getDriverName());
    System.out.println(metaData.getUserName());
}
// url; jdbc:h2:mem:eff41588-44c9-4785-9dc0-0c09de0f7e81
// driverName: H2 JDBC Driver
// userName: SA

테스트를 실행하면 주석과 같은 내용이 출력될 것이다. 이제 AccountRepository에 대한 테스트를 작성해보자.

@Test
public void dt() throws SQLException {
    Account account = new Account();
    account.setUsername("jch");
    account.setPassword("pass");

    Account newAccount = repository.save(account);

    assertThat(newAccount).isNotNull();

    Account existingAccount = repository.findByUsername(newAccount.getUsername());
    assertThat(existingAccount).isNotNull();

    Account nonExistingAccount = repository.findByUsername("asdposda");
    assertThat(nonExistingAccount).isNull();
}

Account 객체를 생성하고 저장한 뒤 해당 객체를 조회하여 notNull 테스트를 수행하고, 존재하지 않는 객체를 조회하여 null 테스트를 수행한다.

AccountRepository에 findByUsername라는 메소드를 추가하기만 하면, 구현체를 만들어서 빈으로 등록하는 것까지 스프링 데이터 JPA가 해준다. 때문에 스프링 데이터 JDBC를 직접 사용하는 일은 드물다. 보통은 JPA API를 통해서 SQL을 사용한다. SQL을 직접 작성할 때도 JPA의 @Query를 사용할 수 있다.

JPA를 사용하면 리턴타입에 Optional을 사용할 수 있다.

public interface AccountRepository extends JpaRepository<Account, Long> {

    Optional<Account> findByUsername(String username);
}

@Test
public void dt() throws SQLException {
    Account account = new Account();
    account.setUsername("jch");
    account.setPassword("pass");

    Account newAccount = repository.save(account);

    assertThat(newAccount).isNotNull();

    Optional<Account> existingAccount = repository.findByUsername(newAccount.getUsername());
    assertThat(existingAccount).isNotEmpty();

    Optional<Account> nonExistingAccount = repository.findByUsername("asdposda");
    assertThat(nonExistingAccount).isEmpty();
}

Optional에 대한 테스트는 isEmpty와 isNotEmpty를 사용해야한다.

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.