Chapter 03. 스프링 부트에서 JPA로 데이터베이스 다뤄보자 (1) - DoDaek/freelec-springboot2-webservice GitHub Wiki

  1. 프로젝트에 Spring Data Jpa 적용하기
    • build.gradleorg.springframework.boot:spring-boot-starter-data-jpa등록하기
      • 스프링 부트용 Spring Data Jpa 추상화 라이브러리입니다.
      • 스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해줍니다.
    • build.gradlecom.h2database:h2등록하기
      • 인메모리 관계형 데이터베이스입니다.
      • 별도으 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있습니다.
      • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 데스트 용도로 많이 사용됩니다.
      • JPA의 테스트, 로컬 환경에서의 구동에서 사용할 예정입니다.

  1. domain 패키지
    • 도메인을 담을 패키지입니다.
    • 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 됩니다.

용어에 대한 어색함을 해결할 수 있을 것인가...


  1. 어노테이션 순서
    • 정해진 어노테이션 작성 순서는 없습니다.( -> 찾아봐야함. )
    • 책에서는 주요 어노테이션을 클래스에 가깝게 둡니다.
    • ex. @Entity, @NoArgsConstructor, @Getter
      • @Entity는 JPA의 어노테이션이며, @NoArgsConstructor, @Getter는 롬복의 어노테이션입니다.
      • 롬복은 코드를 단순화시켜 주지만 필수 어노테이션은 아닙니다.
      • 주요 어노테이션인 @Entity는 클래스에 가깝게 두고, 롬복 어노테이션은 그 위로 둡니다.
    • 코틀린 등의 새 언어 전환으로 롬복이 더 이상 필요없을 경우 쉽게 삭제할 수 있습니다.

  1. Entity 클래스
    • 실제 DB의 테이블과 매칭될 클래스입니다.
    • JPA를 사용하시면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는, 이 Entity 클래스의 수정을 통해 작업합니다.
    • JPA에서 제공하는 어노테이션들이 있습니다.

  1. Entity의 PK
    • 웬만하면 Entity의 PKLong 타입의 Auto_increment를 추천합니다.
      • MySQL 기준으로 bigint 타입
    • 주민등록번호와 같이 비즈니스상 유니크 키나, 여러 키를 복합키로 PK를 잡을 경우 난감한 상황이 종종 발생합니다.
      • FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생합니다.
      • 인덱스에 좋은 영향을 끼치지 못합니다.
      • 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생합니다.
    • 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하시는 것을 추천드립니다.

  1. Posts 클래스의 특이점
    • Setter 메소드가 없습니다.
    • 자바빈 규약을 생각하면서 getter/setter를 무작정 생성하는 경우가 있습니다.
    • 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경 시 정말 복잡해집니다.
    // 잘못된 사용 예
    public class Order {
        public void setStatus(boolean status) {
            this.status = status;
        }
    }
    
    public void 주문서비스의_취소이벤트() {
        order.setStatus(false);
    }
    
    // 올바른 사용 예
    public class Order {
        public void cancelOrder() {
            this.status = false;
        }
    }
    
    public void 주문서비스의_취소이벤트() {
        order.cancelOrder();
    }
    

  1. Setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입해야 할까요?
    • 기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이며, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 합니다.
    • 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용합니다.
    • 생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같습니다. 다만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수가 없습니다.
    • 다음과 같은 생성자가 있다면 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾을 수가 없습니다.
    public Example(String a, String b) {
        this.a = a;
        this.b = b;
    }
    
    • 하지만 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있습니다.
    Example.builder()
        .a(a)
        .b(b)
        .build();
    

  1. Database로 접근하게 해줄 JpaRepository
    • 보통 ibatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자입니다.
    • JPA에선 Repository라고 부르며 인터페이스로 생성합니다.
    • 단순히 인터페이스를 생성 후, JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성됩니다.
    • @Repository를 추가할 필요도 없습니다.
    • 주의하실 점은 Entity 클래스와 기본 Entity Repository는 함께 위치해야 하는 점입니다.
    • Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없습니다.
    • 나중에 프로젝트 규모가 커져 도메인별로 프로젝트를 분리해야 한다면 이때 Entity 클래스와 기본 Repository는 함께 움직여야 하므로 도메인 패키지에서 함께 관리합니다.

  1. H2 데이터베이스
    • 별다른 설정 없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행해 줍니다.
    • 테스트 역시 실행할 경우 H2가 자동으로 실행됩니다.
    • 여기서 한 가지 궁금한 것이 있습니다. "실제로 실행된 쿼리는 어떤 형태일까?"라는 것입니다.
    • 실행된 쿼리를 로그로 보는 방법
      • 쿼리 로그를 ON/OFF 할 수 있는 설정이 있습니다.
      • 설정들은 Java 클래스로 구현할 수 있으나 스프링 부트에서는 application.properties, application.yml 등의 파일로 한 줄의 코드로 설정할 수 있도록 지원하고 권장하니 이를 사용하겠습니다.
      spring.jpa.show_sql=true
      
    • create table 쿼리를 보면 id bigint generated by default as identity라는 옵션으로 생성됩니다.
      • H2의 쿼리 문법이 적용되었기 때문입니다.
      • H2는 MySQL의 쿼리를 수행해도 정상적으로 작동하기 때문에 이후 디버깅을 위해서 출력되는 쿼리 로그를 MySQL 버전으로 변경해 보겠습니다.
      • application.properties에서 설정이 가능합니다.
      spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
      

  1. API 만들기

    • API를 만들기 위해 총 3개의 클래스가 필요합니다.
      • Request 데이터를 받을 Dto
      • API 요청을 받을 Controller
      • 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
    • 많은 사람들이 오해하고 있는 것이, Service에서 비즈니스 로직을 처리해야 한다는 것입니다.
    • Service는 트랜잭션, 도메인 간 순서 보장의 역할만 합니다. -> "그럼 비지니스 로직은 누가 처리하냐?"
    • Spring 웹 계층
    • Web Layer
      • 흔히 사용하는 컨트롤러( @Controller )와 JSP/Freemarker 등의 뷰 템플릿 영역입니다.
      • 이외에도 필터( @Filter ), 인터셉터, 컨트롤러 어드바이스( @ControllerAdvice ) 등 외부 요청과 응답에 대한 전반적인 영역을 이야기합니다.
    • Service Layer
      • @Service에 사용되는 서비스 영역입니다.
      • 일반적으로 Controller와 Dao의 중간 영역에서 사용됩니다.
      • @Transactional이 사용되어야 하는 영역이기도 합니다.
    • Repository Layer
      • Database와 같이 데이터 저장소에 접근하는 영역입니다.
      • 기존에 개발하셨던 분들이라면 Dao( Data Access Object ) 영역으로 이해하시면 쉬울 것입니다.
    • Dtos
      • Dto( Data Transfer Object )는 계층 간에 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 얘기합니다.
      • 예를 들어 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기합니다.
    • Domain Model
      • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화시킨 것을 도메인 모델이라고 합니다.
      • 이를테면 택시 앱이라고 하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있습니다.
      • @Entity를 사용해보신 분들은 @Entity가 사용된 영역 역시 도메인 모델이라고 이해해주시면 됩니다.
      • 다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아닙니다.
      • VO처럼 값 객체들도 이 영역에 해당하기 때문입니다.

  1. 비지니스 로직 처리
    • Web, Service, Repository, Dto, Domain, 이 5가지 레이어에서 비지니스 처리를 담당해야 할 곳은 어디일까요?
      • 바로 Domain입니다.
    • 기존 서비스로 처리하던 방식을 트랜잭션 스크립트라고 합니다.
    • 주문 취소 로직을 작성한다면 다음과 같습니다.
      • 슈도 코드
      @Transactional
      public Order cancelOrder(int orderId) {
          1) 데이터베이스로부터 주문정보( Orders ), 결제정보( Billing ), 배송정보( Delivery ) 조회
          2) 배송 취소를 해야 하는지 확인
          3) if (배송 중이라면) {
                 배송 취소로 변경
             }
          4) 각 테이블에 취소 상태 Update
      }
      
      • 실제 코드
      @Transactional
      public Order cancelOrder(int orderId) {
          
          // 1)
          OrdersDto order = ordersDao.selectOrders(orderId);
          BillingDto billing = billingDao.selectBilling(orderId);
          DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
          
          // 2)
          String deliveryStatus = delivery.getStatus();
          
          // 3)
          if ("IN_PROGRESS".equals(deliveryStatus)) {
              delivery.setStatus("CANCEL");
              deliveryDao.update(delivery);
          }
          
          // 4)
          order.setStatus("CANCEL");
          ordersDao.update(order);
          
          billing.setStatus("CANCEL");
          billingDao.update(billing);
          
          return order;
      }        
      
      • 모든 로직이 서비스 클래스 내붓에서 처리됩니다.
      • 그러다 보니 서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리 역할만 하게 됩니다.
      • 반면 도메인 모델에서 처리할 경우 다음과 같은 코드가 될 수 있습니다.
      @Transactional
      public Order cancelOrder(int orderId) {
          
          // 1)
          OrdersDto order = ordersRepository.findById(orderId);
          BillingDto billing = billingRepository.findByOrderId(orderId);
          DeliveryDto delivery = deliveryRepository.findByOrderId(orderId);
          
          // 2 - 3)
          delivery.cancel();
          
          // 4)
          order.cancel();
          billing.cancel();
          
          return order;
      }      
      
      • order, billing, delivery가 각자 본인의 취소 이벤트 처리를 하며, 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해 줍니다.

  1. 스프링에서 Bean을 주입받는 방식들
    • Controller와 Service에서 @Autowired가 없는 것이 어색하게 느껴집니다.
    • Bean을 주입받는 방식들
      • @Autowired
      • setter
      • 생성자
    • 이 중 가장 권장하는 방식이 생성자로 주입받는 방식입니다.
      • @Autowired는 권장하지 않습니다.
    • 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있다는 것입니다.
    • 그러면 생성자가 없는 클래스에서는 생성자가 어디있을까요?
    • 바로 @RequiredArgsConstructor에서 해결해 줍니다.
    • final이 선언된 모든 필드를 인자값으로 하는 생성자를 롬복의 @RequiredArgsConstructor가 대신 생성해 준 것입니다.
    • 생성자를 직접 안 쓰고 롬복 어노테이션을 사용한 이유
      • 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함입니다.

  1. Dto 클래스
    • Entity 클래스와 거의 유사한 형태임에도 Dto 클래스를 추가로 생성했습니다.
    • 절대로 Entity 클래스를 Request/Response 클래스로 사용해서는 안 됩니다.
      • Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스입니다.
      • Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경됩니다.
      • 화면 변경은 아주 사소한 기능 변경인데, 이를 위해 테이블과 연결된 Entity 클래스를 변경하는 것은 너무 큰 변경입니다.
    • 수많은 서비스 클래스나 비즈니스 로직들이 Entity 클래스를 기준으로 동작합니다.
    • Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만, Request와 Response용 Dto는 View를 위한 클래스라 정말 자주 변경이 필요합니다.
    • View Layer와 DB Layer의 역할 분리를 철저하게 하는 게 좋습니다.
    • 실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번하므로 Entity 클래스만으로 표현하기가 어려운 경우가 많습니다.
    • 꼭 Entity 클래스와 Controller에서 쓸 Dto는 분리해서 사용해야 합니다.

  1. Api Controller 테스트
    • Api Controller를 테스트하는데 HelloController와 달리 @WebMvcTest를 사용하지 않았습니다.
    • @WebMvcTest의 경우 JPA 기능이 작동하지 않기 때문인데, Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 활성화되니 지금 같이 JPA 기능까지 한번에 테스트할 때는 @SpringBootTestTestRestTemplate을 사용하면 됩니다.

  1. Posts 조회 기능
    • 조회 기능은 실제로 톰캣을 실행해서 확인해 보겠습니다.
    • 로컬 환경에선 데이터베이슬 H2를 사용합니다.
    • 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야만 합니다.
    • 웹 콘솔 옵션을 활성화합니다.
    • application.properties에 다음과 같이 옵션을 추가합니다.
      spring.h2.console.enabled=true
      
    • 추가한 뒤 Application 클래스의 main 메소드를 실행하니다. -> 정상적으로 실행됐다면 톰캣이 8080 포트로 실행됐습니다.
    • 웹 브라우저에 http://localhost:8080/h2-console 로 접속하면 다음과 같이 웹 콘솔이 등장합니다. h2_database_web_console
    • 이때 JDBC URL이 화면과 같이 jdbc:h2:mem:testdb로 되어 있지 않다면 똑같이 작성해주셔야 합니다. h2_database_web_console
    • connect 버튼을 누르면 H2를 관리할 수 있는 관리 페이지로 이동합니다.
    • posts 테이블에 데이터를 등록합니다.
      • insert into posts (title, content, author) values ('title', 'content', 'author');
    • 브라우저에 http://localhost:8080/api/v1/posts/1을 입력해 API 조회 기능을 테스트해 봅니다. 조회 api 결과