스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - Jdbc, JdbcTemplate JPA, AOP(easy)

jkky98·2024년 6월 21일
0

Spring

목록 보기
4/77

데이터 베이스 적용

  • H2 DataBase 적용
  • 스프링 부트 데이터베이스 연결 설정 추가
// resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

리포지토리 개선

현재 실습에서 사용 중인 리포지토리는 MemberRepository 인터페이스를 구현한 MemoryMemberRepository이다. 이 구현체는 DB나 파일을 사용하지 않고, 자바의 Map 객체에 데이터를 저장한다.

이로 인해 데이터 휘발성이 매우 높아, 스프링 애플리케이션을 재시작할 때마다 저장된 데이터가 모두 초기화된다. 이러한 특성은 간단한 테스트나 학습용으로는 적합하지만, 실무에서는 데이터의 영속성을 보장하기 위해 DB파일 저장소로 대체해야 한다.

DB를 사용하도록 변경하기 위해 H2 데이터베이스를 설정하였다. 이를 위해 H2를 다운로드한 뒤, application.properties 파일에 다음 항목들을 추가하여 H2 데이터베이스를 연결하고 드라이버를 등록하였다:

  • datasource.url: H2 데이터베이스 연결 설정.

MemberRepository 인터페이스 구현

MemberRepository 인터페이스를 기반으로, 새로운 구현체인 JdbcMemberRepository를 작성하였다.
이 클래스는 H2 데이터베이스를 저장소로 사용하며, 기존의 MemoryMemberRepository와 동일한 역할을 수행한다.


JdbcMemberRepository 주요 내용

  • 데이터베이스 연동을 위해 save, findById, findByName, findAll 메서드를 구현한다.
  • 이 메서드들은 MemberRepository 인터페이스의 메서드를 오버라이딩하여 데이터베이스에 데이터를 저장하고 조회하는 로직을 작성한다.
  • 해당 구현은 20년이 넘은 전통적인 기술로, 코드가 길고 복잡할 수 있어 이 포스팅에서는 코드 세부 내용을 생략한다.

H2를 사용한 설정과 JdbcMemberRepository 구현을 통해, 기존의 메모리 기반 저장소에서 데이터베이스 기반으로 전환하였다.

MemberRepository를 Bean에 올리는 SpringConfig파일을 수정해주어야 한다.

@Bean
    public MemberRepository memberRepository() {
        // return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }

실제로 작성된 코드에서 JdbcMemberRepository만 구현하고 리포지토리 설정 부분만 변경하면, 기존 MemoryMemberRepository를 손쉽게 교체할 수 있다. 이는 MemberRepository 인터페이스 아래에서 구현되었기 때문에 가능하며, 코드 수정 없이 구현체를 교체할 수 있는 OCP(Open-Closed Principle)를 준수한다. 또한, 스프링의 DI(Dependency Injection)를 활용하여 구현 클래스를 유연하게 변경할 수 있다.

JdbcMemberRepositoryDataSource가 필요하다. DataSource는 데이터베이스 커넥션을 획득하고 관리하는 객체로, 데이터베이스와의 연결을 담당한다.

스프링 부트는 DataSource를 자동으로 빈으로 등록하며, 이를 활용하기 위해 JDBC 의존성을 추가하면 필요한 설정이 자동으로 적용된다. 이를 통해 JdbcMemberRepository는 데이터베이스와의 연결을 효율적으로 관리할 수 있다.

스프링 통합 테스트

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional // test실행 끝나고 롤백해줌.
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("hello");
        //when
        Long saveId = memberService.join(member);

        //then
        Member findMemeber = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMemeber.getName());
    }
    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);
        // 람다 처럼 했을 때 해당 예외가 터져야해!
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        //thenx(Assert)
    }

}

@SpringBootTest

이 애노테이션을 사용하면 통합 테스트를 진행할 수 있다. 스프링 컨텍스트를 로드하여 Spring Boot 애플리케이션 전체 환경을 실제 실행 환경과 유사하게 테스트할 수 있도록 지원한다. 이는 주로 애플리케이션의 전반적인 통합과 상호작용을 검증하는 데 사용된다.

다만, 스프링 전체를 로드하므로 일반적인 단위 테스트보다 테스트 속도가 느리다는 단점이 있다. 잘 설계된 테스트는 여러 단위 테스트를 조합하여 통합 테스트로 확장하며, 이를 통해 애플리케이션의 신뢰성을 높일 수 있다.

@Transactional

테스트 시 데이터 생성과 삭제를 처리하기 위해 이전에는 @AfterEach를 사용하여 데이터 초기화를 구현했다. 그러나 @Transactional을 사용하면, 각 단위 테스트가 종료된 후 자동으로 저장소 상태를 롤백하여 초기화할 수 있다. 이를 통해 데이터 일관성을 유지하며, 테스트 간 상태가 독립적으로 관리될 수 있다. @Transactional은 테스트 코드의 간결성과 유지보수성을 크게 향상시킨다.

JPA

JPA는 반복적인 코드 작성과 기본적인 SQL 작성을 자동으로 처리해준다. 이를 통해 개발자는 비즈니스 로직에만 집중할 수 있으며, 데이터베이스 연동 작업이 훨씬 간편해진다.

spring-boot-starter-data-jpa 의존성을 Gradle 설정에 추가하여 JPA를 사용하도록 설정할 수 있다. 이후, 도메인 클래스(Member)를 JPA 엔티티로 동작할 수 있도록 필요한 설정을 추가해야 한다. 이를 통해 JPA가 Member 클래스와 데이터베이스 테이블 간의 매핑을 관리하고, 기본적인 CRUD 작업을 자동으로 처리한다.

package hello.hello_spring.domain;


import jakarta.persistence.*;

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;

    private String name;

    public Long getId() {
        return Id;
    }

    public void setId(Long id) {
        Id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Member 클래스에 @Entity 어노테이션을 추가하면 JPA가 해당 클래스를 데이터베이스 테이블과 연결한다. 이는 모델 객체로서 동작하는 과정이다.

@Id 어노테이션은 해당 속성이 데이터베이스 테이블의 Primary Key임을 나타내며, @GeneratedValue(strategy = GenerationType.IDENTITY)를 함께 사용하면 JPA가 Primary Key 값을 데이터베이스에서 자동으로 생성하도록 설정한다.
GenerationType.IDENTITY자동 증가(Auto Increment) 전략을 사용해 데이터베이스가 고유한 ID를 생성하도록 JPA에 알린다.

JpaMemberRepository

JPA를 사용하는 새로운 MemberRepository 구현 클래스를 작성한다. JPA를 사용하려면 EntityManager가 필요하다.

Gradle에 JPA 의존성이 올바르게 등록되어 있다면, 스프링은 EntityManager를 자동으로 Bean으로 관리한다. 이를 리포지토리 클래스의 생성자에 주입하여 사용할 수 있다.

EntityManager는 JPA의 핵심 인터페이스로, 엔티티와 데이터베이스 간의 상호작용(저장, 조회, 삭제 등)을 관리하며, 이를 통해 JPA 기능을 활용할 수 있다.

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;

import javax.swing.text.html.parser.Entity;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository{

    private final EntityManager em;

    @Autowired
    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 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() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

코드가 jdbc, jdbcTemplate을 사용할 때 보다 매우 짧아진 것을 볼 수 있다. 쿼리문의 사용이 줄었으며 많은 ORM다운 메서드를 지원한다.

스프링 데이터 JPA

JPA라는 ORM 기술을 사용함에도 불구하고, 여전히 쿼리문과 유사한 코드가 일부 작성되었다. 그러나 Spring Data JPA를 사용하면 JPA를 더욱 간편하게 활용할 수 있다.

Spring Data JPA는 기존의 한계를 넘어, 리포지토리의 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있도록 지원한다. 이를 통해 개발자는 데이터 접근 계층을 빠르고 효율적으로 작성할 수 있으며, 반복적인 코드 작성과 유지보수의 부담을 줄일 수 있다.

package hello.hello_spring.repository;

import hello.hello_spring.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);
}

이렇게만 생성한 후에

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

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

이렇게만 셋팅해주면 끝난다.

이렇게란, 일단 다른 구현체처럼 일반적인 클래스 아래에 다시 오버라이딩 함수들을 작성하지 않아도 되며 save, findAll, findById를 구현하지 않았다. 이미 내장된 함수들을 사용할 수 있다.(findByName의 경우 특수한 경우이므로 직접 만들어야 한다.)

AOP

만약 실무에서 "모든 메서드의 실행 시간을 측정하라"는 요구를 받았다고 가정해보자. 프로그램 내 메서드가 1000개라면, 모든 메서드에 실행 시간을 측정하는 코드를 직접 작성해야 하며, 이는 엄청난 반복 작업과 유지보수의 어려움을 초래한다.

그러나 AOP(Aspect-Oriented Programming)를 활용하면, 실행 시간 측정 로직을 하나의 공통 관심사(Aspect)로 분리하여 정의할 수 있다. 이를 통해 1000개의 메서드에 별도 코드를 작성하지 않고도 실행 시간을 측정하는 기능을 전역적으로 적용할 수 있다. AOP는 이러한 횡단 관심사를 해결하는 데 최적화된 기술이다.

다음의 경로에 aop 디렉터리를 만들고, TimeTraceAop 클래스에 시간을 재는 메서드를 만든다.

package hello.hello_spring.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.hello_spring..*(..))")
    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("START: " + joinPoint.toString() + " " + timeMs + "ms");
        }

    }
}

TimeTraceApp을 스프링 Bean으로 등록하고, @Aspect를 추가하여 AOP 기능을 활성화한다. @Aspect를 통해 스프링이 AOP 적용을 위한 환경을 설정하고 관리한다.

JoinPoint는 스프링이 AOP 메서드에 전달하는 객체로, 해당 메서드 실행 지점의 정보를 포함한다. 이를 활용해 메서드 실행 전에 로직을 추가하거나, 실행 결과를 처리할 수 있다.

AOP 메서드는 다음 동작을 수행한다:
1. 원래 메서드 호출: proceed()를 호출하여 원래 메서드를 실행한다.
2. 결과 반환: 원래 메서드의 실행 결과를 반환한다. 이 과정에서 메서드의 실제 실행이 이루어진다.

@Around 어노테이션은 AOP가 동작할 범위(Pointcut)를 지정한다. 예를 들어, hello_spring 내의 모든 메서드에 TimeTraceApp 로직을 적용하고 싶다면, @Around를 사용해 그 범위를 설정할 수 있다. 이를 통해 특정 패키지나 클래스에 일괄적으로 AOP 기능을 적용할 수 있다.


실제로 AOP의 작동원리는 다음과 같다.

profile
자바집사의 거북이 수련법

0개의 댓글