출처) 인프런 스프링 입문 강의
H2 데이터베이스에서 platform independent 설치
이후 압축 풀고 mac은 sh 실행 파일 권한 주기 필요
cd h2/bin
chmod 755 h2.sh
./h2.sh
실행하기
그럼 아래와 같은 화면이 나옴

연결 버튼 누르면 데이터 베이스가 생성됨

home (내 노트북에서는 desktop 상위 폴더)에 test.my.db가 생성됨
이렇게 파일로 접근하는 방식이 아니라, tomcat을 통해 접근하는 식으로 바꾸기
JDBC URL 칸에 jdbc:h2:tcp://localhost/~/test 로 변경
다시 연결해서 열어주고
테이블 생성 sql
id: java에서 long -> sql에서 bigint 타입
genertated by default as identity (DB가 자동으로 채워주기)
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);

그럼 이렇게 MEMBER 테이블이 생성된 것을 확인할 수 있다
테이블에 값을 삽입해보면
insert into member(name) values('chaewon');

이렇게 ID를 삽입하지 않아도 자동으로 들어간다 (1번은 삭제함)
MemoryMemberRepository에서 시퀀스로 아이디 저장했던 것과 맞춤
최상위에서 sql 폴더 생성하고 db.sql 파일 생성하여 관리
hello-spring/sql/db.sql
drop table if exists member CASECADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
작업할 때 터미널에 있는 h2 끄면 안 됨
이렇게 순수하게 연결하는 방식은 예전에 사용하던 방식
build.gradle 파일에 jdbc h2 라이브러리 추가

dependencies 에 추가
implementation 'org.springframework.boo:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
src/main/Application.properties 파일에 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
이렇게 하고 애플리케이션을 하면 스프링이 db를 연결해준다
memoryMember repository에 저장할 jdbc db를 생성하기 위해
src/main/java/hello.hellospring/repository 폴더에 JdbcMemberRepository.class 파일 생성
MemberRepository의 구현체이므로 메소드 오버라이딩 해야함
전체 코드는 상당히 복잡하다🥹 jdbc로 한 것은 20년전의 이야기로 정리는 생략하였다
메소드 오버라이딩한 함수 내부에 쿼리 문을 작성하는 형식이다.
만들어준 JdbcMemberRepository를 bean에 등록하려면 SpringConfig 파일에
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
return new JdbcMemberRepository();
}
MemoryMemberRepository는 주석처리하고 Jdbc를 추가한다
그리고 spring config에
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource){
this.dataSource = dataSource;
}
...
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
이렇게 DataSource를 SpringConfig에 주입하고, MemberRepository 빈에 JdbcMemberRepository로 등록해준다.
그리고 app 다시 돌린다음 localhost:8080 접속해서 회원 목록을 조회

이렇게 잘 나온 것을 확인할 수 있다
회원가입 화면에서 회원가입해도 DB에 잘 들어간 것을 확인


기존의 코드는 손대지 않고, Application 어셈블리 코드만 수정하면 됨
-> SpringConfig의 MemberRepository만 변경
기존의 memberRepository에서 jdbc로 변경

⭐️OCP (Open-Closed-Principle) 개방 폐쇄 원칙
확장에는 열려 있고, 수정, 변경에는 닫혀있다.
스프링의 DI (Dependency Injection)를 사용하면 코드 수정 없이 설정만으로 구현 클래스 변경 O
이제 DB에 저장하기 때문에 application을 재실행해도 남아있다
스프링을 올려서 하기 때문에 속도가 단위 테스트보다 느림, 단위 테스트를 하는 게 더 좋다
이전 MemberServiceTest를 copy해서 MemberServiceIntegrationTest 파일 생성
아래 어노테이션 추가
1. 어노테이션 추가
@SpringBootTest
@Transactional
기존 코드
MemberService memberService;
MemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
바뀐 코드
이렇게 바꾸면 configuration 했던 곳에서 올라옴
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository
기존 코드
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
테스트 결과를 보면 잘 된 것을 알 수 있음

⭐️ Transactional이란?
DB에 insert 해도 커밋을 해야 반영이 됨 (auto commit)
rollback을 하면 다시 돌릴 수 있음
-> @Transactional 을 이용해서 테스트 수행하고나서 rollback 되어, DB에 데이터가 반영되지 않음 (지우지 테스트 반복 수행이 가능함)
@SpringBootTest: 스프링컨테이너와 테스트를 함께 실행
실무에서 많이 사용한 JdbcTemplate
src/main/java/repository/JdbcTemplateMemberRepository 생성
MemberRepository implements하고 메서드 전체 구현하기 클릭 후 시작
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource){
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
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();
}
을 사용해서 쿼리문 대신 이렇게 insert 기능을 하도록 할 수 있음
@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> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
} 테스트 돌리면 잘 된 것을 확인할 수 있음
JPA는 반복 코드, SQL 등도 다 만들어서 실행해줌 -> 개발 생산성을 높일 수 있음
SQL 데이터 중심 설계 -> 객체 중심 설게로 패러다임 전환
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boo:spring-boot-starter-data-jpa'
이후 gradle refresh 버튼 눌러줌
application.properties에 jpa 설정 추가
sql을 보여주는 기능, 객체 테이블 자동 생성 기능인데 현재 예제에서는 이미 테이블이 있으므로 none으로 함
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
@Entity 어노테이션 추가 = JPA에 의해 관리됨
PK 값 (Id, GeneratedValue)을 어노테이션으로 추가해야됨
Id는 프로그램에서 생성해주는 것이기 때문에 strategy = GenerationType.IDENTITY로 명시
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository{
@Override
Optional<Member> findByName(String name);
}
SpringConfig 코드 수정
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository);
}
SpringContainer에서 등록된 것을 찾을 때, interface만 (SpringDataJpaMemberRepository) 해놓으면 JPA가 구현체를 자동으로 만들어줌
어떻게 save나 그런 함수를 자동으로 만들어준걸까?
import한 JpaRepository의 경우 기본 메서드 findAll, CRUD, findById 등을 제공한다
→ 그냥 가져다가 쓰면 됨 !!!!
인터페이스로 공통화가 안되는 (이름 username, name 등) 것들은 만들어야함


실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용, 동적 쿼리는 Querydsl로
AOP가 필요할 때 = 모든 메서드의 호출 시간을 측정하고 싶을 때
MemberService에서 join (회원가입) 함수의 소요 시간 측정
System.currentTimeMillis(); 를 사용해서 측정
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");
}
}
test 코드 돌리면 이렇게 시간이 출력 되는 것을 확인할 수 있다

이렇게 작성하면 문제점
cf) 공통관심 사항 (cross-cuttig concern) vs 핵심관심 사항(core concern)
위 문제 사항을 AOP로 해결할 수 있다
AOP란 공통관심 사항과 핵심관심 사항을 분리하는 것
이런식으로 시간 측정 로직 (공통)과 다른 기능 (핵심)을 분리해서, 원할 때 공통을 적용하는 것

hello.hellospring/aop/ 아래에 aop 클래스 생성
위와 같은 시간 측정 방식
가독성을 위해 joinPoint를 사용해서 현재 수행된 메서드 명을 출력하는 부분을 추가함
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TimeTraceAop {
public Object execute(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");
}
}
}
이후 component scan을 사용해서, 적용될 곳을 타겟팅
hellospring 아래 다 적용하는 코드로 작성
@Around("execution(~)")
이 부분을 수정해서, 적용될 타겟을 일부로 변경할 수도 있다
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))")
원래는 아래처럼 SpringConfig 에 등록해서 timeTraceAop를 쓰는 걸 확실하게 보여주는 방식이 좋다.
@Bean
public TimeTraceAop timeTraceAop(){
return new TimeTraceAop();
}
회원 목록 조회 기능을 누르면 위에서 설정한 것처럼 START, END와 시간 나오는 것을 확인할 수 있다

이렇게 하면, 어느 메서드에서 밀리는지 확인하기 쉽다
처음했던 코드처럼 각각 다 적지 않아도 시간 확인이 가능
AOP 적용 전

AOP 적용 후
Proxy를 통해 가짜 memberService를 만들어서 spring bean에 등록할 때, 진짜보다 앞에 세워서 먼저 올라가게함
이후 joinPoint.proceed() 되면 진짜가 수행되게끔

위 예제에서는 전체에 AOP를 걸어놨기 때문에 이러한 구조로 동작
