public Long join(Member member) {
// 메소드 수행시간을 구한다는 과정에서의 코드
long start = System.currentTimeMillis();
try {
// 중복회원 검증
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMillis();
long times = finish - start;
System.out.println("join = " + times + "ms");
}
}
문제점
회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.
시간을 측정하는 로직은 공통 관심 사항이다.
시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.
AOP는 공통관심사항과 핵심관심사항을 분리하는 것이다. (관점지향 프로그래밍)
원하는 곳에 공통 관심 사항을 적용 시키는 방법 이다.
AOP 패키지를 생성
@Aspect 어노테이션을 상단에 입력하여 해당 클래스가 AOP인것을 명시해 준다.
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
// @Component: 해당 클래스를 빈등록 하는 간단한 방법 -> 서비스 패키지의 SpringConfig에 작성하여도 된다.
//@Component
public class TimeTraceAop {
// 해당 AOP을 어느 부분에 적용할지 선택 -> 여기서는 hello.hellospring 모든 메소드
// 보통 패키지 레밸로 적용하고 원할 경우 검색해서 사용법을 검색해서 사용
// @Around("execution(* hello.hellospring.service..*(..))")
// @Around("execution(* hello.hellospring..*(..))")
// SpringConfig를 통해서 빈 등록을 할 경우 순환참조문제가 발생하여 아래와 같이 AOP대상에서 SpringConfig를 제외시켜준다.
// @Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.service.SpringConfig)")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 현재 진행중인 메서드를 확인
System.out.println("START: " + joinPoint.toString());
try {
// 다음 메서드로 진행
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
}
}
}
joinPoint.proceed(): 프록시 메서드에서 진짜메서드로 진행하도록 하는 명령어
joinPoint.toString(): 진행하는 메서드가 무엇인지 확인하는 명령어
@Around 어노테이션을 이용하여 AOP의 범위를 지정할 수 있다.
SpringConfig클래스를 이용하여 빈 등록을 할 경우 순환참조 에러를 피하기 위해서 SpringConfig 클래스는 범위에서 제외 시켜준다.
SpringConfig클래스에서 빈 등록
// AOP 빈 등록
// 아래 TimeTraceAop을 생성하는 코드도 AOP의 대상으로 지정되기 때문에 @Around어노테이션에서 SpringConfig클래스를 범위에서 제외 시켜줘야 한다.
@Bean
public TimeTraceAop timeTraceAop() {
return new TimeTraceAop();
}
AOP를 적용하면 해당 범위를 입력하면 프록시(해당 가짜 스프링 빈을 만들고) 기술을 이용해서 먼저 거친다음에 진짜 클래스가 동작한다.
MemberController 클래스에서 확인 가능
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
// AOP 프록시 클래스 생성 확인
// memberService: class hello.hellospring.service.MemberService$$EnhancerBySpringCGLIB$$c70ac3a1
System.out.println("memberService: " + memberService.getClass());
}
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이기 때문에 먼저 JPA를 먼저 익혀야 한다.
앞의 JPA 설정을 그대로 사용한다.
SpringDataJpaMemberRepository 인터페이스 생성
인터페이스는 다중 상속이 가능하다.
스프링 데이터 JPA사용을 위해서 JpaRepository<Entity 클래스, PK 타입> 를 상속 받는다.
JpaRepository를 사용하면 스프링 데이터 JPA가 인터페이스에 대한 구현체를 만들어서 자동으로 빈 등록 해준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
// 인터페이스가 인터페이스를 상속받을 때는 extends
// JpaRepository<Entity 클래스, PK 타입>
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository{
// select m from Member m where m.name = ?
@Override
Optional<Member> findByName(String name);
}
SpringConfig 클래스 수정
package hello.hellospring.service;
import hello.hellospring.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import javax.xml.crypto.Data;
// 자바코드로 직접 스프링 빈 등록하는 방법
// 스프링 실행할때 자동 실행
@Configuration
public class SpringConfig {
// JDBC 사용을 위해 DataSource을 주입 받는다.
// private DataSource dataSource;
//
// @Autowired
// public SpringConfig(DataSource dataSource){
// this.dataSource = dataSource;
// }
// JPA사용을 위해 EntityManager을 주입 받는다.
// private EntityManager em;
//
// @Autowired
// public SpringConfig(EntityManager em) {
// this.em = em;
// }
// 스프링 데이터 JPA 사용을 위해 의존성 주입
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 스프링 빈 등록
@Bean
public MemberService memberService() {
// @Autowired와 같이 동작
// return new MemberService(memberRepository());
// 스프링 데이터 JPA 사용
return new MemberService(memberRepository);
}
// @Bean
// public MemberRepository memberRepository() {
// // 구현체를 리턴해줌
//// return new MemoryMemberRepository();
//// JDBC 연결로 변경경
//// return new JdbcMemberRepository(dataSource);
//// JDBCTemplate 연결로 변경
//// return new JdbcTemplateMemberRepository(dataSource);
//// JPA 사용
//// return new JpaMemberRepository(em);
// }
}
JPQL = JPA를 이용하여 쿼리를 생성하여 돌릴 때 만들어지는 SQL -> 테이블이 아닌 객체를 검색하는 객체지향 쿼리
# jpa 작성된 쿼리를 확인 할 수 있도록 설정
spring.jpa.show-sql=true
# 자동 테이블 생성기능 끄기
spring.jpa.hibernate.ddl-auto=none
jpa는 인터페이스이고 hibernate는 구현체라고 생각하면 된다.
Member 클래스를 JPA와 연동 되도록 코드 수정
package hello.hellospring.domain;
import javax.persistence.*;
// @Entity: jpa가 관리하는 객체라고 선언
@Entity
public class Member {
// @Id: PK 맵핑 / @GeneratedValue: ID가 자동으로 생성되도록 설정
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 테이블의 name 컬럼 맵핑 설정
@Column(name = "name")
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;
}
}
JpaMemberRepository 생성
jpa를 사용하기 위해서는 반드시 EntityManager를 주입받아서 사용하여야 한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository{
// jpa를 사용할려면 EntityManager를 주입 받아야 한다.
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) {
// em.find(조회 타입, 식별자);
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
// em.createQuery("실행할 쿼리", 반환할 타입)
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
// 파라미터값 선언
.setParameter("name", name)
// 리스트 형태로 반환
.getResultList();
// Optional로 처리하기 때문에 아래와 같이 반환 stream().findAny(): 첫번째 찾은 데이터를 반환(순서가 중요하지 않을때 사용)
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
// em.createQuery("실행할 쿼리", 반환할 타입)
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
return result;
}
@Override
public void clearStore() {
}
}
jpa의 기능은 항상 트랜잭션기능 안에서 동작되어야 한다.
따라서 각 기능을 구현한 MemberService 클래스에 @Transactional 어노테이션 선언
SpringConfig 클래스에서 jpa기능을 사용하도록 코드 수정
package hello.hellospring.service;
import hello.hellospring.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import javax.xml.crypto.Data;
// 자바코드로 직접 스프링 빈 등록하는 방법
// 스프링 실행할때 자동 실행
@Configuration
public class SpringConfig {
// JDBC 사용을 위해 DataSource을 주입 받는다.
// private DataSource dataSource;
//
// @Autowired
// public SpringConfig(DataSource dataSource){
// this.dataSource = dataSource;
// }
// JPA사용을 위해 EntityManager을 주입 받는다.
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
// 스프링 빈 등록
@Bean
public MemberService memberService() {
// @Autowired와 같이 동작
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// 구현체를 리턴해줌
// return new MemoryMemberRepository();
// JDBC 연결로 변경경
// return new JdbcMemberRepository(dataSource);
// JDBCTemplate 연결로 변경
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
테스트 진행
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
// 테스트는 한글로 바꾸어도 된다.
// 빌드될때 테스트코드는 포함되지 않는다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
// 컨테이너로 부터 주입받는다.
// 테스트를 진행할 때는 편하게 @Autowired 사용하면 된다. -> 필드 주입 방법(DI)
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
// @Commit: 실제로 데이터가 저장되도록 선언
@Commit
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// 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);
// member2를 join 할 때 IllegalStateException이 발생하것을 예상하는 코드
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// assertThrows(NullPointerException.class,() -> memberService.join(member2));
/* try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존해하는 회원입니다.12333");
}*/
// then
}
}
JdbcTemplateMemberRepository 클래스 작성(JDBC템플릿 사용을 위해서)
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
// 생성자가 하나일 경우에는 DI를 위해서 @Autowired 선언을 안해줘도 된다
// @Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
// 테이블명(member), PK(id) 알려주면 sql 작성을 안해도 된다.
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
// executeAndReturnKey: 작업 수행과 동시에 자동 생성된 PK(auto_increment)를 반환한다.
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findById(Long id) {
// 사용법: jdbcTemplate.query("쿼리문", RowMapper(), 쿼리의 ? 파라미터 값)
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(),id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public void clearStore() {
}
// 결과를 받아오기 위한 함수
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}
}
SpringConfig 클래스 수정 -> JDBC템플릿을 사용하는 것으로 설정 변경
package hello.hellospring.service;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import javax.xml.crypto.Data;
// 자바코드로 직접 스프링 빈 등록하는 방법
// 스프링 실행할때 자동 실행
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource){
this.dataSource = dataSource;
}
// 스프링 빈 등록
@Bean
public MemberService memberService() {
// @Autowired와 같이 동작
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// 구현체를 리턴해줌
// return new MemoryMemberRepository();
// JDBC 연결로 변경경
// return new JdbcMemberRepository(dataSource);
// JDBCTemplate 연결로 변경
return new JdbcTemplateMemberRepository(dataSource);
}
}
MemberServiceIntegrationTest -> 기존에 작성한 테스트 코드를 이용하여 테스트 진행
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.assertThrows;
// 테스트는 한글로 바꾸어도 된다.
// 빌드될때 테스트코드는 포함되지 않는다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
// 컨테이너로 부터 주입받는다.
// 테스트를 진행할 때는 편하게 @Autowired 사용하면 된다. -> 필드 주입 방법(DI)
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// 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);
// member2를 join 할 때 IllegalStateException이 발생하것을 예상하는 코드
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// assertThrows(NullPointerException.class,() -> memberService.join(member2));
/* try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존해하는 회원입니다.12333");
}*/
// then
}
}
@Transactional: DB에서는 기본적으로 커밋을 해야 데이터가 최종적으로 저장되는데, 해당 어노테이션을 선언하면 자동적으로 롤백을 해주어서 테스트 데이터가 최종적으로 저장되지 않도록 해준다.
따라서, 반복적으로 같은 데이터로 테스트를 진행할 수 있다.
@SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.-> 스프링이 실제로 구동된다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.assertThrows;
// 테스트는 한글로 바꾸어도 된다.
// 빌드될때 테스트코드는 포함되지 않는다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
// 컨테이너로 부터 주입받는다.
// 테스트를 진행할 때는 편하게 @Autowired 사용하면 된다. -> 필드 주입 방법(DI)
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// 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);
// member2를 join 할 때 IllegalStateException이 발생하것을 예상하는 코드
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// assertThrows(NullPointerException.class,() -> memberService.join(member2));
/* try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존해하는 회원입니다.12333");
}*/
// then
}
}
순수한 단위 테스트는 스프링 컨테이너 없이 하는 순수한 자바코드로 되어진 각각의 기능을 테스트하는 것이다.
단위테스트가 실행속도가 빠르고 최소 단위로 진행하는 테스트이기 때문에 정말 중요하다고 한다.
통합테스트는 스프링 컨테이너와 DB까지 함께 테스트를 진행하는 것을 말한다.
테스트에서의 주의사항은 반드시 테스트용 DB를 이용하거나 로컬에서만 사용하는 DB를 이용해야한다.