Spring DB ๊ธฐ์ ํํค์น๊ธฐ #2
by JiwonDev
๐ JdbcTemplate
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
์ฐธ๊ณ ๋ก JPA๋ฅผ ์ฌ์ฉํ๋ค๋ฉด spring-boot-starter-data-jpa ์ jdbc ์์กด์ฑ๋ ํฌํจ๋์ด์์ด ๋ฐ๋ก ์ถ๊ฐํ ํ์ ์๋ค.
JdbcTemplate์ DataSource๋ฅผ ์ด์ฉํด ์ง์ ์์ฑํ๊ฑฐ๋ ๋น์ผ๋ก ๋ฑ๋กํ ์ ์๋ค. ๋ค๋ง ์คํ๋ง ๋ถํธ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ๊ทธ๋ฅ ๋ฐ๋ก ์ฐ๋ฉด๋๋๋ฐ, JdbcTemplateAutoConfiguration ์ ์ํด JdbcTemplate, NamedParameterJdbcTemplate ๋น์ด ์๋ค๋ฉด ๋ฑ๋กํ๊ฒ ๊ตฌ์ฑ๋์ด์๋ค.
์ดํ ์ฌ์ฉ๋ฒ์ ๋๋ฌด๋ ๊ฐ๋จํ๋ค. dataSource๋ก JdbcTemplate๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ฉด ๋๋ค.
- execute() ๊ทธ๋ฅ sql ์์ฒด๋ฅผ ์คํํ๊ณ ์ถ์ ๋, ์๋น์ค์์ ์ฌ์ฉํ ์ผ์ ๊ฑฐ์ ์๋ค.
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
- update : ์ํฅ๋ฐ์ row ์๋ฅผ ๋ฐํํ๋ค. (update, insert, delete)
- insert ๋ฅผ ํ ๋ ์๋ ์์ฑํ pk ๋ฅผ ์ป๊ณ ์ถ๋ค๋ฉด KeyHolder = new GenereatedKeyHolder()๋ฅผ update์ ๊ฐ์ด ๋๊ธฐ๋ฉด ๋๋ค.
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
- query: ๊ธฐ๋ณธ ๋ฉ์๋, ์ ์ฌ์ฉํ์ง์๋๋ค. ๋จ๊ฑด ์กฐํ๋ queryForObject() ์ฌ์ฉ ๊ถ์ฅ
- queryForXXX: ์ฝ์ด๋ณด๋ฉด ๋ฐ๋ก ์ ์ ์๋ค. ์ฐธ๊ณ ๋ก queryForStream์ ํ์ ๋ง Stream<>์ผ ๋ฟ์ด๋ค. ๋ณ๋ ฌ์ฒ๋ฆฌ๋ ์ง์ํ์ง ์๋๋ค.
int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class,"Joe");
String lastName = jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", String.class, 1212L);
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
}, 1212L);
List<Actor> actors = jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
์ค์ Repository๋ฅผ ๊ตฌํํด๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
public class JdbcTemplateItemRepository implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
template.queryFor
template.update(connection -> {
//์๋ ์ฆ๊ฐ ํค
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@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);
}
@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();
}
}
@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";
//๋์ ์ฟผ๋ฆฌ
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);
}
return template.query(sql, itemRowMapper(), param.toArray());
}
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;
});
}
}
๊ทธ ์ธ ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋ ํธ์๊ธฐ๋ฅ๋ค์ด ์กด์ฌํ๋ค.
- SimpleJdbcInsert ๋ฅผ ๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋ฑ๋กํด๋๋ฉด, insert๋ฅผ ๋ฉ์๋ ํ๋๋ก ํ ์ ์๋ค.
public class JdbcTemplateItemRepository implements ItemRepository {
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")
.usingColumns("item_name", "price", "quantity"); // usingColumns ๋ ์๋ต ๊ฐ๋ฅ
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
}
- NamedParameterJdbcTemplate ๋ฅผ ์ฌ์ฉํ๋ฉด :itemName ์ผ๋ก ๋ฐ์ธ๋ฉ ํ ์ ์๋ค.
- BeanPropertyRowMapper.newInstance(MyClass.class) ๊ฐ์ด ํธ์์ฉ rowMapper๋ฅผ ์ ๊ณตํ๋ค. ํ๋ camel ๋ณํ๋ ์ง์!
-
SimpleJdbcCall ๊ฐ์ ํ๋ก์์ ์ฝ๋ ์์ง๋ง, ์ฌ์ฉํ ์ผ์ด ์์ผ๋ฏ๋ก ์๋ต
public class JdbcTemplateItemRepository implements ItemRepository {
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") // pk ๋ช
// ์๋ตํ๋๋ผ๋ DB ๋ฉํ ์ฝ์ด์ ๋ง์ถฐ์ค. ์๋ต ๊ฐ๋ฅํ๋ ํน์ ์นผ๋ผ๋ง ์ฌ์ฉํ๊ณ ์ถ์ ๋ ์ฌ์ฉ
.usingColumns("item_name", "price", "quantity");
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
// ์ด๋ ๊ฒ ๋งค์ฐ ํธํ๊ฒ ์ธ ์ ์๋ค.
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
@Override
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()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId); //์ด ๋ถ๋ถ์ด ๋ณ๋๋ก ํ์ํ๋ค.
template.update(sql, param);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
//๋์ ์ฟผ๋ฆฌ
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
}
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); //camel ๋ณํ ์ง์
}
}
๐ MyBatis
https://mybatis.org/mybatis-3/ko/getting-started.html
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
์คํ๋ง ํ์์์ผ๋ฉด ๊ณต์๋ฌธ์๋ณด๊ณ org.mybatis.mybatis ์ฐ๋ฉด ๋๋ค
spring:
profiles:
active: local
datasource:
url: jdbc:h2:tcp://localhost/~/test
username: sa
logging:
level:
org:
springframework:
jdbc: debug
mybatis:
# xml ํ์ผ์์ ์ฌ์ฉํ ๊ธฐ๋ณธ ํจํค์ง๋ช
, ์ฌ๊ธฐ์ ์ ์ด์ผ ์๋ต ๊ฐ๋ฅํ๋ค. ( ; ๋ก ์ฌ๋ฌ ๊ณณ์ ์ง์ ํ ์๋ ์๋ค)
type-aliases-package: hello.itemservice.domain
# mapper ํด๋์ค์ ๊ธฐ๋ณธ ์์น. ์ด๊ฒ ์์ผ๋ฉด ์ค์ ํด๋์ค ํจํค์ง ์์น์ ๋์ผํ๊ฒ resource/../ ์ ๋ง๋ค์ด ์ฃผ์ด์ผํ๋ค.
mapper-locations: classpath:sql/**/*.xml
# db camel case -> java camelCase ์๋ ๋ณํ
configuration:
map-underscore-to-camel-case: true
logging:
level:
hello:
itemservice:
repository:
mybatis: trace
Spring MyBatis๋ฅผ ์ฌ์ฉํ๋ฉด ๊ตฌํ์ฒด๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด์ฃผ์ง ์์๋๋๋ค. ๊ทธ๋ฅ @Mapper๋ฅผ ์ธํฐํ์ด์ค ๋ฌ์์ฃผ๋ฉด ๊ตฌํ์ฒด ๋น์ด ์์ฑ๋๋ค
์ฐธ๊ณ ๋ก ๊ทธ๋ฅ myBatis๋ฅผ ์ฌ์ฉํ๋ฉด SqlSessionFactory, SqlSession์ ์๋์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋๋ค.
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// Open a new SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// Retrieve data from the database using MyBatis
List<MyObject> myObjects = sqlSession.selectList("com.example.MyObjectMapper.selectMyObjects");
} ...
์ฐ๋ฆฐ ์คํ๋ง์์ด MyBatis ์ธ์ผ ์์ผ๋ฏ๋ก ์๋์ ๊ฐ์ด @Mapper๋ง ๋ฌ์์ฃผ๋ฉด ๋. ๋น์ผ๋ก ์ค์บ๋๋ค.
@Mapper
public interface ItemMapper {
void save(Item item);
void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
Optional<Item> findById(Long id);
List<Item> findAll(ItemSearchCond itemSearch);
}
@Slf4j
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {
private final ItemMapper itemMapper;
@Override
public Item save(Item item) {
log.info("itemMapper class={}", itemMapper.getClass());
itemMapper.save(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
itemMapper.update(itemId, updateParam);
}
@Override
public Optional<Item> findById(Long id) {
return itemMapper.findById(id);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
return itemMapper.findAll(cond);
}
}
xml path๋ ์ค์ ํด๋์ค์ ๋์ผํ๊ฒ ๋ง์ถฐ์ฃผ๋ฉด ๋๋ค.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item (item_name, price, quantity)
values (#{itemName}, #{price}, #{quantity})
</insert>
<update id="update">
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}
</update>
<select id="findById" resultType="Item">
select id, item_name, price, quantity
from item
where id = #{id}
</select>
<select id="findAll" resultType="Item">
select id, item_name, price, quantity
from item
<where>
<if test="itemName != null and itemName != ''">
and item_name like concat('%', #{itemName}, '%')
</if>
<if test="maxPrice != null">
and price <= #{maxPrice}
</if>
</where>
</select>
</mapper>
sqlSessionTemplate ์ง์ ์ฌ์ฉ
์ด๋ฐ์์ผ๋ก ์ถ์ํํด์ ์ฌ์ฉํ๊ธฐ ์ซ์ ๋ถ๋ค์ ์ํด sqlSessionTemplate ์ ์คํ๋ง์์ ์ ๊ณตํด์ค๋ค. ์กฐ๊ธ ๋ ์ง๊ด์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋ค.๋์ MyBatis์ ๊ตฌ์กฐ์ ๋ํด ์กฐ๊ธ ์ดํดํ๊ณ ์ฌ์ฉํด์ผํ๋ค.
์คํ๋ง์ด ์ ๊ณตํ๋ sqlSessionFactoryBean์ ๋ฑ๋กํด์ ์ฐ๋ฉด ๋๋ค. sqlSessionTemplate์ ์ถ๊ฐํด๋์
๋ฌผ๋ก ์ถ๊ฐํ์ง ์๋๋ผ๋ MybatisAutoConfiguration ์ ์ํด์ ์๋ ๋น๋ค์ ์๋์ผ๋ก ๋ค ๋ฑ๋ก๋๋ค. ์๋์ผ๋ก ์ค์ ํ ๋๋ง ์ฌ์ฉ
@Configuration
public class DatabaseConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
var sqlSessionFactory = new SqlSessionFactoryBean();
// yml ์ค์ ์ ์ฝ๋๋ก ์ง์ ์ถ๊ฐ
sqlSessionFactory.setDataSource(dataSource);
var configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setTypeHandlersPackage("com.your.package");
// mapper-locations: classpath:sql/**/*.xml ๋ฑ๋ก
var resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/xml/*.xml"));
return sqlSessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
์ค์ ์๋ณด๋ฉด typeHandlers ์ค์บ ํจํค์ง ๊ฒฝ๋ก๋ฅผ ์ง์ ํด์ค ์ ์๋๋ฐ, ์ด๋ Enum Converter์ฒ๋ผ ํน์ DB ํ์ ์ ๋งคํํ ๋ ์ฌ์ฉ๋๋ค
// RoleType ์ปจ๋ฒํฐ
@MappedTypes(Role.class)
public final class RoleTypeHandler implements TypeHandler<Role> {
@Override
public void setParameter(PreparedStatement ps, int i,
Role parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.name());
}
@Override
public Role getResult(ResultSet rs, String columnName) throws SQLException {
return Role.findRole(rs.getString(columnName));
}
@Override
public Role getResult(ResultSet rs, int columnIndex) throws SQLException {
return Role.findRole(rs.getString(columnIndex));
}
@Override
public Role getResult(CallableStatement cs, int columnIndex) throws SQLException {
return Role.findRole(cs.getString(columnIndex));
}
}
์ฌ์ฉํ๋ ์ฝ๋
public final class MybatisAccountRepository implements AccountRepository, AccountReader {
private static final String SAVE_FQCN = "com.yam.app.account.domain.AccountRepository.save";
private static final String UPDATE_FQCN = "com.yam.app.account.domain.AccountRepository.update";
private final SqlSessionTemplate template;
public MybatisAccountRepository(SqlSessionTemplate template) {
this.template = template;
}
@Override
public boolean existsByEmail(String email) {
return template.getMapper(AccountReader.class).existsByEmail(email);
}
@Override
public void update(Account entity) {
int result = template.update(UPDATE_FQCN, entity);
if (result != 1) {
throw new IllegalStateException(String.format(
"Unintentionally, more records were updated than expected. : %s", entity));
}
}
@Override
public void save(Account entity) {
int result = template.insert(SAVE_FQCN, entity);
if (result != 1) {
throw new IllegalStateException(String.format(
"Unintentionally, more records were saved than expected. : %s", entity));
}
}
@Override
public Optional<Account> findByEmail(String email) {
return template.getMapper(AccountReader.class).findByEmail(email);
}
@Override
public MemberAccount findByEmailAndMemberId(String email,
Long memberId) {
return template.getMapper(AccountReader.class).findByEmailAndMemberId(email, memberId);
}
}
๐ JPA, Hibernate
https://jiwondev.tistory.com/category/%F0%9F%8C%B1Backend/JDBC%20%26%20JPA?page=1
๐ ์คํ๋ง ๋ฐ์ดํฐ JPA
https://jiwondev.tistory.com/253
ํ๋๋ง ์ถ๊ฐํ์๋ฉด, JPA์ ์์กด์ฑ์ ์ธํฐํ์ด์ค๋ก ์์ ํ ๊ฐ์ถ ์ ์๋ค. ์ฐ์ ์๋์ ๊ฐ์ด ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๋ค. ์ด๋ ๊ฒ ๋ถ๋ฆฌํ ๊ฒฝ์ฐ, ์ธํฐํ์ด์ค ๋ถ๋ฆฌ ์์น์ ๋ฐ๋ผ Repository -> { Reader , Writer } ๋ก ์ธํฐํ์ด์ค๋ฅผ ๋ถ๋ฆฌํด์ ์ฐ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
public interface SampleRepository {
Optional<SampleEntity> findById(Long id);
SampleEntity save(SampleEntity entity);
}
1๏ธโฃ JPA ๊ตฌํ์ฒด๋ง ์ฌ์ฉํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ธํฐํ์ด์ค๋ฅผ ์ถ๊ฐํ๋ฉด ๊น๋ํ๊ฒ ์ฌ์ฉํ ์ ์๋ค. ์ฐธ๊ณ ๋ก QueryDsl๋ฑ์ ์ด๋ค๋ฉด interface CustomJpaRepository ๋ฅผ ๋ง๋ค๊ณ , SampleJpaRepository๊ฐ ์์ํ๊ฒ ํ๋ฉด ๋์ผํ๊ฒ ์ธ ์ ์๋ค.
// ์ฐธ๊ณ ๋ก @Repository๋ฅผ ์๋ตํด๋ @EnableJpaRepositories ์ ์ํด JpaRepository ํ์
์ด ์ค์บ๋์ด ๋น ๋ฑ๋ก๋๋ค.
@Repository
public interface SampleJpaRepository extends SampleRepository, JpaRepository<SampleEntity, Long> {
@Override
Optional<SampleEntity> findById(Long id);
}
2๏ธโฃ ์ฌ๋ฌ DB ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์ปดํฌ์ง์ ์ ํ์ฉํด์, ์์ ๋ณ๋์ RepositoryImpl ํด๋์ค๋ฅผ ๋ง๋ค์ด์ ์ด์ฉํด๋ ์ข๋ค.
@Repository
public interface SampleJpaRepository extends JpaRepository<SampleEntity, Long> {
}
@RequiredArgsConstructor
public class SampleRepositoryImpl implements SampleRepository {
private final SampleJpaRepository jpaRepository;
private final SampleCustomRepository customRepository;
@Override
public Optional<SampleEntity> findById(Long id) {
return customRepository.findById(id);
}
@Override
public SampleEntity save(SampleEntity entity) {
return jpaRepository.save(entity);
}
}
ํ์ง๋ง ๊ฒฐ๊ตญ ๋ชจ๋ ๊ฑด ํธ๋ ์ด๋ ์คํ๋ผ๋ ๊ฑธ ๋ช ์ฌํ์. ์ฐ๋ฆฌ๋ ์ DIP ์์น์ ์งํค๊ณ , ์ถ์ํ์์ผ์ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋๊ฑธ๊น? ๊ฒฐ๊ตญ ์ฝ๋ ์์ ์ ์ํฅ๋๋ฅผ ์ค์ฌ ์ ์ฌ์ ์ธ ๋ฒ๊ทธ์ ๊ฐ๋ฐ ๋น์ฉ์ ์ค์ด๊ธฐ ์ํจ์ด๋ค. ์์น์ ์งํค๊ธฐ ์ํ ์์น์ ์๋ฌด๋ฐ ์๋ฏธ๊ฐ ์๋ค.
ํ์ค์ ์ผ๋ก DB ์์กด์ฑ์ด ๋ฐ๋๋ ๊ฒฝ์ฐ๊ฐ ์ผ๋ง๋ ๋ ๊น? ์๋, ์ ์ด์ ์ธํฐํ์ด์ค๋ก ๋ถ๋ฆฌํ๋ค๊ณ ํด๋ DB๋ฅผ ์ฝ๊ฒ ๋ฐ๊ฟ ์ ์์๊น?
๋ชจ๋ ๊ฒ์ ํธ๋ ์ด๋ ์คํ์ด๋ค. ์๋น์ค์ ๋ฐ๋ผ Repository ์์ฒด๊ฐ ๋๋ฌด๋ ๋ณต์กํ๋ค๋ฉด ์ ์์ ์ฒ๋ผ ๋ถ๋ฆฌํ๋๊ฒ ํจ์ฌ ๋์ ์ ์๋ค. ๋ฐ๋๋ก ๊ทธ ์ ๋๋ก ํฐ ์๋น์ค๊ฐ ์๋๊ฑฐ๋ ๋น์ฅ ๋ง๋๋๊ฒ ๊ธํ๋ค๋ฉด JpaRepository๋ฅผ ๋ฐ๋ก ์ฌ์ฉํ๋ ๊ฒ์ด ์ ๋ต์ผ ์๋ ์๋ค.
๐ Querydsl
https://jiwondev.tistory.com/255
์คํ๋ง์์ QuerydslRepositorySupport๋ผ๊ณ ๋ฐ๋ก ์ง์ํด์ฃผ๋๊ฒ ์๊ธดํ๋ฐ ์ฌ์ฉํด๋ณด๋ฉด ์๊ฒ ์ง๋ง ์ด์ง ๋ถํธํ๋ค.
// ์คํ๋ง ๋ฐ์ดํฐ ๋ฆฌํฌ์งํ ๋ฆฌ์ ์ฌ์ฉ์ ์ ์ ์ธํฐํ์ด์ค ์์ (MemberRepositoryCustom)
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
public MemberRepositoryImpl() {
super(Member.class);
}
// QuerydslRepositorySupport ์ฌ์ฉ
// from์ ์ด ๋จผ์ ์์ํฉ๋๋ค.
private Page<MemberTeamDto> getMemberTeamDtoQueryResults(MemberSearchCondition condition, Pageable pageable) {
JPQLQuery<MemberTeamDto> jpaQuery = from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")));
JPQLQuery<MemberTeamDto> query = getQuerydsl().applyPagination(pageable, jpaQuery);// offset, limit ์ง์
query.fetch();
}
}
์ฐจ๋ผ๋ฆฌ QueryDsl์์ ์ ๊ณตํ๋ JpaQueryFactory (* EntityManager์ 1:1 ๊ด๊ณ)๋ฅผ ์ง์ ์ฌ์ฉํ๋๊ฒ ํจ์ฌ ๊น๋ํ๊ณ ํธํ๋ค. ์ฐธ๊ณ ๋ก Spring Jpa์์๋ `Impl` ์ด๋ผ๋ prefix๋ฅผ ๋ถ์ด๋ฉด ์๋์ผ๋ก ๋น ์ค์บ์ด ๋๋ค.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
// 1. Jpa๊ฐ ์๋ ๋ค๋ฅธ๊ฑธ๋ก ํ์ฅํ๊ณ ์ถ์ ๋ฉ์๋๋ ์ฌ๊ธฐ์๋ค๊ฐ ๋ฐ๋ก ์ ์ํด๋๋ค.
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
//2. ์ฐธ๊ณ ๋ก 'Custom' ์ ๊ผญ ์๋ถ์ฌ๋ ๋๋ค. ๋ค์ 'Impl' ์ด๋ฆ์ ๊ธฐ์ค์ผ๋ก ๊ฐ์ ํจํค์ง์์ ํด๋์ค๋ฅผ ์ค์บํ๋ค.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
public MemberRepositoryImpl(EntityManager em) { this.em = em; }
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
์ฐธ๊ณ ๋ก `Impl` ์ ๋์ฌ ์ค์ ๋ํ @EnableJpaRepository์์ ์ปค์คํ ํ ์ ์๋ค. ์ด๋ ์คํ๋ง ๋ถํธ๊ฐ Jpa ์์กด์ฑ์ด์์ผ๋ฉด ์ค์ ํด์ค.
@EnableJpaRepositories(basePackages = "study.datajpa.repository", // ๋ฌผ๋ก ์ค์บ ํจํค์ง๋ ๋ณ๊ฒฝ๊ฐ๋ฅ
repositoryImplementationPostfix = "Impl") // ์ฌ๊ธฐ
๋ฌผ๋ก ์ด๋ฆ ๊ท์น์ ์ฌ์ฉํ์ง ์๊ณ ๊ทธ๋ฅ JpaRepository, QueryDslRepostiory๋ก ๋ฐ๋ก ๋ง๋ค์ด์ ์ฌ์ฉํด๋ ๋๋ค. ๋ง๋๋๊ฑด ๋ ๊ท์ฐฎ์์ง์ง๋ง service ์์ ํ๋๋ง ์ฐ๊ณ ์ถ๋ค๋ฉด ItemRepository ์๋ฐ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค๊ณ , ItemRepositoryImpl ์์ ์ค์ ๊ตฌํ์ฒด๋ฅผ ๋น์ผ๋ก ๋ฐ์์์ ์๋ ์ฝ๋์ฒ๋ผ ์ฐ๋ฉด ๋๋ค.
@Repository
public class ItemQueryRepository{
private final JPAQueryFactory query;
}
@Service
public class ItemService {
private final ItemRepositoryV2 itemRepositoryV2;
private final ItemQueryRepositoryV2 itemQueryRepositoryV2;
}
https://jiwondev.tistory.com/254
๐ ํธ๋์ญ์ ์ ํ์ต์
์คํ๋ง์ @Transactional๋ก ์์ฝ๊ฒ ํธ๋์ญ์ ์ ๊ฑธ ์ ์๊ฒ ๋ง๋ค์ด๋จ์ง๋ง, ์ฌ์ค ์ค์ ํธ๋์ญ์ ๋์๊ณผ 100% ์ผ์นํ๋๊ฑด ์๋๋๋ค.
- ๊ฐ๋ฐ์๊ฐ @Transactional ๋ก ๊ฑด ๊ฒ์ ๋ ผ๋ฆฌ ํธ๋์ญ์ ์ด๋ผ๊ณ ํํํฉ๋๋ค.
- JDBC๋ฅผ ํตํด ์ค์ ๋ก ๊ฑธ๋ฆฌ๋ ๋ถ๋ถ์ ๋ฌผ๋ฆฌ ํธ๋์ญ์ ์ด๋ผ๊ณ ํํํฉ๋๋ค.
์ด๋ ๊ฒ ๊ฐ๋ ์ ๋๋ ์ JDBC์๋ ์๋ ํธ๋์ญ์ ์ ํ๋ผ๋ ์ฌ๋ฐ๋ ๊ธฐ๋ฅ์ ์คํ๋ง์ด ๊ตฌํํด๋์์ต๋๋ค.
// ๊ธฐ๋ณธ๊ฐ์ REQUIRED, ์์ผ๋ฉด ๊ฑธ๊ณ ์์ผ๋ฉด ๊ธฐ์กด ํธ๋์ญ์
์ ๊ฐ์ด ์ด๋ค.
@Transactional(propagation = Propagation.REQUIRES_NEW)
๐ฅ ๊ธฐ๋ณธ๊ฐ REQUIRED์์ ๊ณ ๋ คํด์ผํ๋ ๋ฌธ์ ๋ค
MemberService, MemberRepository, LogRepository์ ๊ฐ๊ฐ ํธ๋์ญ์ ์ด ๊ฑธ๋ ค์๋ค๊ณ ๊ฐ์ ํด๋ด ์๋ค.
๊ฐ๋ฐ์๋ ๋ ผ๋ฆฌ์ ์ผ๋ก @Transaction์ ์ ์ฒด๋ก ๊ฑธ์์ง๋ง, ์ค์ ๋ก๋ ์๋์ ๊ฐ์ด ํ๋์ JDBC ๋ฌผ๋ฆฌ ํธ๋์ญ์ ์ผ๋ก ์คํ๋ฉ๋๋ค.
1๏ธโฃ (์ธ๋ถ ๋กค๋ฐฑ) ๋น์ฐํ ์ต์์ ํด๋์ค์์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด, ์ ์ฒด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋ฉ๋๋ค.
2๏ธโฃ (๋ด๋ถ ๋กค๋ฐฑ) ์ผ๋ถ๋ง ์ฑ๊ณตํ๊ณ ๋ด๋ถ์์ Runtime ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ ์ฒด๊ฐ ๋กค๋ฐฑ๋ฉ๋๋ค.
์ด ๊ฒฝ์ฐ try-catch๋ก ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋๋ผ๋ UnexpectedRollbackException ๊ฐ ๋ฐ์ํ๋ฉฐ ์ ์ฒด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋ฉ๋๋ค. ๋ฌผ๋ก @Transactional(rollbackOnly=false) ์ค์ ์ด๋ (rollbackFor=MyClass.class) ๋ก ๋ฐ๊ฟ ์ ์์ง๋ง ๊ถ์ฅํ์ง ์์ต๋๋ค.
์ ํํ๋ ์๋์ ๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์ณ ๋กค๋ฐฑ๋ฉ๋๋ค.
3๏ธโฃ AOP ํน์ฑ์ this๋ฅผ ํตํ ๋ด๋ถ ํธ์ถ์ ํธ๋์ญ์ ์ด ๊ฑธ๋ฆฌ์ง ์์ต๋๋ค.
- JVM์ class, method ๋ฑ์ ์ ๋ณด๋ ๋ฑ ํ๋ฒ๋ง static ์์ญ์ ์ ์ฅํฉ๋๋ค.
- ์ดํ ๊ฐ ํด๋์ค์ ์ธ์คํด์ค๊ฐ ๋ง๋ค์ด์ง ๋ ํ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํ ๋นํฉ๋๋ค.
- ์ฆ ์ธ์คํด์ค๋ class, method๋ฅผ ๊ฐ์ง๊ณ ์์ง ์๊ณ ๊ฐ๊ฐ์ ํ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ด ๋ฉ๋ชจ๋ฆฌ๋ this ๋ก ์ฐธ์กฐํ ์ ์์ต๋๋ค.
Spring AOP๋ ์์์ ํ์ฉํ ํ๋ก์ ๊ฐ์ฒด๋ฅผ ํตํด ๊ฑธ๋ฆฝ๋๋ค. this ๋ก ์ฐธ์กฐํด๋ฒ๋ฆฌ๋ฉด ํ๋ก์ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ํธ๋์ญ์ ์ด ๊ฑธ๋ฆฌ์ง ์์ต๋๋ค.
3๏ธโฃ ์คํ๋ง @Transactional ์ public ๋ฉ์๋๋ง ์ง์ํฉ๋๋ค.
ํด๋์ค์ ๋ถ์ด๋๊ฑด ๊ทธ ํด๋์ค๊ฐ ๊ฐ์ง ๋ชจ๋ public ๋ฉ์๋์๋ง ์ ์ฉ๋ฉ๋๋ค. ๋ฌผ๋ก protected์ default ์ ๊ทผ์ ์ด์๋ ์์์ด ๊ฐ๋ฅํ๋ฐ ์คํ๋ง์ด ์ ์ฑ ์ ์ผ๋ก ๋ง์๋์ต๋๋ค. ์ ์ด์ ๊ฑฐ๊ธฐ์ ํธ๋์ญ์ ์ด ๊ฑธ์ด์ผํ๋ ์ํฉ ์์ฒด๋ ์๊ณ , ๊ทธ๋ฐ์์ ๋์๋ ์ด์ํ๋๊น์.
๋คํํ ๋ฉ์๋์ ์ง์ ๊ฑธ๋ฉด IntelliJ๊ฐ ๋ฐ๋ก ๊ฒฝ๊ณ ๋ฅผ ๋์์ฃผ๊ธฐ์ ์ค์ํ ์ผ์ด ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ Class์ @Transactional์ ๋ถ์ด๋ ๊ฒฝ์ฐ ๋ฐ๋ก ์๋ ค์ฃผ์ง ์์ผ๋ ์ฃผ์ํ๋๋ก ํฉ์๋ค. ํธ๋์ญ์ ์ public Method ์๋ง ๊ฑธ๋ฆฝ๋๋ค.
4๏ธโฃ ์ ํ์ต์ ์ผ๋ก ํด๊ฒฐํ ์ ์์ง๋ง.. ๊ทธ๋ฌ์ง๋ง๊ณ ๊ณ์ธต์ ๋ถ๋ฆฌํฉ์๋ค.
Required_New ๋ฑ์ผ๋ก ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค๋ง.. ๋์์ ์์ธกํ๊ธฐ๋ ์ด๋ ต๊ณ ์คํ๋ ค ๋ ํฐ ๋ฒ๊ทธ๋ฅผ ๋ง๋ค์ด ๋ผ ์ ์์ต๋๋ค. ์๋๋ ์๋ฌํฐ์ง๊ณ ๋ง์๋ค๋ฉด, ํธ๋์ญ์ ์ ํ์ต์ ์ ํตํด ๋๊ฐ์ ํ์์ด 3๋ฒ์ฉ ์ ์ฅ๋๋ ์ด์ํ ๋ฒ๊ทธ๋ฅผ ๋ง๋ค ์ ๋ ์์ฃ
๊ทธ๋ฅ ์ ์ด๋ถํฐ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ๋ฉด ๋ฉ๋๋ค. ์ต์๋จ์ @Transacational์ ์์ํ์ง ๋ง์ธ์.
๋น๋๊ธฐ๋ก ๋์ํด์ ์ ๊ทธ๋ฆผ์ฒ๋ผ ์ปจํธ๋กค ํ ์ ์๋ค๋ฉด @TransactionEventListener ๋ฑ์ ํ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์ต๋๋ค.
class Service(
private val eventPublisher: ApplicationEventPublisher
) {
@Transactional
fun signUp(dto: MemberSignUpRequest) {
val member = createMember(dto.toEntity())
eventPublisher.publishEvent(MemberSignedUpEvent(member)) // ์ด๋ฒคํธ ๋ฐ์ก
}
}
class EventHandler{
// ์๋์ ๋ฉ์๋๋ eventPublisher ์ฝ๋์ ์๋ ํธ๋์ญ์
์ด ์ฑ๊ณตํด์ผ ๋์ํฉ๋๋ค.
@TransactionalEventListener
fun memberSignedUpEventListener(event: MemberSignedUpEvent) {
emailSenderService.sendSignUpEmail(event.member)
}
}
ํธ๋์ญ์ ๋ฒ์๊ฐ ์์ฒญ ๋ณต์กํด์ ์์ธํ๊ฒ ๊ฑธ๊ณ ์ถ๋ค๋ฉด, Spring @Transactional์ ์ฌ์ฉํ์ง๋ง๊ณ TransactionTemplate์ ์ง์ ์ฐ๋ฉด ๋ฉ๋๋ค. ์ฌ์ฉ๋ฒ๋ง ๋ค๋ฅผ ๋ฟ ๋๊ฐ์ด ๋์ํฉ๋๋ค.
@Service
@RequiredArgsConstructor
public class MyService {
private final TransactionTemplate transactionTemplate;
/** โญ๏ธ Java8 ๋๋ค๋ก ์ ์๋ ๋ฉ์๋๋ฅผ ์ฐ๋ฉด ๋งค์ฐ ๊น๋ํฉ๋๋ค. */
void myTransactionalLambda() {
transactionTemplate.executeWithoutResult(status -> { /*..ํธ๋์ญ์
๋ก์ง..*/ });
var result = transactionTemplate.execute(status -> { /*..ํธ๋์ญ์
๋ก์ง..*/ return null; });
}
/** ์ต๋ช
ํด๋์ค๋ก ์ฌ์ฉํ ๋ ์๋์ ๊ฐ์ต๋๋ค */
MyResultDto myTransactionalMethod() {
return transactionTemplate.execute(new TransactionCallback<MyResultDto>() {
@Override
public MyResultDto doInTransaction(TransactionStatus status) {
// ์ฌ๊ธฐ์ ํธ๋์ญ์
์ ์ ์ฉํ ๋น์ฆ๋์ค ๋ก์ง์ ์์ฑํฉ๋๋ค.
// ํ์ํ ๊ฒฝ์ฐ status.setRollbackOnly(); ํธ์ถ๋ก ๋กค๋ฐฑ์ ์ง์ํ ์ ์์ต๋๋ค.
MyResultDto result = new MyResultDto();
return result;
}
});
}
void myTransactionalMethod() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// ์ฌ๊ธฐ์ ํธ๋์ญ์
์ ์ ์ฉํ ๋น์ฆ๋์ค ๋ก์ง์ ์์ฑํฉ๋๋ค.
// ํ์ํ ๊ฒฝ์ฐ status.setRollbackOnly(); ํธ์ถ๋ก ๋กค๋ฐฑ์ ์ง์ํ ์ ์์ต๋๋ค.
}
});
}
}
'๐ฑBackend > JDBC & JPA' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring DB ๊ธฐ์ ํํค์น๊ธฐ #1 (1) | 2023.12.31 |
---|---|
JDBC Connection ์ ๋ํ ์ดํด, HikariCP ์ค์ ํ (0) | 2023.11.21 |
์คํ๋งJPA์ ์์์ฑ์ปจํ ์คํธ (EntityManager) (0) | 2022.02.03 |
QueryDSL + JPA (0) | 2022.02.02 |
Spring Data JPA (0) | 2022.01.31 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev