상세 컨텐츠

본문 제목

[스프링 1팀] 스프링 입문 섹션 7~8

24-25/Spring 1

by oze 2024. 11. 8. 10:00

본문

728x90

 

스프링 DB 접근 기술

H2 데이터베이스

: 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공

 

📌 설치 방법

  1. https://www.h2database.com에서 All Platforms 다운로드 및 설치
    + 설치 이후 맥 사용자는 chmod 755 h2.sh로 권한 주기
  2. 실행 : 맥 - ./h2.sh, 윈도우 - ./h2.bat
    + DB가 정상 생성되지 않는 경우  주소창 8082 앞을 localhost로 변경해서 실행

📌 데이터베이스 파일 생성 방법

  1. JSBC URL에 jdbc:h2:~/test 작성 후 연결
  2. ~/test.mv.db 파일 생성 확인
  3. 이후 JSBC URL은 jdbc:h2:tcp://localhost/~/test로 접속
    ⇒ 파일로 접속 시 에플리케이션과 웹 콘솔이 같이 접근이 안될 수 있기 때문에 변경이 필요

테이블 생성하기

: 테이블 관리를 위해 sql/ddl.sql 파일 생성

// H2 데이터베이스에 접근해서 member 테이블 생성

drop table if exists member CASCADE; 
create table member
(
	id bigint generated by default as identity, 
    name varchar(255),
	primary key (id)
);

 

+ generated by default as identity → ID 값이 없는 경우 DB에서 자동으로 값을 넣어주도록 설정

 

순수 Jdbc

 

환경 설정

  • build.gradle 파일 : jdbc, h2 데이터베이스 관련 라이브러리 추가
  • application.properties 파일 : 스프링 부트 데이터베이스 연결 설정 추가 
// build.gradle 파일
implementation 'org.springframework.boot:spring-boot-starter-jdbc' // 자바가 DB와 연동하려면 jdbc드라이버 필요
runtimeOnly 'com.h2database:h2' // DB에서 제공하는 클라이언트


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

 

스프링 설정 변경

@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 MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}
  • DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체
    ⇒ 데이터베이스 커넥션 정 보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어야지 의존성 주입 받음

  • 개방-폐쇄 원칙( OCP ) → 확장에는 열려있고, 수정, 변경에는 닫혀있음
  • 스프링 DI 사용 → 기존 코드 변경 없이 설정 코드만 수정해 구현체 변경 가능

 

스프링 통합 테스트

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    // 다른 곳에서 사용하지 않기 때문에 필드 기반으로 Autowired 받아도 됨
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void join() {
        // given
        Member member = new Member();
        member.setName("hello");

        // 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);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}
  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행
  • @Transactional : 테스트 시작 전에 트랜잭션을 시작, 테스트 완료 후에 항상 롤백 진행
    ⇒  DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않음

 

스프링 Jdbc Template

특징

  • 환경 설정은 Jdbc와 동일 
  • 반복 코드 대부분 제거
  • 단, SQL은 직접 작성해야 함
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

	// @Autowired // 생성자가 하나인 경우만 삭제 가능
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        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(), 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;
        };
    }
}

 

JPA

특징

  • 반복 코드 제거 + 기본적인 SQL JPA가 직접 실행
    ⇒ SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임 전환
  • 개발 생상성 높아짐

 

환경 설정

  • build.gradle 파일 : JPA, h2 데이터베이스 관련 라이브러리 추가
  • application.properties 파일 : 스프링 부트에 JPA 설정 추가
    + JPA는 내부에 jdbc 관련 라이브러리를 포함하여 jdbc는 제거
// build.gradle 파일

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
//	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}


// resources/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 // JPA가 생성하는 SQL을 출력
spring.jpa.hibernate.ddl-auto=none // JPA는 테이블을 자동으로 생성하는 기능을 제공 → none으로 기능 끔

 

 

JPA 엔티티 매핑

@Entity
public class Member {
    private Long id;
    private String name;

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    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;
    }

}
  • @Entity : JPA가 관리하는 엔티티를 의미
  • GenerationType.IDENTITY : DB가 자동으로 ID 매핑

 

JPA 회원 리포지토리

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

   // PK기반이 아난 경우에는 직접 작성해야 함
    @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();
    }
}

 

📌 JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 하기에 서비스 계층에 트랜잭션 추가

⇒ 메서드를 실행할 때 트랜잭션을 시작, 정상 종료 시 트랜잭션 커밋 / 런타임 예외 발생 시 롤백

 

import org.springframework.transaction.annotation.Transactional

@Transactional 
public class MemberService {}

 

 

JPA를 사용하도록 스프링 설정 변경

@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 MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

 

 

스프링 데이터 JPA

 

특징

  • JPA와 설정 동일 → 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술
  • 구현 클래스 없이 인터페이스 만으로 개발을 완료
  • 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공
    ⇒ 단순하고 반복이라 생각했던 개발 코드가 줄어들어 개발자가 핵심 비즈니스로직 개발에 집중할 수 있음

 

스프링 데이터 JPA 회원 리포지토리

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Override
    Optional<Member> findByName(String name);
}
  • JpaRepository : 기본 메서드 제공

📌 구현 코드 없이 SpringDataJpaMemberRepository  인터페이스만 생성

⇒ 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록

 

스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경

@Configuration
public class SpringConfig {
    private final MemberRepository memberRepository;

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

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

 

 

 

제공 클래스
→ 공통화 가능한 코드 모두 제공

 

제공 기능

  • 인터페이스를 통한 기본적인 CRUD
  • findByName(), findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공

 

AOP

AOP가 필요한 상황

" 회원 가입 시간, 회원 조회 시간을 측정하고 싶다면? "

AOP 적용 전으로 각 메소드마다 시간 측정 로칙 추가

@Transactional
public class MemberService {

    /**
     * 회원 가입
     */
    public Long join(Member member) {

        long start = System.currentTimeMillis();

        try {
            validateDuplicateMember(member); //중복 회원 검증
            memberRepository.save(member);
            return member.getId();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("join " + timeMs + "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 적용

: Aspect Oriented Programming

 

📌공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리

 

시간 측정 AOP 등록

@Aspect
@Component
public class TimeTraceAop {
    // 하위 모두 적용
    @Around("execution(* hello.hello_spring..*(..))\n")// 하위 모두 적용
//    @Around("execution(* hello.hello_spring.service..*(..))\n") // 서비스만 진행하는 경우
    public Object excute(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");
        }
    }
}

 

해결

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

 

스프링의 AOP 동작 방식 설명

 

📌 진짜 맴버 서비스가 아닌 Proxy라는 기술이 만들어내는 가짜 멤버 서비스와 연결 ⇒ Proxy 방식의 AOP

 

의존 관계

AOP 적용 전 / AOP 적용 후

 

전체 그림

AOP 적용 전 / AOP 적용 후


Quiz

  1. ( 스프링의 DI )을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
  2. ( @Transactional )는  DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
  3. ( 스프링 JdbcTemplate )는 반복 코드를 대부분 제거해주지만 SQL은 직접 작성해야한다.
  4. ( JPA )는 객체 중심의 설계로 패러다임을 전환할 수 있으며 반복 코드 제거와 기본적인 SQL도 실행해준다.
  5. 스프링 데이터 JPA는 구현 클래스 없이 ( 인터페이스 ) 만으로 개발을 완료할 수 있다.
  6. AOP 적용으로 ( 공통 관심 사항 )과 ( 핵심 관심 사항 )을 분리한다.
  7. ( Proxy )방식의 AOP진짜 맴버 서비스가 아닌 ( Proxy )라는 기술이 만들어내는 가짜 멤버 서비스와 연결한다.

 

Programming Quiz

  1. JPA 엔티티 매핑을 위해 아래 멤버 클래스의 빈칸을 완성해주세요. ( ID의 경우 데이터베이스가 자동으로 id 값을 생성하도록 작성해주세요. )
// 빈칸
public class Member {
    private Long id;
    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;
    }

}

 

정답 : 

더보기

@Entity
public class Member {
    private Long id;
    private String name;

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    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;
    }

}

 

2. 다음 코드에서 hello.hello_spring의 하위 모두 적용이 아닌 서비스만 적용하도록 빈칸을 완성하세요. 

@Aspect
@Component
public class TimeTraceAop {
    
    @Around("execution(// 빈칸 )\n")
    public Object excute(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");
        }
    }
}

 

정답 : 

더보기

@Aspect
@Component
public class TimeTraceAop {
    
    @Around("execution(* hello.hello_spring.service..*(..))\n")
    public Object excute(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");
        }
    }
}


출처: 스프링 입문, 코드로 배우는 스프링 부트, 웹MVC, DB접근기술 강의
Corner Spring 1
Editor:  Luna

728x90

관련글 더보기