스프링 DB 접근 기술
데이터베이스에 데이터를 관리하려면 아주 쉽고 간단한 H2데이터 베이스를 설치할 것이고 jdbc를 통해서 어플리케이션과 데이터 베이스를 연결할 것이다. 그 다음에는 순수한 jdbc로 개발하는 방법이 있고, Jdbc template으로 sql을 편리하게 날리는 방법이 있으며, 또 한가지 sql을 jpa라는 기술을 통해서 조회 쿼리를 자동으로 만들어 날리는 방법이 있다. 이렇게 하면 객체 자체를 db에 저장할 수 있다.
jpa도 스프링만큼 오래된 기술인데 jpa를 아주 편리하게 사용할 수 있도록 한번 더 감싼 기술이 스프링 데이터 Jpa이다. 지금까지 이제 해왔던 회원이라는 객체를 이 기술들을 다 적용해보면서 바꿔 볼것이다. 메모리, 리포지토리가 jdbc, jpa 리포지토리로 바뀌는 것을 볼 수 있다.
1. H2 데이터베이스 설치
H2 데이터베이스는 교육용으로 되게 좋고 용량이 적고 가볍다. 웹으로 admin 용 화면도 제공해준다. 이를 설치하려면 일단 h2 데이터베이스 다운로드 페이지에 들어가서 압축 파일을 다운받은 후, 압축을 풀어서 bin 폴더 밑에 존재하는 h2.sh를 실행시키면 어떤 브라우저 화면이 뜨게 될 것이다. 상황에 따라서 ip주소로 접속하면 안될 때가 있는데 그런 경우에는 ip주소 대신 localhost를 적어주고 접속하면 될 것이다.
최초에는 이 데이터베이스 파일을 만들어야 한다. 지금 있는 jdbc url이 홈에 있는 test라는 파일의 경로를 말해준다. 이것을 그대로 바꾸지 않고 연결 버튼을 누르면 홈에서 test.mv.db 파일이 만들어진다.
이 이후부터는 파일로 접속을 하면 동시에 애플리케이션이랑 웹 콘솔이랑 같이 접근이 안될수도 있으므로, 다음과 같이 경로를 지정해서 접속하는 것이 좋다.
jdbc:h2:tcp://localhost/~/test
이렇게 접속하게 되면 파일이 아니라 소켓을 통해서 접속하게 된다. 이렇게 해야 여러 군데에서 접근할 수 있다.
이렇게 데이터베이스가 만들어지면 그 다음에는 테이블을 생성해보자. 이 테이블에는 다음과 같은 컬럼들이 존재한다.
- id 자바에서는 long 타입으로 지정되었으나 데이터베이스에서는 bigint 타입으로 지정된다. id에 값을 세팅하지 않으면 db가 자동으로 id값을 생성하여 채워준다. 이 컬럼은 주 키로 지정된다.
- name varchar 타입의 회원 이름에 해당하는 컬럼이다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
위의 sql문을 작성해서 실행하면, 다음과 같이 왼쪽 바에서. MEMBER 라는 테이블이 생성된다.
select * from member;
이렇게 작성하면 테이블에 있는 모든 레코드를 조회할 수가 있다. 그런데 귀찮으면 콘솔을 다 비우고 왼쪽에 있는 MEMBER를 누르면 위의 sql문이 자동으로 작성된다. 무엇이 작성된 상태에서 누르면 테이블명이 자동으로 작성된다.
이제 insert문을 통해 레코드 하나를 추가해보자.
insert into member (name) values('spring')
다음과 같이 실행하고 전체 조회를 하면 다음과 같이 레코드 하나가 결과로 나타난다.
id를 넣지 않으면 이처럼 계속 해서 1부터 시작하여 하나씩 자동 증가되는 값이 채워진다.
테이블을 생성하는 sql문들은 이왕이면 프로젝트 바로 밑에 sql이라는 파일을 만들어서 .sql 파일 형태로 관리하는 것이 좋다.
2. 순수 jdbc
애플리케이션에서 db에 연동해서 메모리에 저장되는 것이 아니라, 데이터베이스에 쿼리를 날려 db에 데이터를 넣고 빼는 것을 가장 원시적인 방식으로 해볼 것이다.
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가하기
자바는 기본적으로 db랑 연결하려면 jdbc 드라이버가 꼭 있어야한다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
다음 라이브러리는 데이터베이스가 제공하는 클라이언트가 필요한데, 이 클라이언트가 다음 라이브러리이다.
runtimeOnly 'com.h2database:h2'
스프링 부트 데이터 베이스 연결 설정을 추가하기
db에 불으려면 db에 대한 접속 정보를 모두 설정해줘야 했다. 옛날에는 개발자가 직접 설정해줬으나, 요즘에는 스프링 부트가 다해주므로 appication.properties에서 경로 정보만 간단히 넣어주면된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
그리고 다음은 h2db를 접근하기 때문에 이에 대한 정보를 추가해줘야한다.
spring.datasource.driver-class-name=org.h2.Driver
이때 오류가 발생하는 이유는 아직 build.gradle에서 추가한 라이브러리들이 임포트가 안됐기 때문이다. 따라서 build.gradle에 가서 코끼리 버튼을 누르거나, gradle 뷰에서 리프레시 버튼을 누르면 된다.
spring.datasource.username=sa
이렇게 하면 데이터 베이스에 접근하기 위한 준비는 모두 끝났다. 이제 이렇게 하면 스프링이 필요한 데이터 소스라는 것, 디비와 연결하는 작업을 모두 해주므로 이것만 가져다 사용하면 된다. jdbc api를 갖고 개발할 것이다.
Jdbc 리포지토리 구현
jdbc를 사용하여 데이터베이스에 접그하는 리포지토리 구현체 JdbcMemberRepository를 또 하나 만들면 된다. 인터페이스까지는 어떤 객체를 다룰 것이냐를 지정하고 있지만, 구현체는 그 객체를 메모리에서 다룰 것이냐, 혹은 데이터베이스에서 다룰 것이냐에 따라 각각 다른 객체로 구현하는 것이다.
일단 클래스를 생성하여, MemberRepository를 구현하기 위해 요구되는 메서드들을 모두 구현하면 된다.
public class JdbcMemberRepository implements MemberRepository {
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
일단 db에 붙으려면 javax.sql. DataSource 객체가 필요하다. 그리고 이 객체를 스프링에게 주입받아야 한다. 스프링 부트가 접속 정보를 위한 데이터 소스를 만들어놓기 때문에 이것을 주입받을 수가 있다.
일단 생성자를 통해 데이터 소스 객체를 스프링에게서 받아 필드에 저장하고, 이 객체에게서 나중에 getConnection()을 호출하여 데이터베이스와 연결된 소켓을 얻을 수가 있다. 여기에 sql문을 날려주면 된다.
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
또한 데이터소스 객체를 통해서 커넥션 객체를 얻는 getConnection(), 데이터베이스와 연결하는데 사용된 모든 클로저블 객체들을 close하는 close(Connection conn, PreparedStatement pstmt, ResultSet rs), close(Connection conn) 메서드를 정의한다.
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
{
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
이제 차례차례로 메서드를 구현하여 각 메서드 목적에 맞는 sql문을 데이터베이스에 적절하게 전달해보자. 가장 먼저 save 메서드이다.
save 메서드
-
sql문을 일단 문자열 변수에 저장한다. 이왕이면 상수 변수로 밖에 빼주는 것이 더 낫다.
- ResultSet은 결과를 받는 객체이다.
- 일단 getConnection을 통해 Connection 객체를 받아온다.
- Connection에 대하여 prepareStatement를 호출하여 아까 준비해둔 sql 문자열을 넣어주고, RETURN_GENERATED_KEYS 옵션을 통해 자동생성되는 아이디 값을 얻을 수 있게 한다.
- setString을 통해 파라미터 index를 넣어주면 물음표 위치가 지정되고, 그 뒤에 파라미터로 넣어준 값을 물음표 자리에 넣어줄 수 있다.
- executeUpdate를 하면 실제로 쿼리가 날라간다.
- preparedStatement에 대하여 getGeneratedKeys를 호출하면 자동생성된 키를 반환받을 수 있다.
- 이 ResultSet에서 값을 가지고 있는 경우에 Member 객체를 그 값으로 초기화한다.
- 발생할 수 있는 예외를 try/catch문을 통해 잡아준다.
- 데이터베이스는 외부 네트워크와의 연결이기 때문에 이 관련 리소스를 꼭 반환해줘야 한다. 그렇지 않으면 데이터베이스 커넥션이 계속 쌓인다.
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
findById(Long id) 메서드
- sql문을 변수에 저장한다.
- 위 메서드와 마찬가지로 Connection 객체를 받고, 이에 대하여 sql문과, id 값을 세팅한다.
- 이것은 조회이므로 executeUpdate가 아닌 executeQuery를 통해 결과 값을 받아온다.
- 결과 객체에 값이 있으면 반환한다.
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
findAll() 메서드
findAll은 기본적으로 여러명의 회원 정보를 반환해야하므로 rs.next()가 true를 호출할 때까지 반복문을 돌려 ResultSet 객체에 담긴 Member들을 하나씩 List에 담아준다.
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
findByName도 위의 메서드들과 비슷한 과정을 거친다. 만약 결과가 없으면 Optional.empty()를 호출하여 넘기면 된다.
return Optional.empty();
한가지 중요한 점은 이렇게 한 다음에 dataSource에서 직접 getConnection을 호출하여 Connection을 얻지 않고 메서드를 만든 이유는 직접 하게 될 경우 매번 다른 Connection 객체가 만들어지기 때문이다. 그 대신 DataSourceUtils에 대하여 getConnection을 호출하면 똑같은 객체를 유지시켜준다. 스프링 프레임 워크를 사용할 때에는 이렇게 해줘야 하지만, 스프링 부트를 사용하면 이것을 굳이 코드로 작성할 필요가 없다.
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
닫을 때에도 DataSourceUtils에 대하여 releaseConnection을 호출해줘야한다.
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
configuration을 해줘야 한다. 지금까지했던것은 모두 메모리에서 데이터를 처리하는 MemoryMemberRepository를 사용하던 것이다. 이 객체가 아니라 JdbcMemberRepository를 사용해야 한다.
SpringConfig에서 memberRepository() 메서드에서 MemoryMemberRepository 객체를 생성하던 것을 JdbcMemberRepository로 바꿔준다.
그런데 이때 JdbcMemberRepository의 생성자에서 DataSource 객체가 필요하므로 스프링에게서 객체를 주입받아야 한다. 주입받는 방법은 다음과 같다.
-
DataSource 타입의 필드를 SpringConfig에서 선언하고@Autowired를 앞에 붙인다.
@Autowired DataSource dataSource;
-
SpringConfig 생성자에서 DataSource 파라미터를 추가하고, 생성자 앞에 @Autowired를 붙여 생성자를 통해 주입받는다.
private DataSource dataSource; @Autowired public SpringConfig(DataSource dataSource) { this.dataSource = datasource; }
우리는 일단 후자의 방법으로 생성자를 통해 주입받자. 그렇게 해서 주입받은 DataSource 객체는 JdbcMemberRepository의 생성자 파라미터로 넘겨주면 된다.
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
이렇게 하면 어떤 코드도 변경하지 않고 JdbcMemberRepository를 만들고 인터페이스의 구현체를 만들어 확장한 상태에서 Configuration만 손댔는데 서버가 돌아가면서 데이터베이스에 정상 접근이 된다.
객체지향적인 설계가 좋은 이유는 다형성이 가능하기 때문이다. 스프링은 이것을 굉장히 편리하게 가능하도록 서비스를 제공하고 있다. 즉, 코드를 수정할 필요 없이 애플리케이션을 조립하는 이 코드만 손대면, 원하는 객체를 바꿔끼울 수 있는 것이다.
스프링 컨테이너에서 기존에는 메모리 버전으로 등록했다면 이제는 JDBC 버전의 멤버리포지토리 구현체로 바꿔끼우는 이 과정을, Solid 단일 체계 원칙 중 개방폐쇄 원칙(OCP, Open-Closed Principle)이 제일 중요하다. 이 말의 의미는 확장에는 열려있고 수정에는 닫혀있다는 것을 의미한다. 인터페이스 기반의 다형성, 객체지향의 다형성을 잘 활용하면, 기능을 완전히 변경해도 애플리케이션 전체를 수정하지 않을 수 있는 조건이 개방폐쇄 원칙을 지킨다고 말한다.
스프리의 DI(Dependecies Injection)을 사용하면 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있다.
2. 스프링 통합 테스트
이제 테스트도 단위 테스트가 아니라 스프링을 올리고 DB와 연결까지 해서 전체적인 동작을 확인하는 통합 테스트를 만들어보자.
이전의 테스트들은 잘 보면 스프링과 관련이 없으며 순수한 자바 코드로 테스트했다. 그런데 현재 상황에서는 데이터베이스 커넥션 정보가 스프링 부트가 관리하고 있기 때문에 스프링과 연결된 테스트가 필요하다.
테스트에 필요한 객체들을 가져와야하는데, 테스트는 가장 끝단에 있는 것이며, 실질적으로 애플리케이션에 필요한 것이 아니개 때문에 가장 편한 방법으로 가져오면 된다. 필드 앞에 @Autowired 애노테이션을 붙이면 SpringConfig을 통해 생성된 객체를 스프링이 넣어줄 것이다.
@SpringBootTest
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
원래 기존에 만들었던 단위테스트에는 매테스트가 끝날 때마다 데이터 저장소를 비우는 작업을 실행하도록 afterEach라는 메소드를 만들었지만 이 메소드는 이제 필요가 없다.
그 외에는 단위테스트에서 만들었던 회원가입 메서드, 중복_회원_예외 메서드들의 코드를 그대로 가져와도 된다.
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member(); member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member(); member1.setName("spring");
Member member2 = new Member(); member2.setName("spring");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2)); //예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
이대로 실행해보면 스프링이 모두 올라오면서 실제 DB를 갖고 테스트를 진행하게 된다. 문제는 테스트는 계속 반복이 가능해야 되는데, 원래 기존에 만들었던 단위테스트에는 매테스트가 끝날 때마다 데이터 저장소를 비우는 작업을 실행하도록 afterEach라는 메소드를 만들었지만 이 메소드는 이제 필요가 없다.
데이터베이스는 트랜잭션이라는 개념이 있기 때문에, 커밋을 하지 않으면 실제로 데이터 변경 사항이 실제 데이터베이스에 반영되지 않는다. 오토커밋이라는 모드에서는 자동으로 매번 변경사항이 생길 때마다 DB에 반영된다.
따라서 테스트 코드를 모두 실행 후 롤백을 하면 테스트가 실행되는 동안 발생한 변경사항이 모두 사라진다. 이 방식을 이용해 검증이 가능한데, @Transactional이라는 애노테이션을 테스트 클래스에 붙이면, 테스트를 실행할떄 트랜잭션을 먼저 실행하고, 테스트가 끝나면 롤백을 해준다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
이렇게 해놓으면 회원가입 이런것들을 모두 테스트할 수가 있다. 간단하게 정리만 해보면, @SpringBootTest를 붙이면 스프링 컨테이너와 테스트를 함께 실행한다. @Transactional 테스트 케이스에 애노테이션이 있으면 테스트 시작 전에 트랜잭션을 시작하고 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않아 다음 테스트에 영향을 주지 않는다. 참고로, 이 애노테이션은 서비스나 다른 곳에 붙으면 롤백하지 않고, 정상적으로 커밋이 되며, 오직 테스트 케이스에만 롤백이 된다. 또한 테스트 메서드 앞에 @Commit을 붙이면 롤백하지 않고 그 테스트에만 커밋이 된다.
단위테스트는 일단 굉장히 빠르게 실행이 가능하다. 순수한 단위 테스트가 훨씬 좋은 테스트일 확률이 높다. 컨테이너까지 올려야하는 상황이면 테스트 설계가 잘못됐을 확률이 높다. 진짜 좋은 테스트는 이런 단위 테스트를 잘 만드는게 훨씬 좋은 테스트이다.
3. 스프링 JDBCTemplate
스프링 JdbcTemplate은 MyBatis와 비슷한 라이브러리로, JdbcAPi에서의 반복적인 코드를 제거해준다. 쿼리를 날려주기까지의 복잡하고 반복되는 코드를 제거하는 것이다. Sql문은 직접 작성해줘야 한다.
MemberRepository를 구현하는 JdbcTemplateMemberRepository 클래스를 정의해보자. 일단 JdbcTemplate이라는 객체를 사용해야 한다. 이것은 Injection을 받을 수 있는 객체가 아니므로, 일단. DataSource를 주입받아서 JdbcTemplate 생성자의 파라미터로 넣어주어 객체를 생성한다. 생성자가 딱하나 있다면 Autowired 애노테이션 생략이 가능하다.
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
먼저 회원을 조회하는 쿼리를 먼저 해보자.
findById(Long id)
JdbcTemplate의 query 메서드를 통해 sql문을 작성하고, ?에 들어갈 값을 지정해준다. 추가적으로 조회 sql문을 통해 얻은 결과를 Member 객체의 각 프로퍼티로 매핑해주는 RowMapper<Member>를 파라미터로 넘겨줘야 한다.
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
이 RowMapper<Member>를 리턴할 memberRowMapper 메서드를 구현한다.
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member(); member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
이렇게 하면 순수한 Jdbc를 통해서 코드를 작성했을 때보다 코드가 훨씬 줄어들게 된다. JdbcTemplate은 기존의 반복적인 코드를 템플릿 디자인 패턴을 통해 줄이고 줄여 편리하게 사용할 수 있도록 만들어진 도구라고 할 수 있다.
save(Member member)
SimpleJdbcInsert라는 객체를 이용해서, withTableName()을 호출하면 쿼리를 직접 짜지 않아도 Insert문이 자동으로 만들어진다.
또한 usingGenerateColumns메서드를 호출하면 자바 코드에서 executeAndReturnKey를 통해 쿼리를 실행하는 동시에 데이터베이스에 대하여 자동생성되는 컬럼 값을 알아내어 사용할 수가 있다.
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
findAll()
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
findByName(String name)
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
이제 작업이 끝났다면 SpringConfig에서 JdbcTemplateRepository의 객체를 생성하여 컨테이너에서 관리할 수 있도록 해야 한다.
@Bean
public MemberRepository memberRepository() {
return new JdbcTemplateMemberRepository(dataSource);
}
모든 프로그래밍이 끝났다면 기존에 만들었던 통합테스트 케이스를 돌려보고, 에러가 없는지 확인해본다.
4. JPA
JdbcTemplate을 사용하면 쿼리를 만들고 날리는 과정에서 짜야하는 복잡하고 반복적인 코드를 생략할 수 있었으나, 여전히 Sql문을 짜야한다는 단점이 있다. 이떄 JPA를 사용하면 Sql 쿼리도 자동 처리가 가능하여 개발 생산성을 크게 높일 수가 있다. 단순히 Sql을 만들어주는 것을 넘어서서, Sql보다는 객체 중심의 설계로 패러다임을 전환할 수가 있다.
JPA는 스프링만큼 기술적인 넓이와 깊이가 있는 기술이며, 스프링과 비슷한 시기에 등장했다. 스프링이 JPA를 굉장히 많이 지원하고 있다. 스프링 프레임워크에서 나오는 환경 데이터 베이스 관련되어 나오는 예제들은 대부분 JPA를 베이스로 나오고 있다.
일단 JPA를 쓰려면 build.gradle에서 라이브러리를 추가해줘야한다. JPA는 인터페이스고, 하이버네이트와 같은 것이 구현체이다. JPA라는 것은 자바 진영의 표준 인터페이스고, 구현은 여러 업체에서 만드는 제품인 것이다. 성능이 좋거나 쓰기 편하거나 여러 기준에 따라서 원하는 구현체를 선택해 사용할 수가 있다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
이렇게 라이브러리를 받고 나면, 그 다음부터는 JPA와 관련된 설정을 application.properties에 추가해줘야 한다.
- spring.jpa.show-sql=true JPA가 날리는 SQL문을 볼 수가 있다.
- spring.jpa.hibernate.ddl-auto=none JPA를 사용하면 객체를 보고, 데이터베이스에서 테이블을 자동으로 만들어준다. 우리는 일단 이 기능을 끄고 직접 테이블을 만들 것이다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
이제 라이브러리를 받고, 설정까지 끝나고 나면 데이터베이스에 있는 테이블과 자바 도메인을 매핑해줘야 한다. 이것을 JPA 엔티티 매핑이라고 한다. JPA는 ORM이라는 기술이다. Object Relational Mapping 객체와 Relational Database의 테이블을 매핑한다는 뜻이다.
도메인 클래스인 Member 위에 @Entity를 붙이면 JPA가 관리하는 엔티티가 된다.
@Entity
public class Member {
데이터베이스에서 Pk가 되는 id 필드에는 @Id라는 애노테이션을 붙이며, 추가로 DB에서 이 값을 생성해주고 있기 때문에(이것을 identity 전략이라고 부른다.) @GeneratedValue(strategy = GererationType.IDENTITY) 애노테이션을 붙여줘야 한다.
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
name 필드는 만약 데이터베이스에 있는 컬럼명과 다를 경우(예를 들어, DB 컬럼명이 username이라고 가정하면) @Column(name = “username”) 애노테이션을 필드위에 붙여준다. 그러나 컬럼명과 필드명이 동일하다면 굳이 애노테이션을 붙이지 않아도 자동으로 매핑된다.
@Column(name = "username")
private String name;
Jpa는 엔티티 매니저라는 걸로 모든 것이 동작한다. 따라서 Repository 클래스에서도 엔티티 매니저 역할을 수행하는 객체가 필요하다. build.gradle에서 jpa 라이브러리를 받으면, 스프링부트가 데이터베이스와 모두 연결해서 엔티티매니저 객체를 생성해준다. 우리는 이것을 주입받으면 된다.
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
엔티티매니저를 이제 사용해서 데이터베이스를 다뤄보자.
save(Member member)
엔티티매니저에 대하여 persist() 메서드를 호출하고 파라미터로 Member 객체를 넣어주면 JPA가 insert 쿼리 만들어서 데이터베이스에 집어넣고, id 값까지 생성해서 넣어준다.
public Member save(Member member) {
em.persist(member);
return member;
}
findById(Long id)
엔티티매니저에 대하여 find()메서드를 호출하고, 첫번째 파라미터로 원하는 도메인의 클래스 정보와, 두번째 파라미터로 식별자 pk 값을 넣어주면 조회가 가능하다.
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
findAll()
JPQL이라는 객체지향 쿼리 언어를 사용해야 한다. 일반 Sql과의 차이점은 객체를 대상으로 쿼리를 날리게 되므로, 테이블 이름도 도메인 이름이고, select의 대상도 컬럼명이 아니라, 그 객체 자체가 된다.
엔티티매니저에 대하여 createQuery 메서드를 호출하고, 첫번째 파라미터로 JPQL 쿼리를, 두번째 파라미터로 도메인 클래스 정보를 넘긴다. 그리고 이 결과를 List의 형태로 받을 수 있도록 getResultList() 메서드를 호출한다.
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
findByName(String name)
findByName도 마찬가지로 JPQL을 작성한다. select의 조건을 걸기 위해 where 절을 추가해야 한다. where 절에 들어가는 파라미터 값을 설정해주기 위해 setParameter 메서드를 호출하고, 이 메서드의 파라미터로 result
Comments