: 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
📌 설치 방법
📌 데이터베이스 파일 생성 방법
테이블 생성하기
: 테이블 관리를 위해 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에서 자동으로 값을 넣어주도록 설정
환경 설정
// 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);
}
}
@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("이미 존재하는 회원입니다.");
}
}
특징
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;
};
}
}
특징
환경 설정
// 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;
}
}
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 회원 리포지토리
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
📌 구현 코드 없이 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);
}
}
제공 클래스
→ 공통화 가능한 코드 모두 제공
제공 기능
" 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면? "
@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");
}
}
}
문제
: 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
의존 관계
전체 그림
// 빈칸
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
[스프링 1팀] 5장~6.5장. API 작성과 데이터베이스 연동 (0) | 2024.11.22 |
---|---|
[스프링 1팀] 1장~4장. 스프링 부트 개발 환경과 애플리케이션 개발하기 (0) | 2024.11.15 |
[스프링 1팀] 스프링 입문 섹션 5~6 (0) | 2024.10.11 |
[스프링 1팀] 스프링 입문 섹션4 (0) | 2024.10.04 |
[스프링 1팀] 스프링 입문 섹션 0~3 (0) | 2024.09.27 |