순수 JDBC의 반복 코드를 줄여주지만 SQL 작성은 여전히 필요하다.
🖥️ MemberRepository
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
.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;
};
}
}
🖥️ SpringConfig
@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 JdbcTemplateMemberRepository(dataSource);
}
}
SQL을 직접 생성하지 않고, 객체 중심의 데이터베이스 접근을 가능하게 한다. SQL 대신 JPQL을 사용하며 ORM(Object-Relational Mapping)을 통해 엔티티와 테이블을 매핑한다.
🖥️ MemberRepository
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);
}
@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();
}
}
🖥️ MemberService
@Transactional
public class MemberService {}
@Transactional 추가 : 스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커트랜잭션은 데이터베이스 작업의 최소 단위, 여러 작업을 한 번에 처리하고 모두 성공해야만 최종 반영(커밋), 하나라도 실패하면 되돌림(롤백)
🖥️ SpringConfig
@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 JpaMemberRepository(em);
}
}
JPA를 기반으로 더 높은 생산성을 제공한다.
repository 인터페이스만으로 기본적인 CRUD 및 쿼리를 구현할 수 있다.
🖥️ MemberRepository
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
🖥️ SpringConfig
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
👉 환경설정 (JPA와 스프링 데이터 JPA 동일)
[ 라이브러리 ]
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
[ 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
spring.jpa.hibernate.ddl-auto=none
none은 그 기능을 끈다. create를 사용하면 엔티티 정보를 이용해 테이블도 직접 생성해줌 애플리케이션 개발 시, 모든 메서드에서 공통적으로 수행되는 작업은 핵심 비지니스 로직과 섞이면 코드가 복잡해지고 유지보수가 어려워진다.
예를 들어, 회원 가입 및 조회 시 시간을 측정하는 코드를 추가하려면?
🖥️ MemberService
public Long join(Member member) {
long start = System.currentTimeMillis();
try {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMillis();
System.out.println("join " + (finish - start) + "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를 사용하면 시간 측정과 같은 공통 관심 사항을 별도의 로직으로 분리하고, 핵심 비즈니스 로직을 깔끔하게 유지할 수 있다.
@Component
@Aspect
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))")
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");
}
}
}
joinPoint.proceed(): 핵심 비즈니스 로직을 호출함💡 해결