본문 바로가기

IT공부/스프링부트

스프링부트 3강 - 스프링부트 만들기 II

스프링부트 3강 - 스프링부트 만들기 II

애플리케이션 계층(layerd Spring Application)

데이터가 어떻게 전달이 될 지 처리를 하는 표현계층 부분은 @Controller 어노테이션을 이용한다. 일반적인 개발방향은 repository를 기준으로 엔티티와 레파지토리를 작성하고 이 repository를 이용하는 service를 작성하고 이 서비스를 이용하는 controller를 작성하는 식으로 밑에서부터 올라간다. 

기본적인 스프링 애플리케이션 계층 구성

위와 같이 기본적인 스프링 애플리케이션 계층 구성은 루트 애플리케이션 컨텍스트 영역인 서비스와 레파지토리를 가지고 있는 부분과 컨트롤러가 선언되어 있는 웹 어플리케이션 컨텍스트 영역으로 볼 수 있다. 밑에 있는 WS Application Context라고 하는 부분은 사용하는 경우가 많지는 않다. 


Spring Data JPA

  • ORM(Object-Relational Mapping)
    • 대부분의 개발언어 플랫폼마다 제공
    • 객체(Object)로 관계형 데이터베이스(RDBS)를 관리
  • JPA(Java Persistence API)
    • Java 객체 정보를 영속화하는 중간 과정을 처리한다.
    • 엔티티 객체를 저장하고 변경하고 삭제하면 그에 대응하는 쿼리를 생성하고 실행한다. 

우리가 사용할 ORM 프레임워크는 하이버네이트이다. 이 하이버네이트를 조금 더 사용하기 쉽도록 추상화한 라이브러리가 스프링 데이타 JPA 이다. 자바에서는 JPA라고 하는 스펙이 있다. JPA 스펙을 구현한 구현체 중에 오픈 JPA가 있고 하이버네이트가 있고 우리는 하이버네이트를 사용하고 하이버네이트를 스프링 데이타 JPA로 조금 더 간결하게 쓰는 방식으로 활용할 것이다. 간단하게 동작 방식을 보자.

객체 모델링 저장

@Entity 어노테이션이 선언되있는 클래스 각 필드가 밑에 있는 INSERT QUERY 컬럼과 각각 매핑되는 부분을 JPA에서 처리를 하는 것이다. 

Spring Data JPA Repository interface

/**

 * 

 * @author honeymon

 */

public interface BookRepository extends JpaRepository<Book, Long> {

   List<Book> findByNameLike(String name);

}

 

실제 사용 시에는 JpaRepositoy 인터페이스를 확장해서 사용하게 된다. BookRepository 인터페이스와 이름으로 유사한 것들을 검색하는 findByNameLike 메서드를 선언하고 별도의 쿼리를 작성하지 않아도 스프링 데이타 JPA에서 Named 메서드 쿼리를 통해 findBy 접두사 뒤에 붙은 필드와 어떻게 검색을 할 지 조건을 가지고 자동으로 쿼리를 생성해준다. 그래서 SQL을 잘 모르는 사람들이 쓰기에도 괜찮다. 심화과정을 들어가면 JPA가 하이버네이트를 통해서 어떻게 쿼리를 생성하는 지까지 고려를 하고 그 쿼리에서 최적화하는 부분까지도 고려를 해야한다. 여기서는 findByNameLike 메서드로 검색을 할 수 있다고 알면 된다. 동일한 이름을 찾겠다라고 하면 findByName(단건 검색)이라고만 하면 된다. 

JpaRepository

Optional<T> findById(ID id);

List<T> findAll();

List<T> findAll(Sort sort);

List<T> findAllById(Iterable<ID> ids);

<S extends T> List<S> saveAll(Iterable<S> entities);

void flush();

<S extends T> S saveAndFlush(S entity);

void deleteInBatch(Iterable<T> entities);

void deleteAllInBatch();

T getOne(ID id);

 

JpaRepository는 위와 같이 메서드가 선언되어 있다. findById 메서드는 id로 검색하는 메서드이다. findAll 메서드는 해당하는 엔티티를 찾는 메서드이고 findAll(Sort sort) 메서드는 지정된 정렬값으로 찾는 메서드이다. 실제 코드를 열어보면 위 메서드들을 구현한 코드는 볼 수는 없다. 

Spring Data JPA 작동원리

Spring Data JPA가 JpaRepository나 CrudRepository 인터페이스를 확장한 인터페이스(BookRepository 인터페이스)를 구동할 때 프록시라고 하는 스프링 패턴이 있는데 인터페이스로 선언된 부분을 SimpleJpaRepository라고 하는 JPA에서 구현한 구현체로 바꿔치기 한다. 그래서 BookRepository가 BookRepository 구현체가 되면서 실제로 동작을 하게된다. 

Repository 동작방식

Service에서 Repository를 호출하면 Repository 구현체에서 Spring Data JPA를 통해서 엔티티 부분을 쿼리로 바꿔서 JDBC를 호출하고 JDBC가 데이타베이스의 데이터를 가져와서 다시 객체로 맵핑해서 서비스에 반환하는 과정을 경험할 수 있다. 실제로 JPA가 하는 역할이 빨간색 점선으로 된 부분이다. 

Spring Data JPA는 하위에 Hibernate, JPA, JDBC, Database들이 깔려있다. 학습곡선이 굉장히 높은 ORM 프레임워크이다. 스프링 부트에서 왜 JPA를 기본적인 SQL 프레임워크로 사용했는지 우리나라 개발자가 봤을 때는 갸우뚱하는 부분이 있는데 사실 해외에서는 마이바티스보다는 하이버네이트 쪽이 더 인기가 높아서 더 우선적으로 스프링 부트에 반영이 되었다고 보면 된다. 프로젝트를 빠르게 시작하는 부분에 있어서도 엔티티 객체를 간단하게 가져가기에는 마이바티스보다는 조금 더 쉽다. 

업무(=비즈니스 로직) 구현에만 집중해라!(Repository)

  • 영속화 계층(@Repository)에서는 엔티티 관리만
  • 비즈니스 로직 구현은 도메인 영역에서
  • 서로 다른 도메인 사이에 연계는 서비스 계층(@Service)에서
  • 외부요청에 대한 처리는 컨트롤러 계층(@Controller)에서 
  • 참고할만한 책 : 객체지향의 사실과 오해, DDD START!, 도메인 주도 설계

객체가 있고 객체가 수행하는 업무들이 있는데 그것들을 어떻게 유기적으로 잘 할지 고민해야한다. 자바 프로그래밍 언어를 배우다 보면 사용하려는 기능이 하나하나 추가될 때마다 공부해야 될 부분이 기하급수적으로 늘어난다. 스프링 부트만 해도 스프링 프레임워크도 알아야하고 빌드에 관련된 그레이들을 봐야하고 스타터가 동작하는 방식도 봐야되고 굉장히 공부해야 될 부분이 많다. 우리가 만들 샘플 프로젝트만 해도 톰캣이 동작하는 방식, 스프링이 웹 어플리케이션 구성하는 방식, JPA가 동작하는 방식 등등을 알아야한다. 자바에서 프로토타이핑을 굉장히 빨리할 수 있는 개발 플랫폼이라고는 하지만, 기능을 구현하기 위해서는 더디게 나가게 된다. @Repository, @Service, @Controller 어노테이션에 따라서 각각의 역할과 기능이 구분되어 있기 때문에 이 부분에 대해 이해를 해야 한다. 

 

 Repository를 간단하게 작성해보자. 아래 패키지에 domain 패키지를 생성해보자.

domain 패키지 하위에 아래와 같이 class를 생성하자.

Book 클래스를 아래와 같이 작성하자.

  1. package io.honeymon.tacademy.springboot.domain;
  2.  
  3. import javax.persistence.Entity;
  4.  
  5. import org.springframework.data.jpa.domain.AbstractPersistable;
  6.  
  7. import lombok.Getter;
  8. import lombok.NoArgsConstructor;
  9. import lombok.Setter;
  10.  
  11. //일반적으로 엔티티 객체의 아이디 값은 시리얼라이저블이라고 하는 직렬화가 가능한 객체들을 기준으로 해서
  12. //Long이나 Integer, String 타입을 많이 쓰는데 일반적으로는 Long이나 Integer 통해서
  13. //숫자로 되있는 아이디값을 많이 사용한다.
  14. @Getter
  15. @Setter
  16. @NoArgsConstructor
  17. @Entity
  18. public class Book extends AbstractPersistable<Long> {
  19.       
  20.        //@Entity 어노테이션을 클래스에 붙여주고 나서 보통은 아래와 같이 필드를 선언
  21.        //하는데
  22.       
  23.        //@Id
  24.        //@GeneratedValue
  25.        //private Long id;
  26.       
  27.        //AbstractPersistable<Long> 추상 클래스 안에 id 값이 제네릭 타입으로
  28.        //이미 선언되어 있기 때문에 id 필드를 따로 선언하지 않아도 된다.
  29.        //실제로 AbstractPersistable<Long> 클래스 코드를 보면
  30.        //@Id @GeneratedValue private @Nullable PK id; id값이
  31.        //선언되어 있다.
  32.  
  33.        //@GeneratedValue 어노테이션은 데이터베이스에서 autoincrement되는 컬럼
  34.        //이라고 생각하면 된다. 하이버네이트에서는 데이터베이스의 종류에 따라서 생성되는 다르다.
  35.        //컬럼 안에서 1, 2, 3, 4 증가시키는 케이스가 있고 아니면 별도의 시퀀스 테이블을 만들어서
  36.        //1, 3, 5 이런 식으로 띄엄띄엄되는 경우도 있고 데이터베이스에 따라서 생성되는 다르다.
  37.       
  38.        private String name;
  39.        private String isbn13; //책에는 isbn이라고 해서 국제표준북넘버라고 하는게 있다.
  40.                               //13자리와 10자리가 있다.
  41.        private String isbn10;
  42.       
  43.        //필드 생성 getter, setter 만드는 과정이 필요하다. 클래스 위에
  44.        //@Getter, @Setter, @NoArgsConstructor 붙여주자.
  45.       
  46.  
  47. }

BookRepository 인터페이스를 생성하자. 

  1. package io.honeymon.tacademy.springboot.domain;
  2.  
  3. import java.util.List;
  4.  
  5. import org.springframework.data.jpa.repository.JpaRepository;
  6.  
  7. //JpaRepository<Book(엔티티 클래스명), Long(ID)>
  8. //Book 엔티티를 다루는 BookRepository 작성 완료.
  9. //어떤 책에서는 클래스 위에 @Repository 어노테이션을 붙였는데 필요없다.
  10. //SimpleJpaRepository라는 클래스가 이미 가지고 있다. 
  11. //Spring Data JPA 우리가 작성한 Repository 구현체로 바꾸면서
  12. //SimpleJpaRepository 프록시 처리를 해주기 때문에 @Repository 선언을 하지 않아도 된다.
  13. public interface BookRepository extends JpaRepository<Book, Long> {
  14.       
  15.        List<Book> findByNameLike(String name);
  16.       
  17. }

테스트 코드를 작성해보자. 아래 그림과 같은 경로에 domain 패키지를 만들자. 

패키지에 BookRepositoryTest 클래스를 만들자.

BookRepositoryTest 클래스에 아래와 같이 작성한다.

  1. package io.honeymon.tacademy.springboot.domain;
  2.  
  3. import static org.assertj.core.api.Assertions.assertThat;
  4.  
  5. import org.junit.Test;
  6. import org.junit.runner.RunWith;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
  9. import org.springframework.test.context.junit4.SpringRunner;
  10.  
  11. //BookRepository 동작을 간단하게 보여주려고 만드는 것이다.
  12. @RunWith(SpringRunner.class)
  13. @DataJpaTest
  14. public class BookRepositoryTest {
  15.       
  16.        @Autowired
  17.        BookRepository repository;
  18.       
  19.        @Test //test하는 메서드라는 선언
  20.        public void testSave() {
  21.            Book book = new Book();
  22.                book.setName("boot-spring-boot");
  23.                book.setIsbn10("0123456789");
  24.                book.setIsbn13("0123456789012");
  25.               
  26.                //Book 클래스를 구현하면서 확장했던
  27.                //AbstractPersistable 클래스를 보면
  28.                //@isNew 어노테이션이 있다.
  29.                //하이버네이트 엔티티 매니저가 관리하고 있지 않은 상태라는
  30.                //확인하는 목적으로 많이 사용한다.
  31.                //book 아이디값을 갖지 않는 새로운 객체임을 확인하는 코드
  32.                assertThat(book.isNew()).isTrue();
  33.        }
  34.  
  35. }

 작성 후 아래와 같이 testSave 메서드에 커서를 위치시킨 후 우클릭 > Run as > JUnit test을 클릭하여 실행하자.

실행결과를 보면 녹색으로 테스트가 정상적으로 통과된 것을 확인할 수 있다. 

아래와 같이 testSave 메소드에 라인 25, 29를 추가하여 테스트하고 라인 33 testFindByNameLike 메소드를 추가하여 BookRepository 클래스에 추가한 findByNameLike 메서드를 테스트하자.

  1. //BookRepository 동작을 간단하게 보여주려고 만드는 것이다.
  2. @RunWith(SpringRunner.class)
  3. @DataJpaTest
  4. public class BookRepositoryTest {
  5.       
  6.        @Autowired
  7.        BookRepository repository;
  8.       
  9.        @Test //test하는 메서드라는 선언
  10.        public void testSave() {
  11.            Book book = new Book();
  12.                book.setName("boot-spring-boot");
  13.                book.setIsbn10("0123456789");
  14.                book.setIsbn13("0123456789012");
  15.               
  16.                //Book 클래스를 구현하면서 확장했던
  17.                //AbstractPersistable 클래스를 보면
  18.                //@isNew 어노테이션이 있다.
  19.                //하이버네이트 엔티티 매니저가 관리하고 있지 않은 상태라는
  20.                //확인하는 목적으로 많이 사용한다.
  21.                //book ID(ID필드) 갖지 않는 새로운 객체임을 확인하는 코드
  22.                assertThat(book.isNew()).isTrue();
  23.               
  24.                //BookRepository 저장하기.
  25.                repository.save(book);
  26.               
  27.                //저장을 하면  결과가 false 바뀐다.
  28.                //이유는 ID값이 저장되면서 전달받아 가지고 있기 때문이다.
  29.                assertThat(book.isNew()).isFalse();
  30.        }
  31.       
  32.        @Test
  33.        public void testFindByNameLike() {
  34.                Book book = new Book();
  35.                book.setName("boot-spring-boot");
  36.                book.setIsbn10("0123456789");
  37.                book.setIsbn13("0123456789012");
  38.               
  39.                repository.save(book);
  40.                
  41.                List<Book> books = repository.findByNameLike("boot%");
  42.                assertThat(books).isNotEmpty();
  43.               
  44.                books = repository.findByNameLike("book");
  45.                assertThat(books).isEmpty();
  46.               
  47.        }
  48.  
  49. }

 아래와 같이 테스트 결과가 녹색으로 나오면서 테스트가 정상적으로 통과되었음을 확인할 수 있다.