Today
-
Yesterday
-
Total
-
  • 스프링 부트 핵심 가이드 3주차 : 6장
    책 공부/스프링 부트 핵심 가이드 2023. 12. 4. 00:01

    📖 책 정보

    나를 위한 모바일 가이드

    6장 데이터베이스 연동

    1. 마리아DB 설치
    2. ORM
    3. JPA
    4. 하이버네이트
    5. 영속성 컨텍스트
    6. 데이터베이스 연동
    7. 엔티티 설계
    8. 리포지토리 인터페이스 설계
    9. DAO 설계
    10. DAO 연동을 위한 컨트롤러와 서비스 설계

    6장

    1. 마리아DB 설치
    2. ORM
    3. JPA
    4. 하이버네이트
    5. 영속성 컨텍스트
    6. 데이터베이스 연동
    7. 엔티티 설계
    8. 리포지토리 인터페이스 설계
    9. DAO 설계
    10. DAO 연동을 위한 컨트롤러와 서비스 설계

    키워드



    1. 마리아DB 설치

    - 다운로드

    아래 사이트에서 다운로드 받으면 된다. 설치는 굉장히 간단한 편!

    다운로드 링크 : mariadb.org/download


    설치 후

    windows terminal에서 편하게 접근하기 위해 환경변수를 설정하고 db접근 계정을 추가/삭제하는 과정을 포스팅 했던 링크를 덧붙이는것으로 이부분은 마무리..🐢 >>>> https://velog.io/@zhyun/MariaDB




        🐢🎈




    2. ORM

    Object Relational Mapping의 줄임말로

    객체 관계 매핑을 의미한다.


    자바와 같은 객체지향 언어에서 의미하는 객체( 클래스 )와 RDB의 테이블을 자동으로 매핑하는 방법이다.

    클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에, RDB 테이블과 어쩔 수 없는 불일치가 존재한다.

    ORM은 이 둘의 불일치와 제약사항을 해결하는 역할을 한다.


    ORM을 이용하면 쿼리문 작성이 아닌 코드( 메서드 )로 데이터를 조작할 수 있다.



    - 장점

    ORM을 사용하면서 데이터베이스 쿼리를 객체지향적으로 조작할 수 있다.

    • 쿼리문을 작성하는 양이 현저히 줄어 개발 비용이 줄어든다.

    • 객체지향적으로 데이터베이스에 접근할 수 있어 코드의 가독성을 높인다.


    재사용 및 유지보수가 편리하다.

    • ORM을 통해 매핑된 객체모두 독립적으로 작성되어 있어 재사용이 용이하다.

    • 객체들은 각 클래스로 나뉘어 있어 유지보수가 수월하다.


    데이터베이스에 대한 종속성이 줄어든다.

    • ORM을 통해 자동 생성된 SQL문은 객체를 기반으로 데이터베이스 테이블을 관리하기 때문에 데이터베이스에 종속적이지 않다.

    • 데이터베이스를 교체하는 상황에서도 비교적 적은 리스크를 부담한다.


    - 단점

    ORM만으로 온전한 서비스를 구현하기에는 한계가 있다.

    • 복잡한 서비스의 경우 직접 쿼리를 구현하지 않고 코드로 구현하기 어렵다.

    • 복잡한 쿼리를 정확한 설계 없이 ORM만으로 구성하게 되면 속도 저하 등의 성능 문제가 발생할 수 있다.


    애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치가 발생한다.

    • 세분성 Granularity

          ORM의 자동 설계 방법에 따라        데이터메이스에 있는 테이블의 수와        애플리케이션의 엔티티 클래스의 수가 다른 경우가 생긴다.

          엔티티 클래스가 실제 테이블의 수보다 많아질 수 있음


    • 상속성 Inheritance RDMBS에는 상속이라는 개념이 없다.

    • 식별성 Identity RDBMS는 기본키로 동일성을 정의한다. 하지만 자바는 두 객체의 값이 같아도 다르다고 판단할 수 있다. 식별과 동일성의 문제.

    • 연관성 Associations 객체지향 언어는 객체참조함으로써 연관성을 나타내지만 RDBMS에서는 외래키삽입함으로써 연관성을 표현한다.

          또한 객체지향 언어에서        객체를 참조할 때는 방향성존재하지만,

           RDBMS에서        외래키를 삽입하는 것은        양방향의 관계를 가지기 때문에 방향성없다.


    • 탐색 Navigation 자바와 RDBMS는 어떤 값( 객체 )에 접근하는 방식이 다르다.

          자바에서는        특정 값에 접근하기 위해        객체 참조 같은 연결 수단을 활용한다.

          이 방식은 객체를 연결하고 또 연결해서 접근하는        그래프 형태의 접근 방식이다.

          반면, RDBMS는        쿼리를 최소화하고        조인을 통해 여러 테이블을 로드하고        값을 추출하는 접근 방식을 채택한다.



        🐢🎈




    3. JPA

    Java Persistence API

    자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다.


    ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함한다.


    JPA는 어떻게 동작해야 하는지에 대해 메커니즘을 정리한 표준 명세로 생각하면 된다.


    JPA의 메커니즘을 보면 내부적으로 JDBC를 사용한다.

    개발자가 직접 JDBC를 구현하면 SQL에 의존하게 되는 문제 등이 있어 개발의 효율성이 떨어지는데,

    JPA는 이 같은 문제점을 보완해서 개발자 대신 적절한 SQL을 생성하고 데이터베이스를 조작해서 객체를 자동 매핑하는 역할을 수행한다.


    JPA 기반의 대표적인 구현체 3가지

    1. 하이버네이트    Hibernate
    2. 이클립스 링크    EclipseLink
    3. 데이터 뉴클리어스    DataNucleus

    이 중 가장 많이 사용되는 구현체는 하이버네이트이다.



        🐢🎈




    4. 하이버네이트

    자바의 ORM 프레임워크로,

    JPA가 정의하는 인터페이스를 구현하고 있는 JPA 구현체 중 하나이다.


    Spring Boot 에서는

    JPA를 더욱 편하게 사용하도록 모듈화 한

    Spring Data JPA를 활용하기 때문에

    JPA를 직접 사용할 일은 거의 없다.


    - Spring Data JPA

    JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중하나이다.


    CRUD 처리에 필요한 인터페이스를 제공하며

    하이버네이트의 엔티티 매니저를 직접 다루지 않고

    리포지토리를 정의해 사용함으로써 ex : public implement MyRepository extends JpaRepository<MyEntity, Long> {}

    스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작한다.


    이를 통해 하이버네이트에서 자주 사용되는 기능을

    더 쉽게 사용할 수 있게 구현한 라이브러리이다.



        🐢🎈




    5. 영속성 컨텍스트

    애플리케이션과 데이터베이스 사이에서

    엔티티와 레코드의 괴리를 해소하는 기능과

    객체를 보관하는 기능을 수행한다.


    엔티티 객체가 영속성 컨텍스트에 들어오면

    JPA는

    엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행한다.


    이처럼

    엔티티 객체가 영속성 컨텍스트에 들어와

    JPA의 관리대상이 되는 시점부터는

    해당 객체를 영속 객체 Persistence Object라고 부른다.


    영속성 컨텍스트는 세션 단위생명주기를 가진다.

    데이터베이스에 접근하기 위한 세션이 생성되면

    영속성 컨텍스트가 만들어지고

    세션이 종료되면

    영속성 컨텍스트도 없어진다.


    엔티티 매니저는

    이러한 일련의 과정에서

    영속성 컨텍스트에 접근하기 위한 수단으로 사용된다.


    - 엔티티 매니저

    이름 그대로

    엔티티를 관리하는 객체이다.


    데이터베이스에 접근해서 CRUD 작업을 수행한다.


    Spring Data JPA를 사용하면

    리포지토리를 사용해서 데이터베이스에 접근하는데,

    실제 내부 구현체인 SimpleJpaRepository를 열어보면

    이 클래스에서 엔티티 매니저를 사용하는 것을 알 수 있다.

    ✏️ SimpleJpaRepository
    
    Spring Boot 를 사용하면서, 
    Repository를 다음과 같은 형태로 작성하여 사용하게 되는데,
    
    public interface MyRepository extends JpaRepository<MyEntity, Long> { .. }
    
    
    이 코드를 실행하면 
    
    Spring boot가 
    [ MyRepository ]에 대한 구현체를 자동으로 생성하고 빈으로 등록한다.
    
    이 때 [ SimpleJpaRepository ] 생성되어  
    MyRepository의 [ JpaRepository ]를 처리하게 된다.

    엔티티 매니저는 엔티티 매니저 팩토리가 만든다.

    앤티티 매니저 팩토리는

    데이터베이스에 대응하는 객체로서

    스프링 부트에서는 자동 설정 기능이 있기 때문에

    application.properties에서 작성한 최소한의 설정만으로도 동작하지만,

    JPA 구현체 중 하나인 하이버네이트에서는 persistence.xml이라는 설정 파일을 구성하고 사용해야 하는 객체이다.


    엔티티 매니저 팩토리는

    애플리케이션에서 단 하나만 생성되며,

    모든 엔티티가 공유해서 사용한다.


    엔티티 매니저 팩토리로 생성된

    엔티티 매니저는

    엔티티를 영속성 컨텍스트에 추가해서

    영속 객체로 만드는 작업을 수행하고

    영속성 컨텍스트와 데이터베이스를 비교하면서

    실제 데이터베이스를 대상으로 작업을 수행한다.


    - 엔티티 생명주기

    엔티티 객체는 영속성 컨텍스트에서 다음과 같은 4가지 상태로 구분된다.

    비영속 New

    영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태를 의미

    영속 Managed

    영속성 컨텍스트에 의해 엔티티 객체가 관리 받는 중

    준영속 Detached

    영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태

    삭제 Removed

    데이터베이스에서 레코드를 삭제하기 위해 영속성 컨텍스트에 삭제 요청을 한 상태



        🐢🎈




    6. 데이터베이스 연동

    - 프로젝트 생성

    groupId = com.springboot name = jpa artifactId = jpa


    의존성 선택 단계에서는 다음의 라이브러리를 선택

    Swagger 의존성 추가

    Swagger 의존성을 build.gradle에 추가해주어야 한다.

    springfox-boot-starter 의존성 코드 가져오기 swagger2 의존성 코드 가져오기 swagger-ui 의존성 코드 가져오기

    나의 경우 위에 있는 3개의 코드를 추가해야 Swagger가 실행되었다. 다른 프로젝트에서도, 책에서도 3개를 모두 추가하는 경우는 없었는데..;; Spring Boot 버전 이슈일까? 🤨


    Swagger 설정파일, logback 설정파일 추가

    이전 장에서 생성한 다음의 파일들을 가져와야 한다.

    config/SwaggerConfiguration.java logback-spring.xml


    Swagger 사용 시 Spring Boot 버전 2.x 사용

    나는 이번 프로젝트에서 Spring Boot 3.1.0 을 선택해서 프로젝트를 생성했었는데, swagger를 의존성 추가 해준 후 프로젝트를 실행해보니 실행중 에러가나서 tomcat이 실행 실패가 떴다.


    알아봤더니,

    Swagger 가 jakarta를 지원하지 않기 때문이라고 한다. swagger 의 최신 업데이트 버전 날짜가 2020년으로, javax만을 적용한 채 업데이트되지 않고 있다.

    Swagger를 사용하려면

    Spring boot를 2.x 버전으로 사용해야 한다.


    spring boot 버전 변경 하기( gradle 기준 )

    build.gradle 오픈 후 최상위 plugins 블럭에서 id 'org.springframework.boot'라고 적힌 line의 버전을 변경한 후 빌드해주면 된다.

    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.1.0' // 2.7.12 로 변경!
        id 'io.spring.dependency-management' version '1.1.0'
    }

    JPA 설정 추가

    애플리케이션이 정상적으로 실행될 수 있게 연동할 데이터베이스의 정보를 application.properties or application.yaml에 작성해야 한다.

    spring:
    
      datasource:
        driver-class-name: org.mariadb.jdbc.Driver
        url: jdbc:mariadb://localhost:3306/springboot
        username: root
        password: 1234
        
      jpa:
        hibernate:
          ddl-auto: create
        show-sql: true
        properties:
          hibernate:
            format_sql: true
            

    이걸 실행하기 전에,

    mariaDB에서 springboot 라는 이름의 데이터베이스를 생성해주어야 한다. 그렇지 않으면 아래 에러 만남

    [2023-06-04 22:44:32.981] [WARN ] [main] org.mariadb.jdbc.message.server.ErrorPacket Error: 1049-42000: Unknown database 'springboot'
    [2023-06-04 22:44:33.993] [ERROR] [main] com.zaxxer.hikari.pool.HikariPool HikariPool-1 - Exception during pool initialization.
    

    mariaDB 데이터베이스 생성하기 (windows)

    terminal 실행 인텔리제이에 터미널이 내장되어있다. 버튼 클릭으로 쉽게 실행 가능! (powerShell 적용되있음)

    1 . mariaDB 접속

    > mariadb -u root -p

    2 . db 생성

    > create database springboot;


    이제 프로젝트 실행하면 새로운 에러를 만남. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 😠


    새로운 에러가 나타난 이유는 spring boot 버전을 2.6 이상으로 했을 경우에만 해당되는 Swagger 에러이다.

    이 블로그에 의하면

    Spring boot 2.6부터

    요청 경로를 ControllerHandler에 매칭시키기 위한 전략이 ant_path_matcher 에서 path_pattern_parser로 변경되었는데,

    이게 변경되었기 때문에 swagger에 영향을 미친다고 한다.


    블로그에서 제안하는 해결방법으로는 spring boot의 버전을 2.5.x 로 낮추거나,

    application.yaml에서 spring.mvc.pathmatch.matching-strategy=ant_path_matcher로 default 값을 변경해주면 된다고 한다.

    나는 후자를 선택!

    application.yaml에 아래 설정 추가

      mvc:
        pathmatch:
          matching-strategy: ant_path_matcher

    이제 실행하면 문제 없이 실행 됨



        🐢🎈




    7. 엔티티 설계

    spring data jpa를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없다.

    이 기능을 가능하게 하는 것이 엔티티 Entity 이다.


    JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다.


    엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의한다.

    엔티티에 어노테이션을 사용하면 테이블간의 연관관계를 정의할 수 있다.


    - 엔티티 생성 예시

    데이터베이스 테이블이 아래와 같다면

    상품 테이블 product
    상품번호 int
    상품이름 varchar
    상품 가격 int
    상품 재고 int
    상품 생성 일자 DateTime
    상품 정보 변경 일자 DateTime

    엔티티 클래스는 아래와 같이 작성할 수 있다.

    @Getter
    @Setter
    @Entity
    @Table(name = "product")
    public class Product {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long number;
    
        @Column(nullable = false)
        private String name;
    
        @Column(nullable = false)
        private Integer price;
    
        @Column(nullable = false)
        private Integer stock;
    
        private LocalDateTime createdAt;
    
        private LocalDateTime updatedAt;
    }

    이렇게 작성해서 실행하면

    앞서 설정한 정보인

    application.yaml에 정의한 spring.jpa.hibernate.ddl-auto = create에 의해 데이터베이스에 테이블이 자동으로 생성된다.


    엔티티 관련 기본 어노테이션

    테이블과 매핑하기 위해 사용하는 어노테이션 ,

    다른 테이블과 연관관계를 정의하기 위해 사용하는 어노테이션 ,

    자동으로 값을 주입하기 위한 어노테이션

    등등 엔티티를 작성할 때는 어노테이션을 많이 사용하게 된다.


    지금은 기본적으로 많이 사용하는 어노테이션에 대해 알아본다!


    @Entity

    해당 클래스가 엔티티임을 명시.

    클래스 자체는 테이블과 1:1로 매칭되며

    해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미한다.


    @Table

    @Table 어노테이션은 클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우에 사용한다.

    이 어노테이션을 명시하지 않으면 테이블의 이름과 클래스의 이름이 동일하다는 의미이다.

    서로 다른 이름을 쓰기 위해서는 @Table(name="값") 형태로 테이블명을 적어주어야 한다.

    대체로 , 자바의 명명법과 데이터베이스가 사용하는 명명법이 다르기 때문에 자주 사용한다.


    @Id

    엔티티 클래스의 필드는 테이블의 컬럼과 매핑된다.

    @Id 어노테이션이 선언된 필드는 테이블의 기본키 역할로 사용된다.

    그렇기 때문에 모든 엔티티는 @Id 어노테이션을 필요로 한다.


    @GeneratedValue

    @Id 어노테이션과 함께 사용된다.

    이 어노테이션은 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용한다.

    auto_increment 설정을 여기서 함!

    값 생성 방식은 다음과 같다.

    AUTO

    • @GeneratedValue의 기본 설정 값
    • 기본값을 사용하는 데이터베이스에 맞게 자동 생성

    IDENTITY

    • 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성.
    • 기본값 생성을 데이터베이스에 위임.

    SEQUENCE

    • @SequenceGenerator 어노테이션으로 식별자 생성기를 설정하고 이를 통해 값을 자동 주입 받는다.
    • SequenceGenerator를 정의할 때는 name, sequenceName, allocationSize를 활용한다.
    • @GeneratedValue에 생성기를 설정한다.

    TABLE

    • 어떤 DBMS를 사용하더라도 동일하게 동작하기를 원할 경우 사용
    • 식별자로 사용할 숫자의 보관 테이블을 별도로 생성해서 엔티티를 생성할 때마다 값을 갱신하며 사용
    • @TableGenerator 어노테이션으로 테이블 정보를 설정

    @Column

    필드에 추가 설정을 해줄 때 사용한다. @Column 어노테이션으로 추가할 수 있는 속성은 다음과 같다.


    @Transient

    엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서는 필요 없을 경우 이 어노테이션을 사용해 데이터베이스에서 이용하지 않게 할 수 있다.



        🐢🎈




    8. 리포지토리 인터페이스 설계

    스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.

    - 리포지토리 인터페이스 생성

    여기서 이야기 하는 리포지토리는 Spring Data JPA가 제공하는 인터페이스이다.

    엔티티를 데이터베이스의 테이블과 구조를 생성하는 데 사용했다면

    리포지토리는 엔티티가 생성한 데이터베이스에 접근하는 데 사용한다.

    리포지토리를 생성하기 위해서는 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스를 생성하고, 다음과 같이 JpaRepository를 상속받으면 된다.

    public interface ProductRepository extends JpaRepository<Product, Long> {
    
    }

    이 때 JpaRepository<>에서 <>에 들어갈 값으로 해당 repository에서 사용 될 엔티티 클래스와, 해당 엔티티의 @Id 필드의 타입 클래스를 작성해주면 된다.

    - 리포지토리 메서드의 생성 규칙

    몇가지 명명규칙에 따라 커스텀 메서드를 생성할 수 있다.

    메서드에 이름을 붙일 때는

    첫 단어를 제외한 이후 단어들의 첫 글자를 대문자로 설정해야 JPA에서 정상적으로 인식하고 쿼리를 자동으로 만들어준다.

    조회 메서드( find )에 조건으로 붙일 수 있는 몇 가지 기능을 소개하면 다음과 같다.


    findBy

    sql문의 where절 역할을 수행 findBy 뒤에 엔티티의 필드 값을 입력해서 사용한다.


    And , Or

    조건을 여러개 설정 ex: findByNameAndEmailAndAddressAndAgeAndGenderAndHobby( .. )


    Like , NotLike , Containing , Contains , isContaing

    sql문의 like와 동일한 기능 수행


    StartsWith , StartingWith

    특정 키워드로 시작하는 문자열 조건 설정


    EndsWith , EndingWith

    특정 키워드로 끝나는 문자열 조건 설정


    IsNull , IsNotNull

    레코드 값이 Null이거나 not null인 값 검색


    True , False

    Boolean 타입의 레코드를 검색


    Before , After

    시간을 기준으로 값을 검색


    LessThan , GreaterThan

    특정 값을 기준으로 대소 비교를 할 때 사용


    Between

    두 값 사이의 데이터를 조회


    OrderBy컬럼명Asc , OrderBy컬럼명Desc

    SQL문의 order by와 같은 기능


    CountBy

    SQL문의 count와 같은 기능



        🐢🎈




    9. DAO 설계

    Data Access Object는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다.

    규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 데이터베이스에 접근해서 구현하기도 하지만,

    이번 장에서는 DAO를 서비스 레이어와 리포지토리의 중간 계층을 구성하는 역할로 사용할 예정이다.

    실제로 업무에 필요한 비즈니스 로직을 개발하다 보면 데이터를 다루는 중간 계층을 두는 것이 유지보수 측면에서 용이한 경우가 많다.

    객체지향적인 설계에서는 서비스와 비즈니스 레이어를 분리해서 서비스 레이어에서는 서비스 로직을 수행하고 비즈니스 레이어에서는 비즈니스 로직을 수행해야 한다는 의견도 많다.

    도메인( 엔티티 ) 객체를 중심으로 다뤄지는 로직은 비즈니스 로직으로 볼 수 있다.

    - DAO 클래스 생성

    DAO 클래스는 일반적으로 인터페이스 - 구현체 구성으로 생성한다.

    DAO 클래스는 의존성 결합을 낮추기 위한 디자인이며

    서비스 레이어에 DAO 객체를 주입받을 때

    인터페이스를 선언하는 방식으로 구성할 수 있다.


    ProductDAO 인터페이스 생성

    data/dao/ProductDAO.java

    public interface ProductDAO {
    
        Product insertProduct(Product product);
        Product selectProduct(Long number);
        Product updateProductName(Long number, String name);
        Product deleteProduct(Long number);
    }

    일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달한다.

    이때 데이터 객체를 엔티티 객체로 전달할지, DTO 객체로 전달할지에 대해서는 개발자마다 의견이 분분하다.

    일반적인 설계 원칙에서 엔티티 객체데이터베이스에 접근하는 계층에서만 사용하도록 정의한다.

    다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용한다.

    그러나 이 부분은 회사나 부서마다 견해 차이가 있으므로 각자 정해진 원칙에 따라 진행하는 것이 좋다.


    ProductDAOImpl 클래스 생성

    data/dao/impl/ProductDAOImpl.java

    @RequiredArgsConstructor
    @Component
    public class ProductDAOImpl implements ProductDAO {
    
        private final ProductRepository productRepository;
    
        @Override
        public Product insertProduct(Product product) {
            Product savedProduct = productRepository.save(product);
            
            // 예외 처리 및 로그 삽입 필요시를 대비해서 savedProduct 변수 사용
            return savedProduct;
        }
    
        @Override
        public Product selectProduct(Long number) {
            Optional<Product> selectedProduct = productRepository.findById(number);
    
            return selectedProduct.orElseThrow(() -> new RuntimeException("조회 오류"));
        }
    
        @Override
        public Product updateProductName(Long number, String name) throws Exception {
            Optional<Product> selectedProduct = productRepository.findById(number);
    
            Product updatedProduct;
            if (selectedProduct.isPresent()) {
                Product product = selectedProduct.get();
    
                product.setName(name);
                product.setUpdatedAt(LocalDateTime.now());
    
                // save를 통해 JPA에서 더티체크 라는 변경감지를 하고,
                // 변경이 감지되면 대상 객체에 해당하는 데이터베이스 레코드를 업데이트한다.
                updatedProduct = productRepository.save(product);
            } else {
                throw new Exception();
            }
    
            return updatedProduct;
        }
    
        @Override
        public void deleteProduct(Long number) throws Exception {
            Optional<Product> selectedProduct = productRepository.findById(number);
    
            if (selectedProduct.isPresent()) {
                Product product = selectedProduct.get();
    
                productRepository.delete(product);
            } else {
                throw new Exception();
            }
        }
    }



        🐢🎈




    10. DAO 연동을 위한 컨트롤러와 서비스 설계

    앞에서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면

    컨트롤러와 서비스를 생성해야 한다.


    이를 위해 DAO의 메서드를 호출하고

    그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후

    컨트롤러를 생성한다.


    - 서비스 클래스 만들기

    서비스 레이어에서는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공한다.


    여기서 말하는 핵심 기능을 구현하려면 세부 기능을 정의해야 한다.


    이러한 모든 로직을 서비스 레이어에서 포함하기란 쉽지 않다.


    이 같은 아키텍처의 한계를 극복하기 위해 아키텍처를 서비스 로직과 비즈니스 로직으로 분리하기도 한다.


    도메인을 활용한 세부 기능들을 비즈니스 레이어의 로직에서 구현하고

    서비스 레이어에서는 기능들을 종합해서 핵심 기능을 전달하도록 구성하는 경우가 대표적이다.


    다만 이 책의 목적은 과도한 기능 구현보다는 어떻게 프로젝트를 구성하고 스프링 부트의 기능을 온전히 사용할 수 있는지를 고민하는 것이므로 서비스 레이어에서 비즈니스 로직을 처리하는 아키텍처로 진행한다.


    서비스 객체는 DAO와 마찬가지로 추상화해서 구성한다. ProductService , ProductServiceImpl


    서비스 인터페이스를 작성하기 전에 필요한 DTO 클래스를 생성해준다.

    data/dto/ProductDto.java

    @ToString
    @Getter
    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class ProductDto {
    
        private String name;
        private int price;
        private int stock;
    }

    data/dto/ProductResponseDto.java

    @ToString
    @Getter
    @Setter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class ProductResponseDto {
    
        private Long number;
        
        private String name;
        private int price;
        private int stock;
    }

    이제 서비스 인터페이스를 작성한다. 기본적인 CRUD 기능을 호출하기 위해 간단히 정의!

    service/ProductService.java

    public interface ProductService {
    
        ProductResponseDto getProduct(Long number);
        ProductResponseDto saveProduct(ProductDto productDto);
        ProductResponseDto changeProductName(Long number, String name) throws Exception;
        void deleteProduct(Long number) throws Exception;     
    }

    위 예제를 보면

    리턴 타입이 DTO 객체인 것을 볼 수 있다.


    DAO 객체에서 엔티티 타입을 사용하는 것을 고려하면

    서비스 레이어에서 DTO객체와 엔티티 객체를 각 레이어에

    변환해서 전달하는 역할도 수행한다고 볼 수 있다.


    정리해보면

    데이터베이스와 밀접한 관련이 있는 데이터 엑세스 레이어까지는 엔티티 객체를 사용하고,

    클라이언트와 가까워지는 다른 레이어에서는 데이터를 교환하는 데 DTO 객체를 사용하는 것이 일반적이다.

    서비스와 dao 사이에서의 데이터 전달은 개발 그룹 내 규정에 따라 DTO를 사용하기도 한다.

    단일 데이터나 소량의 데이터를 전달하는 경우 DTO나 엔티티를 사용하지 않기도 한다.


    service/imple/ProductServiceImpl.java

    @RequiredArgsConstructor
    @Service
    public class ProductServiceImpl implements ProductService {
    
        private final ProductDAO productDAO;
    
        @Override
        public ProductResponseDto getProduct(Long number) {
            Product product = productDAO.selectProduct(number);
    
            return ProductResponseDto
                    .builder()
                    .number(product.getNumber())
                    .name(product.getName())
                    .price(product.getPrice())
                    .stock(product.getStock())
                    .build();
        }
    
        @Override
        public ProductResponseDto saveProduct(ProductDto productDto) {
            Product product = new Product();
            product.setName(productDto.getName());
            product.setPrice(productDto.getPrice());
            product.setStock(productDto.getStock());
            product.setCreatedAt(LocalDateTime.now());
            product.setUpdatedAt(LocalDateTime.now());
    
            Product savedProduct = productDAO.insertProduct(product);
    
            return ProductResponseDto
                    .builder()
                    .number(savedProduct.getNumber())
                    .name(savedProduct.getName())
                    .price(savedProduct.getPrice())
                    .stock(savedProduct.getStock())
                    .build();
        }
    
        @Override
        public ProductResponseDto changeProductName(Long number, String name) throws Exception {
            Product changedProduct = productDAO.updateProductName(number, name);
            return ProductResponseDto
                    .builder()
                    .number(changedProduct.getNumber())
                    .name(changedProduct.getName())
                    .price(changedProduct.getPrice())
                    .stock(changedProduct.getStock())
                    .build();
        }
    
        @Override
        public void deleteProduct(Long number) throws Exception {
            productDAO.deleteProduct(number);
        }
    }

    - 컨트롤러 생성

    컨트롤러는 클라이언트로부터 요청을 받고

    해당 요청에 대해

    서비스 레이어에 구현된 적절한 메서드를 호출해서 결괏값을 받는다.

    컨트롤러는 요청과 응답을 전달하는 역할만 맡는 것이 좋다.

    controller/ProductController.java

    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/product")
    public class ProductController {
    
        private final ProductService productService;
    
        @GetMapping
        public ResponseEntity<ProductResponseDto> getProduct(Long number) {
            ProductResponseDto productResponseDto = productService.getProduct(number);
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
    
        @PostMapping
        public ResponseEntity<ProductResponseDto> createProductName(@RequestBody ProductDto productDto) {
            ProductResponseDto productResponseDto = productService.saveProduct(productDto);
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
    
        @PutMapping
        public ResponseEntity<ProductResponseDto> changeProductName(
                @RequestBody ChangeProductNameDto changeProductNameDto
        ) throws Exception {
            ProductResponseDto productResponseDto = productService.changeProductName(
                    changeProductNameDto.getNumber(),
                    changeProductNameDto.getName()
            );
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
        @DeleteMapping
        public ResponseEntity<String> deleteProduct(Long number) throws Exception {
            productService.deleteProduct(number);
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body("정상적으로 삭제되었습니다.");
        }
    }



    - Swagger API를 통한 동작 확인

    지난번에 했던 코드를 참고해서 컨트롤러의 각 기능에 API 명세를 작성한다.

    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/product")
    public class ProductController {
    
        private final ProductService productService;
    
        @ApiOperation(value = "GET 메서드 예제")
        @GetMapping
        public ResponseEntity<ProductResponseDto> getProduct(Long number) {
            ProductResponseDto productResponseDto = productService.getProduct(number);
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
    
        @ApiOperation(value = "POST 메서드 예제")
        @PostMapping
        public ResponseEntity<ProductResponseDto> createProductName(@RequestBody ProductDto productDto) {
            ProductResponseDto productResponseDto = productService.saveProduct(productDto);
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
    
        @ApiOperation(value = "PUT 메서드 예제")
        @PutMapping
        public ResponseEntity<ProductResponseDto> changeProductName(
                @RequestBody ChangeProductNameDto changeProductNameDto
        ) throws Exception {
            ProductResponseDto productResponseDto = productService.changeProductName(
                    changeProductNameDto.getNumber(),
                    changeProductNameDto.getName()
            );
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body(productResponseDto);
        }
    
        @ApiOperation(value = "DELETE 메서드 예제")
        @DeleteMapping
        public ResponseEntity<String> deleteProduct(Long number) throws Exception {
            productService.deleteProduct(number);
    
            return ResponseEntity
                    .status(HttpStatus.OK)
                    .body("정상적으로 삭제되었습니다.");
        }
    }

    애플리케이션을 실행하고 웹 브라우저를 통해 Swagger 페이지로 접속한다.

    주소는 http://localhost:8080/swagger-ui/index.html#/

    POST - createProduct API 테스트

    오른쪽 상단의 Try It Out 버튼을 누르면 아래의 입력칸에 값을 입력할 수 있다.

    값 입력 후 execute 클릭

    입력 확인

    GET - getProduct API 테스트

        🐢🎈




Designed by Tistory / Custom by 얼거스