[JdbcTemplate] JdbcTemplate ์ ์ฉ
by rlaehddnd0422SQL์ ์ง์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ ์คํ๋ง์ด ์ ๊ณตํ๋ JdbcTemplate์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
JdbcTemplate ์ฅ์
- ์ค์ ์ ํธ๋ฆฌํจ
- JdbcTemplate์ spring-jdbc ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌํจ๋์ด, ๋ณ๋์ ๋ณต์กํ ์ค์ ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
- build.gradle ์ ์ถ๊ฐ๋ง ํ๋ฉด ์ฌ์ฉ ๊ฐ๋ฅ
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
- ๋ฐ๋ณต ๋ฌธ์ ํด๊ฒฐ
- JDBC๋ฅผ ์ง์ ์ฌ์ฉํ ๋ ๋ฐ์ํ๋ ๋ฐ๋ณต์ ์ธ ์์ ์ ๋์ ์ฒ๋ฆฌํด ์ฃผ๋ ๋๋ถ์, ๊ฐ๋ฐ์๋ SQL ์์ฑ, ์ ๋ฌ ํ๋ผ๋ฏธํฐ ์ ์, ์๋ต ๊ฐ ๋งคํ๋ง ํ๋ฉด ๋จ.
- JdbcTemplate์ ์ปค๋ฅ์ ํ๋, statement ์ค๋น ์คํ, ๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ณตํ๋๋ก ๋ฃจํ๋ฅผ ์คํ, ์ปค๋ฅ์ -statement-resultset ์ข ๋ฃ, ํธ๋์ญ์ ์ ๋ค๋ฃจ๊ธฐ ์ํ ์ปค๋ฅ์ ๋๊ธฐํ, ์์ธ ๋ฐ์์ ์คํ๋ง ์์ธ ๋ณํ๊ธฐ ์คํ ์ ๋ฐ๋ณต์์ ์ ๋์ ์ฒ๋ฆฌํด์ค๋๋ค!
JdbcTemplate ๋จ์
- ๋์ SQL์ ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ต์ต๋๋ค.
JdbcTemplate
Repository์ JdbcTemplate์ ์ ์ฉํด๋ณด๋ฉด์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์๋ณด๊ฒ ์ต๋๋ค.
Database : H2
Table : Item ( id(big int by default as identity, PK) , item_name(varchar10) , price(int), quantity(int) )
์ฐ์ JdbcTemplate์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ dataSource๋ฅผ ์ฃผ์ ๋ฐ์์ผ ํฉ๋๋ค.
public JdbcTemplateItemRepositoryV1(DataSource dataSource)
{
this.template = new JdbcTemplate(dataSource);
}
jdbctemplate.update() For insert, update, delete
jdbctemplate.update(PreparedStatementCreator, KeyHolder)
- DB์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ (insert) ํ๊ธฐ ์ํด์๋ update() ๋ฉ์๋์ PreparedStatementCreator , KeyHolder๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค.
- update() ๋ฉ์๋์ ๋ฆฌํด ๊ฐ์ ์ํฅ ๋ฐ์ row ์ (int)
- PreparedStatmentCreator์ createPreparedStatement๋ Connection์ ์ธ์๋ก ๋ฐ์ PreparedStatment๋ฅผ ์์ฑํด์ฃผ๋ ๋ฉ์๋๋ฅผ ๊ตฌํํฉ๋๋ค.
@FunctionalInterface
public interface PreparedStatementCreator {
/**
* Create a statement in this connection. Allows implementations to use
* PreparedStatements. The JdbcTemplate will close the created statement.
* @param con the connection used to create statement
* @return a prepared statement
* @throws SQLException there is no need to catch SQLExceptions
* that may be thrown in the implementation of this method.
* The JdbcTemplate class will handle them.
*/
PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
- ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ(insert)ํ ๋ Primary Key (PK) ์์ฑ์ identity ๋ฐฉ์์ ์ฌ์ฉํ๋ฏ๋ก Id๊ฐ์ ์ง์ ์ง์ ํ์ง ์๊ณ ๋น์๋๊ณ ์ ์ฅํฉ๋๋ค.
- PK์ธ ID ๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ์์ฑํ๋ฏ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ insert๊ฐ ์๋ฃ๋ ์ดํ์ ์์ฑ๋ PK ID ๊ฐ์ ํ์ธํ ์ ์์ต๋๋ค.
- KeyHolder ์ connection.prepareStatement(sql, new String[]{"id"}) ๋ฅผ ์ฌ์ฉํด์ id ๋ฅผ ์ง์ ํด์ฃผ๋ฉด INSERT ์ฟผ๋ฆฌ ์คํ ์ดํ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์์ฑ๋ ID ๊ฐ์ ์กฐํํ ์ ์์ต๋๋ค.
- ์ฌ๊ธฐ์๋ PreparedStatementCreator์ createPreparedStatement๋ฅผ ๋๋ค์์ผ๋ก ํํ
- ์ธ์๋ก ๋ฐ์ connection์ ํตํด pstmt ์ธํ ํ ๋ฆฌํด
public Item save(Item item) {
String sql = "insert into item(item_name,price,quantity) values(?,?,?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
// ์๋ ์ฆ๊ฐ ํค
PreparedStatement pstmt = connection.prepareStatement(sql, new String[]{"id"});
pstmt.setString(1,item.getItemName());
pstmt.setInt(2,item.getPrice());
pstmt.setInt(3, item.getQuantity());
return pstmt;
}, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
jdbctemplate.update(sql, args..)
- DB์ ๋ฐ์ดํฐ๋ฅผ ์์ ,์ญ์ (update,delete) ํ๊ธฐ ์ํด์๋ update() ๋ฉ์๋์ sql, args..๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค.
- args์๋ sql์ ์ธ๋ฑ์ค ํ๋ผ๋ฏธํฐ๋ฅผ ์์๋๋ก ์ ๋ ฅ
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, price=?, quantity=? where id = ?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
jdbctemplate.queryForObject(), jdbctemplate.query() For select
- queryForObject() : ๊ฒฐ๊ณผ ๋ก์ฐ๊ฐ ํ๋์ธ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
- query() : ๊ฒฐ๊ณผ ๋ก์ฐ๊ฐ ์ฌ๋ฌ๊ฐ์ธ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
queryForObject(sql, RowMapper, args)
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id=?";
try
{
Item item = template.queryForObject(sql, itemRowMapper(), id);
return Optional.of(item);
}catch(EmptyResultDataAccessException e)
{
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return ( (rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
});
}
- ์ฌ๊ธฐ์ RowMapper๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐํ ๊ฒฐ๊ณผ์ธ ResultSet์ ๊ฐ์ฒด๋ก ๋ณํํฉ๋๋ค.
-
ItemRepository.findById() ์ธํฐํ์ด์ค๋ ๊ฒฐ๊ณผ๊ฐ ์์ ๋ Optional ์ ๋ฐํํด์ผ ํ๊ธฐ ๋๋ฌธ์, ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ์์ธ๋ฅผ ์ก์์ Optional.empty ๋ฅผ ๋์ ๋ฐํํ๋๋ก ์ค์ .
- JDBC๋ฅผ ์ง์ ์ฌ์ฉํ ๋๋ rs.next()๋ก ๋ฃจํ๋ฅผ ์ง์ ๋๋ ค์ฃผ์์ง๋ง, jdbctemplate์ ์ฌ์ฉํ๋ฉด ๋ฃจํ๋ฅผ ์ง์ ๊ตฌํํ ํ์ ์์ด ๋ฃจํ๋ฅผ ๋๋ฆด ๋ด๋ถ ์ฝ๋๋ง ๊ตฌํํด์ ๋ด๋ถ ์ฝ๋๋ง ์ฑ์ฐ๋ฉด ๋ฉ๋๋ค. ๋ฃจํ๋ RowMapper๊ฐ ๋๋ ค์ค๋๋ค.
- query()์ queryForObject()์ ๋น๊ตํด ๊ฒฐ๊ณผ ๋ก์ฐ๊ฐ ํ ๊ฐ ์ด์์ด๊ธฐ ๋๋ฌธ์ ๋ฆฌํด๊ฐ์ด List<T>๋ผ๋ ์ ์ ์ ์ธํ๊ณ ๋ ์ฐจ์ด์ ์ด ์์ผ๋ฏ๋ก ๋ฐ๋ก ์ค๋ช ์ ์๋ตํ๊ฒ ์ต๋๋ค.
jdbctemplate์ ๋์ SQL ์ ์ฉ
item ํ ์ด๋ธ์ ์๋ ํน์ rows๋ฅผ ๋ค์ ๋ค ๊ฐ์ง ์กฐ๊ฑด์ผ๋ก ๊ฒ์ํ๊ธฐ ์ํด์๋ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค.
1. ๊ฒ์ ์กฐ๊ฑด์ด ์๋ ๊ฒฝ์ฐ
select id, item_name, price, quantity from item
2. ์ํ๋ช ๋ง์ผ๋ก ๊ฒ์
select id, item_name, price, quantity from item where item_name like concat('%',?,'%')
3. ์ต๋ ๊ฐ๊ฒฉ๋ง์ผ๋ก ๊ฒ์
select id, item_name, price, quantity from item where price <= ?
4. ์ํ๋ช , ์ต๋ ๊ฐ๊ฒฉ์ผ๋ก ๊ฒ์
select id, item_name, price, quantity from item
where item_name like concat('%',?,'%') and price <= ?
ํ์ง๋ง jdbctemplate์ ๋ฐ๋ก ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฐ์๊ฐ ์ง์ ์๋์ ๊ฐ์ด ๊ตฌํํด ์ฃผ์ด์ผ ํฉ๋๋ค.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String sql = "select id, item_name, price, quantity from item";
// jdbc ๋์ ์ฟผ๋ฆฌ
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName))
{
sql += " item_name like concat('%',?,'%')";
param.add(itemName);
andFlag = true;
}
if (maxPrice != null)
{
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(), param.toArray());
}
์ํฉ์ด ์ปค์ง์๋ก ๋์ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ๋ ์ผ์ ๊ฝค ๋ณต์กํ ๋ฌธ์ ์ ๋๋ค.
๋์ ์ฟผ๋ฆฌ๋ MyBatis๋ querydsl์ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
NamedParameterJdbcTemplate
jdbctemplate์ ์ฌ์ฉํ๋ฉด ํ๋ผ๋ฏธํฐ๋ฅผ ์์๋๋ก ๋ฐ์ธ๋ฉํด์ฃผ์ด์ผ ํฉ๋๋ค.
String sql = "update item set item_name=?, price=?, quantity=? where id = ?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
ํ์ง๋ง NamedParameterJdbcTemplate์ ์ฌ์ฉํ๋ฉด ์ด๋ฆ์ ์ง์ ํด์ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ธ๋ฉ ํ ์ ์์ต๋๋ค.
NamedParameterJdbcTemplate๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๋น์ฐํ dataSource๋ฅผ ์ฃผ์ ๋ฐ์ต๋๋ค.
public JdbcTemplateItemRepositoryV2(DataSource dataSource)
{
this.template = new NamedParameterJdbcTemplate(dataSource);
}
NameParamterJdbcTemplate์ update() ๋ฉ์๋์์ ์๋ ์ธ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
ํ๋ผ๋ฏธํฐ๋ฅผ ์ ๋ฌํ๊ธฐ ์ํด์๋ Map์ฒ๋ผ key, value ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด ์ ๋ฌํด์ผ ํฉ๋๋ค.
์ด Map์ ์ง์ ๋ง๋ค์ด ์ ๋ฌํ๋ ๋ฐฉ๋ฒ๋ ์์ง๋ง, SqlParameterSource๋ผ๋ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํด์ ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
1. BeanPropertySqlParameterSource ( SqlParameterSource Interface )
@Override
public Item save(Item item) {
String sql = "insert into item(item_name,price,quantity) "
+ "values (:itemName, :price, :quantity)";
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
BeanPropertySqlParameterSource(T)
- ํด๋น Type์ ์๋ฐ๋น ํ๋กํผํฐ ๊ท์ฝ(getXxx() -> xxx)์ ํตํด ์๋์ผ๋ก ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- ex) T์ ํ๋กํผํฐ๋ฅผ ํตํด (key=itemName, value=๊ฐ), (key=price, value = ๊ฐ), (key=quantity, value = ๊ฐ) ์์ฑ
2. MapSqlParameterSource( SqlParameterSource Interface )
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
// SqlParameterSource param = new MapSqlParameterSource("itemName",updateParam.getItemName());
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
template.update(sql, param);
}
MapSqlParameterSource()
- ํน์ T๋ก๋ถํฐ ๋ฐ์ธ๋ฉํด์ผ ํ ๋ฟ๋ง ์๋๋ผ, ๊ทธ ์ธ์ ํ์ ๋ ๋ฐ์ธ๋ฉ ํด์ผ ํ ๊ฒฝ์ฐ ์ฌ์ฉํฉ๋๋ค.
- ์ถ๊ฐ๋ก MapSqlParameterSource๋ ์ฌ๋ฌ ๊ฐ์ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฑ๋กํด์ผ ํ ๊ฒฝ์ฐ ์์ฒ๋ผ ๋ฉ์๋ ์ฒด์ธ์ ํตํด ํธ๋ฆฌํ๊ฒ ๋ฑ๋กํ ์ ์์ต๋๋ค.
3. Map์ ์ง์ ์ฌ์ฉ
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id=:id";
try
{
// MapSqlParameterSource param = new MapSqlParameterSource("id", id);
Map<String, Object> param = Map.of("id",id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
}catch(EmptyResultDataAccessException e)
{
return Optional.empty();
}
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
// return ( (rs, rowNum) -> {
// Item item = new Item();
// item.setId(rs.getLong("id"));
// item.setItemName(rs.getString("item_name"));
// item.setPrice(rs.getInt("price"));
// item.setQuantity(rs.getInt("quantity"));
// return item;
// });
}
- Map์ ์ง์ ์ฌ์ฉํด์ param ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด๋ ๋ฉ๋๋ค.
* itemRowMapper()๋ฅผ BeanPropertyRowMapper.newInstance(Item.class)๋ก ์์ฑํด์ฃผ์์ต๋๋ค.
Q. ๊ทธ๋ ๋ค๋ฉด ์ด๋ ๊ฒ ์ฌ์ฉํ๋ฉด itemRowMapper() setItemName()์ด ์๋ setItem_Name()์ผ๋ก ์ ์ฉ๋์ง ์์๊น ?
* ์๋ฐ ๊ฐ์ฒด๋ ์นด๋ฉ ํ๊ธฐ๋ฒ ์ฌ์ฉํ์ง๋ง, ๋ฐ์ดํฐ๋ฒ ์ด์ค์์๋ ์ฃผ๋ก ์ธ๋์ค์ฝ์ด๋ฅผ ์ฌ์ฉํ๋ ์ค๋ค์ดํฌ ํ๊ธฐ๋ฒ์ ์ฌ์ฉํฉ๋๋ค.
* ์ด ๋ถ๋ถ์ ๊ด๋ก๋ก ๋ง์ด ์ฌ์ฉํ๋ค๋ณด๋ BeanPropertyRowMapper๋ ์ธ๋์ค์ฝ์ด ํ๊ธฐ๋ฒ์ ์นด๋ฉ๋ก ์๋์ผ๋ก ๋ณํํด ์ค๋๋ค!
SimpleJdbcInsert
insert์ ํํด์ jdbcTemplate์ insert SQL์ ์ง์ ์์ฑํ์ง ์์๋ ๋๋๋ก SimpleJdbcInsert๋ผ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
SimpleJdbcInsert๋ dataSource๋ฅผ ์ฃผ์ ๋ฐ๊ณ , Table ์ด๋ฆ์ ์ง์ ํ๊ณ key๋ฅผ ์์ฑํ๋ PK ์ปฌ๋ผ๋ช ์ ์ง์ ํฉ๋๋ค.
์ถ๊ฐ์ ์ผ๋ก INSERT SQL์ ์ฌ์ฉํ ํน์ column์ ์ง์ ํ ์ ์๋๋ฐ, ํน์ ๊ฐ๋ง ์ ์ฅํ๊ณ ์ถ์ ๋ ์ง์ ํ ์ ์๋ ์ต์ ์ ๋๋ค. ์๋ต ๊ฐ๋ฅ.
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource)
{
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id");
// .usingGeneratedKeyColumns("item_name","price","quantity"); // ์๋ต ๊ฐ๋ฅ
}
jdbcInsert.executeAndReturnkey(param)์ ์ฌ์ฉํด์ INSERT SQL์ ์คํํ๊ณ , ์์ฑ๋ ํค ๊ฐ๋ ๋งค์ฐ ํธ๋ฆฌํ๊ฒ ์กฐํํ ์ ์์ต๋๋ค.
@Override
public Item save(Item item) {
BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
jdbcTemplate ๊ธฐ๋ฅ ์ ๋ฆฌ
๊ธฐ๋ฅ
- jdbctemplate : ์์ ๊ธฐ๋ฐ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ง์
- NamedParameterJdbcTemplate : ์ด๋ฆ ๊ธฐ๋ฐ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ง์
- BeanPropertySqlParameterSource : ์๋ฐ๋น ํ๋กํผํฐ ๊ท์ฝ์ ํตํด ์๋์ผ๋ก Type์ ๋ํ ํ๋ผ๋ฏธํฐ ๊ฐ์ฒด ์์ฑ
- MapSqlParameterSource : Map๊ณผ ์ ์ฌ. ๋ฉ์๋ ์ฒด์ธ์ ํตํด ์ฌ๋ฌ ํ๋ผ๋ฏธํฐ ๋ฑ๋ก ๊ฐ๋ฅ
- SimpleJdbcInsert : insert ์ฟผ๋ฆฌ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅ
๋ฉ์๋
- ๋ณ๊ฒฝ ( insert, update, delete ) : update()
- ์กฐํ ( select ) : query(), queryForObejct()
- ๊ธฐํ ( DDL ) : execute()
<์ฐธ๊ณ ์๋ฃ>
'๐ Backend > DB Access' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[JPA] JPA(Java Persistent API) (0) | 2023.04.17 |
---|---|
TestCode์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ฒ ์ฐ๋ (1) | 2023.04.15 |
[JdbcTemplate] RowMapper์ ๋ํด (0) | 2023.04.12 |
[JdbcTemplate] JDBC Template ์ด๋? (0) | 2023.04.07 |
Spring์ด ์ ๊ณตํ๋ ์์ธ ์ถ์ํ, ์์ธ ๋ณํ๊ธฐ ์ฌ์ฉ (0) | 2023.04.07 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
Study Repository
rlaehddnd0422