스프링 입문 - 2

개발자가 되고 싶어요 ㅣ 2024. 1. 22. 03:42

스프링 입문 - 2

이번에 배울 것은 어플리케이션 서버에서 디비에 접근하는 기술이다.

디비에 접근하는 방법은 여러 개가 있다.

첫번째로는 순수 Jdbc

두번째로는 Jdbc Template (스프링이 순수 jdbc에서 중복을 제거하여 제공하는 기술)

세번째로는 JPA (객체를 쿼리없이 정해진 함수로 저장가능)

네번째로는 Spring Data JPA (JPA를 더 쉽고 간편하게 사용할 수 있게 나온 인터페이스)

H2 설정법

강의에서는 h2디비를 이용하지만, 나는 MySQL을 이용할 것이다.

처음 디비를 생성하고 내 도메인에 맞는 테이블을 생성해줘야한다.

Id 값은 id bigint generated by default as identity 라고 생성하는데, 이때 generated ~~~는

id값에 null이 들어가면 안되기 때문에 값을 세팅하지 않고 인서트하게 되면 디비가 자동으로 아이디값을 세팅해준다. Pk도 id로 세팅해줘야 하는데 primary key (id) 로해준다.

프로젝트에 sql이라는 패키지를 만들어 ddl.sql파일을 두고 sql에 사용한 ddl들을 정리해준다.

나중에 깃이나 이런 소스 버전을 쓰게 되면 같이 관리가 되기 때문이다.

MySQL 연동 과정

우선 build.gradle에 들어가서 mysql 의존성을 추가해준다.

implementation 'com.mysql:mysql-connector-j'

그리고 나서 application.properties에 들어가서 mysql에 대한 설정을 해준다.

spring.datasource.(url, driver-class-name, username, userpassword) 를 각각 적어줬다.

그리고 테이블 생성 시 h2에서는 id에 big int와 generated by~~ 를 사용했지만

mysql에서는 int와 not null auto_increment를 사용했다.

그리고 우리는 jdbc를 사용해 볼 것이기 때문에 build.gradle에

Implementation ‘org.springframework.boot:spring-boot-starter-jdbc’

를 넣어서 의존성을 추가해준다.

Jdbc를 사용해서 디비에 접근하기위해 이 드라이버가 꼭 필요하기 때문이다.

자 설정을 다하고 실행했는데

java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver 라는 오류가 발생했다.

흠…. mysql의 버전을 찾아 넣어주기도 했는데 현재 스프링 버전에서는 -connector-j를 넣어주는게 맞다고 한다. 서칭을 하며 시간을 보내던 그때 connetor…? 이게 무슨…. 오타였다…. connector로 바꿔주니 정상실행되었다. 휴;;;

하는김에 runtimeOnly와 implementation의 차이점을 찾아봤는데 쉽게 말해서

runtimeOnly런타임 때 라이브러리를 가져다 쓰는거고

implementation컴파일,런타임 둘 다 라이브러리를 가져다 쓰는거였다.

그럼 당연히 compileOnly도 있다. 물론 컴파일 때 라이브러리를 가져다 쓰는거다.

이는 빌드 속도성능에 관여한다. 우리가 작성한 mysql 커넥터에 관한 의존성 추가는 런타임때 디비에 접근하며 작동하는것이기 때문에 runtimeOnly로 바꿔주도록 하겠다.

이제 우리는 새롭게 jdbc를 이용해 디비에 접근할 것이기 때문에 기존 MemoryMemberRepository는 사용할 필요가 없어졌다.

새롭게 JdbcMemberRepository를 구현해주도록하자. 이때 이미 MemberRepository라는 인터페이스를  만들어놨기 때문에 상속받아 각 메소드를 새롭게 구현시켜주기만 하면된다.

그리고 Service는 MemberRepository에게만 의존하고 있기 때문에 스프링 빈에 등록되어 있는 MemoryMemberRepositroy에서 JdbcMemberRepository로만 갈아 끼워주면 완성된다!

이것이 객체 지향 프로그래밍인가….? 일단 순수 jdbc로 리포지토리를 구현해보도록 한다.

먼저 리포지토리 구현체에 DataSource라는 클래스에서 객체를 만들어 생성자 주입을 시킨다.

이때 DataSource 라는 것은 아까 application.properties에서 작성한 datasource를 스프링부트가 받아와서 만들어놓은 클래스??? 라고 일단 대충 이해하고 넘어가면 된다고 하신다.

순수Jdbc는 어어어어엄청 코드가 길다…

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// 우리가 Service에서 사용하고 있는 리포지토리는 MemberRepository이다.
// 기존 임시db를 사용해서 구현시킨  MemoryMemberRepository에서 MySQL을 사용한 리포지토리로 변경하고 싶다면
// 스프링 빈에 등록되어 있는 MemoryMemberRepository를
// 새로 작성한 JdbcMemberRepositroy로 갈아 끼워주기만 하면된다.
public class JdbcMemberRepository implements MemberRepository{

    private DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // 데이터소스에서 계속 새로운 커넥션을 가져올 수 도 있지만, 데이터소스유틸스를 통해서 커넥션을 가져오는 이유는 이전 트랜잭션에 걸리지 않고
    // 똑같은 커넥션을 유지시켜주기 위함이다.
    private Connection getConnection(){
        return DataSourceUtils.getConnection(dataSource);
    }

    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
        try{
            if(rs!=null) {
                rs.close();
            }
        }catch(SQLException e) {
            e.printStackTrace();
        }
        try {
            if(pstmt!=null){
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if(conn!=null) {
                close(conn);
            }
        } catch(SQLException e){
            e.printStackTrace();
        }
    }

    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }

    @Override
    public Member save(Member member) {
        //1. 일단 기능에 맞는 쿼리문을 작성해주고
        String sql = "insert into member(name) values(?)";
/*
        //2. 데이터 소스에서 커넥션을 갖고 오고
        Connection conn = dataSource.getConnection();

        //3. 커넥션의 preparedStatement에다가 sql문과 어떠한 것을 넣어준다.
        PreparedStatement psmt = conn.prepareStatement(sql, );
        //4. 그리고 sql문에서 ? 값에 넣어줄 파라미터를 넣어준다.
        psmt.setString(1,member.getName());
        //5. 쿼리문을 날려준다.
        psmt.executeUpdate();
*/      // 수도 코드 말고 강의자료를 복붙한 내용을 하나하나 적어가며 이해해보록 하겠다.
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            // 1. 커넥션 가져오기
            conn = getConnection();
            // 2. preparestatement에 sql문 넣어주고, statement.return~~ 는 디비에서 auto_increment로 인해 생성된 id값을 가져오는 함수이다.
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

            // 3. sql문의 ?와 매칭되어 값을 넣어주는 함수이다.
            pstmt.setString(1,member.getName());

            // 4. 준비된 쿼리문을 디비에 날려준다.
            pstmt.executeUpdate();

            // 5. 2번에서 작성한 statement.return~~에서 가져온 id값을 반환해주는 함수이다.
            rs = pstmt.getGeneratedKeys();

            // 6-1. 만들어놓은 rs에 값이 있다면?
            if(rs.next()) {
                member.setId(rs.getLong(1));
            } else { //6-2 값이 없다면
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch(Exception e) {
            throw new IllegalStateException(e);
        } finally{
            // 사용한 리소스를 역순으로 닫아줘야 하는데 만약 닫지 않으면 커넥션이 계속 쌓이다가 엄청난 대장애가 올 수 있다.
            close(conn,pstmt,rs);
        }
    }

    @Override
    public Optional<Member> findById(Long id) {
        String sql = "select * from member where id = ?";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn=getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1,id);

            // update가 아니고 query인 이유는 값을 받아와야 하는 메서드이기 때문인듯 하다.
            rs = pstmt.executeQuery();

            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public Optional<Member> findByName(String name) {
        String sql = "select * from member where name = ?";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn=getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1,name);

            // update가 아니고 query인 이유는 값을 받아와야 하는 메서드이기 때문인듯 하다.
            rs = pstmt.executeQuery();

            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public List<Member> findAll() {
        String sql = "select * from member";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();

            List<Member> members = new ArrayList<>();

            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                members.add(member);
            }

            return members;
        } catch(Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn,pstmt,rs);
        }
    }
}

새로보는 Connection, PrepareStatement, ResultSet 들이 등장한다.

커넥션으로 소켓을 통한 디비와의 길을 열어주고..?

prepareStatement에 sql문을 넣어주고, db에서 auto increment될 id값 또한 신경 써준다.

이제 sql문에 임시 변수로 넣어둔 ? 값에 매칭 될 값을 코드에서 넣어주는 과정도 거치고

prepareStatement를 디비에 날려준다.

위에서 auto increment 된 id값을 반환받아와서 rs에 해당 데이터를 가져와주고

rs에 값이 있다면(디비에 잘 생성 되었다면) 코드에서 만든 새로운 멤버객체에 넣어준다.

없다면 sqlExcetion을 통해 실패했다고 날려주고 등등의 코드들을 try catch 문법으로 엄어어어어엄청 길게 작성한다. 가장 중요한 것은 마지막에 finally를 통해 열었던 리소스들(conn, pstmt, rs)을 역순으로 닫아주는 과정을 거친다. 일단 대충 과정은 이러하다.

스프링 DI

그리고 이제 앞서 말했던 repository를 갈아 끼우는 장면이 나온다.

스프링 프레임워크의 장점은 다형성을 활용한다. 예를 들어 인터페이스를 두고 구현체를 바꿔 끼우기를 할 수 있다. 스프링은 이것을 굉장히 편리하게 되도록 스프링 컨테이너가 이걸 지원해준다. 그리고 di 덕분 또한 이것을 편리하게 하게 만들어준다. 우리가 리포지토리를 새롭게 만들고 생성했지만 스프링 빈으로 등록만 시켜주면 리포지토리를 의존하는 서비스단위에서 전혀 코드의 수정이 이루어지지 않아도 된다. 현재 우리 예제로는 멤버리포지토리를 의존하는 서비스가 단 한개여서 와닿지 않을 수 있지만, 큰 단위의 프로젝트를 상상해본다면 여러 서비스에서 이 리포지토리를 의존할 것이다. 이때 우리는 서비스의 코드를 전혀 손대지 않고도 구현체를 딸깍 바꿔 끼우기만 하면 된다.

이 과정을 객체지향의 원칙인 SOLID 중 OCP(Open-closed principle) 개방-폐쇄 원칙

이라고 한다. 스프링의 DI를 사용하면 “기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경” 할 수 있다.

테스트

이제 테스트도 디비까지 연결해서 실제 작동하는 통합테스트를 작성해 보겠다.

일단 @SpringBootTest를 넣어주고, @Transactional는 주석으로 넣어준다.

@Transactional이 뭔지는 뒤에서 설명해준다.

기존에는 스프링 빈에서 객체를 가져와서 생성자 주입을 시키지 않았다.

하지만 이제는 스프링을 이용해서 테스트 해야하는 통합 테스트이기 때문에 @Autowired를 사용해준다. 그리고 테스트는 제일 끝단에 있기 때문에 그냥 가장 편한방법으로 만들어줘도 된다. 고로 필드주입식으로 서비스와 리포지토리를 생성해준다. 그렇게 기존 작성했던 테스트코드를 그대로 작동시켜보면 그 전의 그냥 코드자체에 에러가 있는지 없는지 테스트하던 것과는 다르게 이번에는 스프링 컨테이너와 테스트가 함께 실행된다. 이게 바로 @SpringBootTest의 역할인 것 같다. 테스트에서도 스프링 빈에 등록된 객체를 이용할 수 있게 되고, 연동시킨 db를 사용 할 수 있게 된다.

근데 테스트는 반복해야 테스트의 의미가 있는데, 현재 작동 시켜보면 한 번 이상 실행이 안된다. 왜냐하면 예를 들어 회원가입 기능을 테스트 중인데, 테스트를 실행시키면 디비에 이미 테스트코드에서 작성한 이름으로 회원가입이 완료되어 반영되어 있기 때문에 두번째 회원가입에서는 중복회원검증으로 인해 에러가 발생하는 것이다. 이때 필요한 것이 @Transactional이다.

디비에는 트랜잭션이라는 개념이 있어서 어떠한 데이터를 인서트나 딜리트하더라도 커밋하기 전까지는 디비에 반영이 되지 않는다. @Transactional은 이 테스트케이스에 붙었을 때만 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백시켜준다. 그 결과 테스트가 끝나면 디비에는 아무 결과도 반영되어있지 않다.

순수한 자바코드를 테스트하기 위해서는 @SpringBootTest를 떼고 해주면 된다. 스프링을 같이 실행시키는게 아니기 때문에 실행시간이 매우 빠르다. 이런 것을 단위 테스트라고 한다. 그리고 이러한 단위테스트가 더 좋은 테스트일 확률이 높다고 한다. 최대한 통합테스트를 지양해야 한다고 하신다.

이번에는 Spring Jdbc Template이다. mybatis와 비슷한 라이브러리라고 하신다. jdbc api에서 본 반복 코드를 대부분 제거해준다. 하지만 sql문은 직접 작성해야한다. 환경설정은 기존 순수jdbc와 같다.

jdbcTemplate이라는 클래스를 사용하는데 query라는 함수를 사용한다. query(sql문,memberRowMapper(),sql문에 들어갈 파라미터)를 사용하면 sql문에 해당하는 데이터들이 튀어나온다. 이때 memberRowMapper라는 함수는 RowMapper라는 클래스에서 나오는데, 얘가 resultset으로 나온 디비의 데이터 값을 자바의 객체로 바꿔주는 역할을 하는 것 같다. 이는 따로 생성하여 오버라이드 해서 만들어 줘야 한다. maprow라는 함수를 오버라이드 해야하는데 여기에 순수jdbc에서 사용한 resultset이 들어간다. 근데 뭐… 작동원리나 함수구성에대한 내용은 자세히 모르겠다. 검색해보고 와야겠다.

maprow는 우선 반환받을타겟객체 mapRow(ResultSet rs, int rownum) throws SQLException 으로 구성된다.

이제 이 메서드 안에서 타겟객체.set(rs.get타입(“객체인스턴스”)) 를 통해 하나하나 객체를 만들어가주면 된다.

save함수를 사용하는 인서트문은 코드가 조금 긴데 한번 살펴보면

SimpleJdbcInsert라는 클래스를 사용한다. jdbcTemplate을 매개변수로 넣어서 jdbcInsert라는 객체를 생성시키는데, jdbcInsert.withTableName(“db의 테이블 명”).usingGeneratedKeyColumns(“기본키 이름”)을 넣어주고, 파라미터를 해쉬맵객체로 만들어서 컬럼명과 넣어줄 값을 키와 밸류로 파라미터에 넣어서 만들어준다. 그 뒤에 key값을 뽑아내어 멤버의 아이디에 넣어준다. 나에겐 너무 복잡하다… 뭔가 새로운 클래스와 함수가 많다. 뭐 그냥 이렇게 하는구나 로 일단 알고 넘어가기로 했다.

jdbcTemplate은 기존 순수 jdbc와 비교해보면 예외처리라던지 엄청난 양의 중복 코드 등을 제거해준다. 하지만 여전히 sql문을 작성해야 하고 rs를 통해 가져온 디비의 데이터를 객체로 변환시켜줘서 하나하나 set해줘야 하는 과정을 거친다. 이러한 과정 조차도 단축시켜주는 jpa를 배워볼것이다.

JPA

JPA란 기존의 반복코드는 물론이고, 기본적인 sql도 jpa가 직접 만들어서 실행해준다.

jpa를 사용하면, sql과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.

jpa를 사용하면 개발 생산성을 크게 높일 수 있다.

우선 build.gradle에서 기존 jdbc를 주석처리하고 implementation 'org.springframework.boot:spring-boot-starter-data-jpa'를 넣어준다. 여기에 jdbc도 포함하고 있다고 한다.

그리고 나서 application.properties에 가서 jpa가 날려주는 쿼리문을 볼 수있게 spring.jpa.show-sql=true를 넣어주고, 우리는 이미 디비에 테이블을 만들어 놨기 때문에 jpa가 자동으로 테이블을 생성해주는 기능을 끄기 위해 spring.jpa.hibernate.ddl-auto =none 을 설정해준다.

jpa는 인터페이스다. 그 구현체로 hibernate, eclipse?? 가 있다고 하는데 우리는 그 중 hibernate만 사용한다고 보면 된다고 한다.

jpa를 사용하려면 객체에 @Enitity 애노테이션을 넣어줘야 한다. jpa는 orm 기술이다. 객체와 관계형데이터베이스를 매핑해주는 기술이다. 그렇기 때문에 객체에 @Entity를 넣어서 객체를 관리하게 한다. pk인 id에는 pk임을 알려주기 위해서 @Id를 넣어준다. 그리고 디비가 자동으로 id를 생성해주는 것을 identity전략이라고 한다. 따라서 @GeneratedValue(strategy=GenerationType.IDENTITY)를 넣어준다. 만약 컬럼명이 B인데 내가 코드로 짠 객체의 이름이 A이라면, 이것을 매핑하기 위해서 A객체 위에 @Table(“B”)라고 작성해준다. 그럼 jpa가 A객체를 B칼럼에 매핑해준다.

자 다음은 jpa로 리포지토리를 생성한다. 여기서 EntityManager라는 것을 생성해줘야하는데 이 클래스는 build.gradle에서 jpa를 가져올 때 jpa가 생성해준 클래스다. 이 EntityManager는 우리가 전에 jdbc에서 사용한 커넥션이나 datasource 같은 클래스를 모두 내부적으로 조합해놓은 클래스이다.

save함수는 em.persist(객체) 하나로 끝난다….

findbyid는 em.find(객체.class, id)로 끝난다…..

id를 제외한 즉 pk값을 제외한 나머지 조회는 em.createQuery(“쿼리문”,객체.class)를 사용하는데 이때 이 쿼리문이 조금 특별하다. jpql이라는 쿼리 언어인데 테이블을 대상으로 쿼리를 날리는 것이 아닌 엔티티를 대상으로 쿼리를 날리면 sql문으로 해석된다고 한다. 그렇기 때문에 쿼리문의 select 부분이 조금 특별해진다. 보통은 전부를 조회하고 싶다면 select * 이여야 하는데 만약 대상 객체가 member라면 전체를 조회하든 하나를 조회하든 select member로 시작한다. 그래서 from 절에 별칭을 쓰는가부다. 예) select m from member m ~~~

그렇기 때문에 원래는 디비에서 값을 가져오면 뭔가 이거를 다시 객체에 매핑하는 과정이 필요하지만 jpa에서 jpql을 사용하면 객체 그대로를 반환해온다. 미츼인…

근데 Spring Data Jpa를 사용하면 이 jpql문 조차도 사용 안해도 된다고 한다…..

이건 다음시간에 알아보도록 하고 일단 다시 jpa로 돌아간다.

항상 jpa를 사용하려면 이 @Transactional 애노테이션이 필요하다고 한다.

전에 배웠던 @Transactional을 생각해보면 이건 테스트 케이스에서 사용했던 애노테이션이다.

하나의 테스트마다 트랜잭션을 만들어서 실행하고 종료될 때 롤백해주는 역할을 했던걸로 기억한다. 이 애노테이션을 항상 데이터베이스가 변경되거나 수정될 때 사용해줘야 한다고 한다. 그래서 디비에 변동이 생기는 순간을 다루는 service 계층에 @Transactional을 넣어준다.

그리고 이제 테스트 해보기 위해서 Config 파일에 가서 또 한번 리포지토리를 갈아 끼워준다.

근데 이때 기존의 jdbc에서는 datasource를 주입시켜주기위해 생성해놨었는데, jpa는 EntityManager가 필요하기 때문에 datasource는 주석처리해주고 entitiymanager를 생성해주고 주입시켜준다.

이제 테스트를 실행시켜준다. 근데 분명 강의를 따라했는데 오류가 생겼다. 뭐가 문제지…

Could not resolve root entity 'member' 뭔가 member를 찾을 수 없다는 거 같은데 뭐지…

하다가 구글링을 통해 발견했다. jpql을 잘못 작성했을 때 즉, 객체와 연결이 안될 때 발생하는 문제라고 한다. 난 분명 잘 작성 했는데… 싶었는데 클래스로 생성된 객체는 Member라는 이름을 가진다. 근데 나는 jpql문에 member라고 작성했다. 아니 근데 sql문은 대소문자를 구분하지 않는걸로 알고 있었는데…. jpql은 엔티티 이름을 엔티티와 똑같이 적어주어야 한다고 한다. 즉 jpql은 대소문자를 구분하지 않지만, 별도로 설정하지 않는다면 기본값인 클래스 이름과 같이 맞추어야 한다고 한다. 또 하나 배웠다.

또 한번의 이상한 점.. 분명 sql문이 보이도록 설정했는데 테스트가 끝나도 콘솔 창에는 아무것도 뜨지 않는다…. 이건 설정 문제인데 하고 들어가 봤더니 spring.jpa.show.sql=true라고 되어 있었다.

spring.jpa.show-sql=true 라고 고치니 정상적으로 sql문이 보였다.

그리고 @Transactional을 지우고 @Commit을 넣어주면 디비에 반영까지 된다. 하지만 테스트이기 때문에 굳이 바꿔주지 않았다.

jpa는 spring만큼이나 공부할 양이 어마무시하다고 한다.

Spring Data JPA

다음은 Spring Data Jpa다. 스프링 데이터 JPA는 리포지토리에 구현 클래스없이 인터페이스 만으로 개발을 완료할 수 있다. 그리고 기존 반복해오던 CRUD기능을 JPA가 모두 제공해준다. 스프링부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 프레임워크를 더하면 개발이 쉬워진다. 따라서 개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있게된다.

우선 리포지토리에 인터페이스로 SpringDataJpaMemberRepository를 만들어준다. 그리고 인터페이스가 인터페이스를 상속할때는 extends를 사용하기 때문에 implement대신 extends를 사용해주고 JpaRepository라는 인터페이스를 상속받는다. 제네릭 클래스에는 Member와 식별자값의 타입(Long)을 넣어준다. 그리고 인터페이스는 다중 상속을 받을 수 있으므로 나중에 갈아 끼워주기 위한 MemberRepository까지 상속 받아준다. 그리고 나서 findByName에 매개변수로 name을 넣어준 다음에 오버라이드를 넣어주고 선언하면 끝이 난다. 골 때린다…

심지어 얘는 내가 빈으로 등록할 필요도 없다. JpaRepository를 상속받고 있다면 스프링이 자동으로 구현체를 만들어서 스프링 빈으로 등록해준다. 그래서 config에서 그냥 멤버리포지토리 자체를 만들어서 @Autowired 해주고 서비스에 주입시켜주면 설정도 끝이난다.

우리가 머릿속에서 상상할 수 있는 정도의 공통된 기능들은 이미 구현되어 있기 때문에 가져다가 쓰면 된다. 그리고 추가로 메서드를 만들 수도 있다.

아까 추가했던 findByName 과 같이 선언만 해주면 SpringDataJpa는 이 메서드의 이름만으로 메서드를 구현시켜서 제공해준다.

예를 들어 이름과 아이디로 찾고 싶다 하면 findBy+”NameAndId”로 만들어주고 파라미터에 name과 id를 넣어주면 이 스프링데이터jpa가 알아서 만들어준다. and나 or 등 규칙은 따로 정해져 있다고 한다.

실무에서는 jpa와 스프링데이터jpa를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다고 한다. Querydsl을 사용하면 쿼리도 자바코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다고 한다. 이 조합으로도 해결하기 어려운 쿼리는 jpa가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate을 사용하면 된다. 또한 jpa와 mybatis 조합으로도 사용이 가능하니 조합의 가능성은 무궁무진한 것 같다.

AOP(Aspect Oriented Programming)

이제 AOP를 배울거다. 만약 모든 메소드의 호출 시간을 측정하고 싶다면? 이라는 가정으로 시작한다.

끔찍하지만 aop를 모른다는 가정으로 내가 짠 코드의 메서드들이 들어 있는 클래스마다 메서드 호출 시간을 측정하는 코드를 넣어줘야 한다. 근데 만약? 초단위 말고 ms단위로 고쳐야 하는 경우가 또 발생했다. 이런 경우라고 가정해보자.

이때, 메서드 호출 시간을 측정하는 기능은 핵심 관심 사항이 아니다. 공통 관심 사항이다. 시간을 측정하는 로직과 핵심 비즈니스 로직이 섞여 있어서 유지보수도 힘들다. 그렇다고 별도로 공통 로직으로 만들기도 어렵다. 또한 시간 측정 로직을 변경하게 되면 모든 로직을 찾아가며 변경해야한다. 그렇기 때문에 공통 관심 사항과 핵심 관심 사항을 구분해야한다.

이러한 문제를 해결하기 위한게 aop(Aspect Oriented Programming)이다.

공통 관심 사항과 핵심 관심 사항을 분리하는 것이다. 스프링에서는 이 기능을 제공해준다.

우선 프로젝트에 aop라는 패키지를 만들고 TimeTraceAop라고 클래스를 하나 만들어줬다. 그리고 @Aspect라는 애노테이션을 달아주었다. 스프링 빈에 등록해야 하기 때문에 @Component 애노테이션도 달아주었다. 이 경우에는 config 파일에 가서 @Bean으로 설정해주는게 관례적이라고 한다. 그리고 나서 Object를 반환하는 함수를 만들어 ProceedingJointPoint 클래스를 파라미터로 넣어주고 throws Throwable까지 넣어줘서 예외를 처리해준다. 그리고나서 시간측정로직을 작성해주고 try에 joint.proceed()를 넣어주면된다. 아마 메서드가 정상 실행되면 측정으로 넘어가게끔 설계한것같다. 그리고 나서 이메서드 위에 @Around() 애노테이션을 달아주는데 이거의 역할은 어느 클래스의 어느 메서드에 적용할 것이냐를 결정해주는 애노테이션이다. 우리 가정의 경우 모든 메서드를 측정해야 하기 때문에 괄호 안에 "execution(* hello.hellospring..*(..))"를 넣어준다.

hello디렉토리 안에 hellospring 디렉토리 안에 모든 패키지를 대상으로 할 것이다 라는걸 선언한것이다. 이거의 자세한 내용은 사용시에 doc을 보면 될 것 같다.

이렇게 되면 모든 클래스의 모든 메서드를 시간 측정할 수 도 있고, 다른 로직은 건들이지 않아도 된다. 이것이 aop이다.

이 스프링의 aop기능은 around에서 지정한 대상이 되는 클래스를 스프링컨테이너에서 프록시 클래스(이해를 쉽게 가짜 클래스)로 복제해서 스프링 빈에 등록해준다. 그리고 나서 jointPoint.proceed()가 작동하면 실제 클래스를 호출해준다. 여기서 가짜를 주입해줬다 다시 진짜를 주입해주는 과정조차도 스프링이 di를 제공해주기 때문에 가능한것이다.

이것으로 스프링 입문 강의가 끝이 났다. 자바도 모르고 들었을 때는 여러 번 들어도 이해가 안갔는데 어느정도 공부를 마치고 들으니 정말 이해가 많이 되는 좋은 강의였다. 물론 100%이해를 했다면 거짓말이겠지만 요즘 정말 짧은 시간에 빠르게 성장 하는 것 같다. 뭔가 많이 머리에 집어 넣어 놔서 인 것 같기도 하고 ㅎㅎ;; 공부를 하면 할 수록 배울 것이 많은 것 같고 배워보고 싶은 것도 많아진다. 또 다른 기초 강의에도 눈이 간다. 놓치고 있는 부분이 있어서 앞으로 배울 내용에 어려움이 있지 않을까 하는 두려움 때문인 것 같다. 지금 할 수 있는 최선은 지금 당장 배우는 내용들을 최대한 이해하고 머리에 집어 넣는 것이다. 아무튼 스프링 입문 강의 끝!

'Spring' 카테고리의 다른 글

스프링 핵심 원리 - 3  (0) 2024.01.27
스프링 핵심 원리 - 2  (0) 2024.01.24
스프링 핵심 원리 - 1  (0) 2024.01.23
스프링 입문 - 1  (0) 2024.01.20
Spring의 기본 구조  (0) 2024.01.12