H2 데이터베이스 설치
Windows Installer를 다운로드 받아 설치한다.
H2 Console을 클릭하여 H2 데이터베이스를 실행한다.
~/test
로 연결 하면 Windows 기준 USERPROFILE 경로에 test.mv.db
가 생긴다.
/* DROP TABLE IF EXISTS MEMBER CASCADE; */
CREATE TABLE MEMBER
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY,
NAME VARCHAR(255),
PRIMARY KEY (ID)
);
INSERT INTO MEMBER(NAME) VALUES('member1');
INSERT INTO MEMBER(NAME) VALUES('member2');
INSERT INTO MEMBER(NAME) VALUES(666, '666member');
member 테이블 생성하고 데이터를 삽입한다. ID는 BIGINT 타입으로 지정하고, 값을 입력하지 않았을 때 GENERATED BY DEFAULT AS IDENTITY
가 자동으로 값을 입력해준다. NAME은 VARCHAR 타입으로 255자까지 가변적으로 입력가능한 문자열이다. PK로 ID를 지정하여 고유한 값을 갖게 된다.
좌측 메뉴에서 테이블명을 선택하면 자동으로 Select문이 생성되어 쉽게 조회 할 수 있다. 방금 삽입한 3개의 member가 조회된다.
순수 JDBC
이제는 사용하지 않는 방식이라고 넘어갔지만 코드 작성해서 실행해보았다.
@Configuration
public class SpringConfig {
@Autowired
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
SpringConfig
에 DataSource를 추가해주고, memberRepository에서 작성한 JdbcMemberRepository로 변경한다. DataSource는 데이터베이스와의 커넥션을 획득할 때 사용하는 객체로, 스프링 부트에서는 정보(application.properties)를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 두기 때문에 의존성 주입(DI)을 받을 수 있다.
스프링 통합 테스트
MemberServiceIntegrationTest
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@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("이미 존재하는 회원입니다.");
}
}
이전에 했던 단위 테스트 말고 통합 테스트를 위한 MemberServiceIntegrationTest 클래스 파일을 작성한다. @SpringBootTest 어노테이션으로 스프링 컨테이너를 테스트와 함께 실행하도록 하며, @Transactional 어노테이션으로 테스트 트랜잭션 시작 후 종료 시에 전부 롤백하도록 한다. @Transactional 어노테이션이 없으면 데이터베이스 PK 문제 발생 시 테스트 케이스를 계속 바꿔줘야할 수 있다.
스프링 JdbcTemplate
스프링 JdbcTemplate는 JDBC 코드의 반복적인 부분을 줄여준다.
JdbcTemplateMemberRepository
//import 생략
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
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;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
이후에 Spring Config
에서 JdbcTemplateMemberRepository를 사용하도록 수정해준다.
//생략
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
JPA
JPA는 자바에서 ORM(Object-Relational Mapping, 객체-관계 매핑)을 위한 표준 인터페이스로, 자바 객체와 관계형 데이터베이스 간의 매핑을 쉽게 하여 개발자가 SQL 쿼리를 작성하지 않아도 된다. (인터페이스이므로 실제 구현이 필요하다. 대표적으로 Hibernate, EclipseLink, OpenJPA 등이 있다.)
주요 기능으로는 객체 중심의 개발(쿼리보다 자바 객체에 집중하여 개발 생산성 향상과 유지보수 용이), CRUD 작업 자동화, ORM 기능(자바 객체와 데이터베이스 테이블 간의 매핑 제공으로 패러다임 불일치 해결)이 있고, 주요 구성 요소로는 Entity(데이터베이스 테이블과 매핑되는 자바 클래스, @Entity 어노테이션), Entity Manager(Entity의 생명주기를 관리하는 인터페이스로 CRUD 작업 수행), Persistent Context(영속성 컨텍스트, Entity Manager가 관리하는 Entity 인스턴스의 집합으로 데이터베이스와의 동기화를 도움)가 있다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
우선 build.gradle
파일에 jpa를 추가해준다. jpa 라이브러리 안에 jdbc를 포함하므로 기존에 사용했던 jdbc는 제외한다.
//생략
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
그리고 application.properties
에 옵션을 추가해준다. show-sql은 jpa가 생성한 쿼리를 보여주고, ddl-auto는 jpa가 자동으로 테이블을 생성할지 여부이다. 이미 생성된 테이블을 사용하고 있어서 none으로 설정했지만, create로 하면 테이블을 생성해준다.
//생략
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
Member 도메인을 Entity로 사용하기 위해 @Entity 어노테이션을 설정해주고, @Id 어노테이션으로 PK를 지정, @GeneratedValue 어노테이션으로 값을 자동으로 생성해준다.
JpaMemberRepository
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member);
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
}
JpaMemberRepository
를 작성한다. DataSource를 받아오는게 아니라 EntityManager를 불러와서 사용한다.
//생략
@Transactional
public class MemberService {
`MemberService'에 @Transactional 어노테이션을 달아준다. jpa의 모든 쿼리는 트랙잭션 내에서 실행되어야 한다.
//생략
@Configuration
public class SpringConfig {
private final DataSource dataSource;
private final EntityManager em;
public SpringConfig(DataSource dataSource, EntityManager em) {
this.dataSource = dataSource;
this.em = em;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
SpringConfig
에서 JpaMemberRepository를 사용하도록 변경한다. EntityManager를 추가한다. 이후 테스트를 돌리거나 실행하면 정상적으로 완료된다. jpa는 이런 쿼리를 생성했다.
스프링 데이터 JPA
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
Optional<Member> findByName(String name);
}
SpringDataJpaMemberRepository
를 작성한다. 클래스가 아닌 인터페이스로 작성하고, JpaRepository를 상속하게 되면 스프링 실행 시 자동으로 스프링 빈에 등록되어 사용할 수 있게된다.
//생략
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
SpringConfig
에서 MemberRepository만 의존성 주입 받으면 된다.
스프링 데이터 JPA는 JPA 기반으로 구축되었고, 기본적인 CRUD 기능도 대부분 제공한다. findByID, findByAll 등등. 우선 강의에 있어 스프링 데이터 JPA를 사용해보았으나, 우선 JPA에 대한 이해도를 높일 필요가 있어 보이고, 결국 모든 쿼리를 JPA로 처리할 수 없어서 네이티브 쿼리나 JdbcTemplate과의 조합 등의 사용도 고려해야한다.
'스터디 > Spring' 카테고리의 다른 글
Spring 스터디2 - 1 (0) | 2025.03.23 |
---|---|
Spring 스터디 - 7 (1) | 2025.03.09 |
Spring 스터디 - 5 (0) | 2025.02.17 |
Spring 스터디 - 4 (0) | 2025.02.16 |
Spring 스터디 - 3 (0) | 2025.02.12 |