관리 메뉴

나구리의 개발공부기록

스프링 DB 접근 기술 2 - 스프링 JdbcTemplate, JPA, 스프링 데이터 JPA / AOP - AOP가 필요한 상황, AOP적용 본문

인프런 - 스프링 완전정복 코스 로드맵/코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술(무료)

스프링 DB 접근 기술 2 - 스프링 JdbcTemplate, JPA, 스프링 데이터 JPA / AOP - AOP가 필요한 상황, AOP적용

소소한나구리 2024. 1. 24. 21:30

스프링 JdbcTemplate

 

  • 순수 JDBC와 동일한 환경설정을 진행
  • JDBC API의 반복코드를 대부분 제거해주지만 SQL구문은 직접 입력해야 함

    • JdbcTemplate
    • MyBatis
  • JdbcTemplateMemberRepository class 작성
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    //    @Autowired(생성자가 1개만 있으므로 생략가능)
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {

        // sql구문 없이 insert 구문을 구현하도록 코드 작성
        // 도큐멘트 보고 작성해도 됨(자세히 나와있음)
        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());
        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query
        	("select * from member where name = ?", memberRowMapper());
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }


    private RowMapper<Member> memberRowMapper() {
        // 람다로 변환이 가능(Option + 엔터)
//        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;
//            }
            // 람다로 변환됨
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        }; // 세미콜론 추가
    }
}
  • SpringConfig class수정
@Configuration
public class SpringConfig {

	.... 기타 코드들..
   
    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}

JPA

 

  • JPA = 인터페이스, 자바의 표준 API로 구현은 각 업체들이 구현
  • 구현체로 Hibernate, Eclipse Link 등의 벤더가 있으나 Hibernate를 거의 사용
  • 기존의 반복코드는 물론 기본적인 SQL도 JPA가 직접 만들어서 실행 해줌
  • JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있음
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있음
  • JPA는 객체와 ORM 기술
    • Object
    • Relational
    • Mapping

  •  build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 입력 - 입력 후 우측 상단 코끼리버튼 클릭(새로고침)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // jpa,jdbc 전부 포함
  • ~/resources/application.properties(스프링부트)에 JPA 설정 추가 
// jpa가 날리는 sql을 볼 수 있음
spring.jpa.show-sql=true

// jpa를 사용하면 객체를 보고 테이블을 자동으로 만듦
// 지금은 테이블이 있으므로 none으로 설정
spring.jpa.hibernate.ddl-auto=none
//  spring.jpa.hibernate.ddl-auto=create

 

  • JpaMemberRepository class 작성
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) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }


    // PK기반이 아닌 메서드들은 JPQL을 작성 해야함
    @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();
    }

    @Override
    public List<Member> findAll() {

//        List<Member> result = em.createQuery("select  m from Member m", Member.class).getResultList();
//        return result;

        // 참조변수 값과 return값이 같을 경우 Command + Option + n 하면 합칠 수 있음
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}
  • JPA를 사용하려면 항상 @Transactional이 있어야 함(데이터를 저장하고 변경할 때)
  • MemberService class @Transactional 추가
@Transactional
public class MemberService {

    private final MemberRepository memberRepository;
    ....  
}


// 이런식으로 필요한 곳에 해줘도 됨
@Transactional
    public Long join(Member member) {
        // 같은 이름이 있는 중복 회원 X
    }
  • JPA를 사용하려면 EntityManager라는 것이 필요함
  • SpringConfig class 수정
@Configuration
public class SpringConfig {

//    @PersistenceContext - 원래는 작성해야하지만 스프링에서 알아서 DI해줌
    //JPA는 EntityManager가 필요함
    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }

    // Bean을 등록
    @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

 

  • 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공
  • 스프링부트 + JPA 위에 스프링 데이터 JPA 프레임워크를 더하면 개발이 즐거워지고 단순하고 반복적인 개발 코드들이 줄어듦
  • 핵심 비즈니스로직을 개발하는데 집중할 수 있음
  • 관계형 데이터베이스를 사용하면 스프링 데이터 JPA는 필수가 되고 있음

주의

  • 스프링 데이터 JPA는 JPA편리하게 사용하게 도와주는 기술이므로 JPA를 먼저 학습한 후에 스프링 데이터 JPA학습을 해야 함
  • 앞의 JPA 설정을 그대로 사용
  • SpringDataJpaRepository interface 작성
package start.startspring.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import start.startspring.domain.Member;

import java.util.Optional;

// 스프링 데이터 JPA가 스스로 구현체를 만들어서 스프링 빈에 등록함
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // JPQL이 자동으로 작성되어 SQL로 번역되어 실행 됨,
    // Select m from Member m where m.name = ?
    @Override
    Optional<Member> findByName(String name);
}
  • SpringConfig class 수정
@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    @Autowired // 생성자가 1개여서 생략 가능
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
   
   // Bean을 등록
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

 

스프링 데이터 JPA 제공 기능

 

  • JpaRepository 인터페이스를 들어가보면 여러 Repository 인터페이스들을 상속하여 기본적인 CRUD, 단순 조회하는 다양한 메서드들이 제공
최고조상 Repository Interface
interface JpaRepository PagingAndSortingRepository CrudRepository
method findAll() : List<T>
findAll(Sort) : List<T>
findAll(Iterable<ID> : List<T>
findAll(Sort) : Iterable<T>
findAll(Pageable) : Page<T>
findOne(ID) : T
save(Iterable<S>) : List<S>
saveAndFlush(T) : T
  save(S) : S
flush()   exists(ID) : boolean
deleteInBatch(Iterable<T>)
deleteAllInBatch()
  delete(T)
getOne(ID) : T   count() : long
  • 위 메서드 외에 공통화 할 수 있는 것들은 메서드로 거의 구현 해놓았으니 실제로는 한번 더 찾아볼 것을 권장
    • 해당 메서드들을 오버라이딩 해서 class를 작성하면 편리하게 구현이 가능함
  • 문자열(name 같은)것은 비즈니스마다 사용하는 단어가 다르기 때문에 공통화 클래스로 제공을 할 수가 없음
  • findByName(),findByEmail()처럼 메서드 이름 만으로 조회 하는 기능 제공
    • 그 외에 findByNameAndId 등등.. 
  • 페이징 기능 자동 제공

참고

  • 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용
  • 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용
    • Querydsl을 사용하면 쿼리도 자바코드로 안전하게 작성가능하고 동적 쿼리도 편리하게 작성 가능
  • 그 외에도 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나 JdbcTemplate를 사용하면 됨

 

AOP가 필요한 상황

 

  • 모든 메소드의 호출시간을 측정하거나 회원 가입 시간, 회원 조회 시간등을 측정하고 싶다면?
  • 각 메서드마다 아래처럼 코딩을 해줘야 함
 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");
        }
    }
    
...
    
public Long join(Member member) {

    long start = System.currentTimeMillis();

    try {
    // 같은 이름이 있는 중복 회원 X
    // ifPresent() (Optional에서 사용할 수 있는 메서드) : 값이 있으면 아래의 로직을 수행
    validateDuplicateMember(member);
    memberRepository.save(member);
    return member.getId();
    } finally {
        long finish = System.currentTimeMillis();
        long timeMs = finish - start;
        System.out.println("join = " + timeMs + "ms");
    }
}
...

 

문제

 

  • 회원가입, 회원조회에 시간을 측정하는 기능은 핵심 관심 사항이 아님 -> 공통 관심 사항임
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어려움
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어려움
  • 시간을 측정하는 로직을 변경할 떄 모든 로직을 찾아가면서 변경해야함

AOP적용

 

  • AOP(Aspect Oriented Programming)
  • 관점 지향 프로그래밍이라고도 불림
  • 공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 을 분리
    • 공통 관심 사항을 분리하여 내가 원하는 곳에 적용

  • 시간 측정 AOP등록
  • 새로운 패키지에 TimeTraceAop class 작성
@Aspect
// 스프링 빈에 등록해야 함, Component를 쓰기도 하지만 직접 등록해서 인지할 수 있도록 하는 것이 좋음
@Component
public class TimeTraceAop {

    // 적용할 타켓팅을 선정 할 수 있음 ("execution( 패키지명.패키지명.클래스명*(.파라미터타입))") 
    // 패키지 하위에 모두 적용 - @Component 애노테이션 사용시
    @Around("execution(* start.startspring..*(..))")
    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");
        }
    }
}
  • 만약 직접 Bean을 등록하려면 SpringConfig 클래스를 수정(빈을 등록한 클래스)
// aop 스프링 빈을 직접 등록
@Configuration
public class SpringConfig {
	...
    
	@Bean
	public TimeTraceAop timeTraceAop() {
	return new TimeTraceAop();
	}
}

-------------------------------------------------------------------------------

@Aspect
public class TimeTraceAop {

	// 직접 @Bean을 등록 했을 경우 빈을 등록한 클래스를 AOP대상에서 제외해주면 됨
	@Around("start.startspring..*(..))) && !target(start.startspring.SpringConfig)")
	....
}

 

해결

 

  • 회원가입, 회원 조회 등의 핵심 관심사항과 시간을 측정하는 공통 관심사항을 분리
  • 시간을 측정하는 로직을 별도의 공통 로직으로 생성 / 핵심 관심 사항을 깔끔하게 유지
  • 변경이 필요할 경우 공통 로직만 변경하면 됨
  • 원하는 적용 대상을 선택 할 수 있음

스프링의 AOP 동작 방식

 

AOP 적용 전 /  후 의존관계

좌) AOP 적용 전 / 우) AOP적용 후

  • 컨트롤러가 호출하면 프록시(가짜)에 스프링빈을 두었다가 joinPoint.Proceed() 메서드를 호출 하면 실제 스프링 빈에 적용

AOP 적용 전 / 후 전체 그림

좌) AOP 적용 전 / 우) AOP 적용 후

  • controller에서 생성자가 인젝션 할 때 .getClass()를 찍어보면 알 수 있음

실행 결과 문장 끝에 CG라이브러리가 적용 됨


출처 : 인프런 - 스프링 입문(무료) / 김영한님

https://inf.run/hivx6