상품 관리 애플리케이션에 데이터베이스 연동하기

sookyoung.k·2024년 6월 13일
0

☕Java

목록 보기
7/11
post-thumbnail

🛢️ 데이터베이스 다뤄보기

🐋 도커 설치하기

도커 데스크톱을 통해서 도커의 기능을 쉽게 활용해보도록 할 것이다.
설치는 그냥 설치 파일 받고 기본 설정 ok이 해주고 쭉쭉 다운 받고 실행 시키면 끝이다.

도커로 데이터베이스 실행하기

docker run --name some-mysql -p 3306:3306 `-e` - 데이터베이스의 '관리자(ROOT)' 계정의 비밀번호 지정, 관리자 계정은 자동으로 생성된다. 
- MYSQL_ROOT_PASSWORD={자기 비번 설정} -d mysql:8.0.29 --character-set-server=utf8mb4 --collateion-server=utf8mb4_general_ci
  • docker run - 도커 명령어, 데이터베이스 같은 것을 실행시킬 때 사용
  • --name - 컴퓨터에 여러 개의 도커로 실행시킨 데이터베이스가 있을 때 각각을 구분하기 위해 지어준다
  • -p - 도커 내부에서의 3306 포트와 도커 외부의 3306 포트를 연결시켜 준다. MySQL이 사용하는 포트를 도커 바깥으로 노출시켜준다. 나의 경우엔... 내부에서 3306을 이미 사용 중이라 내부 3307과 외부 3306 포트를 연결시켜서 사용했다.
  • -e - 데이터베이스의 '관리자(ROOT)' 계정의 비밀번호 지정, 관리자 계정은 자동으로 생성된다.
  • -d - 실행시킬 도커가 어떤 것인지 지정, 여기서는 mysql 데이터베이스의 8.0.29 버전 실행
  • --character-set-server=utf8mb4 --collateion-server=utf8mb4_general_ci - MySQL에서 사용될 인코딩 방식 설정, 한글로 된 데이터 저장하기 위해서는 해당 설정 필수

데이터베이스 접속하기

도커 데스크톱을 실행해서 [Containers] 탭의 'some-mysq' 도커 컨테이너 실행!

[Open in terminal]을 선택하면 도커 컨테이너 내부를 보여주는 [Exec] 탭이 열린다. 여기서 명령어 입력 가능!

mysql -u root -p 입력 후 엔터 → 패스워드 입력

우리가 해야 할 일

  • 상품 관리 애플리케이션에 사용할 데이터베이스 만들기
  • 데이터베이스 안에 상품 테이블 생성하기

MySQL 도커 컨테이너 > MySQL 데이터베이스 프로세스 - 인스턴스 (데이터베이스 프로세스 그 자체) > 직접 생성해야 하는 데이터베이스 - 스키마(프로세스 내에 직접 추가해 줘야 하는 요소) > 상품 관리 테이블

SHOW DATABASES; → 현재 데이터베이스 인스턴스에 있는 데이터베이스 스키마들을 확인

CREATE SCHEMA product_management; → 상품 관리 애플리케이션에 사용할 데이터베이스 스키마 추가

USE product_management; → 해당 스키마 사용

SHOW TABLES; → 사용 중인 스키마 내의 테이블 목록

이후에는 일반 쿼리문을 사용하여 테이블을 만들고(CREATE TABLE) 데이터를 넣어주면(INSERT INTO ... VALUES ...) 된다. 기본적인거라 여기서는 생략.

🎁 상품 추가 기능 구현하기

데이터베이스 실행 후 테이블을 만들었다면! 이제 기능 구현을 해야 한다.

✳️ 데이터베이스 사용을 위한 의존성 추가

	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	// https://mvnrepository.com/artifact/mysql/mysql-connector-java
	implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.33'

데이터베이스 접속 정보 추가

properties 파일을 지우고 yaml을 사용해줍시다. 문법이 조금 다를 뿐... 개인 선호에 맞게 사용하면 된다.

application.yaml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    url: jdbc:mysql://localhost:3307/product_management
    password: hanbit
  • url : 데이터베이스의 URL을 입력하면 된다.
    • localhost → 나의 PC, 자기자신! 거기에서 3307 포트로 접속을 시도한다.
    • 데이터베이스 스키마도 함께 지정해주기 위해 만들었던 product_management로 접속한다.
  • username, password → 계정 아이디와 비밀번호 지정
  • driver-class-name → 데이터베이스 연결 시 어떤 드라이버를 사용할지를 지정

데이터베이스 접속 테스트

package kr.co.hanbit.product.management;

import org.modelmapper.ModelMapper;
import org.modelmapper.config.Configuration;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;
import java.sql.Connection;

@SpringBootApplication
public class Application {
  
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
    
	(생략)

	@Bean
	public ApplicationRunner runner(DataSource dataSource) {
		return args -> {
        	// 이 부분에 실행할 코드를 넣으면 된다 
			Connection connection = dataSource.getConnection();
		};
	}

}
  • Application Runner → 스프링 부트 애플링케이션이 시작한 직후 실행하려는 코드를 추가할 수 있는 의존성
    • Application Runner를 빈으로 등록하면서 람다 표현식 함수 내에 실행할 코드를 넣으면 된다.
    • DataSource → 데이터베이스와의 연결을 담당하는 인터페이스, 얘를 통해서 데이터베이스와의 커넥션을 가져올 수 있다.

✳️ 상품 추가 기능 구현

DatabaseProductRepository 추가하기 / 상품 추가 기능을 위한 쿼리와 Product의 getter

  • ListProductRepository가 하는 일을 모두 할 수 있어야 하기 때문에 모든 메서드를 가져와야 한다.
  • 데이터베이스에 상품을 추가하려면 데이터베이스에 INSERT SQL을 전송해야 한다.
    👉🏻 SQL을 전송하기 위해서는 JdbcTemplete 의존성을 사용하면 된다 !
package kr.co.hanbit.product.management.infrastructure;

import kr.co.hanbit.product.management.domain.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Collections;
import java.util.List;

@Repository
public class DatabaseProductRepository {

	// 의존성 주입
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public DatabaseProductRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public Product add(Product product) {
        jdbcTemplate
                .update("INSERT INTO products (name, price, amount) VALUES (?, ?, ?)",
                        product.getName(), product.getPrice(), product.getAmount());

        return product;
    }

    public Product findById(Long id) {
        return null;
    }

    public List<Product> findAll() {
        return Collections.EMPTY_LIST;
    }

    public List<Product> findByNameContaining(String name) {
        return Collections.EMPTY_LIST;
    }

    public Product update(Product product) {
        return null;
    }

    public void delete(Long id) {
        // do nothing
    }
}

값이 들어갈 곳에 물음표가 들어가 있다. 해당 물음표 뒤에는 뒤쪽에 오는 인자들이 순서대로 들어간다.

이제 우리는 Product 인스턴스의 값을 가져와야 한다. 하지만 현재는 Product에 getter가 없기 때문에 만들어 주어야 한다.

package kr.co.hanbit.product.management.domain;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;

import java.util.Objects;

public class Product {
    private Long id;
    @Size(min = 1, max = 100)
    private String name;
    @Max(1_000_000)
    @Min(0)
    private Integer price;
    @Max(9_999)
    @Min(0)
    private Integer amount;

    public String getName() {
        return name;
    }

    public Integer getPrice() {
        return price;
    }

    public Integer getAmount() {
        return amount;
    }

    (생략)
    
}

웬만하면 getter를 사용하지 않는 것이 좋지만... 기술적인 문제로 불가피하게 추가해야 하는 상황이 바로 이런 것이다. (이게 더 실용적)

하지만 여기에서 getter를 사용한다고 해도 애플리케이션 서비스에도 getter를 사용하는 코드를 써선 안된다.

우선 필요한 getter만 구현을 해두었다.

ListProductRepository 대신 DatabaseProductRepository 사용하기

SimpleProductService.java

package kr.co.hanbit.product.management.application;

import kr.co.hanbit.product.management.domain.Product;
import kr.co.hanbit.product.management.infrastructure.DatabaseProductRepository;
import kr.co.hanbit.product.management.infrastructure.ListProductRepository;
import kr.co.hanbit.product.management.presentation.ProductDto;
import org.modelmapper.ModelMapper;

@Service
public class SimpleProductService {

    private DatabaseProductRepository databaseProductRepository;

    private ListProductRepository listProductRepository;
    private ModelMapper modelMapper;
    private ValidationService validationService;

    @Autowired
    SimpleProductService(DatabaseProductRepository databaseProductRepository, ModelMapper modelMapper, ValidationService validationService) {
        this.databaseProductRepository = databaseProductRepository;
        this.modelMapper = modelMapper;
        this.validationService = validationService;
    }
	
    public ProductDto add(ProductDto productDto) {
    	Product product = modelMapper.kmap(productDto, Product.class);
        validationService.checkValid(product);

        Product savedProduct = databaseProductRepository.add(product);
        ProductDto savedProductDto = modelMapper.map(savedProduct, ProductDto.class);
	
    public ProductDto findById(Long id) {
        Product product = databaseProductRepository.findById(id);
        ProductDto productDto = modelMapper.map(product, ProductDto.class);
        return productDto;
    }

    public List<ProductDto> findAll() {
        List<Product> products = databaseProductRepository.findAll();
        List<ProductDto> productDtos = products.stream()
                .map(product -> modelMapper.map(product, ProductDto.class))
                .toList();
        return productDtos;
    }

    public List<ProductDto> findByNameContaining(String name) {
        List<Product> products = databaseProductRepository.findByNameContaining(name);
        List<ProductDto> productDtos = products.stream()
                .map(product -> modelMapper.map(product, ProductDto.class))
                .toList();

    public ProductDto update(ProductDto productDto) {
        Product product = modelMapper.map(productDto, Product.class);
        Product updateProduct = databaseProductRepository.update(product);
        ProductDto updatedProductDto = modelMapper.map(updateProduct, ProductDto.class);
        return updatedProductDto;
    }

    public void delete(Long id) {
        databaseProductRepository.delete(id);
    }
}

ListProductRepository를 사용하던 모든 코드를 DatabaseProductRepository를 사용하도록 전부 바꿔준다.

HTTP 응답에 id 추가하기

DatabaseProductRepository.java

package kr.co.hanbit.product.management.infrastructure;

(생략)

@Repository
public class DatabaseProductRepository {

    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @Autowired
    public DatabaseProductRepository(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    public Product add(Product product) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        SqlParameterSource namedParameter = new BeanPropertySqlParameterSource(product);

        namedParameterJdbcTemplate
                .update("INSERT INTO products (name, price, amount) VALUES (:name, :price, :amount)",
                        namedParameter, keyHolder);

        Long generatedId = keyHolder.getKey().longValue();
        product.setId(generatedId);

        return product;
    }

    (생략)
}
  • JdbcTemplete 의존성을 NamedParameterJdbcTemplate으로 변경한다.
    👉🏻 물음표로 매개변수를 매핑하지 않고 매개변수의 이름을 통해서 SQL 쿼리와 값을 매핑한다.
    👉🏻 물음표는 순서가 바뀌거나 헷갈릴 수 있지만 이를 통해 방지할 수 있다!
  • getter 메서드를 사용하는 코드를 없애고 BeanPropertySqlParameterSource 객체를 사용했다!
    👉🏻 getter 메서드의 기능은 SqlParameterSource namedParameter = new BeanPropertySqlParameterSource(product);에서 처리한다.
    👉🏻 BeanPropertySqlParameterSource이 객체는 Product의 객체를 통해 SQL 쿼리의 매개변수를 매핑시켜주는 객체이다.
  • KeyHolder 객체를 사용하여 id를 채워준다.
    👉🏻 KeyHolder 객체를 생성한 후 update() 메서드의 매개변수로 넘겨준다.
    👉🏻 이렇게 매개변수로 넘겨주면 KeyHolder에는 id가 담겨서 온다.
    👉🏻 INSERT 쿼리가 실행된 후 Long 타입의 id 값을 가져오는 코드와 해당 id를 Product에 지정한다.

🎁 상품 조회/수정/삭제 구현하기

✳️ 상품 조회 기능 구현

id로 상품 조회 기능 추가하기

DatabaseProductRepository.java

package kr.co.hanbit.product.management.infrastructure;

(생략)

@Repository
public class DatabaseProductRepository {
    
    (생략)

    public Product findById(Long id) {
        SqlParameterSource namedParameter = new MapSqlParameterSource("id", id);

        Product product = namedParameterJdbcTemplate.queryForObject(
                "SELECT id, name, price, amount FROM products WHERE id=:id",
                namedParameter,
                new BeanPropertyRowMapper<>(Product.class)
        );

        return product;
    }

    (생략)
    
}
  • MapSqlParameterSource 사용
    👉🏻 Map 형태로 Key-Value 형태를 매핑할 수 있다
  • queryForObject(SQL 쿼리, namedParameter, BeanPropertyRowMapper)
    👉🏻 BeanPropertyRowMapper → 조회된 상품 정보를 Product 인스턴스로 변환

위의 과정을 통해 데이터베이스에서 데이터를 가져온 후 자바의 인스턴스로 만들어준다.

하지만 BeanPropertyRowMapper가 정상적으로 작동하기 위해서는 아래 두 가지 작업을 해주어야 한다.

  1. Product의 인자가 없는 생성자로 Product 인스턴스를 생성한다.
    ⚠️ 인자 없는 생성자가 반드시 필요
  2. 생성된 Product 인스턴스의 setter로 필드를 초기화한다.
    ⚠️ setter가 반드시 필요

때문에 현재 상태로는 이미 setter가 있는 id를 제외하고는 null로 채워진다.

Product.java

package kr.co.hanbit.product.management.domain;

(생략)

public class Product {
    
    (생략)
    
    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    public void setAmount(Integer amount) {
        this.amount = amount;
    }

}

상품 전체 및 이름으로 조회 기능 추가

DatabaseProductRepository.java

package kr.co.hanbit.product.management.infrastructure;

(생략)

@Repository
public class DatabaseProductRepository {
    
    (생략)

    public List<Product> findAll() {
        List<Product> products = namedParameterJdbcTemplate.query(
                "SELECT * FROM products",
                new BeanPropertyRowMapper<>(Product.class)
        );

        return products;
    }

    public List<Product> findByNameContaining(String name) {
        SqlParameterSource namedParameter = new MapSqlParameterSource("name", "%" + name + "%");

        List<Product> products = namedParameterJdbcTemplate.query(
                "SELECT * FROM products WHERE name LIKE :name",
                namedParameter,
                new BeanPropertyRowMapper<>(Product.class)
        );
        return products;
    }

    (생략)
    
}

findAll()

  • queryForObject로 하나의 Product를 조회하던 것이 아닌 List를 조회하도록 바뀌었다.
  • 전체 목록을 조회하는 경우에는 매개변수가 필요 없어서 MapSqlParameterSource를 생성하지 않는다. 자연히 namedParameter도 인자로 넣을 필요가 없어졌다.
  • 조회된 데이터는 BeanPropertyRowMapper에 의해서 변환된다.

findByNameContaining()

  • MapSqlParameterSource를 통해 검색하려는 name을 매핑한다.

✳️ 상품 수정 기능 구현

id로 상품을 찾고 수정하는 기능 추가하기

DatabaseProductRepository.java

package kr.co.hanbit.product.management.infrastructure;

(생략)

@Repository
public class DatabaseProductRepository {
    
    (생략)

    public Product update(Product product) {
        SqlParameterSource namedParameter = new BeanPropertySqlParameterSource(product);

        namedParameterJdbcTemplate.update("UPDATE products SET name=:name, price=:price, amount=:amount WHERE id=:id", namedParameter);

        return product;
    }

    (생략)
    
}

상품 추가 기능과 거의 같다. UPDATE 쿼리를 사용하고, Key Holder 코드가 없다는 점만 다르다.

하지만 현재 구현한 상태로는 에러가 난다! BeanPropertySqlParameterSource는 getter를 통해서 값을 매핑한다. 하지만 id에 대한 getter가 존재하지 않기 때문에 (이전에는 필요가 없었음) 만들지 않았기 때문이다. 이제 만들어줍시다 ~.~

Product.java

    public Long getId() {
        return id;
    }

✳️ 상품 삭제 기능 구현

DatabaseProductRepository.java

package kr.co.hanbit.product.management.infrastructure;

(생략)

@Repository
public class DatabaseProductRepository {
    
    (생략)

    public void delete(Long id) {
        SqlParameterSource namedParameter = new MapSqlParameterSource("id", id);

        namedParameterJdbcTemplate.update(
                "DELETE FROM products WHERE id=:id",
                namedParameter
        );
    }

    (생략)
    
}

MapSqlParameterSource로 id만 매핑해주었고 그 뒤로는 findById와 거의 비슷하다.

* NamedParameterJdbcTemplete의 query/queryForObject와 update

👉🏻 데이터베이스에 SQL 쿼리를 전송하기 위한 여러 메서드 중 가장 대표적인 세 가지다.

  • public <T> T query(String sql, SqlParameterSource parameterSource, ResultSetExtractor<T> rse) throws DataAccessException

  • public <T> T queryForObject(String sql, SqlParameterSource parameterSource, RowMapper<T> rowMapper) throws DataAccessException

  • public int update(String sql, SqlParameterSource parameterSource) throws DataAccessException

query와 queryForObject는 SQL 쿼리 전송 후 그 결과로 특정 클래스의 인스턴스를 받는다.
반면 update는 int 값을 반환한다. update의 반환되는 int 값을 통해 수정 및 삭제의 성공 여부를 알 수 있다.

profile
영차영차 😎

0개의 댓글