๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

Hibernate์˜ @Formula๋ฅผ ์ด์šฉํ•œ ์—ฐ๊ด€ ๊ด€๊ณ„ ์—”ํ‹ฐํ‹ฐ ์ง‘๊ณ„

by ์•ˆ์ฃผํ˜• 2023. 12. 20.

๊ฐœ์š”

Hibernate์˜ @Formula ์–ด๋…ธํ…Œ์ด์…˜์€ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ๋‚ด์—์„œ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” '๊ฐ€์ƒ ์ปฌ๋Ÿผ'์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. @Formula๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค๋ฅธ ์ปฌ๋Ÿผ๋“ค์˜ ๊ฐ’์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ๊ฐ’์€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ๋งŒ ๊ณ„์‚ฐ๋˜์–ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

 

 

์˜ˆ์‹œ ์ฝ”๋“œ

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

์•„๋ž˜๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String title;

    @Column
    private String content;

    @Formula("(select count(c.id) from Comment c where c.post_id = id)")
    private int commentCount;
}

์œ„ ์ฝ”๋“œ์—์„œ 'commentCount'๋Š” Post ์—”ํ‹ฐํ‹ฐ์— ์ถ”๊ฐ€๋œ ๊ฐ€์ƒ ์ปฌ๋Ÿผ์ž…๋‹ˆ๋‹ค. @Formula ์–ด๋…ธํ…Œ์ด์…˜์— ์ •์˜๋œ ๋„ค์ดํ‹ฐ๋ธŒ SQL ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€๊ณผ ์—ฐ๊ฒฐ๋œ ๋Œ“๊ธ€ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋ฉฐ, 'commentCount'๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์—์„œ๋Š” ์‹ค์ œ๋กœ ์กด์žฌํ•˜์ง€ ์•Š์ง€๋งŒ ์—”ํ‹ฐํ‹ฐ ๋‚ด์—์„œ๋Š” ์‹ค์ œ ์ปฌ๋Ÿผ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์–ธ์ œ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„๊นŒ?

@Formula ์–ด๋…ธํ…Œ์ด์…˜์€ ํŠนํžˆ ์—”ํ‹ฐํ‹ฐ ๊ฐ„์˜ ์—ฐ๊ด€ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ, ์ง‘๊ณ„ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ๋“ค์–ด ๊ฒŒ์‹œ๊ธ€(Post)๊ณผ ๋Œ“๊ธ€(Comment)๊ณผ ๊ฐ™์ด 1:N ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๋Š” ์—”ํ‹ฐํ‹ฐ์—์„œ ๊ฒŒ์‹œ๊ธ€์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€์˜ ์ˆ˜๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค. ์ด๋•Œ ๋งŒ์•ฝ ๋Œ“๊ธ€์˜ ์ˆ˜๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ๋‹จ์ˆœํžˆ Post ์—”ํ‹ฐํ‹ฐ ๋‚ด๋ถ€์˜ comments ๋ฆฌ์ŠคํŠธ์˜ ํฌ๊ธฐ๋ฅผ ์ง์ ‘ ์กฐํšŒํ•œ๋‹ค๋ฉด Lazy Loading ์„ค์ •์œผ๋กœ ์ธํ•ด ๋Œ“๊ธ€์˜ ์ „์ฒด ์ปฌ๋ ‰์…˜์ด ๋กœ๋“œ๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String title;

    @Column
    private String content;

    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();

    public int getCommentCount() {
        return comments.size();
    }
}

์œ„์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, ๋Œ“๊ธ€์˜ ์ˆ˜๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด ํ•ด๋‹น Post์˜ ๋ชจ๋“  ๋Œ“๊ธ€ ๋ฐ์ดํ„ฐ(์ปฌ๋Ÿผ)๋ฅผ ๋กœ๋“œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์‹คํ–‰ ์†๋„๊ฐ€ ๋Š๋ ค์งˆ ์ˆ˜ ์žˆ๊ณ , ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋˜ํ•œ ๋งŽ์•„์ง€๋ฉฐ ๊ฒฐ๊ณผ์ ์œผ๋กœ ์‹œ์Šคํ…œ ์ „์ฒด์˜ ์„ฑ๋Šฅ์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋•Œ @Formula ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๋Œ“๊ธ€ ์ˆ˜๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ๊ณ„์‚ฐํ•˜์—ฌ ๊ฐ€์ ธ์™€ ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์—ฌ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Entity
public class Post {
    // ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ - ์—ฐ๊ด€ ๊ด€๊ณ„ ์ •์˜
    @OneToMany(mappedBy = "post")
    private List<Comment> comments = new ArrayList<>();
    
    // @Formula๋ฅผ ํ†ตํ•œ ๋Œ“๊ธ€ ์ˆ˜ ์ง์ ‘ ๊ณ„์‚ฐ
    @Formula("(SELECT COUNT(*) FROM comment c WHERE c.post_id = id)")
    private int commentCount;

    // ๋Œ“๊ธ€ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋Š” ์ด์ œ ๋” ์ด์ƒ comments ์ปฌ๋ ‰์…˜์„ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ
    public int getCommentCount() {
        return this.commentCount;
    }
}

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

๋˜ํ•œ @Formula์— ์ง€์—ฐ ๋กœ๋”ฉ(Lazy Loading)์„ ์ ์šฉํ•˜๊ณ  ์‹ถ์–ด๋„ Hibernate๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์›์‹œ ํƒ€์ž…(primitive type)์— ๋Œ€ํ•ด ์ง€์—ฐ ๋กœ๋”ฉ์„ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @Basic(fetch = FetchType.LAZY)์™€ ํ•จ๊ป˜ ๋ณ„๋„์˜ ๋ฐ”์ดํŠธ ์ฝ”๋“œ ์กฐ์ž‘(ByteCodeEnhancement)์„ ํ†ตํ•ด ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ @Formula ์–ด๋…ธํ…Œ์ด์…˜์˜ ์‚ฌ์šฉ์€ ํŠน์ • ์ƒํ™ฉ์—์„œ์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ๊ฒƒ์ด๋ฉฐ, ๊ทธ ์™ธ์˜ ๊ฒฝ์šฐ์—๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ๊ฒ€ํ† ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

 

์ฃผ์˜์ 

@Formula ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ •์˜๋œ ํ•„๋“œ๋Š” ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์กฐํšŒ๋  ๋•Œ๋งˆ๋‹ค SQL ํ‘œํ˜„์‹์ด ์‹คํ–‰๋˜๋ฉฐ ๊ทธ ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ๋งŒ์•ฝ ๋Œ“๊ธ€์ด ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋œ ๊ฒฝ์šฐ์—๋„, @Formula๋ฅผ ํ†ตํ•ด ๊ณ„์‚ฐ๋œ ํ•„๋“œ ๊ฐ’์€ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ @Formula๋กœ ์ •์˜๋œ ํ•„๋“œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ณ„์‚ฐ๋œ ๊ฐ’์„ ์ฝ์–ด์˜ค๋Š” ๋ฐ์—๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด ํ•„๋“œ์— ๋Œ€ํ•œ ๋ณ€๊ฒฝ์„ ์‹œ๋„ํ•˜๋ฉด Hibernate๋Š” ์ด๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ˜์˜ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์‹ค์ œ๋กœ ์ด ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ ค ํ•˜๋ฉด ๋ฌด์‹œ๋˜๊ฑฐ๋‚˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ @Formula๋กœ ์ƒ์„ฑ๋œ ํ•„๋“œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐ์—๋งŒ ์ ํ•ฉํ•˜๋ฉฐ, ์—”ํ‹ฐํ‹ฐ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋กœ์ง์—์„œ๋Š” ์‚ฌ์šฉ์„ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ @Formula๊ฐ€ ์ ์šฉ๋œ ํ•„๋“œ์˜ ๊ฐ’์„ ๊ฐฑ์‹ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋ณ„๋„๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€์—์„œ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

 

ํ…Œ์ŠคํŠธ

@SpringBootTest
@Transactional
class FormulaApplicationTests {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private Long postId = 1L;

    @BeforeEach
    void setUp() {
        // Post ํ…Œ์ด๋ธ”์— ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€
        jdbcTemplate.update("INSERT INTO post (id, title, content) VALUES (?, ?, ?)", 1L, "๊ธ€ ์ œ๋ชฉ", "๊ธ€ ๋‚ด์šฉ");

        // Comment ํ…Œ์ด๋ธ”์— ๋ ˆ์ฝ”๋“œ ์ถ”๊ฐ€
        jdbcTemplate.update("INSERT INTO comment (id, content, post_id) VALUES (?, ?, ?)", 2L, "์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€", 1);
        jdbcTemplate.update("INSERT INTO comment (id, content, post_id) VALUES (?, ?, ?)", 3L, "๋‘ ๋ฒˆ์งธ ๋Œ“๊ธ€", 1);
    }

    @Test
    void shouldReturnCorrectCommentCount() {
        // when
        Post post = postRepository.findById(postId).orElseThrow(() -> new AssertionError("Post not found"));

        // then
        Assertions.assertThat(post.getCommentCountFromCollections()).isEqualTo(2);
        Assertions.assertThat(post.getCommentCountFromFormula()).isEqualTo(2);
    }

    @DisplayName("Colletctions.size() ์ฟผ๋ฆฌ ํ™•์ธ")
    @Test
    void shouldQueryWhenGettingCountFromCollections() {
        Post post = postRepository.findById(postId).orElseThrow(() -> new AssertionError("Post not found"));

        post.getCommentCountFromCollections();
        /**
         *
         * Hibernate:
         *     select
         *         post0_.id as id1_1_0_,
         *         post0_.content as content2_1_0_,
         *         post0_.title as title3_1_0_,
         *         (select
         *             count(*)
         *         from
         *             comment c
         *         where
         *             c.post_id = post0_.id) as formula1_0_
         *     from
         *         post post0_
         *     where
         *         post0_.id=?
         * Hibernate:
         *     select
         *         comments0_.post_id as post_id3_0_0_,
         *         comments0_.id as id1_0_0_,
         *         comments0_.id as id1_0_1_,
         *         comments0_.content as content2_0_1_,
         *         comments0_.post_id as post_id3_0_1_
         *     from
         *         comment comments0_
         *     where
         *         comments0_.post_id=?
         */
    }

    @DisplayName("Formula ์ฟผ๋ฆฌ ํ™•์ธ")
    @Test
    void shouldQueryWhenGettingCountFromFormula() {
        Post post = postRepository.findById(postId).orElseThrow(() -> new AssertionError("Post not found"));

        post.getCommentCountFromFormula();
        /**
         *
         * Hibernate:
         *     select
         *         post0_.id as id1_1_0_,
         *         post0_.content as content2_1_0_,
         *         post0_.title as title3_1_0_,
         *         (select
         *             count(*)
         *         from
         *             comment c
         *         where
         *             c.post_id = post0_.id) as formula1_0_
         *     from
         *         post post0_
         *     where
         *         post0_.id=?
         */
    }
}

๋Œ“๊ธ€