๐Ÿ“‚ ์Šคํ”„๋ง DB 2

yeopยท2023๋…„ 6์›” 4์ผ

โœ”๏ธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ธฐ์ˆ  - ๋ฒ ์ด์Šค

์šฉ์–ด ์ •๋ฆฌ

SQL Mapper

  • JdbcTemplate, MyBatis
  • ๊ฐœ๋ฐœ์ž๋Š” SQL๋งŒ ์ž‘์„ฑํ•˜๋ฉด ํ•ด๋‹น SQL์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ฒด๋กœ ํŽธ๋ฆฌํ•˜๊ฒŒ ๋งคํ•‘ํ•ด์ค€๋‹ค.
  • JdbcTemplate์€ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ค‘๋ณต์ฝ”๋“œ๋“ค์„ ์ œ๊ฑฐํ•ด์ค€๋‹ค.

ORM

  • JPA, Hibernate, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA, Querydsl
  • ๊ธฐ๋ณธ์ ์ธ SQL์€ JPA๊ฐ€ ๋Œ€์‹  ์ž‘์„ฑํ•˜๊ณ  ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
  • JPA๋Š” ์ž๋ฐ” ์ง„์˜์˜ ORM ํ‘œ์ค€์ด๊ณ  Hibernate๋Š” JPA์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๊ตฌํ˜„์ฒด์ด๋‹ค.

DTO

  • Data Transfer Object
  • ๊ธฐ๋Šฅ์€ ์—†๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ์ฒด
  • DTO๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ์—”๋“œํฌ์ธํŠธ์— ์„ ์–ธํ•˜๋ฉด๋œ๋‹ค.

@EventListener

  • @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));
       }
      }

ItemServiceApplication ์„ค์ • + ํ”„๋กœํ•„

@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") : ํŠน์ • ํ”„๋กœํ•„์˜ ๊ฒฝ์šฐ์—๋งŒ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.
  • ์Šคํ”„๋ง์€ ๋กœ๋”ฉ ์‹œ์ ์— application.properties์˜ spring.profiles.active ์†์„ฑ์„ ์ฝ์–ด์„œ ํ”„๋กœํ•„๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
  • ํ”„๋กœํ•„์€ ๋กœ์ปฌ, ์šด์˜ ํ™˜๊ฒฝ, ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋“ฑ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ์„œ ๋‹ค๋ฅธ ์„ค์ •์„ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
  • ์„ค์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด default ๋ผ๋Š” ํ”„๋กœํ•„๋กœ ์‹คํ–‰

โœ”๏ธ ์Šคํ”„๋ง JdbcTemplete

JdbcTemplete ์ •๋ฆฌ

์žฅ์ 

  • ์„ค์ •์˜ ํŽธ๋ฆฌํ•จ
    • JdbcTemplate์€ spring-jdbc ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”๋ฐ ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ์Šคํ”„๋ง์œผ๋กœ JDBC๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ธฐ๋ณธ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.
  • ๋ฐ˜๋ณต ๋ฌธ์ œ ํ•ด๊ฒฐ
    • ํ…œํ”Œ๋ฆฟ ์ฝœ๋ฐฑ ํŒจํ„ด์„ ํ†ตํ•ด ๋ฐ˜๋ณต ์ž‘์—…์„ ๋Œ€์‹  ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
      • ์ปค๋„ฅ์…˜ ํญ๋“
      • statement ์ค€๋น„, ์‹คํ–‰
      • ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜๋ณตํ•˜๋„๋ก ๋ฃจํ”„๋ฅผ ์‹คํ–‰
      • ์ปค๋„ฅ์…˜ ์ข…๋ฃŒ, statement, resultset ์ข…๋ฃŒ
      • ํŠธ๋žœ์žญ์…˜์„ ๋‹ค๋ฃฉ ์œ„ํ•œ ์ปค๋„ฅ์…˜ ๋™๊ธฐํ™”
      • ์˜ˆ์™ธ ๋ฐœ์ƒ์‹œ ์Šคํ”„๋ง ์˜ˆ์™ธ ๋ณ€ํ™˜๊ธฐ ์‹คํ–‰

๋‹จ์ 

  • ๋™์  SQL์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

๊ตฌํ˜„ - V1

์ฝ”๋“œ

@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;
        });
    }
}

Save ๋ฉ”์„œ๋“œ

  • ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ PK ์ƒ์„ฑ์— identity (auto increment) ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— PK์ธ ๊ฐ’์„ ๋น„์›Œ๋‘๊ณ  ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.
    • KeyHolder ์™€ connection.prepareStatement(sql, new String[]{"id}) ๋ฅผ ์‚ฌ์šฉํ•ด์„œ id๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด Insert ์ฟผ๋ฆฌ ์‹คํ–‰ ์ดํ›„์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ƒ์„ฑ๋œ ID๊ฐ’์„ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋’ค์—์„œ ์„ค๋ช…ํ•  SimpleJdbcTemplate ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ํŽธ๋ฆฌํ•˜๋‹ค

FindById ๋ฉ”์„œ๋“œ

  • template.queryForObject()
    • ๊ฒฐ๊ณผ ๋กœ์šฐ๊ฐ€ ํ•˜๋‚˜์ผ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
    • ResultSet์„ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” RowMapper๋ฅผ ์ธ์ž๋กœ ๋„ฃ์–ด์ค˜์•ผํ•œ๋‹ค.
    • ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด EmptyResultDataAccessException ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
    • ๊ฒฐ๊ณผ๊ฐ€ ๋‘˜ ์ด์ƒ์ด๋ฉด IncorrectResultSizeDataAccessException ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

โ€ป JdbcTemplate์€ dataSource๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” application.properties์— ์„ค์ • ์ •๋ณด๋งŒ ๋„ฃ์–ด ๋†“์œผ๋ฉด ํ•ด๋‹น ์„ค์ •์„ ์‚ฌ์šฉํ•ด์„œ ์ปค๋„ฅ์…˜ ํ’€๊ณผ DataSource, ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ์ž๋™ ๋“ฑ๋กํ•œ๋‹ค. โ†’ ์Šคํ”„๋ง ๋ถ€ํŠธ์˜ ์ž๋™ ๋ฆฌ์†Œ์Šค ๋“ฑ๋ก

NamedParameterJdbcTemplate - V2

  • ์ด๋ฆ„ ์ง€์ • ํŒŒ๋ผ๋ฏธํ„ฐ
    • ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋ ค๋ฉด Map์ฒ˜๋Ÿผ Key, Value ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌํ•ด์•ผ ํ•œ๋‹ค.
    • ์ด๋ฆ„ ์ง€์ • ๋ฐ”์ธ๋”ฉ์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ์ข…๋ฅ˜๋Š” ์•„๋ž˜์˜ 3๊ฐ€์ง€ ์ด๋‹ค.
  • SQL ๋ฌธ์—์„œ ?๋กœ ๋˜์–ด ์žˆ๋Š” ๋ถ€๋ถ„์„ :Key๊ฐ’ ํ˜•ํƒœ๋กœ ๋ฐ”๊ฟ”์ค€๋‹ค.

1.BeanPropertySqlParamterSource

  • ์ž๋ฐ”๋นˆ ํ”„๋กœํผํ‹ฐ ๊ทœ์•ฝ์„ ํ†ตํ•ด์„œ ์ž๋™์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    • getXxx() -> xxx, getItemName() -> itemName
    • SQL๋ฌธ์˜ ์ธ์ž๋กœ ๋“ค์–ด๊ฐ€๋Š” ๊ฐ’ ๋ชจ๋‘๊ฐ€ Dto ์ธ์ž๋กœ ๋“ค์–ด์˜ฌ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ
SqlParameterSource param = new BeanPropertySqlParameterSource(item);

2.MapSqlParamterSource, 3.Map

SqlParameterSource 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);

BeanPropertyRowMapper

private RowMapper<Item> itemRowMapper() {
        return BeanPropertyRowMapper.newInstance(Item.class);
}

๋ณ„์นญ

  • select item_name ์˜ ๊ฒฝ์šฐ setItem_name() ์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ ์—†๊ธฐ ๋•Œ๋ฌธ์— Param์„ ๋ฐ›์„ ๋•Œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ๊ฐœ๋ฐœ์ž๊ฐ€ SQL์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด as ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ณ ์น˜๋ฉด ๋œ๋‹ค.
    • select item_name as itemName
    • ํ•˜์ง€๋งŒ BeanPropertyRowMapper ๋Š” ์–ธ๋”์Šค์ฝ”์–ด ํ‘œ๊ธฐ๋ฒ•์„ ์นด๋ฉœ ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ปฌ๋Ÿผ ์ด๋ฆ„๊ณผ ๊ฐ์ฒด ์ด๋ฆ„์ด ์™„์ „ํžˆ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ as๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๊ด€๋ก€์˜ ๋ถˆ์ผ์น˜

  • ์ž๋ฐ” ๊ฐ์ฒด๋Š” ์นด๋ฉœ ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜๊ณ  ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ๋Š” ์ฃผ๋กœ ์–ธ๋”์Šค์ฝ”์–ด ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

SimpleJdbcInsert - V3

  • ์ฝ”๋“œ
    @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 ์ปฌ๋Ÿผ ๋ช…์„ ์ง€์ •ํ•œ๋‹ค.
  • SimpleJdbcInsert๋Š” ์ƒ์„ฑ ์‹œ์ ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์˜ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ์–ด๋–ค ์นผ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ usingColumns๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ”๏ธ DB๊ด€๋ จ ํ…Œ์ŠคํŠธ

DB ์—ฐ๋™

  • ๋ฐ์ŠคํŠธ ์ผ€์ด์Šค๋Š” src/test ์— ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ํ•˜์œ„์— ์žˆ๋Š” [application.properties](http://application.properties) ํŒŒ์ผ์ด ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๊ณ  ์‹คํ–‰๋œ๋‹ค.
    • ํ•ด๋‹น ํŒŒ์ผ์— DB ์„ค์ • ์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

SpringBootTest

  • @SpringBootTest Annotation์€ @SpringBootApplication์„ ์ฐพ์•„์„œ ์„ค์ •์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
    • ํ˜„์žฌ DB์„ค์ •์ด @Import(JdbcTemplateV3Config.class) ๋กœ ์„ค์ •๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— JdbcTemplate์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋œ๋‹ค.

DB ๋ถ„๋ฆฌ

  • ๋กœ์ปฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„์™€ ํ…Œ์ŠคํŠธ์—์„œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๋“ค๋กœ์ธํ•ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ด๋ ‡๊ฒŒ DB๋ฅผ ๋ถ„๋ฆฌํ–ˆ์ง€๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ 2๋ฒˆ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ค‘๋ณต์œผ๋กœ ์ธํ•ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ์˜ ์ค‘์š” ์›์น™

  1. ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์™€ ๊ฒฉ๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
  2. ํ…Œ์ŠคํŠธ๋Š” ๋ฐ˜๋ณตํ•ด์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.

๋ฐ์ดํ„ฐ ๋กค๋ฐฑ

  • ์œ„์˜ ๋ฌธ์ œ์ ์„ ํŠธ๋žœ์žญ์…˜์„ ํ†ตํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๊ณ  ๋‚˜์„œ ํŠธ๋žœ์žญ์…˜์„ ๊ฐ•์ œ๋กœ ๋กค๋ฐฑํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๊ฑฐ๋œ๋‹ค.
  • ์ค‘๊ฐ„์— ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•ด์„œ ๋กค๋ฐฑ์— ์‹คํŒจํ•ด๋„ ์ปค๋ฐ‹์ด ๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š”๋‹ค.

BeforeEach, AfterEach๋ฅผ ํ†ตํ•œ ์ง์ ‘ ๊ตฌํ˜„ - ์ฝ”๋“œ

@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);
    }
}
  • PlatformTransactionManager๋ฅผ ์ฃผ์ž…๋ฐ›์•„ ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— getTransaction์„ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด RollBack ํ•œ๋‹ค.

Transactional Annotation์„ ํ†ตํ•œ ๊ตฌํ˜„

  • ํด๋ž˜์Šค ๋˜๋Š” ๋ฉ”์„œ๋“œ์— @Transactional์„ ๋ถ™์ด๋ฉด ๋œ๋‹ค.
  • ์›๋ž˜ @Transactional์€ ๋กœ์ง์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜ํ–‰๋˜๋ฉด ์ปค๋ฐ‹๋˜์ง€๋งŒ ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ์—๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์‹คํ–‰ํ•˜๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ์ž๋™์œผ๋กœ ๋กค๋ฐฑ์‹œํ‚จ๋‹ค.

โ€ป ํ…Œ์ŠคํŠธ์™€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„, ๋‘˜ ๋‹ค์—์„œ @Transactional์ด ์‚ฌ์šฉ๋˜๋Š” ๊ฒฝ์šฐ์— ๋Œ€ํ•ด์„œ๋Š” ๋’ค์—์„œ ๋‹ค๋ฃฐ ์˜ˆ์ •์ด๋‹ค.

  • ๊ฐ€๋” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ๋ณด๊ด€๋˜์—ˆ๋Š”์ง€ ๊ฒฐ๊ณผ๋ฅผ ๋ˆˆ์œผ๋กœ ํ™•์ธํ•˜๊ณ  ์‹ถ์„ ๊ฒฝ์šฐ
    • ํ•ด๋‹น ๋ฉ”์„œ๋“œ์— @Commit ๋˜๋Š” @Rollback(false) ๋ฅผ ๋ถ™์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.

์ž„๋ฒ ๋””๋“œ ๋ชจ๋“œ DB

์ž„๋ฒ ๋””๋“œ ๋ชจ๋“œ

  • H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ์ž๋ฐ”๋กœ ๊ฐœ๋ฐœ๋˜์–ด ์žˆ๊ณ , JVM์•ˆ์—์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ชจ๋“œ๋กœ ๋™์ž‘ํ•˜๋Š” ํŠน๋ณ„ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ๋•Œ H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋„ ํ•ด๋‹น JVM๋ฉ”๋ชจ๋ฆฌ์— ํฌํ•จํ•ด์„œ ํ•จ๊ป˜ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • DB๋ฅผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋‚ด์žฅํ•ด์„œ ํ•จ๊ป˜ ์‹คํ–‰ํ•œ๋‹ค๊ณ  ํ•ด์„œ ์ž„๋ฒ ๋””๋“œ ๋ชจ๋“œ๋ผ ํ•œ๋‹ค.

์ง์ ‘ ์„ค์ •

  • ItemServiceApplication์— Bean Annotaion๊ณผ Test Profile์„ ํ†ตํ•ด ์ง์ ‘ ๋“ฑ๋กํ•œ๋‹ค.
    	@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;
    	}
  • ๋ฉ”๋ชจ๋ฆฌ DB๋Š” ์„œ๋ฒ„๊ฐ€ ๊บผ์งˆ ๋•Œ๋งˆ๋‹ค ์™„์ „ํžˆ ๋‹ค์šด๋˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ํ•ญ์ƒ ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
    • JdbcTemplate์„ ์ง์ ‘ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๋Š” DDL์„ ํ˜ธ์ถœํ•ด๋„ ๋˜์ง€๋งŒ ๋„ˆ๋ฌด ๋ถˆํŽธํ•˜๋‹ค.
    • ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” SQL ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•ด์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ๋”ฉ ์‹œ์ ์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.
    • src/test/resources/schema.sql ์„ ์ƒ์„ฑํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
      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์— DB ์„ค์ •์ •๋ณด๋ฅผ ์‚ญ์ œํ•˜๊ณ  ItemServiceApplication์— ๋“ฑ๋กํ•œ ๋นˆ ๋˜ํ•œ ์ œ๊ฑฐํ•œ ํ›„ ๋ณ„๋‹ค๋ฅธ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” ์ž„๋ฒ ๋””๋“œ ๋ชจ๋“œ๋กœ ์ ‘๊ทผํ•˜๋Š” ๋ฐ์ดํ„ฐ์†Œ์Šค๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ œ๊ณตํ•œ๋‹ค.

โœ”๏ธ MyBatis

์žฅ์ 

  • SQL ๋ฌธ์„ xml ํŒŒ์ผ์— ํŽธํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
    • JdbcTemplate์˜ ๊ฒฝ์šฐ ๋ฌธ์žฅ์ด ๊ธธ์–ด์งˆ ๊ฒฝ์šฐ +๋ฅผ ํ†ตํ•ด ๋ถ™์—ฌ์•ผํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ์žˆ์—ˆ์Œ
  • ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•ด์„œ ๋™์  ์ฟผ๋ฆฌ๋ฅผ ํŽธํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹จ์ 

  • JdbcTemplate์€ ์Šคํ”„๋ง์— ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ์ด๊ณ , ๋ณ„๋„์˜ ์„ค์ •์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ MyBatis๋Š” ์•ฝ๊ฐ„์˜ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

์„ค์ •

#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-package
    • ๋งˆ์ด๋ฐ”ํ‹ฐ์Šค์—์„œ ํƒ€์ž… ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํŒจํ‚ค์ง€ ์ด๋ฆ„์„ ์ ์–ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ, ์—ฌ๊ธฐ์— ๋ช…์‹œํ•˜๋ฉด ํŒจํ‚ค์ง€ ์ด๋ฆ„์„ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์ง€์ •ํ•œ ํŒจํ‚ค์ง€์™€ ๊ทธ ํ•˜์œ„ ํŒจํ‚ค์ง€๊ฐ€ ์ž๋™์œผ๋กœ ์ธ์‹๋œ๋‹ค.
    • ์—ฌ๋Ÿฌ ์œ„์น˜๋ฅผ ์ง€์ •ํ•˜๋ ค๋ฉด , , ; ๋กœ ๊ตฌ๋ถ„ํ•˜๋ฉด ๋œ๋‹ค.
  • mybatis.configuration.map-underscore-to-camel-case
    • JdbcTemplate์˜ BeanPropertyRowMapper ์—์„œ ์ฒ˜๋Ÿผ ์–ธ๋”๋ฐ”๋ฅผ ์นด๋ฉœ๋กœ ์ž๋™ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™” ํ•œ๋‹ค.
  • logging.level.hello.itemservice.repository.mybatis=trace
    • MyBatis์—์„œ ์‹คํ–‰๋˜๋Š” ์ฟผ๋ฆฌ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค

์ ์šฉ

ItemMapper ์ธํ„ฐํŽ˜์ด์Šค

@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์„ ํ˜ธ์ถœํ•ด์ฃผ๋Š” ๋งคํผ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ ์–ธํ•ด์ค€๋‹ค.
  • ์ด ์ธํ„ฐํŽ˜์ด์Šค์—๋Š” MyBatis๊ฐ€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก @Mapper๋ฅผ ๋ถ™์—ฌ์ค€๋‹ค.
  • ์ด ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋ฆฌ์†Œ์Šค ํŒจํ‚ค์ง€ ์•„๋ž˜ ๊ฐ™์€ ๊ฒฝ๋กœ๋กœ ์„ค์ •ํ•œ Xml ํŒŒ์ผ์„ ์ฝ์–ด์„œ ํ•ด๋‹น SQL์„ ์‹คํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

XML ํŒŒ์ผ

<?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 &lt;= #{maxPrice}
            </if>
        </where>
    </select>
</mapper>
  • namespace : ์•ž์„œ ๋งŒ๋“  ๋ฉ”ํผ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

โ€ป XML ํŒŒ์ผ์˜ ๊ฒฝ๋กœ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด application.properties์— mybatis.mapper-location=classpath:mapper/**/*.xml ์„ ์„ค์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

  • ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด resource/mapper ๋ฅผ ํฌํ•จํ•œ ํ•˜์œ„ ํด์— ์žˆ๋Š” XML์„ XML ๋งคํ•‘ ํŒŒ์ผ๋กœ ์ธ์‹
  • finById
    • resultType ์—๋Š” ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ๋ช…์‹œํ•œ๋‹ค.
      • application.properties์— mybatis.type-aliases-package=hello.itemservice.domain ์†์„ฑ์„ ์ง€์ •ํ•œ ๋•๋ถ„์— ๋ชจ๋“  ํŒจํ‚ค์ง€๋ช…์„ ๋‹ค ์ ์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

ํŠน์ˆ˜๋ฌธ์ž ์‚ฌ์šฉ

  • xml์—์„œ <, > ํ•ด๋‹น ๋ฌธ์ž๋Š” ํƒœ๊ทธ์— ํ•ด๋‹น๋˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.
    • ๋ฌธ์ž ์‚ฌ์šฉ

      < : <
      : >
      & : &

    • CDATA ์‚ฌ์šฉ
      <![CDATA[
       and price <= #{maxPrice}
       ]]>

์›๋ฆฌ

### ๋งคํผ ๊ตฌํ˜„์ฒด
  • ๋งคํผ ๊ตฌํ˜„์ฒด๋Š” ์˜ˆ์™ธ ๋ณ€ํ™˜๊นŒ์ง€ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
    • MyBatis์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋ฅผ ์Šคํ”„๋ง ์˜ˆ์™ธ ์ถ”์ƒํ™”์ธ DataAccessExeption ์— ๋งž๊ฒŒ ๋ณ€ํ™˜ํ•ด์„œ ๋ฐ˜ํ™˜ํ•ด์ค€๋‹ค.

๊ธฐํƒ€๊ธฐ๋Šฅ

  • PDF ์ฐธ๊ณ 

โœ”๏ธ JPA

์ด๋ก  ์ •๋ฆฌ

  • ๊ฐ์ฒด๋ฅผ ๊ด€๊ณ„ํ˜• DB์— ์ €์žฅํ•ด์•ผํ•˜๋Š”๋ฐ ์ด ๋‘˜ ์‚ฌ์ด์—๋Š” ์ฐจ์ด์ ์ด ์กด์žฌํ•œ๋‹ค.
    • ์ƒ์†
    • ์—ฐ๊ด€๊ด€๊ณ„
    • ๋ฐ์ดํ„ฐ ํƒ€์ž…
    • ๋ฐ์ดํ„ฐ ์‹๋ณ„ ๋ฐฉ๋ฒ•
  • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ์ฒด๋‹ต๊ฒŒ ๋ชจ๋ธ๋ง ํ• ์ˆ˜๋ก ๋งคํ•‘ ์ž‘์—…๋งŒ ๋Š˜์–ด๋‚œ๋‹ค.
  • ๊ฐ์ฒด๋ฅผ ์ž๋ฐ” ์ปฌ๋ ‰์…˜์— ์ €์žฅ ํ•˜๋“ฏ์ด DB์— ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ• โ†’ JPA(Java Persistence APIp)
    • ๊ฐ์ฒด๋Š” ๊ฐ์ฒด๋Œ€๋กœ ์„ค๊ณ„
    • ๊ด€๊ณ„ํ˜•DB๋Š” ๊ด€๊ณ„ํ˜•DB๋Œ€๋กœ ์„ค๊ณ„

JPA๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

  • SQL ์ค‘์‹ฌ์ ์ธ ๊ฐœ๋ฐœ์—์„œ ๊ฐ์ฒด ์ค‘์‹ฌ์œผ๋กœ ๊ฐœ๋ฐœ
  • ์ƒ์‚ฐ์„ฑ, ์œ ์ง€๋ณด์ˆ˜
  • ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ๋ถˆ์ผ์น˜ ํ•ด๊ฒฐ
  • ์„ฑ๋Šฅ
    1. 1์ฐจ ์บ์‹œ์™€ ๋™์ผ์„ฑ ๋ณด์žฅ
      • ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ๋Š” ๊ฐ™์€ ์—”ํ‹ฐ๋””๋ฅผ ๋ฐ˜ํ™˜
      • DB Isolation Level์ด Read Commit์ด์–ด๋„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Repeatable Read ๋ณด์žฅ
    2. ํŠธ๋žœ์žญ์…˜์„ ์ง€์›ํ•˜๋Š” ์“ฐ๊ธฐ ์ง€์›
      • ํŠธ๋žœ์žญ์…˜์„ ์ปค๋ฐ‹ํ•  ๋•Œ๊นŒ์ง€ INSERT SQL์„ ๋ชจ์Œ
      • JDBC BATCH SQL ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด์„œ ํ•œ๋ฒˆ์— SQL ์ „์†ก
      • Update, Delete๋กœ ์ธํ•œ ๋กœ์šฐ๋ฝ ์‹œ๊ฐ„ ์ตœ์†Œํ™”
    3. ์ง€์—ฐ ๋กœ๋”ฉ
  • ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์ถ”์ƒํ™”์™€ ๋ฒค๋” ๋…๋ฆฝ์„ฑ

์ ์šฉ

Item Class

@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 ์†์„ฑ์œผ๋กœ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ด๋ฆ„์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • @Column์„ ์ƒ๋žตํ•˜๋ฉด ํ•„๋“œ์˜ ์ด๋ฆ„์„ ํ…Œ์ด๋ธ” ์ปด๋Ÿผ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ๊ฐ์ฒด ํ•„๋“œ์˜ ์นด๋ฉœ์ผ€์ด์Šค๋ฅผ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์˜ ์–ธ๋”์Šค์ฝ”์–ด๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— itemName ๊ฐ™์€ ๊ฒฝ์šฐ @Column์„ ์ƒ๋žตํ•ด๋„ ๋œ๋‹ค.
  • JPA๋Š” public ๋˜๋Š” protected ๊ธฐ๋ณธ ์ƒ์„ฑ์ž๊ฐ€ ํ•„์ˆ˜์ด๋‹ค.

JPAItemRepository ์ฝ”๋“œ

@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();
    }
}
  • EntityManager ์ฃผ์ž…
    • JPA๋Š” ๋ชจ๋“  ๋™์ž‘์„ ์—”ํ‹ฐํ‹ฐ ๋งค๋‹ˆ์ €๋ฅผ ํ†ตํ•ด์„œ ์ด๋ฃจ์–ด์ง„๋‹ค.
    • ์—”ํ‹ฐ๋”” ๋งค๋‹ˆ์ €๋Š” ๋‚ด๋ถ€์— ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.
  • @Transactional
    • JPA์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•œ๋‹ค. ๋ณดํ†ต์˜ ๊ฒฝ์šฐ Service ๊ณ„์ธต์—์„œ Transactional์„ ๊ฑธ์–ด์ฃผ๋Š”๊ฒŒ ์ผ๋ฐ˜์ ์ด๋‹ค. ์ด ํ”„๋กœ์ ํŠธ๋งŒ ์˜ˆ์™ธ

โ€ป JPA๋ฅผ ์„ค์ •ํ•˜๋ ค๋ฉด EntityManagerFactory, JPA ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €, ๋ฐ์ดํ„ฐ์†Œ์Šค ๋“ฑ๋“ฑ ๋‹ค์–‘ํ•œ ์„ค์ •์„ ํ•ด์•ผ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” ์ด ๊ณผ์ •์„ ๋ชจ๋‘ ์ž๋™ํ™” ํ•ด์ค€๋‹ค.

์˜ˆ์™ธ ๋ณ€ํ™˜

  • EntityManager๋Š” ์ˆœ์ˆ˜ํ•œ JPA ๊ธฐ์ˆ ์ด๊ณ  ,์Šคํ”„๋ง๊ณผ๋Š” ๊ด€๊ณ„๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์—”ํ‹ฐํ‹ฐ ๋งค๋‹ˆ์ €๋Š” ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด JPA ๊ด€๋ จ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
  • JPA๋Š” PersistenceException ๊ณผ ๊ทธ ํ•˜์œ„ ์˜ˆ์™ธ IllegalStateException, IllegalArgumentException ์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
    • ์ด๋ ‡๊ฒŒ ๋˜๋ฉด ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ์ธ ํ•ด๋‹น ์—๋Ÿฌ๋Š” ์„œ๋น„์Šค ๊ณ„์ธต๊นŒ์ง€ ๋˜์ ธ์ ธ ์„œ๋น„์Šค ๊ณ„์ธต์ด JPA๊ธฐ์ˆ ์— ์˜์กด์ ์ด๊ฒŒ ๋œ๋‹ค.

@Repository

  • @Repository Annotation์€ ํ•ด๋‹น ํด๋ž˜์Šค๊ฐ€ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์˜ ๋Œ€์ƒ์ด๋ผ๋Š” ์„ ์–ธ ๊ธฐ๋Šฅ๊ณผ ์˜ˆ์™ธ ๋ณ€ํ™˜ AOP์˜ ์ ์šฉ ๋Œ€์ƒ์ด ๋˜๋Š” ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.

โœ”๏ธ ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA

์ด๋ก 

  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋Š” JPA๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.
  • ๋Œ€ํ‘œ ๊ธฐ๋Šฅ
    • ๊ณตํ†ต ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋Šฅ
    • ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ

์ ์šฉ - JpaRepository Interface

์‚ฌ์šฉ๋ฒ•

  • JpaRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ƒ์†๋ฐ›๊ณ , ์ œ๋„ค๋ฆญ์— ๊ด€๋ฆฌํ•  <์—”ํ‹ฐํ‹ฐ, ์—”ํ‹ฐํ‹ฐID>๋ฅผ ์ค€๋‹ค.
    • ๊ทธ๋Ÿฌ๋ฉด JpaRepository๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ CRUD ๊ธฐ๋Šฅ์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๊ฐ€ ํ”„๋ก์‹œ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•ด์„œ ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  ๊ตฌํ˜„์ฒด์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค์–ด์„œ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.
      • ๋”ฐ๋ผ์„œ ๊ฐœ๋ฐœ์ž๋Š” ๊ตฌํ˜„ ํด๋ž˜์Šค ์—†์ด ์ธํ„ฐํŽ˜์ด์Šค๋งŒ์œผ๋กœ CRUD ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ

  • ๊ธฐ๋ณธ CRUD ์™ธ์— ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋Š” ์ธํ„ฐํŽ˜์ด์Šค์— ๋ฉ”์„œ๋“œ๋งŒ ์ ์–ด๋‘๋ฉด ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ๋ถ„์„ํ•ด์„œ ์ฟผ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ๋งŒ๋“ค๊ณ  ์‹คํ–‰ํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ ํ•„ํ„ฐ ์กฐ๊ฑด ๊ณต์‹ ๋ฌธ์„œ

JPQL ์ง์ ‘ ์ž‘์„ฑ

  • ๋ฉ”์„œ๋“œ ์ด๋ฆ„์ด ๋„ˆ๋ฌด ๋ณต์žกํ•˜๊ณ  ๊ธด ๊ฒฝ์šฐ @Query ๋ฅผ ์‚ฌ์šฉํ•ด ์ง์ ‘ JPQL์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋Š” JPQL ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ SQL๋กœ๋„ ์ง์ ‘ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค.
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);
}

์ฃผ์˜์ ๊ณผ ํ”Œ๋กœ์šฐ

์˜์กด๊ด€๊ณ„์™€ ๊ตฌ์กฐ

  • ItemService๋Š” ItemRepository์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ItemService์—์„œ SpringDataJpaItemRepository ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
    • JpaItemRepositoryV2๋ฅผ ๋งŒ๋“ค์–ด ItemRepository ์™€ SpringDataJpaItemRepository ์‚ฌ์ด๋ฅผ ๋งž์ถ”๊ธฐ ์œ„ํ•œ ์–ด๋Œ‘ํ„ฐ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•œ๋‹ค.

ํด๋ž˜์Šค ์˜์กด๊ด€๊ณ„

๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด ์˜์กด๊ด€๊ณ„

์˜ˆ์™ธ ๋ณ€ํ™˜

  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๋„ ์Šคํ”„๋ง ์˜ˆ์™ธ ์ถ”์ƒํ™”๋ฅผ ์ง€์›ํ•œ๋‹ค.
  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA๊ฐ€ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ”„๋ก์‹œ์—์„œ ์ด๋ฏธ ์˜ˆ์™ธ ๋ณ€ํ™˜์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— @Repository ์— ๊ด€๊ณ„์—†์ด ์˜ˆ์™ธ๊ฐ€ ๋ณ€ํ™˜๋œ๋‹ค.

Querydsl

์„ค์ •

  • dependecy ์ถ”๊ฐ€, build-clean ์ถ”๊ฐ€
  • Querydsl ์‚ฌ์šฉ์„ ์œ„ํ•ด์„œ๋Š” Qํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค.

๋นŒ๋“œ ํˆด์— ๋”ฐ๋ฅธ Qํด๋ž˜์Šค ์ƒ์„ฑ ๋ฐฉ๋ฒ•

  1. Gradle
    1. Gradle -> Tasks -> build -> clean
    2. Gradle -> Tasks -> other -> compileJava
    • build โ†’ generated ํ•˜์œ„ ํŒจํ‚ค์ง€์— ํ™•์ธํ•ด๋ณด๋ฉด QItem์ด ์ƒ์„ฑ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค
    • Qํƒ€์ž… ์‚ญ์ œ๋ฅผ ์œ„ํ•ด์„œ๋Š” gradle clean์„ ์ˆ˜ํ–‰ํ•˜๋ฉด build ํด๋” ์ž์ฒด๊ฐ€ ์‚ญ์ œ๋œ๋‹ค.
  2. IntelliJ IDEA
    1. Build โ†’ Build Project ๋˜๋Š” Build โ†’ Rebuild ๋˜๋Š” main() ์‹คํ–‰ ๋˜๋Š” ํ…Œ์ŠคํŠธ ์‹คํ–‰
    • src โ†’ main โ†’ generated ํ•˜์œ„์— QItem ์ƒ์„ฑ

โ€ป Qํƒ€์ž…์€ ์ปดํŒŒ์ผ ์‹œ์ ์— ์ž๋™ ์ƒ์„ฑ๋˜๋ฏ€๋กœ ๋ฒ„์ „๊ด€๋ฆฌ์— ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. src/main/generated ๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค.

์ ์šฉ

QueryDslRepository ์ฝ”๋“œ

@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;
    }
}
  • JpaQueryFactory๋ฅผ ์ฃผ์ž…๋ฐ›์•„์•ผ ํ•˜๋ฉฐ JpaQueryFactory๋Š” JPA ์ฟผ๋ฆฌ์ธ JPQL์„ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— EntityManager๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
  • JPAQueryFactory๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด์„œ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.

์˜ˆ์™ธ ๋ณ€ํ™˜

  • Querydsl ์€ ๋ณ„๋„์˜ ์Šคํ”„๋ง ์˜ˆ์™ธ ์ถ”์ƒํ™”๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋Œ€์‹ ์— JPA์—์„œ ํ•™์Šตํ•œ ๊ฒƒ์ฒ˜๋Ÿผ @Repository ์—์„œ ์Šคํ”„๋ง ์˜ˆ์™ธ ์ถ”์ƒํ™”๋ฅผ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.

์žฅ์ 

  • ์˜คํƒ€๊ฐ€ ์žˆ์–ด๋„ ์ปดํŒŒ์ผ ์‹œ์ ์— ์˜ค๋ฅ˜๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฉ”์„œ๋“œ ์ถ”์ถœ์„ ํ†ตํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

โœ”๏ธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ธฐ์ˆ  ํ™œ์šฉ ๋ฐฉ์•ˆ - ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„

์–ด๋Œ‘ํ„ฐ ๋„์ž…, ๊ตฌ์กฐ ์œ ์ง€ - ํ˜„์žฌ ์ƒํƒœ

ํด๋ž˜์Šค ์˜์กด๊ด€๊ณ„

๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด ์˜์กด๊ด€๊ณ„

  • ํ˜„์žฌ์˜ ๊ตฌ์กฐ๋Š” JpaItemRepositoryV2๊ฐ€ ์ค‘๊ฐ„์—์„œ ์–ด๋Œ‘ํ„ฐ ์—ญํ• ์„ ํ•ด์ค€ ๋•๋ถ„์— ItemService๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ItemRepository ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๊ณ  ItemService์˜ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.
    • DI, OCP ์›์น™์„ ์ง€ํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•˜๊ณ  ์–ด๋Œ‘ํ„ฐ ์ฝ”๋“œ์™€ ์‹ค์ œ ์ฝ”๋“œ๊นŒ์ง€ ํ•จ๊ป˜ ์œ ์ง€๋ณด์ˆ˜ ํ•ด์•ผ ํ•˜๋Š” ์–ด๋ ค์›€์ด ๋ฐœ์ƒํ•œ๋‹ค.

์‹ค์šฉ์ ์ธ ๊ตฌ์กฐ

  • 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;
        }
    }
  • Service ๊ณ„์ธต์—์„œ ๋‘๊ฐ€์ง€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋ชจ๋‘ ์ฃผ์ž…๋ฐ›๋Š”๋‹ค.
    • ๋‘๊ฐ€์ง€ ๋ชจ๋‘ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋ก
    • ItemRepositoryV2๋Š” ์Šคํ”„๋ง์—์„œ ๊ตฌํ˜„์ฒด๋ฅผ ์ƒ์„ฑํ•ด ์ธ์Šคํ„ด์Šค๋ฅผ ์Šคํ”„๋ง ๋นˆ์— ๋“ฑ๋กํ•ด์ค€๋‹ค.

ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„

  • DI, OCP๋ฅผ ์ง€ํ‚ค๊ธฐ ์œ„ํ•ด ์–ด๋Œ‘ํ„ฐ๋ฅผ ๋„์ž…ํ•˜๊ณ , ๋” ๋งŽ์€ ์ฝ”๋“œ๋ฅผ ์œ ์ง€ํ•œ๋‹ค.
  • ์–ด๋Œ‘ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ•˜๊ฒŒ ๊ฐ€์ ธ๊ฐ€์ง€๋งŒ, DI, OCP๋ฅผ ํฌ๊ธฐํ•˜๊ณ  ItemService ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•œ๋‹ค.

โ†’ ๊ตฌ์กฐ์˜ ์•ˆ์ •์„ฑ VS ๋‹จ์ˆœํ•œ ๊ตฌ์กฐ

  • ์ถ”์ƒํ™”๋„ ๋น„์šฉ์ด ๋“ ๋‹ค. - ์œ ์ง€ ๋ณด์ˆ˜ ๊ด€์ ์—์„œ์˜ ๋น„์šฉ
    • ์ด ์ถ”์ƒํ™” ๋น„์šฉ์„ ๋„˜์–ด์„ค ๋งŒํผ ํšจ๊ณผ๊ฐ€ ์žˆ์„ ๋•Œ ์ถ”์ƒํ™”๋ฅผ ๋„์ž…ํ•˜๋Š” ๊ฒƒ์ด ์‹ค์šฉ์ ์ด๋‹ค.
  • ํ˜„์žฌ ์ƒํ™ฉ์— ๋งž๋Š” ์„ ํƒ์„ ํ•˜์ž โ†’ ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์™€ ๋ณ€๋™์„ฑ์— ๋”ฐ๋ผ ์„ ํƒ

๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ธฐ์ˆ  ์กฐํ•ฉ

  • ์‹ค๋ฌด์—์„œ 95% ์ •๋„๋Š” JPA, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA, Querydsl ๋“ฑ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ณ , ๋‚˜๋จธ์ง€ 5%๋Š” SQL์„ ์ง์ ‘ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋‹ˆ JdbcTemplate์ด๋‚˜ MyBatis๋กœ ํ•ด๊ฒฐํ•œ๋‹ค.

ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ € ์„ ํƒ

  • JPA, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA, Querydsl์€ ๋ชจ๋‘ JPA ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— JpaTransactionManager ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋œ๋‹ค. ํ•ด๋‹น ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜๋ฉด ์Šคํ”„๋ง ๋ถ€ํŠธ๋Š” ์ž๋™์œผ๋กœ ํ•ด๋‹น ๋งค๋‹ˆ์ €๋ฅผ ์Šคํ”„๋ง ๋นˆ์— ๋“ฑ๋กํ•œ๋‹ค.
  • ๊ทธ๋Ÿฐ๋ฐ JdbcTemplate, MyBatis ์™€ ๊ฐ™์€ ๊ธฐ์ˆ ๋“ค์€ ๋‚ด๋ถ€์—์„œ JDBC๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— DataSourceTransactionManager ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €๊ฐ€ ๋‹ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์ง€๋งŒ **JpaTransactionManager ๋Š” DataSourceTransactionManager ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋Œ€๋ถ€๋ถ„ ์ œ๊ณตํ•œ๋‹ค.**
    • JPA๋ผ๋Š” ๊ธฐ์ˆ ๋„ ๊ฒฐ๊ตญ ๋‚ด๋ถ€์—์„œ๋Š” DataSource์™€ JDBC ์ปค๋„ฅ์…˜์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฃผ์˜์ 

  • JPA์™€ JdbcTemplate์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ JPA์˜ ํ”Œ๋Ÿฌ์‹œ ํƒ€์ด๋ฐ์— ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.
    • JPA ๊ฐ•์˜์—์„œ ๋‹ค๋ฃฐ ์˜ˆ์ •

โœ”๏ธ ์Šคํ”„๋ง ํŠธ๋žœ์žญ์…˜

ํŠธ๋žœ์žญ์…˜์˜ ์ดํ•ด

  • DB 1ํŽธ ํŠธ๋žœ์žญ์…˜ ์ฐธ๊ณ 

ํŠธ๋žœ์žญ์…˜ ์ ์šฉ ํ™•์ธ

  • ์ฝ”๋“œ
    @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 ๋Œ€์‹ ์— ํ”„๋ก์‹œ์ธ BasicService$$CGLIB ์„ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค.
      • ๋ถ€๋ชจ๋Š” ์ž์‹์„ ํ’ˆ์„ ์ˆ˜ ์žˆ๋‹ค.

ํŠธ๋žœ์žญ์…˜ ํ”„๋ก์‹œ ๋™์ž‘ ๋ฐฉ์‹

  • basicService.tx ํ˜ธ์ถœ
    • ํ”„๋ก์‹œ์˜ tx()๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค. ํ”„๋ก์‹œ๋Š” tx() ๋ฉ”์„œ๋“œ๊ฐ€ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณธ๋‹ค.
    • ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•œ ๋‹ค์Œ ์‹ค์ œ basicService.tx() ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
    • ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ basicService.tx()์˜ ํ˜ธ์ถœ์ด ๋๋‚˜์„œ ํ”„๋ก์‹œ๋กœ ์ œ์–ด๊ฐ€ ๋Œ์•„์˜ค๋ฉด ํ”„๋ก์‹œ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์ปค๋ฐ‹ํ•˜๊ฑฐ๋‚˜ ๋กค๋ฐฑํ•ด์„œ ํŠธ๋žœ์žญ์…˜์„ ์ข…๋ฃŒํ•œ๋‹ค.
  • nonTx ํ˜ธ์ถœ
    • ํŠธ๋žœ์žญ์…˜ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ๋ฐ”๋กœ basicService.nonTx() ๋ฅผ ์ฐธ์กฐํ•ด์„œ ํ˜ธ์ถœํ•˜๊ณ  ์ข…๋ฃŒํ•œ๋‹ค.
  • TransactionSynchronizationManager.isActualTransactionActive()
    • ํ˜„์žฌ ์“ฐ๋ ˆ๋“œ์— ํŠธ๋žœ์žญ์…˜์ด ์ ์šฉ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
    • Boolean๊ฐ’ ๋ฐ˜ํ™˜

ํŠธ๋žœ์žญ์…˜ ์ ์šฉ ์œ„์น˜ - ์šฐ์„ ์ˆœ์œ„

  • ์Šคํ”„๋ง์—์„œ ์šฐ์„ ์ˆœ์œ„๋Š” ํ•ญ์ƒ ๋” ๊ตฌ์ฒด์ ์ด๊ณ  ์ž์„ธํ•œ ๊ฒƒ์ด ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„๋‹ค.

์ธํ„ฐํŽ˜์ด์Šค์— @Transactional ์ ์šฉ

  • ์šฐ์„  ์ˆœ์œ„
    1. ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ
    2. ํด๋ž˜์Šค์˜ ํƒ€์ž…
    3. ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฉ”์„œ๋“œ
    4. ์ธํ„ฐํŽ˜์ด์Šค์˜ ํƒ€์ž…
  • ๊ทธ๋Ÿฐ๋ฐ ์ธํ„ฐํŽ˜์ด์Šค์— @Transactional ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์Šคํ”„๋ง ๊ณต์‹ ๋ฉ”๋‰ด์–ผ์—์„œ ๊ถŒ์žฅํ•˜์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
    • AOP๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์— ๋”ฐ๋ผ์„œ ์ธํ„ฐํŽ˜์ด์Šค์— ์• ๋…ธํ…Œ์ด์…˜์„ ๋‘๋ฉด AOP๊ฐ€ ์ ์šฉ ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

ํŠธ๋žœ์žญ์…˜ AOP ์ฃผ์˜ ์‚ฌํ•ญ - ํ”„๋ก์‹œ ๋‚ด๋ถ€ ํ˜ธ์ถœ

  • ํŠธ๋žœ์žญ์…˜ AOP๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ”„๋ก์‹œ ๋ฐฉ์‹์˜ AOP๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
  • @Transactional ์„ ์ ์šฉํ•˜๋ฉด ํ”„๋ก์‹œ ๊ฐ์ฒด๊ฐ€ ์š”์ฒญ์„ ๋จผ์ € ๋ฐ›์•„์„œ ํŠธ๋žœ์žญ์…˜์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ํ˜ธ์ถœํ•ด์ค€๋‹ค.
    • ๋”ฐ๋ผ์„œ ํŠธ๋žœ์žญ์…˜์„ ํ˜ธ์ถœํ•˜๋ ค๋ฉด ํ•ญ์ƒ ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•ด์„œ ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.
  • ๋งŒ์•ฝ ํ”„๋ก์‹œ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด AOP๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๊ณ , ํŠธ๋žœ์žญ์…˜๋„ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋Œ€์ƒ ๊ฐ์ฒด์˜ ๋‚ด๋ถ€์—์„œ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์ด ๋ฐœ์ƒํ•˜๋ฉด ํ”„๋ก์‹œ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ๋Œ€์ƒ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

ํ”„๋ก์‹œ์™€ ๋‚ด๋ถ€ ํ˜ธ์ถœ

  • ์ฝ”๋“œ
    @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);
            }
        }
    }
  1. ํด๋ผ์ด์–ธํŠธ์ธ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ํŠธ๋žœ์žญ์…˜ ํ”„๋ก์‹œ์—์„œ external()์„ ํ˜ธ์ถœํ•œ๋‹ค.
  2. callService์˜ ํŠธ๋žœ์žญ์…˜ ํ”„๋ก์‹œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.
  3. external() ๋ฉ”์„œ๋“œ์—๋Š” @Transactional์ด ์—†๋‹ค.
  4. ๊ทธ๋Ÿฌ๋ฏ€๋กœ ์‹ค์ œ ๊ฐ์ฒด์˜ ์‹ค์ œ callService์˜ external()์ด ํ˜ธ์ถœ๋œ๋‹ค.
  5. external()์€ ๋‚ด๋ถ€์—์„œ internal() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. โ†’ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

๋ฌธ์ œ ์›์ธ

  • ์ž๋ฐ” ์–ธ์–ด์—์„œ ๋ฉ”์„œ๋“œ ์•ž์— ๋ณ„๋„์˜ ์ฐธ์กฐ๊ฐ€ ์—†์œผ๋ฉด 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 ๋ฉ”์„œ๋“œ

  • ์Šคํ”„๋ง์˜ ํŠธ๋žœ์žญ์…˜ AOP ๊ธฐ๋Šฅ์€ public ๋ฉ”์„œ๋“œ์—๋งŒ ํŠธ๋žœ์žญ์…˜์„ ์ ์šฉํ•˜๋„๋ก ๊ธฐ๋ณธ ์„ค์ •์ด ๋˜์–ด์žˆ๋‹ค.
    • protected, private, package-visible ์—๋Š” ํŠธ๋žœ์žญ์…˜์ด ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.

ํŠธ๋žœ์žญ์…˜ AOP ์ฃผ์˜ ์‚ฌํ•ญ - ์ดˆ๊ธฐํ™” ์‹œ์ 

  • ์Šคํ”„๋ง ์ดˆ๊ธฐํ™” ์‹œ์ ์—๋Š” ํŠธ๋žœ์žญ์…˜ AOP๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.
@PostConstruct
@Transactional
public void initV1(){
    boolean active = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init @PostConstruct tx active={}", active);
}
  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ ์–ธ ์‹œ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ๊ฐ€ ๋จผ์ € ํ˜ธ์ถœ ๋˜๊ณ  ๊ทธ ๋‹ค์Œ์— ํŠธ๋žœ์žญ์…˜ AOP๊ฐ€ ์ ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฉ”์„œ๋“œ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ํญ๋“ํ•  ์ˆ˜ ์—†๋‹ค.

๋Œ€์•ˆ

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2(){
    boolean active = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init ApplicationReadyEvent tx active={}", active);
}
  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ ์–ธ ์‹œ ํŠธ๋žœ์žญ์…˜ AOP๋ฅผ ํฌํ•จํ•œ ์Šคํ”„๋ง์ด ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์™„์ „ํžˆ ์ƒ์„ฑ๋˜๊ณ  ๋‚œ ๋‹ค์Œ์— ์ด๋ฒคํŠธ๊ฐ€ ๋ถ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

ํŠธ๋žœ์žญ์…˜ ์˜ต์…˜

  • PDF ์ฐธ๊ณ 

์˜ˆ์™ธ์™€ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹, ๋กค๋ฐฑ

  • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ ๋‚ด๋ถ€์—์„œ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„(@Transactional์ด ์ ์šฉ๋œ AOP) ๋ฐ–์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋ฉด?

    - **์–ธ์ฒดํฌ ์˜ˆ์™ธ๋Š” ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑํ•œ๋‹ค.** - **์ฒดํฌ ์˜ˆ์™ธ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์ปค๋ฐ‹ํ•œ๋‹ค.** - **์ด ์ •์ฑ…์ด ๋””ํดํŠธ์ด๊ณ  ์ด ์ •์ฑ…์„ ๋”ฐ๋ฅด๊ณ  ์‹ถ์ง€ ์•Š๋‹ค๋ฉด `rollbackFor` ์ด๋ผ๋Š” ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์„œ ์ฒดํฌ ์˜ˆ์™ธ๋„ ๋กค๋ฐฑ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.**

์ฒดํฌ, ์–ธ์ฒดํฌ์— ๋”ฐ๋ฅธ ์ปค๋ฐ‹, ๋กค๋ฐฑ ์ด์œ 

  • ์ฒดํฌ ์˜ˆ์™ธ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค์ƒ ์˜๋ฏธ๊ฐ€ ์žˆ์„ ๋•Œ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ปค๋ฐ‹ํ•œ๋‹ค. (์„œ๋น„์Šค ๊ณ„์ธต์ด๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ ๊ณ„์ธต์—์„œ ์˜ˆ์™ธ๋ฅผ ์žก์•„์„œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ)
  • ์–ธ์ฒดํฌ ์˜ˆ์™ธ๋Š” ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ์˜ˆ์™ธ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋กค๋ฐฑํ•œ๋‹ค.
  • OrderService ์ฝ”๋“œ
    @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("๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค ์™„๋ฃŒ");
        }
    }
  • OrderServiceTest ์ฝ”๋“œ
    @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("๋Œ€๊ธฐ");
        }
    }

โœ”๏ธ ์Šคํ”„๋ง ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์ด๋ก 

  • ํŠธ๋žœ์žญ์…˜์ด ๋‘˜ ์ด์ƒ ์žˆ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์•Œ์•„๋ณธ๋‹ค.

๋ณ„๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜ 2๋ฒˆ ์‚ฌ์šฉ

  • ์ฝ”๋“œ
    @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);
        }
  • ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด ํŠธ๋žœ์žญ์…˜1๊ณผ ํŠธ๋žœ์žญ์…˜1๋Š” ๊ฐ™์€ conn0 ์ปค๋„ฅ์…˜์„ ์‚ฌ์šฉ์ค‘์ด๋‹ค.
    • ์ด ๊ฒƒ์€ Hikari ์ปค๋„ฅ์…˜ ํ’€์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • ์ด ๋‘˜์„ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.
    • ํžˆ์นด๋ฆฌ ์ปค๋„ฅ์…˜ ํ’€์—์„œ ์ปค๋„ฅ์…˜์„ ํญ๋“ํ•˜๋ฉด ์‹ค์ œ ์ปค๋„ฅ์…˜์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋‚ด๋ถ€ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํžˆ์นด๋ฆฌ ํ”„๋ก์‹œ ์ปค๋„ฅ์…˜์ด๋ผ๋Š” ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    • ์ด ๊ฐ์ฒด์˜ ์ฃผ์†Œ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ปค๋„ฅ์…˜ ํ’€์—์„œ ํญ๋“ํ•œ ์ปค๋„ฅ์…˜์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์ด๋ก 

  • ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜์ด ์ˆ˜ํ–‰ ์ค‘์ธ๋ฐ, ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์ด ์ถ”๊ฐ€๋กœ ์ˆ˜ํ–‰๋  ๊ฒฝ์šฐ
    • ์Šคํ”„๋ง์ด ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜๊ณผ ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์„ ๋ฌถ์–ด์„œ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์„ ๋งŒ๋“ค์–ด์ค€๋‹ค.
    • ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์ด ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜๊ณผ ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜

  • ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์€ ํ•˜๋‚˜์˜ ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์ธ๋‹ค.
  • ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์€ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ์šฉ๋˜๋Š” ํŠธ๋žœ์žญ์…˜์„ ๋œปํ•œ๋‹ค.
  • ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์€ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋‹จ์œ„์ด๋‹ค.

โ†’ ํŠธ๋žœ์žญ์…˜์ด ์‚ฌ์šฉ ์ค‘์ผ ๋•Œ ๋˜ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๋‚ด๋ถ€์— ์‚ฌ์šฉ๋˜๋ฉด ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋ณต์žกํ•œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ์ˆœํ•œ ์›์น™์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋ฌผ๋ฆฌ, ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด๋ผ๋Š” ๊ฐœ๋…์„ ๋„์ž…ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

โš ๏ธ ์›์น™

  • **๋ชจ๋“  ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜์–ด์•ผ ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋œ๋‹ค.
  • ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด๋ผ๋„ ๋กค๋ฐฑ๋˜๋ฉด ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์€ ๋กค๋ฐฑ๋œ๋‹ค.**

์ „ํŒŒ ์˜ˆ์ œ, ํ”Œ๋กœ์šฐ

  • ์ฝ”๋“œ
    @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 ๋Ÿฐํƒ€์ž„ ์˜ˆ์™ธ๋ฅผ ๋˜์ง„๋‹ค.

์ „ํŒŒ - REQUIRES_NEW

  • ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜๊ณผ ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•ด์„œ ๊ฐ๊ฐ ๋ณ„๋„์˜ ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.
  • ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์— ํ•ด๋‹น ์˜ต์…˜์„ ์„ค์ •ํ•˜๋ฉด conn0 ์ด ์•„๋‹Œ conn1, ์ฆ‰ ๋‹ค๋ฅธ ์ปค๋„ฅ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•œ๋‹ค.
    • isNewTransaction()=true ๊ฐ€ ๋œ๋‹ค.

๋‹ค์–‘ํ•œ ์ „ํŒŒ ์˜ต์…˜

  • isolation, timeout, readOnly ๋Š” ํŠธ๋žœ์žญ์…˜์ด ์ฒ˜์Œ ์‹œ์ž‘๋  ๋•Œ๋งŒ ์ ์šฉ๋œ๋‹ค.
    • ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
  • ์‹ค๋ฌด์—์„œ๋Š” ๋Œ€๋ถ€๋ถ„ REQUIRED - Default ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๊ณ  ์•„์ฃผ ๊ฐ€๋” REQUIRES_NEW ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋‚˜๋จธ์ง€ ์˜ต์…˜์€ PDF ์ฐธ๊ณ 

โœ”๏ธ ์ „ํŒŒ ํ™œ์šฉ

์ฝ”๋“œ

@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());
    }

}

outerTxOn_fail

  • ์ด ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜C์—์„œ rollbackOnly๋ฅผ ์„ค์ •ํ•˜์ง€๋งŒ ํŠธ๋žœ์žญ์…˜A๊นŒ์ง€ ์˜ˆ์™ธ๊ฐ€ ์žกํžˆ์ง€ ์•Š๊ณ  ๋˜์ ธ์กŒ๊ธฐ ๋•Œ๋ฌธ์— <ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €์— ์ปค๋ฐ‹์„ ์š”์ฒญํ•œ ํ›„ rollBackOnly๋ฅผ ํ™•์ธ ํ›„ ๋กค๋ฐฑ์„ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ>์ด ์•„๋‹ˆ๋ผ ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €์— ๋กค๋ฐฑ์„ ์š”์ฒญํ•œ๋‹ค.
    • ์ด ๊ฒฝ์šฐ์—๋Š” rollBackOnly ์„ค์ •์„ ์ฐธ๊ณ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

recoverException_fail

  • ์ด ๊ฒฝ์šฐ MemberService์—์„œ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ •์ƒ ํ๋ฆ„์œผ๋กœ ๋Œ์•„๊ฐ„๋‹ค.
  • ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜A๋Š” ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €์— ์ปค๋ฐ‹์„ ์š”์ฒญํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €๋Š” rollbackOnly ์„ค์ •์„ ์ฐธ๊ณ ํ•œ๋‹ค.
    • rollBackOnly๊ฐ€ True์ด๊ธฐ ๋•Œ๋ฌธ์— ๋กค๋ฐฑ์„ ํ•˜๊ณ  UnexpectedRollbackException ์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋˜์ง„๋‹ค.

recoverException_success - REQUIRES_NEW

  • ํšŒ์› ๊ฐ€์ž…์„ ์‹œ๋„ํ•œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋Š”๋ฐ ์‹คํŒจํ•˜๋”๋ผ๋„ ํšŒ์› ๊ฐ€์ž…์€ ์œ ์ง€๋˜์–ด์•ผ ํ•  ๊ฒฝ์šฐ
  • Propagation.REQUIRES_NEW๋ฅผ ์„ค์ •ํ•  ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜์ด ๊ธฐ์กด ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์— ์ฐธ์—ฌํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์‹ ๊ทœ ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด ์‹œ์ž‘๋œ๋‹ค.
    • ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜C์—์„œ ๋กค๋ฐฑ์ด ๋˜๋ฉด ํ•ด๋‹น ํŠธ๋žœ์žญ์…˜์€ ํŠธ๋ž™์žญ์…˜A์™€๋Š” ๋ณ„๊ฐœ๋กœ ์ข…๋ฃŒ๋œ๋‹ค.
    • ํŠธ๋žœ์žญ์…˜์€ ๋ถ„๋ฆฌ์‹œ์ผฐ์ง€๋งŒ ๋ฐœ์ƒํ•œ ๋Ÿฐํƒ€์ž„์˜ˆ์™ธ๋Š” ๋ฐ–์œผ๋กœ ๋˜์ ธ์ง„๋‹ค.
      • ํ•˜์ง€๋งŒ MemberService์—์„œ ํ•ด๋‹น ์˜ˆ์™ธ๋ฅผ ์žก๊ธฐ ๋•Œ๋ฌธ์— ์ •์ƒํ๋ฆ„์œผ๋กœ ๋Œ์•„์˜จ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€