참고:

TASK 4: 데이터베이스 질의 구현 및 Micronaut 애플리케이션 구축

이 실습에서는 데이터베이스 쿼리를 구현하고 Oracle Autonomous Database에 연결되는 Micronaut 애플리케이션을 로컬로 구축합니다.

예상 시간: 30분

태스크 콘텐츠

이 작업에서 다음을 수행합니다.

1단계: Oracle Database 테이블에 매핑되는 Micronaut 데이터 엔티티 생성

이전 작업에서 OWNER이라는 테이블을 생성하는 SQL 스크립트와 PET라는 테이블이 한 번씩 실행되었습니다. 다음으로 데이터베이스 테이블에서 데이터를 읽는 데 사용할 수 있는 엔티티 클래스를 정의해야 합니다.

  1. src/main/java/com/example 패키지에 Owner를 나타낼 엔티티 클래스를 생성합니다. src/main/java/com/example을 마우스 오른쪽 단추로 눌러 Content 메뉴를 확장하고 New File을 선택하고 이름을 Owner.java로 지정한 다음 코드를 붙여넣습니다.

    package com.example;
    
    import io.micronaut.core.annotation.Creator;
    import io.micronaut.data.annotation.GeneratedValue;
    import io.micronaut.data.annotation.Id;
    import io.micronaut.data.annotation.MappedEntity;
    
    @MappedEntity
    public class Owner {
    
      // The ID of the class uses a generated sequence value
      @Id
      @GeneratedValue
      private Long id;
      private final String name;
      private int age;
    
      // the constructor reads column values by the name of each constructor argument
      @Creator
      public Owner(String name) {
          this.name = name;
      }
    
      // each property of the class maps to a database column
      public int getAge() {
          return age;
      }
    
      public void setAge(int age) {
          this.age = age;
      }
    
      public String getName() {
          return name;
      }
    
      public Long getId() {
          return id;
      }
    
      public void setId(Long id) {
          this.id = id;
      }
    }
    

    @MappedEntity 주석은 엔티티가 데이터베이스 테이블에 매핑되었음을 나타내는 데 사용됩니다. 기본적으로 이 테이블은 클래스와 동일한 이름(이 경우 owner)을 사용하는 테이블입니다.

    테이블의 열은 각 Java 속성으로 표시됩니다. 위의 경우 기본 키를 나타내는 데 id 열이 사용되고 @GeneratedValue에서 Autonomous Database의 identity 열을 사용하도록 매핑을 설정합니다.

    @Creator 주석은 매핑된 엔티티를 인스턴스화하는 데 사용될 생성자에서 사용되며 필수 열을 표현하는 데도 사용됩니다. 이 경우 age 열이 필요하지 않고 setAge 설정자를 사용하여 독립적으로 설정할 수 있는 반면 name 열은 필수이며 변경할 수 없습니다.

  2. src/main/java/com/example 아래에 pet 테이블을 모델링하기 위해 Pet 엔티티를 나타낼 Pet.java 파일을 생성합니다.

    package com.example;
    
    import io.micronaut.core.annotation.Creator;
    import io.micronaut.core.annotation.Nullable;
    import io.micronaut.data.annotation.AutoPopulated;
    import io.micronaut.data.annotation.Id;
    import io.micronaut.data.annotation.MappedEntity;
    import io.micronaut.data.annotation.Relation;
    
    import java.util.UUID;
    
    @MappedEntity
    public class Pet {
    
        // This class uses an auto populated UUID for the primary key
        @Id
        @AutoPopulated
        private UUID id;
    
        private final String name;
    
        // A relation is defined between Pet and Owner
        @Relation(Relation.Kind.MANY_TO_ONE)
        private final Owner owner;
    
        private PetType type = PetType.DOG;
    
        // The constructor defines the columns to be read
        @Creator
        public Pet(String name, @Nullable Owner owner) {
            this.name = name;
            this.owner = owner;
        }
    
        public Owner getOwner() {
            return owner;
        }
    
        public String getName() {
            return name;
        }
    
        public UUID getId() {
            return id;
        }
    
        public void setId(UUID id) {
            this.id = id;
        }
    
        public PetType getType() {
            return type;
        }
    
        public void setType(PetType type) {
            this.type = type;
        }
    
        public enum PetType {
            DOG,
            CAT
        }
    }
    

    Pet 클래스는 자동으로 채워진 UUID을 기본 키로 사용하여 ID 생성에 대한 다른 접근 방식을 보여줍니다.

    Pet 클래스와 Owner 클래스 사이의 관계도 @Relation(Relation.Kind.MANY_TO_ONE) 주석을 사용하여 정의되며 이는 다대일 관계임을 나타냅니다.

    이렇게 하면 쿼리를 구현하기 위해 저장소 인터페이스를 정의할 때 사용합니다.

2단계: Micronaut 데이터 저장소를 정의하여 질의 구현

Micronaut 데이터는 데이터 저장소 패턴을 사용하여 컴파일 시간에 SQL 쿼리를 자동으로 구현하는 인터페이스 정의를 지원합니다. 이 섹션에서는 이 Micronaut 데이터 기능을 활용할 수 있습니다.

  1. src/main/java/com/example 아래에 repositories라는 별도의 폴더를 생성합니다.

  2. OwnerRepository.java 파일에서 ORACLE dialect를 사용하여 CrudRepository에서 확장되고 @JdbcRepository로 주석이 달린 새 저장소 인터페이스를 정의합니다.

    package com.example.repositories;
    
    import com.example.Owner;
    import io.micronaut.data.jdbc.annotation.JdbcRepository;
    import io.micronaut.data.model.query.builder.sql.Dialect;
    import io.micronaut.data.repository.CrudRepository;
    
    import java.util.List;
    import java.util.Optional;
    
    // The @JdbcRepository annotation indicates the database dialect
    @JdbcRepository(dialect = Dialect.ORACLE)
    public interface OwnerRepository extends CrudRepository<Owner, Long> {
    
        @Override
        List<Owner> findAll();
    
        // This method will compute at compilation time a query such as
        // SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ?
        Optional<Owner> findByName(String name);
    }
    

    CrudRepository 인터페이스는 두 개의 일반 인수 유형을 사용합니다. 첫번째 유형은 엔티티 유형(이 경우 Owner)이고 두번째 유형은 ID 유형(이 경우 Long)입니다.

    CrudRepository 인터페이스는 데이터베이스에서 컴파일 시 자동으로 계산되는 적절한 SQL 삽입, 선택, 갱신 및 삭제를 사용하여 CRUD(엔티티 생성, 읽기, 업데이트 및 삭제)를 수행하는 방법을 정의합니다. 자세한 내용은 CrudRepository의 Javadoc를 참조하십시오.

    인터페이스 내에서 JDBC 질의를 수행하고 정확한 트랜잭션 의미(질의의 경우 읽기 전용 트랜잭션) 정의, 질의를 실행하고 결과 집합을 이전에 정의한 Owner 엔티티 클래스에 매핑하는 등 모든 복잡한 세부 정보를 자동으로 처리하는 메소드를 정의할 수 있습니다.

    위에 정의된 findByName 메소드는 컴파일 시에 SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ?와 같은 query를 자동으로 생성합니다.

    질의 방법 및 정의할 수 있는 질의 유형에 대한 자세한 내용은 Micronaut 데이터 설명서의 질의 방법 설명서를 참조하십시오.

  3. OwnerRepository를 그대로 사용하여 다른 저장소를 정의하고 이번에는 DTO(데이터 전송 객체)를 사용하여 최적화된 질의를 수행합니다. 따라서 먼저 src/main/java/com/example/repositoriesNameDTO.java 파일에 DTO 클래스를 생성해야 합니다.

    package com.example.repositories;
    
    import io.micronaut.core.annotation.Introspected;
    
    @Introspected
    public class NameDTO {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    DTO는 특정 query에 필요한 열만 선택하여 보다 최적화된 query를 생성할 수 있는 간단한 POJO입니다.

  4. 동일한 위치 src/main/java/com/example/repositories에서 DTO를 사용하는 Pet 엔티티에 대해 PetRepository.java라는 파일에 PetRepository라는 저장소를 정의합니다.

    package com.example.repositories;
    
    import com.example.Pet;
    import io.micronaut.data.annotation.Join;
    import io.micronaut.data.jdbc.annotation.JdbcRepository;
    import io.micronaut.data.model.query.builder.sql.Dialect;
    import io.micronaut.data.repository.PageableRepository;
    
    import java.util.List;
    import java.util.Optional;
    import java.util.UUID;
    
    @JdbcRepository(dialect = Dialect.ORACLE)
    public interface PetRepository extends PageableRepository<Pet, UUID> {
    
        List<NameDTO> list();
    
        @Join("owner")
        Optional<Pet> findByName(String name);
    }
    

    DTO를 반환하는 list 메소드를 기록해 둡니다. 이 메소드는 컴파일 시에 다시 구현되지만 이번에는 Pet 테이블의 모든 열을 검색하는 대신 name 열과 사용자가 정의할 수 있는 다른 열만 검색합니다.

    @Join 주석은 조인된 객체(Owner)를 질의 및 인스턴스화하여 질의된 PetOwner 필드에 지정합니다.

    findByName 메소드는 @Join 주석인 Micronaut 데이터의 다른 중요한 기능을 사용하므로 흥미롭습니다. 이 도구를 사용하면 데이터베이스 조인을 통해 필요한 데이터를 정확히 검색하여 훨씬 효율적으로 query할 수 있도록 조인 경로를 지정할 수 있습니다.

데이터 저장소를 적절히 사용하면 REST 엔드포인트를 노출하기 위해 이동할 수 있습니다.

3단계: Micronaut 컨트롤러를 REST 끝점으로 노출합니다.

Micronaut의 REST 끝점은 쓰기가 쉬우며 컨트롤러( MVC 패턴별)로 정의됩니다.

  1. src/main/java/com/example/ 아래에 controllers 폴더를 생성합니다.

  2. OwnerController.java라는 파일에 새 OwnerController 클래스를 정의합니다.

    package com.example.controllers;
    
    import java.util.List;
    import java.util.Optional;
    
    import javax.validation.constraints.NotBlank;
    
    import com.example.Owner;
    import com.example.repositories.OwnerRepository;
    import io.micronaut.http.annotation.Controller;
    import io.micronaut.http.annotation.Get;
    import io.micronaut.scheduling.TaskExecutors;
    import io.micronaut.scheduling.annotation.ExecuteOn;
    
    @Controller("/owners")
    @ExecuteOn(TaskExecutors.IO)
    class OwnerController {
    
        private final OwnerRepository ownerRepository;
    
        OwnerController(OwnerRepository ownerRepository) {
            this.ownerRepository = ownerRepository;
        }
    
        @Get("/")
        List<Owner> all() {
            return ownerRepository.findAll();
        }
    
        @Get("/{name}")
        Optional<Owner> byName(@NotBlank String name) {
            return ownerRepository.findByName(name);
        }
    }
    

    컨트롤러 클래스는 컨트롤러가 매핑되는 루트 URI(이 경우 /owners)를 정의하는 데 사용할 수 있는 @Controller 주석으로 정의됩니다.

    Micronaut에 컨트롤러가 데이터베이스와 I/O 통신을 수행하므로 I/O 스레드 풀에서 실행해야 함을 알리는 데 사용되는 @ExecuteOn 주석을 확인합니다.

    OwnerController 클래스는 Micronaut 종속성 주입을 사용하여 이전에 정의한 OwnerRepository 저장소 인터페이스에 대한 참조를 얻고 두 끝점을 구현하는 데 사용됩니다.

    • / - 루트 끝점에는 모든 소유자가 나열됩니다.
    • /{name} - 두번째 끝점은 URI 템플리트를 사용하여 이름별로 소유자를 조회할 수 있도록 허용합니다. URI 변수 {name}의 값은 byName 메소드에 대한 매개변수로 제공됩니다.
  3. 그런 다음 src/main/java/com/example/controllersPetController.java 파일에 PetController라는 다른 REST 끝점을 정의합니다.

    package com.example.controllers;
    
    import java.util.List;
    import java.util.Optional;
    
    import com.example.repositories.NameDTO;
    import com.example.Pet;
    import com.example.repositories.PetRepository;
    import io.micronaut.http.annotation.Controller;
    import io.micronaut.http.annotation.Get;
    import io.micronaut.scheduling.TaskExecutors;
    import io.micronaut.scheduling.annotation.ExecuteOn;
    
    @ExecuteOn(TaskExecutors.IO)
    @Controller("/pets")
    class PetController {
    
        private final PetRepository petRepository;
    
        PetController(PetRepository petRepository) {
            this.petRepository = petRepository;
        }
    
        @Get("/")
        List<NameDTO> all() {
            return petRepository.list();
        }
    
        @Get("/{name}")
        Optional<Pet> byName(String name) {
            return petRepository.findByName(name);
        }
    }
    

    이번에는 PetRepository가 삽입되어 이름으로 애완동물 및 애완동물 목록이 노출됩니다.

4단계: 응용 프로그램 시작 시 데이터 채우기

다음 단계는 시작 시 일부 응용 프로그램 데이터를 채우는 것입니다. 이렇게 하려면 Micronaut 응용 프로그램 이벤트를 사용할 수 있습니다.

src/main/java/com/example/Application.java 클래스를 열고 초기 파일 내용을 다음으로 바꿉니다.

package com.example;

import com.example.repositories.OwnerRepository;
import com.example.repositories.PetRepository;
import io.micronaut.context.event.StartupEvent;
import io.micronaut.runtime.Micronaut;
import io.micronaut.runtime.event.annotation.EventListener;

import jakarta.inject.Singleton;
import javax.transaction.Transactional;
import java.util.Arrays;

@Singleton
public class Application {
    private final OwnerRepository ownerRepository;
    private final PetRepository petRepository;

    Application(OwnerRepository ownerRepository, PetRepository petRepository) {
        this.ownerRepository = ownerRepository;
        this.petRepository = petRepository;
    }

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }

    @EventListener
    @Transactional
    void init(StartupEvent event) {
        // clear out any existing data
        petRepository.deleteAll();
        ownerRepository.deleteAll();

        // create data
        Owner fred = new Owner("Fred");
        fred.setAge(45);
        Owner barney = new Owner("Barney");
        barney.setAge(40);
        ownerRepository.saveAll(Arrays.asList(fred, barney));

        Pet dino = new Pet("Dino", fred);
        Pet bp = new Pet("Baby Puss", fred);
        bp.setType(Pet.PetType.CAT);
        Pet hoppy = new Pet("Hoppy", barney);

        petRepository.saveAll(Arrays.asList(dino, bp, hoppy));
    }
}

생성자는 데이터를 지속할 수 있도록 저장소 정의를 종속성 주입하도록 수정되었습니다.

마지막으로 init 메소드는 StartupEvent를 수신하는 인수를 사용하여 @EventListener로 주석 처리됩니다. 이 이벤트는 응용 프로그램이 실행되고 나면 호출되며, 응용 프로그램이 이를 수행할 준비가 되면 데이터를 지속하는 데 사용할 수 있습니다.

나머지 예제는 CrudRepository 인터페이스의 saveAll 메소드를 사용하여 일부 엔티티를 저장하는 방법을 보여줍니다.

메소드 실행 중에 예외가 발생하면 롤백된 JDBC 트랜잭션에서 Micronaut 데이터가 init 메소드 실행을 래핑하도록 하는 메소드에서 javax.transaction.Transactional가 선언됩니다.

5단계: Micronaut 애플리케이션에 대한 통합 테스트 실행

응용 프로그램이 응용 프로그램을 성공적으로 시작할 수 있는지 여부를 확인하는 단일 테스트로 이미 설정되었습니다. 따라서 이전 섹션에 정의된 init 메소드의 논리를 테스트합니다.

  1. 위쪽 탐색에서 Terminal, New Terminal으로 이동합니다.

  2. test 목표를 실행하여 테스트를 실행합니다.

    ./mvnw test
    

    VS 코드의 단말기 창에서 파일 구조를 확인하고 테스트를 실행합니다.

    또는 Gradle을 사용하는 경우 test 작업을 실행합니다.

    ./gradlew test
    

    테스트 실행이 완료되면 BUILD SUCCESS 메시지가 표시됩니다.

이제 다음 작업 진행할 수 있습니다.

더 알아보기