Java Spring Framework

저번에는 h2 database를 설정하고 이를 spring에서 jdbc,jdbc-template방식으로 접근하는 방식을 하였다. 하지만, 이렇게 했을 때는 중복되는 객체들이 많이 있고, 무엇보다도 sql 구문을 직접 구현해서 해야핬다. 하지만 JPA를 통해 객체 중심의 DB 접근이 가능하다.

Dependency 추가

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

jpa 관련 library를 추가해준다.

Connection Configuration

application.properties

//기존의 h2 접근은 유지하고, JPA 관련 설정만 추가해준다.
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
Variables Description
show-sql JPA가 생성하는 sql을 출력할 수 있다.
ddl-auto 해당 값을 “create”로 해놓으면, entity 기반으로 jpa에서 자동적으로 ddl을 통한 테이블을 자동 생성한다. 하지만, 우리는 이미 테이블을 만들어 놓은 상태이므로 해당 기능을 꺼놓는다.

Entity Mapping

ORM(Object Relational Mapping) 객체 기반의 DB 접근을 지원하기 위해, Entity 정보를 설정해줘야한다.

이 project에서 주로 이용하는 entity는 Member이다.

Member.java

package hello.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    public String getName() {
        return name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}
Annotations Description
Entity 해당 객체가 entity임을 알린다.
id 해당 필드가 Private Key임을 알린다.
GeneratedValue 해당 column의 값을 자동을 생성한다. 이때, IDENTITY로 설정되어 있는데 이는 자동설정을 DB에 위임하겠다는 의미이다.

JPA Repository

이제는 JDBC와 다른 방식으로 MemoryRepository를 구현하고 있으니, 새로운 Repository를 생성해줘야한다.

JpaRepository.java

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

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) {
        return Optional.ofNullable(em.find(Member.class,id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> results=em.createQuery("select m from Member m where m.name= :name",Member.class)
                .setParameter("name",name)
                .getResultList();
        return results.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m",Member.class)
                .getResultList();
    }
}

위를 보면, save나 findById 같은 method들은 아주 간단하게 구현이 된다. 다만, findByName이나 findAll 같은 경우는 기존 sql 방식이 아닌 객체지향 sql인 jpql을 이용해서 query를 생성하고 있다.

DI 설정 부분

private final EntityManager em;

    public JPAMemberRepository(EntityManager em) {
        this.em = em;
    }

JPA방식을 이용하기 위해서는 EntityManager 객체를 입력받아야하는데, 이는 spring에서 자동으로 connection configuration을 토대로 DI를 해준다.

SpringConfig

package hello.hellospring;

import hello.hellospring.aop.TimeTraceAop;
import hello.hellospring.repository.JPAMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    private EntityManager entityManager;
    @Autowired
    public SpringConfig(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
    .
    .
    .


    public MemberRepository memberRepository(){
        /*return  new MemoryMemberRepository();
        return new JDBCMemberRepositry(dataSource);
        return new JDBCTemplateMemberRepositry(dataSource);*/
        return new JPAMemberRepository(entityManager);
    }
}

Spring Configuration 부분에서도, 구현클래스를 변경해줘야한다. 이렇게만 설정해주면, 무사히 JPA 기반으로 DB 접근하는 것이 가능하다.

Spring Data JPA

JPA를 이용하면 sql를 쓰지 않고, 객체 지향 방식의 DB 접근이 가능했다. 하지만, jpql을 적용해야하는 일부 메서드들이 있었지만, Spring Data JPA를 이용하면 이마저도 생략할 수 있다.

Spring Data JPA Repository

SpringDataJpaMemoryRepository.interface

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);
}

위와 같이 concrete class형태가 아닌 interface 형태로, JpaRepository와 MemberRepository를 상속하는 interface를 생성하기만 해주면 JPA 내부에서 저절로 이를 인식해서 Concrete Class을 자동으로 생성해준다.

DI Configuration

SpringConfig

package hello.hellospring;

import hello.hellospring.aop.TimeTraceAop;
import hello.hellospring.repository.JPAMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Autowired
    private MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository);
    }
}

위와 같이 기본 설정대로 해두면, 스프링 내부에서 SpringDataJpaMemberRepository를 자동으로 Spring Beand으로 등록한다.

jpa_repositry 위 처럼, Jpa Repository를 상속하는 것만으로도, 그 내부에 있는 메소드들을 사용하므로써 대부분의 기능들을 이용할 수 있다.

AOP

만약, 모든 메소들의 실행 시간을 구하고자 한다면 어떻게 해야할까?, 각각의 메소드에 대해서 아래와 같은 코드를 넣는 작업을 해야한다.

Time Measure

long start=System.currentTimeMillis();
//method
long end=System.currentTimeMillis();
long timePassed=end-start

System.out.println(timePassed);

하지만, 이렇게 하면 시간도 오래 걸릴 뿐만 아니라, 이러한 측정 코드의 수정은 불가능하다.

따라서 Spring에서는 AOP 기술을 이용해서 쉽게 해결할 수 있다.

우선, AOP는 공통 관심사항과 핵심 관심 사항의 분리가 요구된다. 회원 등록,조회와 같은 비지니스 로직은 핵심 관심 사항이고, 시간 측정 같은 요소는 공통 관심 사항이다.

AOP는 핵심 관심 사항에 대해서, 공통 관심 사항을 언제든지 원하는 곳에 적용할 수 있도록 제공해준다.

hello.hellospring.aop TimeTraceAop.java

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
public class TimeTraceAop {
    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        long start=System.currentTimeMillis();
        System.out.println("Job: "+joinPoint.toString()+ "started");
        try{
            return joinPoint.proceed();
        }
        finally{
            long finish=System.currentTimeMillis();
            long timePassed=finish-start;
            System.out.println("Job: "+joinPoint.toString()+ "finished "+timePassed+" ms");
        }
    }
}

|Annotations|Description| |———|————-| |Aspect|해당 class가 Aspect(관심사항)임을 알린다.| |Around|해당 Aspect을 어디서 실행시킬지 알린다. 여기서는 hello.hellospring package안에 모든 클래스|

이렇게 설정해놓으면 Spring이 돌아가는 동안, 비지니스 로직에서는 자동으로 TimeTraceAop의 execute method가 실행된다.

AOP가 적용되기 전에는 Controller와 Service 간에 의존관계는 아래와 같다. before_aop

AOP가 적용되고 나면 아래와 같이 중간에 Proxy가 생겨 해당 component가 실행되기 전/후에 대한 작업을 수행할 수 있다.

after_aop

또한, Spring 에서는 이런 Spring Bean에 대한 Configuration을 자동으로 설정해 두었기 때문에 이런 proxy 같은 component들이 자동적으로 삽입된다.

References

link: inflearn

link:jpa

link: aop

댓글남기기