Spring Boot, 데이터베이스 연동 (MariaDB)

Jihu Kim·2024년 1월 15일
0

Spring 입문

목록 보기
6/14
post-thumbnail

데이터베이스 생성하기 (MariaDB)

애플리케이션은 데이터를 주고받는 것이 주 목적이다. 정상적으로 로직이 동작하기 위해서는 데이터베이스가 필요할 것이다.

MariaDB를 애플리케이션에 적용해 연동을 해보겠다.

MairaDB는 기본 포트 번호로 3306을 사용한다.

MariaDB를 설치하면 서드파티 도구로 HeidiSQL이 함께 설치된다.
HeidiSQL은 데이터베이스에 접속해서 관리하는 GUI 도구이다.

데이터베이스의 접속 정보를 등록하기 위해 '신규' 버튼을 누른다.

세션이름을 지정해주고, 호스트명/IP는 127.0.0.1 또는 localhost, 사용자는 root, 암호는 MariaDB 설치 단계에서 지정한 패스워드, 포트는 3306으로 설정한 후 '열기' 버튼을 누른다.

기본적으로 4개의 데이터베이스가 존재한다. 이 4개의 데이터베이스에는 MariaDB의 환경설정 등과 같은 정보를 담고있어 데이터베이스를 튜닝할 때 이외에는 건드리지 않도록 한다.

이 화면에서 스프링 부트 애플리케이션에서 사용할 데이터베이스를 하나 생성한다.
나는 springboot_study라는 이름의 데이터베이스를 생성해주었다.


Spring의 DB 접근 기술

순수 JDBC

JDBC API로 직접 코딩하는 것은 20년 전의 이야기이다. 참고만 하도록 하자.

public class JdbcMemberRepository implements MemberRepository {
    private final DataSource dataSource;
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
}

스프링 JDBC Template

JDBC API에서 반복 코드를 대부분 제거한 형태이다. 하지만, SQL은 직접 작성해야 한다.

public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());
        Number key = jdbcInsert.executeAndReturnKey(new
                MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

JPA

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

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

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

public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
 }

Spring Data JPA

Spring Data JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다.

리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다. 반복 개발해온 기본 CRUD 기능도 Spring Data JPA가 모두 제공한다.

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

알고 가야할 기본개념

ORM이란?

ORM은 Object Relation Mapping의 줄임말로 객체 관계 매핑을 의미한다.

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

여기서 이야기하는 객체지향 언어에서의 객체는 클래스를 의미한다.

클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 RDB 테이블과 어쩔 수 없는 불일치가 존재한다. ORM은 이 둘의 불일치와 제약사항을 해결하는 역할이다.

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

JPA란?

JPA(Java Persistence API)는 자바진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다.

JPA의 메커니즘을 보면 내부적으로 JDBC를 사용한다. 개발자가 직접 JDBC를 사용하면 SQL에 의존하게 되는 등 개발의 효율성이 떨어지지만, JPA는 적절한 SQL을 생성하고 데이터베이스를 조작해서 객체를 자동으로 매핑하는 역할을 수행한다.

하이버네이트

하이버네이트는 자바의 ORM 프레임워크로, JPA가 정의하는 인터페이스를 구현하고 있는 JPA 구현체 중 하나이다.

Spring Data JPA

Spring Data JPA는 JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나이다. CRUD 처리에 필요한 인터페이스를 제공하며, 하이버네이트의 엔티티 매니저를 직접 다루지 않고 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작한다. 이를 통해 하이버네이트에서 자주 사용되는 기능을 더 쉽게 사용할 수 있게 구현한 라이브러리이다.

영속성 컨텍스트

영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행한다. 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행한다.

영속성 컨텍스트는 세션 단위의 생명주기를 갖는다. 데이터베이스에 접근하기 위한 세션이 생성되면 영속성 컨텍스트가 만들어지고, 세션이 종료되면 영속성 컨텍스트도 없어진다. 엔티티 매니저는 이러한 일련의 과정에서 영속성 컨텍스트에 접근하기 위한 수단으로 사용된다.

엔티티 매니저

엔티티 매니저는 엔티티를 관리하는 객체이다. 엔티티 매니저는 데이터베이스에 접근해서 CRUD 작업을 수행한다.

Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터베이스에 접근한다. (리포지토리에서 엔티티 매니저를 사용한다.)

엔티티 매니저는 엔티티 매니저 팩토리가 만든다. 엔티티 매니저 팩토리는 데이터베이스에 대응하는 객체로서 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정만으로도 동작하지만, JPA의 구현체 중 하나인 하이버네이트에서는 persistence.xml이라는 설정파일을 구성하고 사용해야 하는 객체이다.


데이터베이스 연동해보기

build.gradle 파일에 JPA, 데이터베이스 관련 라이브러리 추가
resources/application.properties에 데이터베이스 연결 설정 추가(이때, spring.datasource.username=sa를 꼭 추가해야한다.)

build.gradle 설정

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
//	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
//	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

application.properties 설정

연동할 데이터베이스의 정보를 application.properties에 작성해야한다.

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/springboot_study
spring.datasource.username=root
spring.datasource.password=비밀번호


spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

spring.datasource.url 항목에서 마리아DB의 경로임을 명시하고 경로와 데이터베이스명을 입력한다. 설정한 계정정보를 입력해 데이터베이스를 연동하는 값을 설정한다.

spring.jpa.hibernate.ddl-auto는 데이터베이스를 자동으로 조작하는 옵션이다. 여기서 사용할 수 옵션으로 (create, create-drop, update, validate, none) 등이 있다.
운영환경에서는 create, create-drop, update 기능은 사용하지 않는다. 데이터베이스에 축적된 데이터를 지워버릴 수도 있고, 객체 정보가 변경됐을 때 데이터베이스 정보까지 변경될 수 있기 때문이다.
운영환경에서는 대체로 validate나 none을 사용한다. 반면 개발환경에서는 create 또는 update를 사용하는 편이다.

spring.jpa.show-sql은 로그에 하이버네이트가 생성한 쿼리문을 출럭하는 옵션이다. 아무 설정이 없으면 저장에 용이한 형태로 출력되기 때문에 사람이 보기에는 불편하다. 이 때, spring.jpa.properties.hibernate.format_sql 옵션으로 사람이 보기 좋게 포메팅 할 수 있다.

엔티티 설계

Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없다. 이 기능을 가능하게 하는 것이 엔티티이다.

JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의한다. 엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있다.

엔티티 관련 어노테이션

@Entity : 해당 클래스가 엔티티임을 명시하기 위한 어노테이션이다.
@Table : 엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 @Table 어노테이션이 필요하지 않다. 클래스의 이름과 테이블의 이름을 다르게 지정해야하는 경우 사용한다.
@Id : 엔티티 클래스의 필드는 테이블의 칼럼과 매핑된다. @Id 어노테이션이 선언된 필드는 테이블의 기본 역할로 사용된다. 모든 엔티티는 @Id 어노테이션이 필요하다.
@GeneratedValue : 일반적으로 @Id 어노테이션과 함께 사용된다. 이 어노테이션은 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용한다. (AUTO, IDENTITY, SEQUENCE, TABLE, @GeneratedValue를 사용하지 않고 직접할당 등으로 설정할 수 있다.)
@Column : 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑된다. (name, nullable, length, unique 등으로 설정할 수 있다.)
@Transient : 엔티티 클래스에는 선언되어 있지만 데이터베이스에서는 필요 없는 경우 사용한다.

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

Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공한다. Spring Boot로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.

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

repository 패키지를 만들고 해당 패키지 안에 JpaRepository를 상속하는 인터페이스(repository)를 생성해보자.

엔티티는 데이터베이스의 테이블과 구조를 생성하는데 사용되고, 리포지토리는 엔티티가 생성한 데이터베이스에 접근하는데 사용된다.

JpaRepository를 상속받을 때는 대상 엔티티와 기본값 타입을 지정해야한다. 대상 엔티티를 Product로 설정하고 해당 엔티티의 @ID 필드타입인 Long을 설정하면 된다.

JpaRepository를 상속받으면 별도의 메서드 구현 없이도 많은 기능을 제공한다.

public interface ProductRepository extends JpaRepository<Product, Long> {

}

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

JpaRepository를 상속받으면서 별도의 메소드 구현 없이도 많은 기능을 사용할 수 있지만, 몇가지의 명명규칙으로 커스텀 메서드도 생성할 수 있다.

(findByName(String name), findByNameAndEmail(String name, String email), List<Product> findByOrderByPriceAsc(String name); ) 등 필요하다면 찾아서 사용해보자.

DAO 설계

DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다. 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행한다.

Spring Data JPA에서 DAO의 개념은 리포지토리가 대체하고 있다.

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

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

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

DAO와 리포지토리의 역할이 비슷하다. 고민해보는 시간을 갖어보자.

DAO 클래스 생성

DAO 클래스는 일반적으로 인터페이스-구현체 구성으로 생성한다. DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴이며, 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있다.

CRUD를 다루기 위해 인터페이스에 메서드를 정의한다.

ProductDAO.interface 예시

public interface ProductDAO {

    Product insertProduct(Product product);

    Product selectProduct(Long number);

    Product updateProductName(Long number, String name) throws Exception;

    void deleteProduct(Long number) throws Exception;

}

일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달한다. 이때 데이터 객체를 엔티티 객체로 전달할지, DTO 객체로 전달할지 정한다.

인터페이스 설계를 마쳤다면, 해당 인터페이스의 구현체를 만들어야한다.

ProductDAOImpl.class 예시

@Component
public class ProductDAOImpl implements ProductDAO {

    private ProductRepository productRepository;

    @Autowired
    public ProductDAOImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // 예제 6.11
    @Override
    public Product insertProduct(Product product) {
        Product savedProduct = productRepository.save(product);

        return savedProduct;
    }

    // 예제 6.12
    @Override
    public Product selectProduct(Long number) {
        Product selectedProduct = productRepository.getById(number);

        return selectedProduct;
    }

    // 예제 6.15
    @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());

            updatedProduct = productRepository.save(product);
        } else {
            throw new Exception();
        }

        return updatedProduct;
    }

    // 예제 6.17
    @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();
        }
    }
}

ProductDAOImpl(구현체) 클래스를 스프링이 관리하는 빈으로 등록한다. (@Component, @Service 어노테이션을 지정한다.)

빈으로 등록된 객체는 다른 클래스가 인터페이스를 가지고 의존성을 주입 받을 때 이 구현체를 찾아 주입하게 된다.

마찬가지로 DAO 객체에서도 데이터베이스에 접근하기 위해 리포지토리 인터페이스를 사용해 의존성을 주입 받아야한다.

인터페이스에 정의한 메소드를 구현해야한다.

insertProduct()를 통해 Product 엔티티를 데이터베이스에 저장하는 기능을 수행한다.

selectProduct()를 통해 조회한다.

updateProductName()을 통해 업데이트한다.

deleteProduct()를 통해 삭제한다.

SimpleJpaRepository에 구현돼 있는 save() 메서드를 보면, @Transactional 어노테이션이 선언되어 있는 것을 확인할 수 있다.
이 어노테이션이 지정돼 있으면 메서드 내 작업을 마칠 경우 자동으로 flush() 메서드를 실행한다. 이 과정에서 변경이 감지되면 데이터베이스 레코드를 업데이트하는 쿼리가 실행된다.

JPA를 통한 모든 변경은 트랜잭션 안에서 실행해야 한다.

DAO 연동

클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야한다.

서비스 클래스 만들기

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

컨트롤러 만들기

서비스 객체의 설계를 마친 후 비즈니스 로직과 클라이언트의 요청을 연결하는 컨트롤러를 생성한다.

컨트롤러는 클라이언트로부터 요청을 받고 해당 요청에 대해 서비스 레이어에 구현된 적절한 메서드를 호출해서 결괏값을 받는다.

컨트롤러는 요청과 응답을 전달하는 역할만 맡도록 한다.

다시보는 스프링 부트 구조

동작확인


profile
Jihukimme

0개의 댓글