도커 데스크톱을 통해서 도커의 기능을 쉽게 활용해보도록 할 것이다.
설치는 그냥 설치 파일 받고 기본 설정 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
입력 후 엔터 → 패스워드 입력
우리가 해야 할 일
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
→ 스프링 부트 애플링케이션이 시작한 직후 실행하려는 코드를 추가할 수 있는 의존성 DataSource
→ 데이터베이스와의 연결을 담당하는 인터페이스, 얘를 통해서 데이터베이스와의 커넥션을 가져올 수 있다. 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만 구현을 해두었다.
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를 사용하도록 전부 바꿔준다.
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;
}
(생략)
}
SqlParameterSource namedParameter = new BeanPropertySqlParameterSource(product);
에서 처리한다.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;
}
(생략)
}
queryForObject(SQL 쿼리, namedParameter, BeanPropertyRowMapper)
BeanPropertyRowMapper
→ 조회된 상품 정보를 Product 인스턴스로 변환 위의 과정을 통해 데이터베이스에서 데이터를 가져온 후 자바의 인스턴스로 만들어준다.
하지만 BeanPropertyRowMapper가 정상적으로 작동하기 위해서는 아래 두 가지 작업을 해주어야 한다.
- Product의 인자가 없는 생성자로 Product 인스턴스를 생성한다.
⚠️ 인자 없는 생성자가 반드시 필요- 생성된 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()
findByNameContaining()
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 값을 통해 수정 및 삭제의 성공 여부를 알 수 있다.