[스프링 1팀] 스프링 입문 섹션 7~8
스프링 DB 접근 기술
H2 데이터베이스
: 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
📌 설치 방법
- https://www.h2database.com에서 All Platforms 다운로드 및 설치
+ 설치 이후 맥 사용자는 chmod 755 h2.sh로 권한 주기 - 실행 : 맥 - ./h2.sh, 윈도우 - ./h2.bat
+ DB가 정상 생성되지 않는 경우 ⇒ 주소창 8082 앞을 localhost로 변경해서 실행
📌 데이터베이스 파일 생성 방법
- JSBC URL에 jdbc:h2:~/test 작성 후 연결
- ~/test.mv.db 파일 생성 확인
- 이후 JSBC URL은 jdbc:h2:tcp://localhost/~/test로 접속
⇒ 파일로 접속 시 에플리케이션과 웹 콘솔이 같이 접근이 안될 수 있기 때문에 변경이 필요
테이블 생성하기
: 테이블 관리를 위해 sql/ddl.sql 파일 생성
// H2 데이터베이스에 접근해서 member 테이블 생성
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
+ generated by default as identity → ID 값이 없는 경우 DB에서 자동으로 값을 넣어주도록 설정
순수 Jdbc
환경 설정
- build.gradle 파일 : jdbc, h2 데이터베이스 관련 라이브러리 추가
- application.properties 파일 : 스프링 부트 데이터베이스 연결 설정 추가
// build.gradle 파일
implementation 'org.springframework.boot:spring-boot-starter-jdbc' // 자바가 DB와 연동하려면 jdbc드라이버 필요
runtimeOnly 'com.h2database:h2' // DB에서 제공하는 클라이언트
// resources/application.properties 파일
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
스프링 설정 변경
@Configuration
public class SpringConfig {
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);
}
}
- DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체
⇒ 데이터베이스 커넥션 정 보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어야지 의존성 주입 받음
- 개방-폐쇄 원칙( OCP ) → 확장에는 열려있고, 수정, 변경에는 닫혀있음
- 스프링 DI 사용 → 기존 코드 변경 없이 설정 코드만 수정해 구현체 변경 가능
스프링 통합 테스트
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
// 다른 곳에서 사용하지 않기 때문에 필드 기반으로 Autowired 받아도 됨
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void join() {
// given
Member member = new Member();
member.setName("hello");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// 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("이미 존재하는 회원입니다.");
}
}
- @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행
- @Transactional : 테스트 시작 전에 트랜잭션을 시작, 테스트 완료 후에 항상 롤백 진행
⇒ DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않음
스프링 Jdbc Template
특징
- 환경 설정은 Jdbc와 동일
- 반복 코드 대부분 제거
- 단, SQL은 직접 작성해야 함
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;
};
}
}
JPA
특징
- 반복 코드 제거 + 기본적인 SQL JPA가 직접 실행
⇒ SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임 전환 - 개발 생상성 높아짐
환경 설정
- build.gradle 파일 : JPA, h2 데이터베이스 관련 라이브러리 추가
- application.properties 파일 : 스프링 부트에 JPA 설정 추가
+ JPA는 내부에 jdbc 관련 라이브러리를 포함하여 jdbc는 제거
// build.gradle 파일
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'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// resources/application.properties 파일
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true // JPA가 생성하는 SQL을 출력
spring.jpa.hibernate.ddl-auto=none // JPA는 테이블을 자동으로 생성하는 기능을 제공 → none으로 기능 끔
JPA 엔티티 매핑
@Entity
public class Member {
private Long id;
private String name;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- @Entity : JPA가 관리하는 엔티티를 의미
- GenerationType.IDENTITY : DB가 자동으로 ID 매핑
JPA 회원 리포지토리
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member); }
// PK기반이 아난 경우에는 직접 작성해야 함
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
@Override
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();
}
}
📌 JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 하기에 서비스 계층에 트랜잭션 추가
⇒ 메서드를 실행할 때 트랜잭션을 시작, 정상 종료 시 트랜잭션 커밋 / 런타임 예외 발생 시 롤백
import org.springframework.transaction.annotation.Transactional
@Transactional
public class MemberService {}
JPA를 사용하도록 스프링 설정 변경
@Configuration
public class SpringConfig {
private final EntityManager em;
public SpringConfig(EntityManager em) {
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);
}
}
스프링 데이터 JPA
특징
- JPA와 설정 동일 → 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술
- 구현 클래스 없이 인터페이스 만으로 개발을 완료
- 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공
⇒ 단순하고 반복이라 생각했던 개발 코드가 줄어들어 개발자가 핵심 비즈니스로직 개발에 집중할 수 있음
스프링 데이터 JPA 회원 리포지토리
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
- JpaRepository : 기본 메서드 제공
📌 구현 코드 없이 SpringDataJpaMemberRepository 인터페이스만 생성
⇒ 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록
스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
제공 클래스
→ 공통화 가능한 코드 모두 제공
제공 기능
- 인터페이스를 통한 기본적인 CRUD
- findByName(), findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
- 페이징 기능 자동 제공
AOP
AOP가 필요한 상황
" 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면? "
@Transactional
public class MemberService {
/**
* 회원 가입
*/
public Long join(Member member) {
long start = System.currentTimeMillis();
try {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("join " + timeMs + "ms");
}
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
long start = System.currentTimeMillis();
try {
return memberRepository.findAll();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("findMembers " + timeMs + "ms");
}
}
}
문제
- 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아님
- 시간을 측정하는 로직은 공통 관심 사항 임
- 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어려움
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어려움
- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 함
AOP 적용
: Aspect Oriented Programming
📌공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리
시간 측정 AOP 등록
@Aspect
@Component
public class TimeTraceAop {
// 하위 모두 적용
@Around("execution(* hello.hello_spring..*(..))\n")// 하위 모두 적용
// @Around("execution(* hello.hello_spring.service..*(..))\n") // 서비스만 진행하는 경우
public Object excute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try{
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish -start;
System.out.println("END: " + joinPoint.toString() +" "+ timeMs + "ms");
}
}
}
해결
- 회원가입, 회원 조회 등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리
- 시간을 측정하는 로직을 별도의 공통 로직으로 생성
- 핵심 관심 사항을 깔끔하게 유지 가능
- 변경이 필요하면 이 로직만 변경하면 됨
- 원하는 적용 대상을 선택 가능
스프링의 AOP 동작 방식 설명
📌 진짜 맴버 서비스가 아닌 Proxy라는 기술이 만들어내는 가짜 멤버 서비스와 연결 ⇒ Proxy 방식의 AOP
의존 관계
전체 그림
Quiz
- ( 스프링의 DI )을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
- ( @Transactional )는 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
- ( 스프링 JdbcTemplate )는 반복 코드를 대부분 제거해주지만 SQL은 직접 작성해야한다.
- ( JPA )는 객체 중심의 설계로 패러다임을 전환할 수 있으며 반복 코드 제거와 기본적인 SQL도 실행해준다.
- 스프링 데이터 JPA는 구현 클래스 없이 ( 인터페이스 ) 만으로 개발을 완료할 수 있다.
- AOP 적용으로 ( 공통 관심 사항 )과 ( 핵심 관심 사항 )을 분리한다.
- ( Proxy )방식의 AOP는진짜 맴버 서비스가 아닌 ( Proxy )라는 기술이 만들어내는 가짜 멤버 서비스와 연결한다.
Programming Quiz
- JPA 엔티티 매핑을 위해 아래 멤버 클래스의 빈칸을 완성해주세요. ( ID의 경우 데이터베이스가 자동으로 id 값을 생성하도록 작성해주세요. )
// 빈칸
public class Member {
private Long id;
private String name;
// 빈칸
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
정답 :
@Entity
public class Member {
private Long id;
private String name;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2. 다음 코드에서 hello.hello_spring의 하위 모두 적용이 아닌 서비스만 적용하도록 빈칸을 완성하세요.
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(// 빈칸 )\n")
public Object excute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try{
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish -start;
System.out.println("END: " + joinPoint.toString() +" "+ timeMs + "ms");
}
}
}
정답 :
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.hello_spring.service..*(..))\n")
public Object excute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try{
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish -start;
System.out.println("END: " + joinPoint.toString() +" "+ timeMs + "ms");
}
}
}
출처: 스프링 입문, 코드로 배우는 스프링 부트, 웹MVC, DB접근기술 강의
Corner Spring 1
Editor: Luna