@EventListener(ApplicationReadyEvent.class) : ์คํ๋ง ์ปจํ
์ด๋๊ฐ ์์ ํ ์ด๊ธฐํ๋ฅผ ๋ค ๋๋ด๊ณ ์คํ ์ค๋น๊ฐ ๋์์ ๋ ๋ฐ์ํ๋ ์ด๋ฒคํธ์ด๋ค.
์ด ๊ธฐ๋ฅ ๋์ @PostContruct ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ AOP ๊ฐ์ ๋ถ๋ถ์ด ๋ค ์ฒ๋ฆฌ๋์ง ์์ ์์ ์ ํธ์ถ๋ ์ ์๊ธฐ ๋๋ฌธ์, ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
@Slf4j
@RequiredArgsConstructor
public class TestDataInit {
private final ItemRepository itemRepository;
/**
ํ์ธ์ฉ ์ด๊ธฐ ๋ฐ์ดํฐ ์ถ๊ฐ
*/
@EventListener(ApplicationReadyEvent.class)
public void initData() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
@Import(MemoryConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Bean
@Profile("local")
public TestDataInit testDataInit(ItemRepository itemRepository) {
return new TestDataInit(itemRepository);
}
}
@Import(MemoryConfig.class) : ์์ ์ค์ ํ MemoryConfig ๋ฅผ ์ค์ ํ์ผ๋ก ์ฌ์ฉํ๋ค.scanBasePackages = "hello.itemservice.web" : ์ปดํฌ๋ํธ ์ค์บ ๊ฒฝ๋ก - ํด๋น ํจํค์ง๋ถํฐ ํ์ ๋ชจ๋ ํด๋์ค@Profile("local") : ํน์ ํ๋กํ์ ๊ฒฝ์ฐ์๋ง ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํ๋ค.spring.profiles.active ์์ฑ์ ์ฝ์ด์ ํ๋กํ๋ก ์ฌ์ฉํ๋ค.default ๋ผ๋ ํ๋กํ๋ก ์คํspring-jdbc ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌํจ๋์ด ์๋๋ฐ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์คํ๋ง์ผ๋ก JDBC๋ฅผ ์ฌ์ฉํ ๋ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.statement ์ค๋น, ์คํstatement, resultset ์ข
๋ฃ@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(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.update(con -> {
PreparedStatement ps = con.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);
}
log.info("sql={}", sql);
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;
});
}
}
identity (auto increment) ๋ฐฉ์์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ PK์ธ ๊ฐ์ ๋น์๋๊ณ ์ ์ฅํด์ผ ํ๋ค.KeyHolder ์ connection.prepareStatement(sql, new String[]{"id}) ๋ฅผ ์ฌ์ฉํด์ id๋ฅผ ์ง์ ํด์ฃผ๋ฉด Insert ์ฟผ๋ฆฌ ์คํ ์ดํ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์์ฑ๋ ID๊ฐ์ ์ฐธ์กฐํ ์ ์๋ค.SimpleJdbcTemplate ์ ์ฌ์ฉํ๋ฉด ๋ ํธ๋ฆฌํ๋คtemplate.queryForObject()EmptyResultDataAccessException ์์ธ๊ฐ ๋ฐ์ํ๋ค.IncorrectResultSizeDataAccessException ์์ธ๊ฐ ๋ฐ์ํ๋ค.โป JdbcTemplate์ dataSource๋ฅผ ์ฌ์ฉํ๋๋ฐ ์คํ๋ง ๋ถํธ๋ application.properties์ ์ค์ ์ ๋ณด๋ง ๋ฃ์ด ๋์ผ๋ฉด ํด๋น ์ค์ ์ ์ฌ์ฉํด์ ์ปค๋ฅ์ ํ๊ณผ DataSource, ํธ๋์ญ์ ๋งค๋์ ๋ฅผ ์คํ๋ง ๋น์ผ๋ก ์๋ ๋ฑ๋กํ๋ค. โ ์คํ๋ง ๋ถํธ์ ์๋ ๋ฆฌ์์ค ๋ฑ๋ก
NamedParameterJdbcTemplate - V2:Key๊ฐ ํํ๋ก ๋ฐ๊ฟ์ค๋ค.BeanPropertySqlParamterSourcegetXxx() -> xxx, getItemName() -> itemNameSqlParameterSource param = new BeanPropertySqlParameterSource(item);
MapSqlParamterSource, 3.MapSqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId);
Map<String, Object> param = Map.of("id", id);
BeanPropertyRowMapperprivate RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
}
select item_name ์ ๊ฒฝ์ฐ setItem_name() ์ด๋ผ๋ ๋ฉ์๋ ์๊ธฐ ๋๋ฌธ์ Param์ ๋ฐ์ ๋ ๋ฌธ์ ๊ฐ ์๊ธด๋ค. ์ด๋ฐ ๊ฒฝ์ฐ ๊ฐ๋ฐ์๊ฐ SQL์ ๋ค์๊ณผ ๊ฐ์ด as ๋ฅผ ์ฌ์ฉํด์ ๊ณ ์น๋ฉด ๋๋ค.select item_name as itemNameBeanPropertyRowMapper ๋ ์ธ๋์ค์ฝ์ด ํ๊ธฐ๋ฒ์ ์นด๋ฉ ํ๊ธฐ๋ฒ์ผ๋ก ๋ณํํด์ฃผ๊ธฐ ๋๋ฌธ์ ์ปฌ๋ผ ์ด๋ฆ๊ณผ ๊ฐ์ฒด ์ด๋ฆ์ด ์์ ํ ๋ค๋ฅธ ๊ฒฝ์ฐ์๋ง as๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.@Slf4j
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert simpleJdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id");
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = simpleJdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
}withTableName: ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ํ
์ด๋ธ ๋ช
์ ์ง์ ํ๋ค.usingGeneratedKeyColumns: key๋ฅผ ์์ฑํ๋ PK ์ปฌ๋ผ ๋ช
์ ์ง์ ํ๋ค.usingColumns๋ฅผ ์๋ตํ ์ ์๋ค.src/test ์ ์๊ธฐ ๋๋ฌธ์ ๊ทธ ํ์์ ์๋ [application.properties](http://application.properties) ํ์ผ์ด ์ฐ์ ์์๋ฅผ ๊ฐ์ง๊ณ ์คํ๋๋ค.@Import(JdbcTemplateV3Config.class) ๋ก ์ค์ ๋์ด ์๊ธฐ ๋๋ฌธ์ JdbcTemplate์ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํธ์ถํ๊ฒ ๋๋ค.@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach(){
//ํธ๋์ญ์
์์
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
@AfterEach
void afterEach() {
//MemoryItemRepository ์ ๊ฒฝ์ฐ ์ ํ์ ์ผ๋ก ์ฌ์ฉ
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
//ํธ๋์ญ์
๋กค๋ฐฑ
transactionManager.rollback(status);
}
}
@Transactional์ ๋ถ์ด๋ฉด ๋๋ค.โป ํ
์คํธ์ ์ ํ๋ฆฌ์ผ์ด์
์๋ฒ, ๋ ๋ค์์ @Transactional์ด ์ฌ์ฉ๋๋ ๊ฒฝ์ฐ์ ๋ํด์๋ ๋ค์์ ๋ค๋ฃฐ ์์ ์ด๋ค.
@Commit ๋๋ @Rollback(false) ๋ฅผ ๋ถ์ฌ์ฃผ๋ฉด ๋๋ค. @Bean
@Profile("test")
public DataSource dataSource(){
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}drop table if exists item CASCADE;
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);#application.properties MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace
mybatis.type-aliases-packagemybatis.configuration.map-underscore-to-camel-caselogging.level.hello.itemservice.repository.mybatis=trace@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 itemSearchCond);
}
<?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>
namespace : ์์ ๋ง๋ ๋ฉํผ ์ธํฐํ์ด์ค๋ฅผ ์ง์ ํด์ฃผ๋ฉด ๋๋ค.โป XML ํ์ผ์ ๊ฒฝ๋ก๋ฅผ ์์ ํ๊ณ ์ถ๋ค๋ฉด application.properties์ mybatis.mapper-location=classpath:mapper/**/*.xml ์ ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
resource/mapper ๋ฅผ ํฌํจํ ํ์ ํด์ ์๋ XML์ XML ๋งคํ ํ์ผ๋ก ์ธ์resultType ์๋ ๋ฐํ ํ์
์ ๋ช
์ํ๋ค.mybatis.type-aliases-package=hello.itemservice.domain ์์ฑ์ ์ง์ ํ ๋๋ถ์ ๋ชจ๋ ํจํค์ง๋ช
์ ๋ค ์ ์ง ์์๋ ๋๋ค.< : <
: >
& : &
<![CDATA[
and price <= #{maxPrice}
]]>
### ๋งคํผ ๊ตฌํ์ฒด
DataAccessExeption ์ ๋ง๊ฒ ๋ณํํด์ ๋ฐํํด์ค๋ค.@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "item_name", length = 10)
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Entity : JPA๊ฐ ์ฌ์ฉํ๋ ๊ฐ์ฒด๋ผ๋ ๋ป@Id: ํ
์ด๋ธ์ PK์ ํด๋น ํ๋๋ฅผ ๋งคํํ๋ค.@Column : ๊ฐ์ฒด์ ํ๋๋ฅผ ํ
์ด๋ธ์ ์ปฌ๋ผ๊ณผ ๋งคํํ๋ค.name ์์ฑ์ผ๋ก ํ
์ด๋ธ ์ปฌ๋ผ์ด๋ฆ์ ์ง์ ํ ์ ์๋ค.@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item item = em.find(Item.class, itemId);
item.setItemName(updateParam.getItemName());
item.setPrice(updateParam.getPrice());
item.setQuantity(updateParam.getQuantity());
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
@Transactionalโป JPA๋ฅผ ์ค์ ํ๋ ค๋ฉด EntityManagerFactory, JPA ํธ๋์ญ์
๋งค๋์ , ๋ฐ์ดํฐ์์ค ๋ฑ๋ฑ ๋ค์ํ ์ค์ ์ ํด์ผ ํ๋ค. ํ์ง๋ง ์คํ๋ง ๋ถํธ๋ ์ด ๊ณผ์ ์ ๋ชจ๋ ์๋ํ ํด์ค๋ค.
PersistenceException ๊ณผ ๊ทธ ํ์ ์์ธ IllegalStateException, IllegalArgumentException ์ ๋ฐ์์ํจ๋ค.
@Query ๋ฅผ ์ฌ์ฉํด ์ง์ JPQL์ ์์ฑํ ์ ์๋ค.public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
List<Item> findByItemNameLike(String itemName);
List<Item> findByPriceLessThanEqual(Integer price);
//์ฟผ๋ฆฌ ๋ฉ์๋
List<Item> findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);
//์ฟผ๋ฆฌ ์ง์ ์คํ
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName,@Param("price") Integer price);
}
SpringDataJpaItemRepository ๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ค.ItemRepository ์ SpringDataJpaItemRepository ์ฌ์ด๋ฅผ ๋ง์ถ๊ธฐ ์ํ ์ด๋ํฐ์ฒ๋ผ ์ฌ์ฉํ๋ค.
@Repository ์ ๊ด๊ณ์์ด ์์ธ๊ฐ ๋ณํ๋๋ค.Gradle -> Tasks -> build -> cleanGradle -> Tasks -> other -> compileJavaโป Qํ์
์ ์ปดํ์ผ ์์ ์ ์๋ ์์ฑ๋๋ฏ๋ก ๋ฒ์ ๊ด๋ฆฌ์ ํฌํจํ์ง ์๋ ๊ฒ์ด ์ข๋ค. src/main/generated ๋ฅผ ํฌํจํ์ง ์๋๋ค.
@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {
private final EntityManager em;
private final JPAQueryFactory query;
public JpaItemRepositoryV3(EntityManager em) {
this.em = em;
this.query = new JPAQueryFactory(em);
}
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return query.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
}
private BooleanExpression maxPrice(Integer maxPrice) {
if(maxPrice != null){
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)){
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}
Querydsl ์ ๋ณ๋์ ์คํ๋ง ์์ธ ์ถ์ํ๋ฅผ ์ง์ํ์ง ์๋๋ค. ๋์ ์ JPA์์ ํ์ตํ ๊ฒ์ฒ๋ผ @Repository ์์ ์คํ๋ง ์์ธ ์ถ์ํ๋ฅผ ์ฒ๋ฆฌํด์ค๋ค.
ItemRepositoryV2 : ์คํ๋ง ๋ฐ์ดํฐ JPA์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๋ฆฌํฌ์งํ ๋ฆฌpublic interface ItemRepositoryV2 extends JpaRepository<Item, Long> {
}ItemQueryRepositoryV2 : Querydsl์ ์ฌ์ฉํด์ ๋ณต์กํ ์ฟผ๋ฆฌ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๋ฆฌํฌ์งํ ๋ฆฌ@Repository
public class ItemQueryRepositoryV2 {
private final JPAQueryFactory query;
public ItemQueryRepositoryV2(EntityManager em) {
this.query = new JPAQueryFactory(em);
}
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
return query.select(item)
.from(item)
.where(likeItemName(itemName), maxPrice(maxPrice))
.fetch();
}
private BooleanExpression maxPrice(Integer maxPrice) {
if(maxPrice != null){
return item.price.loe(maxPrice);
}
return null;
}
private BooleanExpression likeItemName(String itemName) {
if (StringUtils.hasText(itemName)){
return item.itemName.like("%" + itemName + "%");
}
return null;
}
}ItemService ์ฝ๋๋ฅผ ์ง์ ๋ณ๊ฒฝํ๋ค.โ ๊ตฌ์กฐ์ ์์ ์ฑ VS ๋จ์ํ ๊ตฌ์กฐ
JpaTransactionManager ๋ฅผ ์ ํํ๋ฉด ๋๋ค. ํด๋น ๊ธฐ์ ์ ์ฌ์ฉํ๋ฉด ์คํ๋ง ๋ถํธ๋ ์๋์ผ๋ก ํด๋น ๋งค๋์ ๋ฅผ ์คํ๋ง ๋น์ ๋ฑ๋กํ๋ค.JdbcTemplate, MyBatis ์ ๊ฐ์ ๊ธฐ์ ๋ค์ ๋ด๋ถ์์ JDBC๋ฅผ ์ง์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ DataSourceTransactionManager ๋ฅผ ์ฌ์ฉํ๋ค.**JpaTransactionManager ๋ DataSourceTransactionManager ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ๋๋ถ๋ถ ์ ๊ณตํ๋ค.**@Slf4j
@SpringBootTest
public class TxBasicTest {
@Autowired BasicService basicService;
@Test
void proxyCheck(){
log.info("aop class={}", basicService.getClass());
Assertions.assertThat(AopUtils.isAopProxy(basicService)).isTrue();
}
@Test
void txTest(){
basicService.tx();
basicService.nonTx();
}
@TestConfiguration
static class TxApplyBasicConfig{
@Bean
BasicService basicService(){
return new BasicService();
}
}
@Slf4j
static class BasicService{
@Transactional
void tx(){
log.info("call tx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
void nonTx(){
log.info("call nonTx");
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}AopUtils.isAopProxy() : ์ ์ธ์ ํธ๋์ญ์
๋ฐฉ์์์ ์คํ๋ง ํธ๋์ญ์
์ AOP๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ค.@Transactional ์ ํด๋์ค๋ ๋ฉ์๋์ ๋ถ์ด๋ฉด ํด๋น ๊ฐ์ฒด๋ ํธ๋์ญ์
AOP์ ๋์์ด๋๊ณ , ํด๋์ค๋ด์ ํ๋์ @Transactional ์ด๋ผ๋ ์์ผ๋ฉด ์คํ๋ง ๋น์ ์๋น์ค ๊ฐ์ฒด ๋์ ์ ์๋น์ค ํ๋ก์ ๊ฐ์ฒด๊ฐ ๋ฑ๋ก๋๋ค.BasicService ๋ฅผ ์์ํด์ ๋ง๋ค์ด์ง๊ธฐ ๋๋ฌธ์ ๋คํ์ฑ์ ํ์ฉํ ์ ์๋ค.BasicService$$CGLIB ์ ์ฃผ์
ํ ์ ์๋ค.
tx() ๋ฉ์๋๊ฐ ํธ๋์ญ์
์ ์ฌ์ฉํ ์ ์๋์ง ํ์ธํด๋ณธ๋ค.basicService.tx() ๋ฅผ ํธ์ถํ๋ค.basicService.nonTx() ๋ฅผ ์ฐธ์กฐํด์ ํธ์ถํ๊ณ ์ข
๋ฃํ๋ค.TransactionSynchronizationManager.isActualTransactionActive()@Transactional ์ ์ฌ์ฉํ๋ ๊ฒ์ ์คํ๋ง ๊ณต์ ๋ฉ๋ด์ผ์์ ๊ถ์ฅํ์ง ์๋ ๋ฐฉ๋ฒ์ด๋ค.@Transactional ์ ์ ์ฉํ๋ฉด ํ๋ก์ ๊ฐ์ฒด๊ฐ ์์ฒญ์ ๋จผ์ ๋ฐ์์ ํธ๋์ญ์
์ ์ฒ๋ฆฌํ๊ณ ์ค์ ๊ฐ์ฒด๋ฅผ ํธ์ถํด์ค๋ค.
@Slf4j
@SpringBootTest
public class InternalCallV1Test {
@Autowired
CallService callService;
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void internalCall() {
callService.internal();
}
@Test
void externalCall() {
callService.external();
}
@TestConfiguration
static class InternalCallV1Config {
@Bean
CallService callService() {
return new CallService();
}
}
@Slf4j
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive =
TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", txActive);
}
}
}
@Transactional์ด ์๋ค.this ๋ผ๋ ๋ป์ผ๋ก ์๊ธฐ ์์ ์ ์ธ์คํด์ค๋ฅผ ๊ฐ๋ฆฌํจ๋ค.this.internal() ์์ this ๋ ์๊ธฐ ์์ , ์ค์ ๋์ ๊ฐ์ฒด์ ์ธ์คํฐ์ค๋ฅผ ๋ปํ๋ฏ๋ก ์ด๋ฌํ ๋ด๋ถ ํธ์ถ์ ํ๋ก์๋ฅผ ๊ฑฐ์น์ง ์๋๋ค.@Slf4j
@SpringBootTest
public class InternalCallV2Test {
@Autowired CallService callService;
@Test
void externalCallV2(){
callService.external();
}
@TestConfiguration
static class InternalCallV1Config{
@Bean
CallService callService(){
return new CallService(internalService());
}
@Bean
InternalService internalService(){
return new InternalService();
}
}
@RequiredArgsConstructor
static class CallService{
private final InternalService internalService;
public void external(){
log.info("call external");
printTxInfo();
internalService.internal();
}
private void printTxInfo(){
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", active);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly={}", readOnly);
}
}
@Slf4j
static class InternalService{
@Transactional
public void internal(){
log.info("call internal");
printTxInfo();
}
private void printTxInfo(){
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx active={}", active);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("readOnly={}", readOnly);
}
}
}
public ๋ฉ์๋์๋ง ํธ๋์ญ์
์ ์ ์ฉํ๋๋ก ๊ธฐ๋ณธ ์ค์ ์ด ๋์ด์๋ค.protected, private, package-visible ์๋ ํธ๋์ญ์
์ด ์ ์ฉ๋์ง ์๋๋ค.@PostConstruct
@Transactional
public void initV1(){
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", active);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2(){
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", active);
}
์์ธ๊ฐ ๋ฐ์ํ๋๋ฐ ๋ด๋ถ์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ์ง ๋ชปํ๊ณ ํธ๋์ญ์ ๋ฒ์(@Transactional์ด ์ ์ฉ๋ AOP) ๋ฐ์ผ๋ก ์์ธ๋ฅผ ๋์ง๋ฉด?
- **์ธ์ฒดํฌ ์์ธ๋ ํธ๋์ญ์
์ ๋กค๋ฐฑํ๋ค.**
- **์ฒดํฌ ์์ธ๋ ํธ๋์ญ์
์ ์ปค๋ฐํ๋ค.**
- **์ด ์ ์ฑ
์ด ๋ํดํธ์ด๊ณ ์ด ์ ์ฑ
์ ๋ฐ๋ฅด๊ณ ์ถ์ง ์๋ค๋ฉด `rollbackFor` ์ด๋ผ๋ ์ต์
์ ์ฌ์ฉํด์ ์ฒดํฌ ์์ธ๋ ๋กค๋ฐฑ์ํฌ ์ ์๋ค.**
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
@Transactional
public void order(Order order) throws NotEnoughMoneyException {
log.info("order ํธ์ถ");
orderRepository.save(order);
log.info("๊ฒฐ์ ํ๋ก์ธ์ค ํธ์ถ");
if (order.getUsername().equals("์์ธ")){
log.info("์์คํ
์์ธ");
throw new RuntimeException();
} else if (order.getUsername().equals("์๊ณ ๋ถ์กฑ")){
log.info("๋น์ฆ๋์ค ์์ธ");
order.setPayStatus("๋๊ธฐ");
throw new NotEnoughMoneyException("์๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค");
} else{
log.info("์ ์ ์น์ธ");
order.setPayStatus("์๋ฃ");
}
log.info("๊ฒฐ์ ํ๋ก์ธ์ค ์๋ฃ");
}
}@Slf4j
@SpringBootTest
public class OrderServiceTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void complete() throws NotEnoughMoneyException {
Order order = new Order();
order.setUsername("์ ์");
orderService.order(order);
Order findOrder = orderRepository.findById(order.getId()).get();
Assertions.assertThat(findOrder.getPayStatus()).isEqualTo("์๋ฃ");
}
@Test
void runtimeException() {
Order order = new Order();
order.setUsername("์์ธ");
Assertions.assertThatThrownBy(() -> orderService.order(order))
.isInstanceOf(RuntimeException.class);
Optional<Order> byId = orderRepository.findById(order.getId());
Assertions.assertThat(byId.isEmpty()).isTrue();
}
@Test
void bizException(){
Order order = new Order();
order.setUsername("์๊ณ ๋ถ์กฑ");
try{
orderService.order(order);
fail("์๊ณ ๋ถ์กฑ ์์ธ๊ฐ ๋ฐ์ํด์ผ ํฉ๋๋ค");
}catch (NotEnoughMoneyException e){
log.info("๊ณ ๊ฐ์๊ฒ ์๊ณ ๋ถ์กฑ์ ์๋ฆฌ๊ณ ๋ณ๋์ ๊ณ์ข๋ก ์
๊ธํ๋๋ก ์๋ด");
}
Order findOrder = orderRepository.findById(order.getId()).get();
Assertions.assertThat(findOrder.getPayStatus()).isEqualTo("๋๊ธฐ");
}
}@Test
void double_commit(){
log.info("ํธ๋์ญ์
1 ์์");
TransactionStatus status1 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("ํธ๋์ญ์
1 ์ปค๋ฐ");
txManager.commit(status1);
log.info("ํธ๋์ญ์
2 ์์");
TransactionStatus status2 = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("ํธ๋์ญ์
2 ์ปค๋ฐ");
txManager.commit(status2);
}conn0 ์ปค๋ฅ์
์ ์ฌ์ฉ์ค์ด๋ค.
โ ํธ๋์ญ์ ์ด ์ฌ์ฉ ์ค์ผ ๋ ๋ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ๋ด๋ถ์ ์ฌ์ฉ๋๋ฉด ์ฌ๋ฌ๊ฐ์ง ๋ณต์กํ ์ํฉ์ด ๋ฐ์ํ๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๋ค์๊ณผ ๊ฐ์ ๋จ์ํ ์์น์ ๋ง๋ค๊ธฐ ์ํด ๋ฌผ๋ฆฌ, ๋ ผ๋ฆฌ ํธ๋์ญ์ ์ด๋ผ๋ ๊ฐ๋ ์ ๋์ ํ๋ ๊ฒ์ด๋ค.
โ ๏ธ ์์น
- **๋ชจ๋ ๋ ผ๋ฆฌ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋์ด์ผ ๋ฌผ๋ฆฌ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋ค.
- ํ๋์ ๋ ผ๋ฆฌ ํธ๋์ญ์ ์ด๋ผ๋ ๋กค๋ฐฑ๋๋ฉด ๋ฌผ๋ฆฌ ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋๋ค.**
@Test
void inner_commit(){
log.info("์ธ๋ถ ํธ๋์ญ์
์์");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
log.info("๋ด๋ถ ํธ๋์ญ์
์์");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction());
log.info("๋ด๋ถ ํธ๋์ญ์
์ปค๋ฐ");
txManager.commit(inner);
log.info("์ธ๋ถ ํธ๋์ญ์
์ปค๋ฐ");
txManager.commit(outer);
}
TransactionStatus ์ ๋ด์์ ๋ฐํํ๋๋ฐ, ์ฌ๊ธฐ์ ์ ๊ท ํธ๋์ญ์
์ ์ฌ๋ถ๊ฐ ๋ด๊ฒจ ์๋ค.isNewTransaction ์ ํตํด ์ ๊ท ํธ๋์ญ์
์ฌ๋ถ๋ฅผ ํ์ธํ ์ ์ด๋ฐ.
rollbackOnly=true ๋ผ๋ ํ์๋ฅผ ํด๋๋ค.rollbackOnly ๊ฐ ์๋์ง ํ์ธํ๋ค.UnexpectedRollbackException ๋ฐํ์ ์์ธ๋ฅผ ๋์ง๋ค.conn0 ์ด ์๋ conn1, ์ฆ ๋ค๋ฅธ ์ปค๋ฅ์
์ ์ฌ์ฉํ์ฌ ํธ๋์ญ์
์ ์์ํ๋ค.isNewTransaction()=true ๊ฐ ๋๋ค.
isolation, timeout, readOnly ๋ ํธ๋์ญ์
์ด ์ฒ์ ์์๋ ๋๋ง ์ ์ฉ๋๋ค.REQUIRED - Default ์ต์
์ ์ฌ์ฉํ๊ณ ์์ฃผ ๊ฐ๋ REQUIRES_NEW ๋ฅผ ์ฌ์ฉํ๊ณ ๋๋จธ์ง๋ ๊ฑฐ์ ์ฌ์ฉํ์ง ์๋๋ค.@Slf4j
@SpringBootTest
class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Autowired LogRepository logRepository;
/**
* MemberService @Transactional:OFF
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:OFF
*/
@Test
void outerTxOff_success(){
String username = "outerTxOff_success";
memberService.joinV1(username);
Assertions.assertTrue(memberRepository.find(username).isPresent());
Assertions.assertTrue(logRepository.find(username).isPresent());
}
/**
* MemberService @Transactional:OFF
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:ON Exception
*/
@Test
void outerTxOff_fail(){
String username = "๋ก๊ทธ์์ธ_outerTxOff_fail";
assertThatThrownBy(() -> memberService.joinV1(username))
.isInstanceOf(RuntimeException.class);
Assertions.assertTrue(memberRepository.find(username).isPresent());
Assertions.assertTrue(logRepository.find(username).isEmpty());
}
/**
* MemberService @Transactional:ON
* MemberRepository @Transactional:OFF
* MemberRepository @Transactional:OFF
*/
@Test
void singleTx(){
String username = "singleTx";
memberService.joinV1(username);
Assertions.assertTrue(memberRepository.find(username).isPresent());
Assertions.assertTrue(logRepository.find(username).isPresent());
}
/**
* MemberService @Transactional:ON
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:ON
*/
@Test
void outerTxOn_success(){
String username = "outerTxOn_success";
memberService.joinV1(username);
Assertions.assertTrue(memberRepository.find(username).isPresent());
Assertions.assertTrue(logRepository.find(username).isPresent());
}
/**
* MemberService @Transactional:ON
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:ON Exception
*/
@Test
void outerTxOn_fail(){
String username = "๋ก๊ทธ์์ธ_outerTxOn_fail";
assertThatThrownBy(() -> memberService.joinV1(username))
.isInstanceOf(RuntimeException.class);
Assertions.assertTrue(memberRepository.find(username).isEmpty());
Assertions.assertTrue(logRepository.find(username).isEmpty());
}
/**
* MemberService @Transactional:ON
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:ON Exception
*/
@Test
void recoverException_fail(){
String username = "๋ก๊ทธ์์ธ_recoverException_fail";
assertThatThrownBy(() -> memberService.joinV2(username))
.isInstanceOf(UnexpectedRollbackException.class);
Assertions.assertTrue(memberRepository.find(username).isEmpty());
Assertions.assertTrue(logRepository.find(username).isEmpty());
}
/**
* MemberService @Transactional:ON
* MemberRepository @Transactional:ON
* MemberRepository @Transactional:ON(REQUIRES_NEW) Exception
*/
@Test
void recoverException_success(){
String username = "๋ก๊ทธ์์ธ_recoverException_success";
memberService.joinV2(username);
Assertions.assertTrue(memberRepository.find(username).isPresent());
Assertions.assertTrue(logRepository.find(username).isEmpty());
}
}
UnexpectedRollbackException ์ ํด๋ผ์ด์ธํธ์๊ฒ ๋์ง๋ค.