-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathrss.xml
2594 lines (2408 loc) · 488 KB
/
rss.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RSS Feed of 괜찮을지도]]></title><description><![CDATA[안녕하세요! 괜찮을지도 기술블로그입니다.]]></description><link>https://map-befine-official.github.io</link><generator>GatsbyJS</generator><lastBuildDate>Wed, 01 Nov 2023 08:41:57 GMT</lastBuildDate><item><title><![CDATA[공간 인덱스 적용기]]></title><description><![CDATA[…]]></description><link>https://map-befine-official.github.io/spatial-index/</link><guid isPermaLink="false">https://map-befine-official.github.io/spatial-index/</guid><pubDate>Thu, 19 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우아한테크코스 괜찮을지도팀의 <code class="language-text">쥬니</code>가 작성했습니다.</p>
</blockquote>
<h3>배경</h3>
<p><code class="language-text">괜찮을지도</code> 서비스는 아래와 같이, 위치와 관련된 정보를 별도의 테이블로 관리하고 있습니다.</p>
<p><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/9eb08cee88053e732c4f75755a33f56a/3a6ec/diagram.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 39.411764705882355%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWklEQVR42n2R3UvDMBTF+9eLD+qb+CAqOAXdBBEcMhUcw02ngmMyEEWUtlu72Tb9WtqmrcckutLB9OFA7s09v5xwlftnDau1a6xwrR21sXzQQrX5hK88Q5qmhRhLwZv4MB0cNvvYv+yhcvEotcfPW2cP6L8ZUPqvGiqNO1Sveqhx7dQ7aNwOuDcHK2AMSZJI4Is6xnb9BpunXawft6U2TjpY2j1Hd/AOJYmmoD4BUm7IYsSBi5iGSLOsgAkVaVmCqUcQh64U8gQ5o4gCwhERlCAIoGoaLNuG5/twCIHrusgWAEWP0giapkPTdZjjMVzPx6dlQx+NEAQhlDAMMeIF4SABJ7/AMqwMjKJIzluWBZ8HEBIe0zQhWD8JVRWO48hLwzBg87SLvjsDDofDOaDwFkBKKSaTCTzPkw0BE4+UNzy/bQbhEWCxKKE4jmUt7pTZy+UUf8H+m5n1vgGdU1mhOVDnIQAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='diagram.png' title='' src='/static/9eb08cee88053e732c4f75755a33f56a/ca1dc/diagram.png' srcset='/static/9eb08cee88053e732c4f75755a33f56a/e7570/diagram.png 170w,
/static/9eb08cee88053e732c4f75755a33f56a/f46e7/diagram.png 340w,
/static/9eb08cee88053e732c4f75755a33f56a/ca1dc/diagram.png 680w,
/static/9eb08cee88053e732c4f75755a33f56a/3a6ec/diagram.png 686w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>diagram.png</figcaption>
</figure></p>
<p>장소를 등록할 때, 주소 검색을 사용할 수도 있지만, <strong>지도 위의 좌표를 클릭하여 등록</strong>할 수도 있습니다.
<br> 사용자들이 동일한 장소를 등록하기 위해 지도상의 특정 위치를 클릭한다고 가정해 보겠습니다.
<br> 이때, 미터 혹은 그 이하의 오차 없이 동일한 위치를 선택할 확률은 얼마나 될까요 ?
<br> 거의 불가능에 가깝다고 생각합니다.
<br> 물론, 사용자의 입력을 그대로 저장할 필요도 있었습니다.
<br> 하지만, 데이터의 정확성이 크게 요구되는 서비스는 아니라는 점에서, DB 공간을 효율적으로 관리하는 것이 우선시 되었습니다.
<br> 이에 따라, 등록 장소 반경 10M 이내에 좌표가 존재하는 경우, 기존 좌표를 재사용하기 위해 테이블을 분리한 것이었죠.</p>
<p>데이터 공간의 효율성을 위해, 장소 등록시 마다 매번 위치 좌표와 관련된 쿼리가 수행되고 있었습니다.
<br> 물론, 조회용 API에서 해당 로직을 수행하지 않기 때문에 인덱싱의 중요성이 그리 높지 않았습니다.
<br> 하지만, 추후 사용자의 접속 위치를 기반으로 여러 기능들을 제공할 계획을 가지고 있습니다.
<br> 현재의 기능을 위해서도, 앞으로의 기능을 위해서도 공간 인덱스 적용은 반드시 필요한 상황이었습니다.</p>
<h3>공간 인덱스</h3>
<p>공간 인덱스는 2차원 공간 개념 값을 다루는 R-Tree 알고리즘을 사용합니다.
<br> 이러한 R-Tree 알고리즘을 이해하기 위해서는 <code class="language-text">MBR</code>이라는 개념이 필수적입니다.
<br> MBR(Minimum Bounding Rectangle)은 아래 사진 하나로 설명이 가능할 것 같습니다.</p>
<p><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/3c9f6647f7be530335496f99755fd238/2d81a/mbr.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 44.11764705882353%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABRUlEQVR42nVS2W6DQAzk/z+nqvoBjSrlgYcoQjlIRZo2XOFIOAMkYcq4WgREtWTtrr0ez3hXu91uSNMUbdtCGWN5no9itPv9jjRLUdf1KF4UBeI4QlVV0MqygGmaAjK8cDz+YGplWWK9XmO5XI6anU4nrFYrZFkGbVjAS2RBi+MYURSNWF+rKy6XC3a7HXzf73Pczz/msgrgZrNB0zSSZAHd8/yu8FP2tCRJYBhGD05Vytj89eUN+/3XHyAl0jmD7XYrIGTKlTIUCM/ME8C27V4VZzp7nyFLJ5Ifj4cUclYEoivmahwcg+d5Io85EuF99QYa/jEWs4A/gHLZjArIhnvmCTL9CU+ALA7DULqez7GcyUrXdSwWCwRB0OUDiXMEXIeg2vCF1RewLAtBtx6+D508V2bmOI6467oimU0dxxbpZKwAfwG83rW/bmsBfAAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='img.png' title='' src='/static/3c9f6647f7be530335496f99755fd238/ca1dc/mbr.png' srcset='/static/3c9f6647f7be530335496f99755fd238/e7570/mbr.png 170w,
/static/3c9f6647f7be530335496f99755fd238/f46e7/mbr.png 340w,
/static/3c9f6647f7be530335496f99755fd238/ca1dc/mbr.png 680w,
/static/3c9f6647f7be530335496f99755fd238/2d81a/mbr.png 840w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>img.png</figcaption>
</figure></p>
<blockquote>
<p>출처 : Real My SQL 8.0</p>
</blockquote>
<p>MySQL에서는 공간 정보를 나타내는 데이터 타입으로 Point, Line, Polygon, Geometry를 지원합니다.
<br> MBR은 이러한 데이터 타입을 감쌀 수 있는 최소 크기의 사각형을 의미합니다.</p>
<p><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 548px; '>
<a class='gatsby-resp-image-link' href='/static/ad58f108c7db6fa790c4ed37ea50ede1/9079b/r-tree.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 65.29411764705883%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR42nVTa28SQRTl/38yJiam0fiiVNimSW2t0mXB1FQp9hUFF4QmtgoEdpf37vJajnPudmg1cZLLXIY7555z5pIYj8dwHAeLxQKz2UyC+f3o9/tg3Wg0wmQyWZ+HYYhpOMV0GgdXwvd9AdRrtVqtQy8za+H87AKe56HT6cDpOqjVfiiQUIDmszn8iY9oGSHBC57XQ6l0ipNiCY3GlXRnIdnyknloIfvORBQtMZ/PpdbIbAtrNmbd9c9r9fst4M3NL7x4toknG8/x6bgorKIoZshiAqaSafR6PQEcDocIggBUR9a1Wh3lciWWzA96Q58YpB4DRmufcmYBG4+fovm7KWcE1YsEki+3xIY1QwKym/aQ6+PRMZKvUqhWayhYR9jZ3hVW2gpd9+1rBVbuAyZK/kI1EkBKaTabUkxGjFarhXq9oR6hi/03b9WlvAAQlICaSOa1gapdQxAGwlwAmdx/1X/X2ek5rtRjEYj+acnM01sGPNdTDCd3gPTlf4DL5VJ2267i4YNHKH4+geu5ck7PvqgX9/0Ag8Hwb4Zaqh5SnVPiUEnj3O3tHoinnW5HAQwk2u22jB1zNkloZgNF//LiUl6anrquKzvHotvtot/rCzAv6hqyorpK+TsMY0eGPqFnzbZtMZ6RtwpqVCxYZh6HWROZtIHs+5zKc/Kd+/7egfx7tB2byZQi4dxJJiOazO5kQQbxbN4yUrLIkhEr8KSe1tAWBtn+AZpBynJQvy4jAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='r-tree.png' title='' src='/static/ad58f108c7db6fa790c4ed37ea50ede1/9079b/r-tree.png' srcset='/static/ad58f108c7db6fa790c4ed37ea50ede1/e7570/r-tree.png 170w,
/static/ad58f108c7db6fa790c4ed37ea50ede1/f46e7/r-tree.png 340w,
/static/ad58f108c7db6fa790c4ed37ea50ede1/9079b/r-tree.png 548w' sizes='(max-width: 548px) 100vw, 548px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>r-tree.png</figcaption>
</figure></p>
<blockquote>
<p>출처 : Real My SQL 8.0</p>
</blockquote>
<p>이러한 MBR의 포함 관계를 위 사진처럼 나타낼 수 있습니다.
<br> 이를 B-Tree 형태로 구현한 인덱스가 R-Tree 인덱스입니다.</p>
<p>이 정도의 지식만 있으면, 공간 인덱스를 적용하는 데에 큰 어려움은 없습니다. </p>
<h3>기존 코드</h3>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token comment">// LocationRepository.class</span>
<span class="token annotation punctuation">@Query</span><span class="token punctuation">(</span>
<span class="token string">"SELECT l FROM Location l "</span>
<span class="token operator">+</span> <span class="token string">"WHERE ( 6371000 * acos( cos( radians(:#{#current_coordinate.latitude}) ) "</span>
<span class="token operator">+</span> <span class="token string">" * cos( radians( l.coordinate.latitude ) ) "</span>
<span class="token operator">+</span> <span class="token string">" * cos( radians( l.coordinate.longitude ) - radians(:#{#current_coordinate.longitude}) ) "</span>
<span class="token operator">+</span> <span class="token string">" + sin( radians(:#{#current_coordinate.latitude}) ) "</span>
<span class="token operator">+</span> <span class="token string">" * sin( radians( l.coordinate.latitude ) ) ) ) &lt;= :distance"</span>
<span class="token punctuation">)</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Location</span><span class="token punctuation">></span></span> <span class="token function">findAllByCoordinateAndDistanceInMeters</span><span class="token punctuation">(</span>
<span class="token annotation punctuation">@Param</span><span class="token punctuation">(</span><span class="token string">"current_coordinate"</span><span class="token punctuation">)</span> <span class="token class-name">Coordinate</span> coordinate<span class="token punctuation">,</span>
<span class="token annotation punctuation">@Param</span><span class="token punctuation">(</span><span class="token string">"distance"</span><span class="token punctuation">)</span> <span class="token keyword">double</span> distance
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<p>공간 인덱스를 적용하기 전, 등록 좌표를 기준으로 특정 반경 내에 존재하는 좌표를 조회하는 로직입니다.
<br> 위 로직은 <code class="language-text">좌표 기반 거리 계산을 위한 Haversine 공식</code>을 적용한 것입니다.
<br> 로직을 이해하기도 어렵고, Table Full Scan을 통해 쿼리가 수행되므로, 성능 개선이 필요한 상태입니다.</p>
<h3>공간 인덱스 적용</h3>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token comment">//build.gradle</span>
implementation 'org<span class="token punctuation">.</span>hibernate<span class="token operator">:</span>hibernate<span class="token operator">-</span>spatial<span class="token operator">:</span><span class="token number">6.2</span><span class="token number">.5</span><span class="token punctuation">.</span>Final'</code></pre></div>
<p>우선, 공간 인덱스를 적용하기 위해 위와 같은 의존성을 추가해 줍니다.
<br> 버전은 프로젝트에서 사용하고 있는 JPA 버전에 따라 달라집니다.</p>
<hr>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token comment">// Location.class</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token comment">/*
* 4326은 데이터베이스에서 사용하는 여러 SRID 값 중, 일반적인 GPS기반의 위/경도 좌표를 저장할 때 쓰이는 값입니다.
* */</span>
<span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token class-name">GeometryFactory</span> geometryFactory <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">GeometryFactory</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">PrecisionModel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">4326</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@Column</span><span class="token punctuation">(</span>columnDefinition <span class="token operator">=</span> <span class="token string">"geometry SRID 4326"</span><span class="token punctuation">,</span> nullable <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Point</span> coordinate<span class="token punctuation">;</span>
<span class="token keyword">private</span> <span class="token class-name">Coordinate</span><span class="token punctuation">(</span><span class="token class-name">Point</span> point<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span>coordinate <span class="token operator">=</span> point<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token class-name">Coordinate</span> <span class="token function">of</span><span class="token punctuation">(</span><span class="token keyword">double</span> latitude<span class="token punctuation">,</span> <span class="token keyword">double</span> longitude<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">validateRange</span><span class="token punctuation">(</span>latitude<span class="token punctuation">,</span> longitude<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Point</span> point <span class="token operator">=</span> geometryFactory<span class="token punctuation">.</span><span class="token function">createPoint</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>locationtech<span class="token punctuation">.</span>jts<span class="token punctuation">.</span>geom<span class="token punctuation">.</span></span>Coordinate</span><span class="token punctuation">(</span>longitude<span class="token punctuation">,</span> latitude<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Coordinate</span><span class="token punctuation">(</span>point<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span></code></pre></div>
<p>위 코드에서 주의 깊게 보아야 할 점은 <code class="language-text">SRID</code>입니다.
<br> SRID는 여러 좌표(평면 좌표, 공간 좌표 등)를 나타내는 식별자라고 생각하시면 됩니다.
<br> 만약, SRID 값이 서로 다를 경우, 공간 인덱스를 적용할 수 없습니다.
<br> (엄밀히 이야기하자면, 인덱스를 타지 않는다고 표현하는 것이 맞는 것 같습니다.)
<br> 힘들게 공간 인덱스를 적용했는데 ! 실제 쿼리에서 Full-Scan이 일어난다면.. 마음이 아프겠죠 ?
<br> 코드 주석으로도 남겨져 있지만, 일반적인 위/경도 좌표를 저장할 때 쓰이는 값인 <code class="language-text">4326</code>으로 SRID 값을 설정하였습니다.</p>
<hr>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token comment">// LocationRepository.class</span>
<span class="token annotation punctuation">@Query</span><span class="token punctuation">(</span>
<span class="token string">"SELECT l FROM Location l "</span>
<span class="token operator">+</span> <span class="token string">"WHERE ST_Contains(ST_Buffer(:coordinate, :distance), l.coordinate.coordinate)"</span>
<span class="token punctuation">)</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Location</span><span class="token punctuation">></span></span> <span class="token function">findAllByCoordinateAndDistanceInMeters</span><span class="token punctuation">(</span>
<span class="token annotation punctuation">@Param</span><span class="token punctuation">(</span><span class="token string">"coordinate"</span><span class="token punctuation">)</span> <span class="token class-name">Point</span> coordinate<span class="token punctuation">,</span>
<span class="token annotation punctuation">@Param</span><span class="token punctuation">(</span><span class="token string">"distance"</span><span class="token punctuation">)</span> <span class="token keyword">double</span> distance
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<p>드디어 마지막 단계입니다.
<br> 위 코드에서 <code class="language-text">ST_Contains</code>와 <code class="language-text">ST_Buffer</code>라는 함수가 생소하실 텐데요.
<br> 이는 MySQL에서 공간 인덱스를 지원해 주는 함수입니다.
<br> 각 함수를 간단하게 설명하자면 다음과 같습니다.
<strong><br> ST_Buffer(Coordinate, Distance) : Coordinate로부터, Distance(Meter)내에 존재하는 MBR을 반환한다.</strong>
<strong><br> ST_Contains(MBR, Coordinate) : MBR 이내에 해당 Coordinate의 포함 여부를 반환한다.</strong>
<br> 이처럼, MySQL에서 지원해주는 함수를 통해 공간 인덱스를 적용할 모든 준비가 끝났습니다.</p>
<p>물론, 위 로직에서는 한 가지 문제가 존재합니다.
<br><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 282px; '>
<a class='gatsby-resp-image-link' href='/static/4a703b890030da2757d4709ef387cf27/75fda/st_buffer-st_contains.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 95.29411764705883%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAACaklEQVR42o2Uz0sbQRTH/VP0oKjg7x8XQS8qCOKPsydvXiQ5eNGDPzBUhYKYksbSELQE66qYtiElevAHKJQWGgvSaw89FCnYRO12d7Mz376ZTdbsZtNm4TGzb9989r3vvJkqzjkKxhgrMW6aDvOMKWJUFb8AwpwPc5nX4wkUz+d0GkepFI5PTpA8P8enw0P8DAaRCwRgLi/jdn0dV8oe3p+d4ej0FMcUe3F56YA6gPuKggVa/HRlBR9HRqDW14PX1ADV1dLEXK+txfXgIJ4tLWFhdRUvwuHywHgigeDaGm6GhoCGBvD2dqCz89E6OixfYyOyfX2I0M+jsZhUyhN4kEziijITCxgtRlubp8lvFPOtvx9b29ulGdJWwSTnh0gEGgVCZFEGVjAusqUqLhYXocvdYwUgvZhMOm/m5wHSSJZXARCk8Y/JSeRkiqyoZNFP5DN8fqCuriIg8mUb4+Oy2ZwainRFoj6fBfyHfm5gbmzMDbTqN2jIzM5WXLIEkoa3ExNSf7g3RQCvNzaQI10qATIRQ7FfZmbcmyJKttrm3Zs4vvf2Ai0tMP/XNjT+6u7G3mZY6i8Ynn0Yo7JNCkRrK1hXl93QsqlpLnxcgOl7gjR/ubNjl1sCfBuPI0DndX9uDpmeHik6b2oCmpulyTn5VIIm/X48IYm2olF3Y8MGpuiwh0MhPKfuf0Vjenoad6OjYAMD4HQqfg8P4+vUFF4TKEQxm3SOlV3F+ywLpqZpUFUVGtmDriNDQj/oBsy7e7Bs1vbdG4aMEbF/aE3Z+1Ba/lbkVurIDyiuxHkHltyHcDi9Lk27+csCYc//Al2kyPAhJdWGAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='img.png' title='' src='/static/4a703b890030da2757d4709ef387cf27/75fda/st_buffer-st_contains.png' srcset='/static/4a703b890030da2757d4709ef387cf27/e7570/st_buffer-st_contains.png 170w,
/static/4a703b890030da2757d4709ef387cf27/75fda/st_buffer-st_contains.png 282w' sizes='(max-width: 282px) 100vw, 282px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>img.png</figcaption>
</figure></p>
<blockquote>
<p>출처 : 매튜</p>
</blockquote>
<p>위 사진 속 원의 원점이 좌표라고 가정하면 특정 거리 내의 좌표들은 빨간색 원 이내에 존재해야 합니다.
<br> 하지만, 위 로직에서는 회색의 잉여 공간(?)이 존재하게 됩니다.
<br> 즉, 특정 거리보다 더 멀리 있는 좌표들도 조회할 수 있게 되는 문제가 발생합니다.
<br> 이를 해결하는 방법은 <code class="language-text">ST_DISTANCE</code>라는 함수를 사용하는 것입니다.
<br> 하지만, 해당 함수는 공간 인덱스를 지원하지 않는다는 단점이 존재합니다.
<br> 정확성이 요구되는 서비스라면, 공간 인덱스를 사용하기 어렵다는 뜻이죠.
<br> 우리 서비스에서는 <code class="language-text">엄청난 정확성을 요구하지 않는다는 점</code>과 <code class="language-text">성능 향상</code>의 이유에서 위의 로직을 그대로 가져가기로 결정했습니다. </p>
<hr>
<div class="gatsby-highlight" data-language="sql"><pre class="language-sql"><code class="language-sql"><span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span> location <span class="token keyword">ADD</span> <span class="token keyword">COLUMN</span> coordinate <span class="token keyword">GEOMETRY</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span> SRID <span class="token number">4326</span><span class="token punctuation">;</span></code></pre></div>
<p><strong>만약, 해당 테이블 새로 생성한 것이 아니라, 기존 테이블 구조를 변경한다면 SRID 값을 명시적으로 설정해 주어야 합니다.</strong>
<strong><br> 단순히 컬럼만 추가한다면, SRID 값이 0으로 설정되기 때문이죠.</strong></p>
<div class="gatsby-highlight" data-language="sql"><pre class="language-sql"><code class="language-sql"><span class="token keyword">create</span> spatial <span class="token keyword">index</span> location_idx01 <span class="token keyword">on</span> location<span class="token punctuation">(</span>coordinate<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<p>마지막으로, 데이터베이스에 위와 같은 쿼리를 수행함으로써 공간 인덱스 적용이 끝나게 됩니다.</p>
<h3>성능 테스트</h3>
<p>총 데이터는 약 10만건이 존재하며, 1km 이내의 데이터, 3km 이내의 데이터 조회를 수행하였습니다.</p>
<p>공간 인덱스 적용 전
1km 이내 (40건)<br>
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 631px; '>
<a class='gatsby-resp-image-link' href='/static/864352edc04fe0fb8a9ce4fc2b6fc381/077d3/test-not-spatial-1km.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 37.05882352941176%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA9klEQVR42oWR2U6FMBRFC2EqBaSFMIaZa7ySG///67Y9RzFGDT7slBS69uJUlGUJGUUIZYzeaOypxFRoLFWJlyyGUTFyYyClhOM4EEJc5zgOzPOMqqrwlOcoohC178E4Asr5feA79Hz+USS4vW4adG2LpuvQTxMbqyRBlmWc3JalaQrf968NT7pSik1f73e8PR54vt0wjSO2bcO+70gsnKC0BkHAiWhUYcgrSX2WfZDpg9YaruvKkMla1nWNYRg4WmsuXJYFxs6UbIui4BJ6RyGoOGdAwL7v2YZyznW0lgQkMzpARpeX43ne1++cm67rcv690T/yDhUVneoxoQXOAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='test-not-spatial-1km.png' title='' src='/static/864352edc04fe0fb8a9ce4fc2b6fc381/077d3/test-not-spatial-1km.png' srcset='/static/864352edc04fe0fb8a9ce4fc2b6fc381/e7570/test-not-spatial-1km.png 170w,
/static/864352edc04fe0fb8a9ce4fc2b6fc381/f46e7/test-not-spatial-1km.png 340w,
/static/864352edc04fe0fb8a9ce4fc2b6fc381/077d3/test-not-spatial-1km.png 631w' sizes='(max-width: 631px) 100vw, 631px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>test-not-spatial-1km.png</figcaption>
</figure></p>
<p>3km 이내 (300건)<br>
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/211d1b93e00e49752380933d5a6356f8/00c79/test-not-spatial-3km.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 52.94117647058824%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWklEQVR42pVS2W6DQBBbbrIQyMENCYqCCAX1/z/PHW8Filq1TR+sZWeQx+uxapoGWZbhdDqZs6lr1FWFSGtEUYS2bU0vCAKEYWjA75/uyvd9c3FdF47rwSY8D5ZlwbZtsM+eUuo1xHEMItrvUScxxjTGIJiOCao0gZbeXnpJkmzg/bn2fFecvhOpWgrvqcZSF5ivHeYyxxSHOIoNtIKvoGIq/w3qer1+ThD2bBeishVa30EniGz5SUhI9PKT8zxHI8ZXsggdxShkScFOS9MST11DxleseCb/po51fnCbfd/jdrvhbZrQi+pcntl1ndkycT6fjUcvLEiZqVR4v9+xLAvmeQatIFgj2RqhWmKVpikOhwOKotjiRtA6tUovyxLjOOLxeBgwnyQchgGXy8UQ8SQJ8+ZJtHiusdvi5TiOUUh2klIpTyqgEk7WEvLNo79AInrFiWvxX0H+gg+BVPc0ndVKzgAAAABJRU5ErkJggg=='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='test-not-spatial-3km.png' title='' src='/static/211d1b93e00e49752380933d5a6356f8/ca1dc/test-not-spatial-3km.png' srcset='/static/211d1b93e00e49752380933d5a6356f8/e7570/test-not-spatial-3km.png 170w,
/static/211d1b93e00e49752380933d5a6356f8/f46e7/test-not-spatial-3km.png 340w,
/static/211d1b93e00e49752380933d5a6356f8/ca1dc/test-not-spatial-3km.png 680w,
/static/211d1b93e00e49752380933d5a6356f8/00c79/test-not-spatial-3km.png 879w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>test-not-spatial-3km.png</figcaption>
</figure></p>
<p>공간 인덱스 적용 후
1km 이내 (40건)<br>
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/14c3ee91ccfcfdb7d96661b680facd32/3bd09/test-spatial-1km.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 8.235294117647058%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAXElEQVR42o2OzQqAMAyDu11q1/1UGQMvvoD4/o8X7UDPHsKXlAZChy64uuG0jC0JWAQhBBDR5B/FGL8Ojd6xr4ZhDaVktOYsSCmh1gpVnfS7+ze7/EeeAU7vMDNum+ksGmreviMAAAAASUVORK5CYII='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='test-spatial-1km.png' title='' src='/static/14c3ee91ccfcfdb7d96661b680facd32/ca1dc/test-spatial-1km.png' srcset='/static/14c3ee91ccfcfdb7d96661b680facd32/e7570/test-spatial-1km.png 170w,
/static/14c3ee91ccfcfdb7d96661b680facd32/f46e7/test-spatial-1km.png 340w,
/static/14c3ee91ccfcfdb7d96661b680facd32/ca1dc/test-spatial-1km.png 680w,
/static/14c3ee91ccfcfdb7d96661b680facd32/3bd09/test-spatial-1km.png 896w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>test-spatial-1km.png</figcaption>
</figure></p>
<p>3km 이내 (300건)<br>
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/fc391f90d439899ede99d95b34f42266/6f18b/test-spatial-3km.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 9.411764705882353%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAX0lEQVR42mWNSwrAIAxERQU1mvjBnqD0AL3/4aZGKBS6eGTyFjOGa8U5Ku4puIQgpSCEAOccvPc/vt5a+/OGmdFW6TEnRmvovUNEkFICEW3KGlFel3PeV4djjPvXrO4B2k4sZc5AZW0AAAAASUVORK5CYII='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='test-spatial-3km.png' title='' src='/static/fc391f90d439899ede99d95b34f42266/ca1dc/test-spatial-3km.png' srcset='/static/fc391f90d439899ede99d95b34f42266/e7570/test-spatial-3km.png 170w,
/static/fc391f90d439899ede99d95b34f42266/f46e7/test-spatial-3km.png 340w,
/static/fc391f90d439899ede99d95b34f42266/ca1dc/test-spatial-3km.png 680w,
/static/fc391f90d439899ede99d95b34f42266/6f18b/test-spatial-3km.png 886w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>test-spatial-3km.png</figcaption>
</figure></p>
<p><strong>성능이 최소 10배 이상 차이 나는 것을 확인할 수 있었습니다.</strong></p>
<p><strong>예비군으로 인해, 대신 성능 테스트를 진행해 준 <code class="language-text">준팍</code> 고맙습니다 !</strong></p>
<h3>마치며</h3>
<p>공간 인덱스 적용을 위해 사용한 함수들은 <code class="language-text">MySQL</code>에서 지원하는 함수입니다.
<br> 즉, 테스트 환경이 <code class="language-text">H2</code>라면 오류가 발생할 수 있습니다.
<br> <code class="language-text">H2</code>에서는 지원해 주지 않는 함수들이기 때문이죠.
<br> 이런 상황에서는 <code class="language-text">TestContainer</code>를 통해 해결할 수 있습니다.</p>
<p>테스트 환경이 <code class="language-text">MySQL</code>이더라도, <code class="language-text">@DataJpaTest</code>를 사용한다면 오류가 발생할 수 있습니다.
<br> 내부적으로 인메모리 <code class="language-text">H2</code>를 사용하기 때문인데요.
<br> <code class="language-text">@AutoConfigureTestDatabase(replace = Replace.NONE)</code>설정을 통해 해결할 수 있습니다.</p>
<h3>참고</h3>
<p>Real My SQL 8.0 1판/2판</p></content:encoded></item><item><title><![CDATA[괜찮을지도 사이트 최적화 진행]]></title><description><![CDATA[…]]></description><link>https://map-befine-official.github.io/how-to-optimize-website/</link><guid isPermaLink="false">https://map-befine-official.github.io/how-to-optimize-website/</guid><pubDate>Wed, 18 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우테코 괜찮을지도팀의 <code class="language-text">패트릭</code>이 작성했습니다.</p>
</blockquote>
<p>홈페이지에서 용량을 많이 차지하고 페이지 로딩 속도에 영향을 미치는 이미지와 번들 사이즈 최적화를 진행하였습니다.</p>
<h2>이미지 최적화</h2>
<p>홈페이지에서 가장 많은 용량을 차지하고 페이지 로딩 속도에 가장 큰 영향을 미치는 것은 이미지라는 의견이 나왔습니다. 백엔드 쪽에서는 이미지 업로드 용량 한계치를 두고 프론트 단에서 용량을 줄이기로 하였습니다.</p>
<h3>browser-image-compression 라이브러리 사용</h3>
<p><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/b9abf696ad9f3c8375d6cd4623544367/4a768/browser-compression.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 19.411764705882355%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA3klEQVR42jVQ7WqEMBD0/V/BX30FU5Ur9o+Uo3/sKxwqfqMxxpjoXZhmUxoYws7ODLsbVFWFvu8xDAO6rsM0TSjLEuM4em5ZFs81TeO5tm29vqpr8HlCu2iEnzWkfoJekGUZ4jhGmqZgEcNPUeDjdsM7YwjDEIWr8zxHFEVgjiNdkiQe3/cvPHqBt7yDOl9/gcdxgHCdF/R1YtU79stgN9rzQgg/pZQSnHMopTxP9bqu7t+c13iNtRYBGahhjIF2jUFyzLvAZhTsy2J2Qlqdwmjl/1A6A4G8Qmz+JBT4C4xBJXG8L9MZAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='Alt text' title='' src='/static/b9abf696ad9f3c8375d6cd4623544367/ca1dc/browser-compression.png' srcset='/static/b9abf696ad9f3c8375d6cd4623544367/e7570/browser-compression.png 170w,
/static/b9abf696ad9f3c8375d6cd4623544367/f46e7/browser-compression.png 340w,
/static/b9abf696ad9f3c8375d6cd4623544367/ca1dc/browser-compression.png 680w,
/static/b9abf696ad9f3c8375d6cd4623544367/4a768/browser-compression.png 738w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>Alt text</figcaption>
</figure></p>
<p>그리고 하나의 이미지와 여러 개의 이미지를 압축하는 곳이 따로 있어 구분해서 적용하기 위해 custom hook으로 분리하였습니다.</p>
<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> imageCompression <span class="token keyword">from</span> <span class="token string">'browser-image-compression'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token function-variable function">useCompressImage</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> <span class="token function-variable function">compressImage</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>file<span class="token operator">:</span> File<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> resizingBlob <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">imageCompression</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> <span class="token punctuation">{</span>
maxSizeMB<span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token comment">// 최대 이미지 용량</span>
maxWidthOrHeight<span class="token operator">:</span> <span class="token number">750</span><span class="token punctuation">,</span> <span class="token comment">// 최대 이미지 크기</span>
useWebWorker<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token comment">// 비동기 처리 유무</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> resizingFile <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">File</span><span class="token punctuation">(</span><span class="token punctuation">[</span>resizingBlob<span class="token punctuation">]</span><span class="token punctuation">,</span> file<span class="token punctuation">.</span>name<span class="token punctuation">,</span> <span class="token punctuation">{</span>
type<span class="token operator">:</span> file<span class="token punctuation">.</span>type<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> resizingFile<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token function-variable function">compressImageList</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>files<span class="token operator">:</span> FileList<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> compressedImageList<span class="token operator">:</span> File<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> file <span class="token keyword">of</span> files<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> compressedImage <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">compressImage</span><span class="token punctuation">(</span>file<span class="token punctuation">)</span><span class="token punctuation">;</span>
compressedImageList<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>compressedImage<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> compressedImageList<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span> compressImage<span class="token punctuation">,</span> compressImageList <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> useCompressImage<span class="token punctuation">;</span></code></pre></div>
<p>이를 통해 성능이 개선된 지표입니다.
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/87d8b8cffc0e2dc00a071b2320b0147c/544f7/optimize-compress.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 35.88235294117647%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABMklEQVR42m2RbauCQBCF/f+/xi9aFGYpiCBRmUigpYUovQdpJpXncgaMe+EOLDsze3bm2VmlLEvcbjdwf71eeDwe4r/fbzyfT6zXa+z3e8Rx/F3UnE4nRFGENE0lx5im0HFdF4vFAkmSYDabwbZtbDYbNE0Dy7Lkoud56Pf7mEwmyLJMtKZpwnEcjEYj0UjBz+cjZDT6LEI67iSt61qIuNq2/WqY786o70w5HA5CNZ1OpdN8PpcGfOpgMIBhGNA0TXw+nfkgCKDrOnq9HlRVxW63k2JsKITswE6Xy0W60hh3s62qSuZJXUfIc+73+/0v4fl8RhiGMtjlconVaiWFr9crxuMxhsOhUB2PRxRFITNkLs/zL9VvU0jEp/ATfN+XS6Qh2Xa7leGTlCTUdh/C+L+CP2MEB6mVkpqxAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='Alt text' title='' src='/static/87d8b8cffc0e2dc00a071b2320b0147c/ca1dc/optimize-compress.png' srcset='/static/87d8b8cffc0e2dc00a071b2320b0147c/e7570/optimize-compress.png 170w,
/static/87d8b8cffc0e2dc00a071b2320b0147c/f46e7/optimize-compress.png 340w,
/static/87d8b8cffc0e2dc00a071b2320b0147c/ca1dc/optimize-compress.png 680w,
/static/87d8b8cffc0e2dc00a071b2320b0147c/544f7/optimize-compress.png 684w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>Alt text</figcaption>
</figure></p>
<h2>번들 사이즈</h2>
<p>이미지 다음으로 번들 사이즈가 페이지 로딩 속도에 영향을 끼친다는 것을 알았습니다. 동적 import(lazy, suspense 이용)와 tree shaking을 통해 개선할 수 있었습니다.</p>
<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> SelectedTopic <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/SelectedTopic'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> NewPin <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/NewPin'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> NewTopic <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/NewTopic'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> SeeAllPopularTopics <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/SeeAllPopularTopics'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> SeeAllNearTopics <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/SeeAllNearTopics'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> SeeAllLatestTopics <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/SeeAllLatestTopics'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> KakaoRedirect <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/KakaoRedirect'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> Profile <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/Profile'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> AskLogin <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/AskLogin'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> Bookmark <span class="token operator">=</span> <span class="token function">lazy</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span><span class="token punctuation">(</span><span class="token string">'./pages/Bookmark'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">function</span> <span class="token function">SuspenseComp</span><span class="token punctuation">(</span><span class="token punctuation">{</span> children <span class="token punctuation">}</span><span class="token operator">:</span> SuspenseCompProps<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token operator">&lt;</span>Suspense fallback<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">null</span><span class="token punctuation">}</span><span class="token operator">></span><span class="token punctuation">{</span>children<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>Suspense<span class="token operator">></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'topics/:topicId'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>SelectedTopic <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'new-topic'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>NewTopic <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'new-pin'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>NewPin <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'see-all/popularity'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>SeeAllPopularTopics <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'see-all/near'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>SeeAllNearTopics <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'see-all/latest'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>SeeAllLatestTopics <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'favorite'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>Bookmark <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'my-page'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>Profile <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span>
path<span class="token operator">:</span> <span class="token string">'/askLogin'</span><span class="token punctuation">,</span>
element<span class="token operator">:</span> <span class="token punctuation">(</span>
<span class="token operator">&lt;</span>SuspenseComp<span class="token operator">></span>
<span class="token operator">&lt;</span>AskLogin <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator">&lt;</span><span class="token operator">/</span>SuspenseComp<span class="token operator">></span>
<span class="token punctuation">)</span><span class="token punctuation">,</span>
withAuth<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre></div>
<ul>
<li>개선 전
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/fd8d5231661eed9ec1b3b8cdabd41d85/58c38/before.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 38.82352941176471%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB0UlEQVR42n2Q7U9SYRiH+Q8thqM3AnNpvgKGKwRna7NVH/pa683VahYqtbKyjWjmUgIJKjgdgVNaULAz5JDjZZ4DVweoPtTyfvbbnu3Zc+26fwalkKZerSDXZYTtD7yRQqxJrwlvhdgof0QsC3qSbNY3Wck/wZ+5TFXboTVN/fw9hmIpT0Nr8E6Jc/f7LXz5WR7k5/HnfNwWZrgRu8qd1AzPi0tcEc/gDXVT3i3uAdzZbr2w+O0xvREro5EhvJIbl+TAlXEyJto5KdnxfJ7gnDiCe30fiiq3PzcajT/RNK0D/PRFoqkD/RsLmJ+acCWdDCcGMa3ux6zbmNdMdK8aORg9wP3wTZYDD6nVKyhKmVKphCzLZLPZdlRVxVBRq23yvDCH0dfF4Eo/DmEEa+wIPfGj2GIWbG8tHMtYCVx/gTS0haqo7XVbVTV1m9+WbcOqVmtflgrPOBE9zvj7MTyp09iTw7hEB05hFEdiFHuin2tzr5i9UOCHstvp8N8KW8CO4WLhEX2JHsZFJwPxPozBLiyRQxyOmLGtWxhI9nJvOUhgIUe1ugew9svwpRzEK7q5lL7I+fQ0E6lTTKW9TKY9TGUmmZbOEs2HyeW+6stq/G9+AohlHK0lnoWUAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='Alt text' title='' src='/static/fd8d5231661eed9ec1b3b8cdabd41d85/ca1dc/before.png' srcset='/static/fd8d5231661eed9ec1b3b8cdabd41d85/e7570/before.png 170w,
/static/fd8d5231661eed9ec1b3b8cdabd41d85/f46e7/before.png 340w,
/static/fd8d5231661eed9ec1b3b8cdabd41d85/ca1dc/before.png 680w,
/static/fd8d5231661eed9ec1b3b8cdabd41d85/02d09/before.png 1020w,
/static/fd8d5231661eed9ec1b3b8cdabd41d85/58c38/before.png 1260w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>Alt text</figcaption>
</figure></li>
<li>개선 후
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/234b5b3f41dd5ce6efb8d64d758535cd/5745f/after.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 40.588235294117645%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAACBElEQVR42m3S30+SURzHcf48L7ppdeHKi26aXRW5tXK1NnNaelGYFmuSJGCWQ7GLLEOkNQwV04wfCciPB56HB5yiTwOBBx7enWWu1vqenYuzs/Pa+ZzvMe0VZbS9JLV6ja3UF0L5VXKNPJslD77kIO/CgyzG7rMij5Gd87M77ENxrGHUmvyqdpu/y6SoCodamUajiSfrZnhngHHZhjt+F0fwHM7QRexrncxEu1m/Y2O1Y4y42U2r0jjxjLYw/0xTRi1yUKlQ1VuMZiyc9Z+hK3ABT8HGkjRBUH3JbnyCyLaF9KaPmdcJvN4Uum7874KYviUzSEqaphhPpVEubXTRE77OUNjM1eUOrDEzO6ERIoE+ohvTjHiyOLxFqsc6Bwf7HB0dUSgUUBQFVVUxfRVgTskIrsVI6hGdgfNc2bzMZOoZk9tWFvIu/N9vEwwPUJwaImj/yKsnh9Sr4kSrSalUQpZlyuWyWLcEmEgLMCs4A7tko2f7Gg92+3govaA3OcFYcprI1DiR+ed82LrH6uInAsE4x+2qyAvaDw1N06jVaieR4zmZvX1VcAaOnJ0bERE33c/cho/p+RXee9eJWmeJuuZ5u2wlFvKTycdotvXfr/ZPl3WxUTfq6IbOlOykN3yT4Xw/b5yfWeiWWLJkeax4ce0ESd5KkFjOEJdyYJz+mpPunoI/AbfWJVgmBIeYAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='Alt text' title='' src='/static/234b5b3f41dd5ce6efb8d64d758535cd/ca1dc/after.png' srcset='/static/234b5b3f41dd5ce6efb8d64d758535cd/e7570/after.png 170w,
/static/234b5b3f41dd5ce6efb8d64d758535cd/f46e7/after.png 340w,
/static/234b5b3f41dd5ce6efb8d64d758535cd/ca1dc/after.png 680w,
/static/234b5b3f41dd5ce6efb8d64d758535cd/02d09/after.png 1020w,
/static/234b5b3f41dd5ce6efb8d64d758535cd/5745f/after.png 1258w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>Alt text</figcaption>
</figure></li>
</ul></content:encoded></item><item><title><![CDATA[백엔드와 협력해 S3에 이미지 저장하기]]></title><description><![CDATA[이 글은 우테코 괜찮을지도팀의 가 작성했습니다. 백엔드 크루 '매튜'와 협력해 S…]]></description><link>https://map-befine-official.github.io/how-to-store-image-s3/</link><guid isPermaLink="false">https://map-befine-official.github.io/how-to-store-image-s3/</guid><pubDate>Wed, 18 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우테코 괜찮을지도팀의 <code class="language-text">패트릭</code>가 작성했습니다.</p>
</blockquote>
<p>백엔드 크루 '매튜'와 협력해 S3에 이미지를 저장하는 작업을 진행하였습니다. 생각보다 시간도 오래 걸리고 어려움도 많았지만 많이 배웠고 보람있는 시간이었습니다.</p>
<h3>이미지 관련 사용자 피드백</h3>
<p>이미지를 string으로 저장하고 있어 이미지를 저장하는데 번거럽다는 피드백을 받았습니다.</p>
<h2>formData를 통해 이미지 서버에 전송하기</h2>
<p>file type의 Input 태그를 만듭니다.<br>
file을 서버로 전송하면 해당 file 데이터를 multipart/form-data형태로 받습니다.</p>
<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token operator">&lt;</span>ImageInputButton id<span class="token operator">=</span><span class="token string">"file"</span> type<span class="token operator">=</span><span class="token string">"file"</span> name<span class="token operator">=</span><span class="token string">"image"</span> onChange<span class="token operator">=</span><span class="token punctuation">{</span>onHandler<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span></code></pre></div>
<p>form 데이터를 동적으로 생성하고 전송 가능한 객체인 formData를 써서 내용을 key와 value 형식으로 보내도록 했습니다.</p>
<div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> formData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FormData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>formImage<span class="token punctuation">)</span> <span class="token punctuation">{</span>
formData<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token string">'image'</span><span class="token punctuation">,</span> formImage<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<h3>문제 상황 : 1차 서버 에러 발생</h3>
<p>서버 Post 요청 시 서버쪽에서 아래와 같은 에러가 발생했습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token comment">// 에러</span>
<span class="token class-name">Caused</span> by<span class="token operator">:</span> <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span></span>FileUploadException</span><span class="token operator">:</span> the request was rejected because no multipart boundary was found
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span>impl<span class="token punctuation">.</span></span>FileItemIteratorImpl</span><span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token class-name">FileItemIteratorImpl</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">189</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span>impl<span class="token punctuation">.</span></span>FileItemIteratorImpl</span><span class="token punctuation">.</span><span class="token function">getMultiPartStream</span><span class="token punctuation">(</span><span class="token class-name">FileItemIteratorImpl</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">205</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span>impl<span class="token punctuation">.</span></span>FileItemIteratorImpl</span><span class="token punctuation">.</span><span class="token function">findNextItem</span><span class="token punctuation">(</span><span class="token class-name">FileItemIteratorImpl</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">224</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span>impl<span class="token punctuation">.</span></span>FileItemIteratorImpl</span><span class="token punctuation">.</span><span class="token generics"><span class="token punctuation">&lt;</span>init<span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token class-name">FileItemIteratorImpl</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">142</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span></span>FileUploadBase</span><span class="token punctuation">.</span><span class="token function">getItemIterator</span><span class="token punctuation">(</span><span class="token class-name">FileUploadBase</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">252</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>tomcat<span class="token punctuation">.</span>util<span class="token punctuation">.</span>http<span class="token punctuation">.</span>fileupload<span class="token punctuation">.</span></span>FileUploadBase</span><span class="token punctuation">.</span><span class="token function">parseRequest</span><span class="token punctuation">(</span><span class="token class-name">FileUploadBase</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">276</span><span class="token punctuation">)</span>
at <span class="token class-name"><span class="token namespace">org<span class="token punctuation">.</span>apache<span class="token punctuation">.</span>catalina<span class="token punctuation">.</span>connector<span class="token punctuation">.</span></span>Request</span><span class="token punctuation">.</span><span class="token function">parseParts</span><span class="token punctuation">(</span><span class="token class-name">Request</span><span class="token punctuation">.</span>java<span class="token operator">:</span><span class="token number">2799</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token number">46</span> common frames omitted</code></pre></div>
<p>에러의 원인은 boundary를 찾지 못해 나는 것이었고 formData에 파일이 있을 경우 브라우저는 자동으로 boundary를 붙여준다는 것을 알았습니다. 여기서 Post 요청을 할 때 개발자가 content-type을 multipart/form-data로 지정해주면 브라우저가 자동으로 생성한 boundary를 덮어써 버립니다. 그리고 서버는 요청 본문을 올바르게 파싱하지 못하여 파일 및 폼 데이터를 올바르게 처리하지 못합니다.<br>
그래서 content-type을 지우면 해결이 될거라고 생각해 지웠습니다. 그러나 역시 문제는 쉽게 해결되지 않는 법! 또 다른 에러가 발생하였습니다.</p>
<h3>문제 상황 : 2차 서버 에러 발생</h3>
<p>json 데이터를 application/json 타입으로 명시해 주지 않아 octet-stream(8비트 단위의 이진 데이터)로 인식하여 발생한 에러였습니다. 'Blob(Binary Large Object)'을 사용하여 해결할 수 있었습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>objectData<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Blob처리로 JSON 문자열이 이진 데이터로 변환. 또한 type에 application/json을 명시</span>
<span class="token keyword">const</span> jsonBlob <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Blob</span><span class="token punctuation">(</span><span class="token punctuation">[</span>data<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> type<span class="token operator">:</span> 'application<span class="token operator">/</span>json' <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
formData<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>'request'<span class="token punctuation">,</span> jsonBlob<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<p>이러한 과정을 통해 서버에 이미지와 데이터를 전송할 수 있었고 이를 바탕으로 S3에 이미지를 저장할 수 있었습니다!</p></content:encoded></item><item><title><![CDATA[N + 1 문제 해결 도중 맞닥뜨린 Set 과 List 의 차이]]></title><description><![CDATA[…]]></description><link>https://map-befine-official.github.io/set-or-list/</link><guid isPermaLink="false">https://map-befine-official.github.io/set-or-list/</guid><pubDate>Sun, 15 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우테코 괜찮을지도의 <code class="language-text">매튜</code>가 작성하였습니다.</p>
</blockquote>
<h3>서론</h3>
<h2>간단한 도메인 설명</h2>
<p>저희 서비스에는 <code class="language-text">Topic</code> 이라는 도메인이 존재하고, 이는 <code class="language-text">지도</code>를 의미합니다.</p>
<p>그리고 해당 <code class="language-text">지도</code>는 <code class="language-text">Permission(권한)</code>, <code class="language-text">Pin(핀)</code>, <code class="language-text">Bookmark(즐겨찾기)</code> 들과 <code class="language-text">1:N</code> <code class="language-text">연관관계</code>를 이루고 있습니다.</p>
<h2>문제 발생 상황</h2>
<h3>문제 상황</h3>
<p>이 글에서 주로 탐구하고 있는 문제는 서비스 홍보를 앞두고 성능 개선을 하기 위해 <code class="language-text">N + 1</code> 문제를 해결하고 있던 와중 발생하였습니다.</p>
<p>일단 유의미한 성능 차이를 보기 위해, 우선적으로 <code class="language-text">Topic</code> 과 <code class="language-text">Pin</code> 데이터를 각각 <code class="language-text">10만개</code>씩 넣고 진행하였습니다.</p>
<p>또한 요청은 <code class="language-text">PostMan</code> 을 이용하여 테스트 하였습니다.</p>
<p>아래는 그 때의 <code class="language-text">코드</code>입니다.</p>
<ul>
<li>
<p>Topic</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token annotation punctuation">@Entity</span>
<span class="token annotation punctuation">@NoArgsConstructor</span><span class="token punctuation">(</span>access <span class="token operator">=</span> <span class="token constant">PROTECTED</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@Getter</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Topic</span> <span class="token keyword">extends</span> <span class="token class-name">BaseTimeEntity</span> <span class="token punctuation">{</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략
<span class="token annotation punctuation">@ManyToOne</span><span class="token punctuation">(</span>fetch <span class="token operator">=</span> <span class="token class-name">FetchType</span><span class="token punctuation">.</span><span class="token constant">LAZY</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@JoinColumn</span><span class="token punctuation">(</span>name <span class="token operator">=</span> <span class="token string">"member_id"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Member</span> creator<span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Permission</span><span class="token punctuation">></span></span> permissions <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Pin</span><span class="token punctuation">></span></span> pins <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">,</span> orphanRemoval <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Bookmark</span><span class="token punctuation">></span></span> bookmarks <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략</code></pre></div>
</li>
</ul>
<p>}</p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- TopicRepository
```java
@Repository
public interface TopicRepository extends JpaRepository&lt;Topic, Long> {
@EntityGraph(attributePaths = {"creator", "permissions", "bookmarks", "pins"})
List&lt;Topic> findAll();
}</code></pre></div>
<p>당연히 위 코드는 <code class="language-text">MultipleBagFetchExcepion</code> 예외가 발생하였습니다. (<code class="language-text">MultipleBagFetchExcepion</code> 자세한 설명은 <a href="https://map-befine-official.github.io/jpa-multibag-fetch-exception/">https://map-befine-official.github.io/jpa-multibag-fetch-exception/</a> 해당 글을 확인해주세요!)</p>
<h3>MultipleBagFetchExcepion 해결</h3>
<p>우리는 해당 예외를 해결하기 위해 <code class="language-text">Topic</code> 의 <code class="language-text">Collection</code> 들의 <code class="language-text">자료구조</code>를 <code class="language-text">Set</code> 으로 바꿔주었습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token annotation punctuation">@Entity</span>
<span class="token annotation punctuation">@NoArgsConstructor</span><span class="token punctuation">(</span>access <span class="token operator">=</span> <span class="token constant">PROTECTED</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@Getter</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Topic</span> <span class="token keyword">extends</span> <span class="token class-name">BaseTimeEntity</span> <span class="token punctuation">{</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략
<span class="token annotation punctuation">@ManyToOne</span><span class="token punctuation">(</span>fetch <span class="token operator">=</span> <span class="token class-name">FetchType</span><span class="token punctuation">.</span><span class="token constant">LAZY</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@JoinColumn</span><span class="token punctuation">(</span>name <span class="token operator">=</span> <span class="token string">"member_id"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Member</span> creator<span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Permission</span><span class="token punctuation">></span></span> permissions <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HasSet</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Pin</span><span class="token punctuation">></span></span> pins <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashSet</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">,</span> orphanRemoval <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Bookmark</span><span class="token punctuation">></span></span> bookmarks <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashSet</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략
<span class="token punctuation">}</span></code></pre></div>
<p>이렇게 해서 단 한번의 <code class="language-text">Query</code> 로 존재하는 모든 <code class="language-text">Topic</code> 을 불러 올 수 있었습니다.</p>
<p>하지만, <code class="language-text">카테시안 곱</code> 으로 인해 요청에 대한 응답시간이 어마어마 했습니다. (대략 <code class="language-text">20초</code> 정도?)</p>
<h3>Topic findAll성능 개선 완료</h3>
<p><code class="language-text">Topic</code>을 전체 조회할 때 사실 <code class="language-text">Bookmark(즐겨찾기)</code>, <code class="language-text">Pin(핀)</code> 의 세부 정보가 아닌, 이들의 개수만이 필요하기 때문에, 반정규화를 통해 이 문제를 해결하였습니다.</p>
<p>결론적으로 아래와 같이, <code class="language-text">Collection</code> 중에는 <code class="language-text">Permission</code> 만을 <code class="language-text">join</code> 해오면 되는 거죠!</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token annotation punctuation">@Repository</span>
<span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">TopicRepository</span> <span class="token keyword">extends</span> <span class="token class-name">JpaRepository</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Topic</span><span class="token punctuation">,</span> <span class="token class-name">Long</span><span class="token punctuation">></span></span> <span class="token punctuation">{</span>
<span class="token annotation punctuation">@EntityGraph</span><span class="token punctuation">(</span>attributePaths <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">"creator"</span><span class="token punctuation">,</span> <span class="token string">"permissions"</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Topic</span><span class="token punctuation">></span></span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<p>근데 이렇게 반정규화를 진행하던 도중, 어쩌다가 <code class="language-text">Topic</code> 을 아래와 같이 바꾸는 일이 있었습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token annotation punctuation">@Entity</span>
<span class="token annotation punctuation">@NoArgsConstructor</span><span class="token punctuation">(</span>access <span class="token operator">=</span> <span class="token constant">PROTECTED</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@Getter</span>
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Topic</span> <span class="token keyword">extends</span> <span class="token class-name">BaseTimeEntity</span> <span class="token punctuation">{</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략
<span class="token annotation punctuation">@ManyToOne</span><span class="token punctuation">(</span>fetch <span class="token operator">=</span> <span class="token class-name">FetchType</span><span class="token punctuation">.</span><span class="token constant">LAZY</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@JoinColumn</span><span class="token punctuation">(</span>name <span class="token operator">=</span> <span class="token string">"member_id"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Member</span> creator<span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Permission</span><span class="token punctuation">></span></span> permissions <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HasSet</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Pin</span><span class="token punctuation">></span></span> pins <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Set --> List 로 바꿈</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">,</span> orphanRemoval <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">Set</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Bookmark</span><span class="token punctuation">></span></span> bookmarks <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashSet</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> 생략
<span class="token punctuation">}</span></code></pre></div>
<p>이 때 <code class="language-text">Collection</code> 의 자료구조를 <code class="language-text">Set</code> 만을 썼을때보다 속도가 굉장히 빨라졌었습니다.</p>
<p>이 때 당시에 모두 이에 대해 왜 이런 것이지? 하는 의문을 가졌었지만, 시간이 없어 어쩔 수 없이 넘어갔었습니다.</p>
<p>그리고 시간이 지나, 어느정도 여유가 생긴 지금, 해당 문제에 대해 탐구해보고자 글을 작성하게 된 것입니다.</p>
<h2>재연</h2>
<h3>상황을 그때와 동일하게 구성해보자.</h3>
<p><code class="language-text">프로시저를</code> 통해 <code class="language-text">Local DB</code> 에다가 <code class="language-text">Topic</code>, <code class="language-text">Bookmark</code> 데이터를 <code class="language-text">10만개</code> 가량을 넣어주고 테스트를 진행했습니다. (내 컴퓨터 살려..)</p>
<p><code class="language-text">Pin</code> 데이터를 넣지 않은 이유는, 현재 <code class="language-text">Pin</code> 은 <code class="language-text">반정규화</code>가 진행되어 있어 <code class="language-text">Topic</code> 전체 목록을 조회할 때, 성능에 전혀 영향을 끼치지 않기 때문입니다.</p>
<p>그렇다고 <code class="language-text">Pin</code> <code class="language-text">반정규화</code>를 풀자니, 요청과 응답 시간이 비 정상적으로 너무 길어졌습니다. (대략 1분 30초 정도)</p>
<p>그렇기 때문에 일단 <code class="language-text">Pin</code> 은 일단 <code class="language-text">반정규화</code>를 유지하였고, <code class="language-text">Bookmark</code> 만 <code class="language-text">반정규화</code>를 해제하고 진행하였습니다.</p>
<p>그렇기 때문에 테스트를 위해서 <code class="language-text">자료구조</code>를 변경하게 될 <code class="language-text">Collection</code> 은 사실상 <code class="language-text">Permission</code> 과 <code class="language-text">Bookmark</code> 뿐인 것 입니다.</p>
<h3>테스트 진행</h3>
<p>말씀드린 두 <code class="language-text">Collection</code>의 <code class="language-text">자료구조</code>를 바꿔가며 테스트 해본 결과는 아래와 같습니다. (<code class="language-text">Permission</code>, <code class="language-text">Bookmark</code> 모두 <code class="language-text">Set</code> 이 아닌 경우는 <code class="language-text">MultipleBagFetchException</code> 이 발생하기 때문에 테스트하지 않았습니다.)</p>
<ul>
<li>
<p>Permission, Bookmark 모두 Set</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-2] 2042 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 5.442s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
<li>
<p>Permission 만 Set </p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-3] 2181 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 7.074s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
<li>
<p>Bookmark 만 Set</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-1] 2072 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 5.348s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
</ul>
<p>위 테스트 결과들만으로는 <code class="language-text">유의미한 차이</code>가 보이지 않아 <code class="language-text">원인</code>을 추론해보기 어려웠습니다.</p>
<h3>지금까지 무의미한 데이터로 테스트 해본 것은 아닐까?</h3>
<p>위와 같이 테스트하다가, 문득 <code class="language-text">Permission</code> 에 <code class="language-text">데이터</code>도 넣어봐야 유의미하지 않을까? 란 생각이 머리를 스쳐 지나갔고, <code class="language-text">Permission</code> 데이터도 추가해주었습니다.</p>
<p>하지만, <code class="language-text">Permission</code> 을 추가해주니, <code class="language-text">카제인 곱</code>이 엄청나게 발생되어, <code class="language-text">Permission</code>, <code class="language-text">Bookmark</code> 각각 데이터 개수가 <code class="language-text">3000개</code>만 넘어가도 <code class="language-text">Java Heap</code> 이 터지는 예외가 발생하게 되어, 적당히 <code class="language-text">2000</code> 개 가량의 데이터를 각각 넣어주고, 이어서 테스트를 진행해본 결과 아래와 같은 결과가 나오게 되었습니다.</p>
<ul>
<li>
<p>Permission 과 Bookmark 모두 Set</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-3] 1863 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 4.924s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
<li>
<p>Permission 만 Set 일 때</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-2] 1952 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 5.159s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
<li>
<p>Bookmark 만 Set 일 때</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">[http-nio-8080-exec-3] 2000 INFO com.mapbefine.mapbefine.common.filter.LatencyLoggingFilter - Latency : 7.253s, Query count : 1, Request URI : /topics</code></pre></div>
</li>
</ul>
<p>테스트 데이터를 변경하더라도, 역시나 <code class="language-text">유의미한 차이</code>를 볼 수 없었습니다.</p>
<h3>내린 결론 (가설)</h3>
<p><code class="language-text">Set</code> 자료구조를 사용함에 따라, <code class="language-text">List</code> 보다는 부하가 더 발생할 수 있다고 생각합니다.</p>
<p>자료구조 특성상 <code class="language-text">Set</code> 은 중복을 제거해주는 연산을 실행해주어야 하니까요.</p>
<p>하지만, 어짜피 <code class="language-text">List</code> 를 사용하더라도 <code class="language-text">hibernate</code> 에서 <code class="language-text">distinct</code> 를 통해 <code class="language-text">중복</code>을 제거해주기 때문에 더더욱이 유의미한 성능상의 차이를 가져오지 못하는 것 같습니다.</p>
<p>정말 많이 테스트해보면서, 가끔 컴퓨터의 상태에 따라 응답시간이 비정상적으로 길어지는 경우가 있었습니다.</p>
<p>저희는 그것을 보았던 것 아닐까요??</p>
<h2>이대로 끝내기는 아쉬우니까!</h2>
<h3>JPA 에서 Set 을 사용할 때 주의할 점</h3>
<p><code class="language-text">문제</code>를 탐구하다가 <code class="language-text">재미있는 글</code>을 발견했습니다.</p>
<p><a href="https://www.inflearn.com/questions/321256/collection-type%EC%9C%BC%EB%A1%9C-set-%EB%8C%80%EC%8B%A0-list%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EA%B0%80-%EC%9E%88%EB%8A%94%EC%A7%80%EC%9A%94">JPA 에서 Set 을 사용할 때 주의점</a></p>
<p>질문은 아래와 같았습니다.</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">Collection type으로 Set 대신 List를 사용하시는 이유가 궁금합니다.
지금까지 나온 Collection들이 모두 unique한 Entity(또는 값 타입)들의 collection이기 때문에, Set을 활용할 경우 중복 확인 관련 부분이 깔끔해지고, 다른 질문의 답변에서 답해주신대로 값 타입 컬렉션에도 row를 모두 날리고 다시 넣는 문제를 막을 수 있어 Set에 대해 좋은 인상을 가지게 되었습니다.
그런데 기본적으로 예제가 List를 사용하여, Set을 사용하였을 때 제가 놓친 문제가 있는지 의문이 들었습니다.</code></pre></div>
<p>그에 대한 영한님의 답변은 이랬습니다.</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">안녕하세요. Catnip님
좋은 질문입니다. Set이 개념적으로 좋지만 실무에서는 성능 이슈가 있습니다.
Set은 중복을 제거해야 하는데, 그렇다는 것은 기존 데이터 중에 중복이 있는지 비교를 해야 합니다. 이게 일반적으로는 크게 문제가 없는데, 지연 로딩으로 컬렉션을 조회했을 때 문제가 됩니다.
컬력션이 아직 초기화 되지 않은 상태에서 컬렉션에 값을 넣게 되면 프록시가 강제로 초기화 되는 문제가 발생합니다. 왜냐하면 중복 데이터가 있는지 비교해야 하는데, 그럴러면 컬렉션에 모든 데이터를 로딩해야 하기 때문입니다.
반면에 List는 이런 중복 체크가 필요없이 때문에 데이터를 추가할 때 초기화가 발생하지 않습니다.
감사합니다.</code></pre></div>
<p>아주 흥미로웠습니다.</p>
<p>이를 제대로 확인해보기 위해서 테스트를 진행하였습니다.</p>
<h3>테스트</h3>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token annotation punctuation">@Test</span>
<span class="token annotation punctuation">@Transactional</span>
<span class="token keyword">void</span> <span class="token class-name">Topic</span>의_Collection_의_자료구조에_따른_초기화를_확인해보자<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//given </span>
<span class="token class-name">Member</span> savedMember <span class="token operator">=</span> memberRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span><span class="token class-name">MemberFixture</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"member"</span><span class="token punctuation">,</span> <span class="token string">"member@naver.com"</span><span class="token punctuation">,</span> <span class="token class-name">Role</span><span class="token punctuation">.</span><span class="token constant">USER</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Topic</span> savedTopic <span class="token operator">=</span> topicRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span><span class="token class-name">TopicFixture</span><span class="token punctuation">.</span><span class="token function">createPublicAndAllMembersTopic</span><span class="token punctuation">(</span>savedMember<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Location</span> savedLocation <span class="token operator">=</span> locationRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span><span class="token class-name">LocationFixture</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// when </span>
entityManager<span class="token punctuation">.</span><span class="token function">clear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Topic</span> findTopic <span class="token operator">=</span> topicRepository<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>savedTopic<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Pin</span> savedPin <span class="token operator">=</span> pinRepository<span class="token punctuation">.</span><span class="token function">save</span><span class="token punctuation">(</span><span class="token class-name">PinFixture</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span>savedLocation<span class="token punctuation">,</span> savedTopic<span class="token punctuation">,</span> savedMember<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// then </span>
<span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"=============Add Pin 이전"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
findTopic<span class="token punctuation">.</span><span class="token function">addPin</span><span class="token punctuation">(</span>savedPin<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"=============Add Pin 이후"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
entityManager<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<p>이와 같이 테스트 코드를 짜고 <code class="language-text">Pins</code> 를 <code class="language-text">List</code> 혹은 <code class="language-text">Set</code> 으로 진행해보았다.</p>
<ul>
<li>
<p>Pins 가 List 일 때 쿼리</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">... 이전 쿼리들
=============Add Pin 이전
=============Add Pin 이후
Hibernate:
update
topic
set
... 수 많은 컬럼들
where
id=?</code></pre></div>
</li>
<li>
<p>Pins 가 Set 일 때 쿼리</p>
<div class="gatsby-highlight" data-language="txt"><pre class="language-txt"><code class="language-txt">... 이전 쿼리들
=============Add Pin 이전
Hibernate:
select
... 수 많은 컬럼들
from
pin p1_0
left join
member c1_0
on c1_0.id=p1_0.member_id
left join
location l1_0
on l1_0.id=p1_0.location_id
where
p1_0.topic_id=?
and (
p1_0.is_deleted = false
)
=============Add Pin 이후
Hibernate:
update
topic
set
... 수 많은 컬럼들
where
id=?</code></pre></div>
</li>
</ul>
<p>영한님의 말씀대로 <code class="language-text">List</code> 는 <code class="language-text">Collection</code> 에 값을 <code class="language-text">추가</code> 를 진행할 때, 기존의 데이터가 필요 없으니, 초기화를 진행하지 않지만, <code class="language-text">Set</code> 을 쓰는 경우 중복을 방지하기 위해 기존의 데이터가 필요하기 때문에 <code class="language-text">select</code> 를 통해 값을 가져와 초기화를 진행해주는 것을 볼 수 있었습니다.</p>
<p>즉, 이렇게 <code class="language-text">fetch</code> 전략으로 <code class="language-text">Lazy Loading</code> 을 사용하는 경우 <code class="language-text">자료구조</code>로 <code class="language-text">Set</code> 을 사용하는 경우, <code class="language-text">연관관계 매핑</code>을 하게 되었을 때, 해당 <code class="language-text">부작용</code>이 발생할 수 있는 것입니다.</p>
<p>조심해서 써야겠습니다.</p>
<h2>최종적인 결론</h2>
<p>이 글의 최종적인 결론은 아래와 같습니다.</p>
<ul>
<li>사실 <code class="language-text">Set</code> 과 <code class="language-text">List</code> 로 인한 <code class="language-text">성능 차이</code>는 <code class="language-text">유의미하지 않은</code> 것 같다. 우리가 착각했던 것일지도..?</li>
<li>하지만, <code class="language-text">Set</code> 을 무지성으로 써도 되는 것은 아니다, 이로부터 얻는 부작용이 상당히 많으니, 고심해서 사용하자 (순서 보장 x, 위에서 설명한 초기화 문제)</li>
</ul>
<p>긴 글 봐주셔서 정말 감사합니다~!</p></content:encoded></item><item><title><![CDATA[JPA 엔티티를 삭제할 때 영속성과 연관 관계가 중요한 이유]]></title><description><![CDATA[이 글은 우테코 괜찮을지도의 가 작성하였습니다. 삭제 기능에 대한 리팩터링 중, 회원 차단에 대한 기존 테스트가 실패해 이를 해결해야 했는데요. JPA에 대한 지식이 부족한 상태에서 삽질을 하며 알게 된 것들을 기록하고자 합니다. 문제 상황…]]></description><link>https://map-befine-official.github.io/trouble-shooting-jpa-delete-and-persistence/</link><guid isPermaLink="false">https://map-befine-official.github.io/trouble-shooting-jpa-delete-and-persistence/</guid><pubDate>Fri, 13 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우테코 괜찮을지도의 <code class="language-text">도이</code>가 작성하였습니다.</p>
</blockquote>
<p>삭제 기능에 대한 리팩터링 중, 회원 차단에 대한 기존 테스트가 실패해 이를 해결해야 했는데요.<br>
JPA에 대한 지식이 부족한 상태에서 삽질을 하며 알게 된 것들을 기록하고자 합니다.</p>
<h2>문제 상황 1 : 쿼리의 발생 시점 찾기</h2>
<h3>도메인: 회원 차단 기능</h3>
<p>도메인에 대해 먼저 설명드리겠습니다.<br>
관리자 API에서 회원을 차단하면, 차단한 회원의 지도 <code class="language-text">Topic</code>, 핀 <code class="language-text">Pin</code>, 핀 이미지 <code class="language-text">PinImage</code>를 삭제 상태(soft delete)로 변경합니다.<br>
그리고 매핑 테이블 역할을 하는 엔티티인 <code class="language-text">Bookmark 즐겨찾기</code>, <code class="language-text">Atlas 모아보기</code>, <code class="language-text">Permission 권한</code>은 실제로 삭제(hard delete)합니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">blockMember</span><span class="token punctuation">(</span><span class="token class-name">Long</span> memberId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">Member</span> member <span class="token operator">=</span> <span class="token function">findMemberById</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
member<span class="token punctuation">.</span><span class="token function">updateStatus</span><span class="token punctuation">(</span><span class="token class-name">Status</span><span class="token punctuation">.</span><span class="token constant">BLOCKED</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">deleteAllRelated</span><span class="token punctuation">(</span>member<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">deleteAllRelated</span><span class="token punctuation">(</span><span class="token class-name">Member</span> member<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Long</span><span class="token punctuation">></span></span> pinIds <span class="token operator">=</span> <span class="token function">extractPinIdsByMember</span><span class="token punctuation">(</span>member<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Long</span> memberId <span class="token operator">=</span> member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
permissionRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
atlasRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
bookmarkRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
pinImageRepository<span class="token punctuation">.</span><span class="token function">deleteAllByPinIds</span><span class="token punctuation">(</span>pinIds<span class="token punctuation">)</span><span class="token punctuation">;</span>
pinRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
topicRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<p>아래와 같이 동작하기를 기대했습니다.</p>
<ol>
<li>차단할 회원에 대한 정보 조회</li>
<li>회원의 상태를 차단으로 변경하는 update 쿼리 발생</li>
<li>매핑 테이블 엔티티들을 먼저 삭제해야 함, 이 때 delete 쿼리 발생</li>
<li>주요 도메인 엔티티들을 삭제 상태로 변경하는 update 쿼리 발생</li>
</ol>
<h3>예상과 다른 영속 동작</h3>
<p>테스트 코드는 아래와 같습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token annotation punctuation">@DisplayName</span><span class="token punctuation">(</span><span class="token string">"Member를 차단(탈퇴시킬)할 경우, Member가 생성한 지도, 핀, 핀 이미지를 삭제 상태(soft delete)로 변경한다."</span><span class="token punctuation">)</span>
<span class="token annotation punctuation">@Test</span>
<span class="token keyword">void</span> <span class="token function">blockMember_Success</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">//given</span>
<span class="token comment">// ... </span>
<span class="token comment">// 객체 생성, 저장 및 기존 상태 검증 코드 생략</span>
<span class="token comment">// ...</span>
<span class="token comment">//when</span>
adminCommandService<span class="token punctuation">.</span><span class="token function">blockMember</span><span class="token punctuation">(</span>member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">//then</span>
<span class="token class-name">Member</span> blockedMember <span class="token operator">=</span> memberRepository<span class="token punctuation">.</span><span class="token function">findById</span><span class="token punctuation">(</span>member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">assertAll</span><span class="token punctuation">(</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>blockedMember<span class="token punctuation">.</span><span class="token function">getStatus</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isEqualTo</span><span class="token punctuation">(</span><span class="token class-name">Status</span><span class="token punctuation">.</span><span class="token constant">BLOCKED</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 실패</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>bookmarkRepository<span class="token punctuation">.</span><span class="token function">existsByMemberIdAndTopicId</span><span class="token punctuation">(</span>member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> topic<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 실패</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>atlasRepository<span class="token punctuation">.</span><span class="token function">existsByMemberIdAndTopicId</span><span class="token punctuation">(</span>member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> topic<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// 실패</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>permissionRepository<span class="token punctuation">.</span><span class="token function">existsByTopicIdAndMemberId</span><span class="token punctuation">(</span>topic<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// 실패</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>topicRepository<span class="token punctuation">.</span><span class="token function">existsById</span><span class="token punctuation">(</span>topic<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>pinRepository<span class="token punctuation">.</span><span class="token function">existsById</span><span class="token punctuation">(</span>pin<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token function">assertThat</span><span class="token punctuation">(</span>pinImageRepository<span class="token punctuation">.</span><span class="token function">existsById</span><span class="token punctuation">(</span>pinImage<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isFalse</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<p>하지만 테스트가 실패해 로그를 보니, 실제 동작은 아래와 같았습니다. 😧😧</p>
<ol>
<li>차단할 회원에 대한 정보 조회</li>
<li><del>회원의 상태를 차단으로 변경하는 update 쿼리 발생</del></li>
<li><del>매핑 테이블 엔티티들을 먼저 삭제해야 함, 이 때 delete 쿼리 발생</del></li>
<li>주요 도메인 엔티티들을 삭제 상태로 변경하는 update 쿼리 발생</li>
</ol>
<p><code class="language-text">// when</code> 절의 코드를 호출한 뒤 entityManager로 flush, clear를 해주어도 마찬가지였습니다. </p>
<h2>원인</h2>
<p>이전에는 잘 통과하던 테스트인데, 왜 갑자기 예상과 다르게 작동할까요?<br>
JPA 영속성 컨텍스트의 쓰기 지연 때문이었습니다. </p>
<ol>
<li>hard delete 메서드를 통해 해당 id를 가진 엔티티를 영속성 컨텍스트에서 제거한다.</li>
<li>soft delete 메서드를 통해 update를 호출하면서, 연관된 엔티티들이 모두 영속화된다.<br>
➡️ (1)에서 delete 해도, 2에서 조회할 때 함께 불러와지는 <code class="language-text">member</code>, <code class="language-text">permission</code>, <code class="language-text">atlas</code>, <code class="language-text">bookmark</code>까지 다시 영속화된다. </li>
<li>영속성 컨텍스트에는 <code class="language-text">(차단 상태가 아닌)member</code>, <code class="language-text">permission</code>, <code class="language-text">atlas</code>, <code class="language-text">bookmark</code>가 존재한다.</li>
<li><code class="language-text">blockMember()</code> 호출 후 flush를 할 때, 영속성 컨텍스트의 상태를 기준으로 쿼리가 발생한다.<br>
➡️ member의 차단 상태에 대한 변경 감지도 되지 않고, delete 쿼리도 나가지 않는다.</li>
</ol>
<p>그래서 <code class="language-text">blockMember()</code> 메서드를 호출한 뒤 flush를 해줘도 소용이 없었던 것입니다. </p>
<h2>해결</h2>
<p>단순히 해결부터 해보자면,<br>
soft delete 메서드로 인한 영속화가 되기 이전에 flush해서 member의 변경, hard delete에 대한 쿼리를 발생시키면 됩니다. </p>
<h3>flush로 쿼리 발생 시점 조정하기</h3>
<p>아래 처럼 서비스 단에서 해주거나, <code class="language-text">@Modifying</code> 어노테이션을 사용할 수 있습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">deleteAllRelated</span><span class="token punctuation">(</span><span class="token class-name">Member</span> member<span class="token punctuation">)</span> <span class="token punctuation">{</span>
permissionRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
atlasRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
bookmarkRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// flush!</span>
bookmarkRepository<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
pinImageRepository<span class="token punctuation">.</span><span class="token function">deleteAllByPinIds</span><span class="token punctuation">(</span>pinIds<span class="token punctuation">)</span><span class="token punctuation">;</span>
pinRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
topicRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<h2>문제 상황 2 : 테스트에서 발생하지 않는 일부 쿼리</h2>
<p>그런데도 테스트는 성공하지 않았습니다. 🥲<br>
<code class="language-text">delete bookmark~</code> 쿼리는 여전히 발생하지 않고 있었습니다. </p>
<p>테스트 메서드와 <code class="language-text">blockMember()</code>는 같은 트랜잭션으로 묶이기 때문에 <code class="language-text">// when</code>절에서의 영속화 상태 때문일 것이라 짐작했습니다.<br>
<code class="language-text">bookmark</code>를 참조하는 <code class="language-text">topic</code>, 또는 <code class="language-text">member</code> 객체가 영속화되어있기 때문에 delete 쿼리가 나가지 않는 것이라 생각했습니다. </p>
<p>그래서 이에 대해서는 <code class="language-text">blockMember()</code> 호출 전에 <code class="language-text">testEntityManager.clear()</code>로 영속성 컨텍스트를 초기화해두는 것으로 해결했습니다. </p>
<p><strong>하지만 정확한 원인이 궁금했습니다.
왜 <code class="language-text">atlas</code>, <code class="language-text">permission</code>은 삭제되고 <code class="language-text">bookmark</code>만 삭제되지 않았을까요?</strong> </p>
<h2>원인</h2>
<p>다른 매핑 테이블 엔티티들과 <code class="language-text">bookmark</code>의 차이점을 살펴보았습니다.<br>
해당 엔티티만이 <code class="language-text">topic</code>과의 연관 관계에서 <code class="language-text">CasecadeType.PERSIST</code>가 걸려 있었습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token comment">// Topic.class</span>
<span class="token annotation punctuation">@OneToMany</span><span class="token punctuation">(</span>mappedBy <span class="token operator">=</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> cascade <span class="token operator">=</span> <span class="token class-name">CascadeType</span><span class="token punctuation">.</span><span class="token constant">PERSIST</span><span class="token punctuation">,</span> orphanRemoval <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token keyword">private</span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Bookmark</span><span class="token punctuation">></span></span> bookmarks <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<p>테스트 메서드의 트랜잭션 내에서, 영속성 컨텍스트에 존재하는 <code class="language-text">topic</code>에 <code class="language-text">bookmark</code>가 살아있기 때문에<br>
<code class="language-text">PERSIST OPERATION</code>이 발생하고 <code class="language-text">bookmark</code>에 대한 delete 쿼리는 무시됩니다. </p>
<blockquote>
<p><a href="https://download.oracle.com/otndocs/jcp/persistence-2_2-mrel-eval-spec/index.html">JPA 2.2 specification</a> 문서 3.2 장 Entity Instance's Life Cycle에 따르면,<br>
flush가 발생할 때 <code class="language-text">CascadeType.PERSIST</code>나 <code class="language-text">CascadeType.ALL</code>이 있을 경우 자식에 연쇄적으로 <code class="language-text">PERSIST OPERATION</code>이 발생합니다.
<code class="language-text">PERSIST OPERATION</code>은 연관 관계 매핑된 list의 엔티티에 대해 모두 이루어지며, 기존에 없던 값이면 새로 저장합니다.</p>
</blockquote>
<h2>해결</h2>
<p>이에 대해서 엔티티에 걸어놓은 조건에 따라 정상적으로 동작하도록 하려면,<br>
<code class="language-text">bookmarkRepository</code>를 통해 delete 쿼리를 호출하는 대신 연관 관계를 제거하는 방식으로 삭제해주었어야 했던 것입니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"> <span class="token comment">// Topic.class</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">removeBookmarkBy</span><span class="token punctuation">(</span><span class="token class-name">Member</span> member<span class="token punctuation">)</span> <span class="token punctuation">{</span>
bookmarks<span class="token punctuation">.</span><span class="token function">stream</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>bookmark <span class="token operator">-></span> bookmark<span class="token punctuation">.</span><span class="token function">getMember</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">isSame</span><span class="token punctuation">(</span>member<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">findFirst</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">ifPresent</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token operator">::</span><span class="token function">removeBookmark</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
bookmarkCount<span class="token operator">--</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// AdminCommandService.class</span>
<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">deleteAllRelatedMember</span><span class="token punctuation">(</span><span class="token class-name">Member</span> member<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Long</span><span class="token punctuation">></span></span> pinIds <span class="token operator">=</span> <span class="token function">extractPinIdsByMember</span><span class="token punctuation">(</span>member<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token class-name">Long</span> memberId <span class="token operator">=</span> member<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
permissionRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
atlasRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 변경된 부분</span>
topicRepository<span class="token punctuation">.</span><span class="token function">findTopicsByBookmarksMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span>topic <span class="token operator">-></span> topic<span class="token punctuation">.</span><span class="token function">removeBookmarkBy</span><span class="token punctuation">(</span>member<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
atlasRepository<span class="token punctuation">.</span><span class="token function">flush</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
pinImageRepository<span class="token punctuation">.</span><span class="token function">deleteAllByPinIds</span><span class="token punctuation">(</span>pinIds<span class="token punctuation">)</span><span class="token punctuation">;</span>
pinRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
topicRepository<span class="token punctuation">.</span><span class="token function">deleteAllByMemberId</span><span class="token punctuation">(</span>memberId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre></div>
<p>아래와 같이 <code class="language-text">bookmark</code>에 대한 삭제 로직을 수정하니,<br>
테스트 코드에서 별도의 <code class="language-text">clear</code>를 호출하지 않아도 잘 통과하는 것을 확인할 수 있었습니다.</p>
<h2>결론</h2>
<p>이번 삽질을 계기로 JPA를 잘 학습한 뒤 사용해야 한다는 교훈을 다시 한 번 몸소 느꼈습니다.. </p>
<p>엔티티의 생명 주기에 대해 잘 이해하고 객체를 생성 및 삭제해야한다는 것, 삭제할 때에도 연관 관계의 관리가 중요하다는 것을 알았습니다.
이처럼 예상하지 못한 동작을 피하기 위해 삭제 로직에서도 연관 관계 편의 메서드를 정의하는 방식으로 코드를 잘 작성할 필요가 있어 보입니다. </p>
<h2>참고 자료</h2>
<ul>
<li><a href="https://velog.io/@jsb100800/spring-12">[Spring boot] JPA Delete is not Working, 영속성와 연관 관계를 고려했는가.</a> </li>
<li><a href="https://joont92.github.io/jpa/CascadeType-PERSIST%EB%A5%BC-%ED%95%A8%EB%B6%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0/">[jpa] CascadeType.PERSIST를 함부로 사용하면 안되는 이유</a></li>
</ul></content:encoded></item><item><title><![CDATA[HikariCP 적용기]]></title><description><![CDATA[이 글은 우아한테크코스 괜찮을지도팀의 가 작성했습니다. 배경 CPU…]]></description><link>https://map-befine-official.github.io/how-to-hikariCP/</link><guid isPermaLink="false">https://map-befine-official.github.io/how-to-hikariCP/</guid><pubDate>Tue, 10 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우아한테크코스 괜찮을지도팀의 <code class="language-text">쥬니</code>가 작성했습니다.</p>
</blockquote>
<h3>배경</h3>
<p>CPU 코어가 하나인 컴퓨터라도 수십 또는 수백 개의 쓰레드를 <code class="language-text">동시에</code> 지원할 수 있습니다.
<br> 하지만, 실제로 하나의 코어는 하나의 쓰레드만 실행할 수 있습니다.
<br> <code class="language-text">쓰레드 스케줄링</code>을 통해, 하나의 코어에서도 여러 개의 쓰레드를 동시에 지원할 수 있는 것이죠.</p>
<p>물론, 두 개의 쓰레드 A, B가 존재할 때 이를 순차적으로 실행하는 것이 성능적으로는 더 빠릅니다.
<br> <code class="language-text">Context Switching Overhead</code>가 없기 때문이죠 !</p>
<p>그렇다면, 어플리케이션과 관련된 쓰레드의 개수를 설정할 때, 코어의 개수와 동일하게 가져가면 될까요 ?
<br> <strong>결론부터 이야기하면, 그렇지 않습니다 !</strong>
<br> 실제 어플리케이션에서는 데이터를 저장하고 있는 <code class="language-text">디스크</code>를 고려해야 하기 때문입니다.</p>
<p>어플리케이션에서 사용하는 데이터베이스에는 디스크가 존재합니다.
<br> 데이터를 읽기 위해서는, 물리적인 <code class="language-text">Disk Arm과 Spindle</code>이 동작합니다.
<br> 원하는 데이터를 찾기 위해 위와 같은 동작을 수행하며, 이때 드는 비용은 상당히 큽니다.
<br> 이 시간을, <code class="language-text">I/O 대기 시간</code>이라고 부르며, 해당 시간 동안 쓰레드들은 <code class="language-text">waiting</code> 상태로 대기하게 됩니다.</p>
<p>그렇기 때문에, I/O 대기 시간으로 인해 waiting 상태인 쓰레드가 생기게 됩니다.
<br> 즉, 일하지 않고 놀게 되는 코어가 생기게 됩니다.</p>
<p>그렇다면, 데이터베이스 Connection과 관련된 <code class="language-text">Thread Pool</code>의 크기는 어느 정도로 관리하는 게 좋을까요 ?</p>
<h3>Hikari Connection Pool</h3>
<p><code class="language-text">HikariCP</code> 공식 문서에서는 아래와 같은 추천 공식을 제공하고 있습니다.</p>
<p><code class="language-text">Connection Pool Size = (Core count * 2) + Effective spindle count</code></p>
<p>위 공식에서, <code class="language-text">Effective spindle count</code>의 값은 사실상 하드디스크의 개수와 같다고 보면 됩니다.
<br> 디스크 1개당, 1개의 물리적 spindle을 가지고 있기 때문이죠.</p>
<p><code class="language-text">괜찮을지도</code> 서비스의 운영 서버 EC2 환경은 2개의 코어와 1개의 디스크를 사용하고 있습니다.
<br> 이를 위 공식에 대입해 보면, <code class="language-text">Connection Pool Size = 2 * 2 + 1 = 5</code></p>
<p><strong>즉, <code class="language-text">HikariCP</code>에서 추천하는 커넥션 풀 사이즈는 5가 나오게 됩니다.</strong></p>
<h3>성능 테스트</h3>
<p><code class="language-text">HikariCP</code>에서 추천하는 <code class="language-text">괜찮을지도</code> 서비스의 커넥션 풀 사이즈는 <code class="language-text">5</code>임을 알 수 있었습니다.
<br> 하지만, 이는 단순히 이론적인 내용일 뿐 맹신할 수는 없습니다.
<br> 그래서, <code class="language-text">JMETER</code>를 이용한 성능 테스트를 수행하였습니다.
<br> 테스트에서는 다른 설정을 동일하게 두고, 커넥션 풀 사이즈만 변경하며 진행했습니다.</p>
<p>Connection Pool Size는 아래와 같이 yml 파일을 작성하여 수정할 수 있습니다.</p>
<div class="gatsby-highlight" data-language="yaml"><pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">spring</span><span class="token punctuation">:</span>
<span class="token key atrule">datasource</span><span class="token punctuation">:</span>
<span class="token key atrule">hikari</span><span class="token punctuation">:</span>
<span class="token key atrule">maximum-pool-size</span><span class="token punctuation">:</span> <span class="token number">5</span></code></pre></div>
<p><strong>- Connection Pool Size 10</strong>
<br> 기본 설정값인 10으로 설정한 뒤, 테스트를 수행하였습니다.
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/10a26b46520773c5069bbf584dacfc38/90f05/connection_pool_size_10.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 47.64705882352941%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABkUlEQVR42o2R3XKbMBCFef+Hyk1nOpM06bSOsZ1icOyMkzgIIYEkJP5Od8GxL3rRMPPNWS1nj4SI0sMR6TbF29s7CllCiAKikP+HfWc+CKk0lvEaUd8P4GccR4ysX2Bg73hd943nJo6vr4isD/Bthya0k36FhpXnnIe3DVypMRiL/f6AyPkeLvSwvpv0X6g/eQa4dpi9LsCpGq52hIVpWnT0bpft6YQNGahhXEvaXXFnppre2QCrLawyMLKae76fqMnTEln6jEjbFowypGauL1CgrhpURYVK1qiUg2ZMOHvCNFPWdGI/IEkokIMUNUozh3KteIPSQOUaisIuvbOHA5QJl7WsPAz9l6eU7lDQjpIMhTSQpUVRtyh0AymqCe4XNMA9Vh7+1E+Eoq9wPTYJ3eFJOojSIT8piPcSOTHpRwVB4dJ0FBBo0w5CB8Ijp4BcXfVE88oOWP2hwN+rFMvNDovNMx7XOzzGWyzWGeLtC+5/rXH7sMT3uwV+cP0zxip5Ac+skgNpNmuc4ik74ubbHf4CH4H7TrRWz6MAAAAASUVORK5CYII='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='connection_pool_size_10.png' title='' src='/static/10a26b46520773c5069bbf584dacfc38/ca1dc/connection_pool_size_10.png' srcset='/static/10a26b46520773c5069bbf584dacfc38/e7570/connection_pool_size_10.png 170w,
/static/10a26b46520773c5069bbf584dacfc38/f46e7/connection_pool_size_10.png 340w,
/static/10a26b46520773c5069bbf584dacfc38/ca1dc/connection_pool_size_10.png 680w,
/static/10a26b46520773c5069bbf584dacfc38/90f05/connection_pool_size_10.png 917w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>connection_pool_size_10.png</figcaption>
</figure></p>
<p><strong>- Connection Pool Size 5</strong>
<br> HikariCP에서 제공하는 공식을 통해 도출된 값으로 설정한 뒤, 테스트를 수행하였습니다.
<figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/8128bf50f73117b45859fedc872bdb32/3fcec/connection_pool_size_5.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 47.05882352941176%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABVUlEQVR42l2R2XajQBBD+f9PnAzBxomTGJql9wVHoy7MHCcP96haJRUPNLfLG+ZVY9UGMSaUsgm5lJ+aH7pVj5ntjlzzPiCTFCISM402lssN9+/vPXSEn1UO3fe384Icsg6lwrlUnx9ovoYRPmaEVISYN+GYw/Ob5eACgo+I1GMfyh0xJPGbtuvh0wYbiuBCJnWmVt8luMXCzwbOeNi4CS5uj/yj5yKc5cG/3QXGF5KhWTYuy1tX1R5GaRgG9eEfO6Kl98AwowOaP+2ZgYLFZswMLTZhnS2WkT9qdljpL4dvd/0/m12FNWBZPZrz+wC1Rkw0JmUwDQumyUItHhMLSnNHnvW3J9Q+e8110Bh4UM0eSvGQ5hGTMVLHNfFopneQhOqP7PzQumO/afsvvF4GdO8K5+uMlvP5OtG7of+Y8XL6lPnl9IG2v6F7G9Ex00tmkMyu+/sf6Qu0us/J3qoAAAAASUVORK5CYII='); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='connection_pool_size_5.png' title='' src='/static/8128bf50f73117b45859fedc872bdb32/ca1dc/connection_pool_size_5.png' srcset='/static/8128bf50f73117b45859fedc872bdb32/e7570/connection_pool_size_5.png 170w,
/static/8128bf50f73117b45859fedc872bdb32/f46e7/connection_pool_size_5.png 340w,
/static/8128bf50f73117b45859fedc872bdb32/ca1dc/connection_pool_size_5.png 680w,
/static/8128bf50f73117b45859fedc872bdb32/3fcec/connection_pool_size_5.png 914w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>connection_pool_size_5.png</figcaption>
</figure></p>
<p>유의미한 TPS 차이를 통해, 커넥션 풀 사이즈가 5일 때의 성능이 좋다는 것을 확인할 수 있었습니다.
<br> 물론, 트래픽 양에 따라, 다른 값을 적용하였을 때가 더 최적의 성능을 나타낼 수 있습니다.</p>
<p><code class="language-text">괜찮을지도</code> 서비스에서는 성능 테스트 목표치량을 기준으로 테스트를 수행하였기 때문에, 위와 같은 결과가 도출되었습니다.</p>
<h3>마치며</h3>
<p><strong>위에서도 이야기했지만, 공식을 통해 도출된 값이 항상 최적인 것은 아닙니다.</strong>
<br><strong>각 서비스의 특성에 맞게, 커넥션 풀 사이즈를 설정해 보시면서 테스트를 수행하여 최적의 값을 도출해 내시길 바랍니다.</strong></p>
<p>또한, 커넥션 풀 외에도 HikariCP에서 튜닝해야할 설정을 추천하고 있습니다.
<br> 이 부분도 <a href="https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration">참고</a>하면 좋을 것 같습니다.</p>
<h3>참고</h3>
<p><a href="https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing">HikariCP docs</a>
<br><a href="https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration">HikariCP MySQL Configuration</a></p></content:encoded></item><item><title><![CDATA[Fetch join 사용 시 MultipleBagFetchException의 발생 이유와 해결 방법]]></title><description><![CDATA[이 글은 우테코 괜찮을지도팀의 가 작성했습니다. N+1과 Fetch join 백엔드에서는 조회 API로 여러 연관 관계로 인해 발생하는 N+1 문제를 해결해야 했는데요. 예를 들면 장소(이하 핀) 다건 조회의 경우, 각 핀이 속한 지도(topic…]]></description><link>https://map-befine-official.github.io/jpa-multibag-fetch-exception/</link><guid isPermaLink="false">https://map-befine-official.github.io/jpa-multibag-fetch-exception/</guid><pubDate>Wed, 04 Oct 2023 00:00:00 GMT</pubDate><content:encoded><blockquote>
<p>이 글은 우테코 괜찮을지도팀의 <code class="language-text">도이</code>가 작성했습니다.</p>
</blockquote>
<h2>N+1과 Fetch join</h2>
<p>백엔드에서는 조회 API로 여러 연관 관계로 인해 발생하는 N+1 문제를 해결해야 했는데요.<br>
예를 들면 장소(이하 핀) 다건 조회의 경우, 각 핀이 속한 지도(topic), 위치, 생성자, 핀 이미지들을 모두 별도로 조회하는 심각한 문제가 있었습니다.<br>
지도(topic) 조회 시에도 생성자, 권한 정보, 즐겨찾기 목록을 매번 불러왔고요. </p>
<p><figure class='gatsby-resp-image-figure' style='margin-bottom: 16px;'>
<span class='gatsby-resp-image-wrapper' style='position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 680px; '>
<a class='gatsby-resp-image-link' href='/static/d27c9c172301ef1549f5e11026dc75e9/9ae5c/img.png' style='display: block' target='_blank' rel='noopener'>
<span class='gatsby-resp-image-background-image' style="padding-bottom: 122.94117647058822%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEmElEQVR42k1VaW/bRhDl//8VAQoE/VAURe0ARpMYjWzrjG5RFEWK96Wbknjo9OvMMlQtYERid+fN9fZRCjcJHt9GeHjtoTL2hf1QAjRUH5frFS9DB5+fe3jsmHjo2GhqAa60Xh3Z+P1rAw/VIb4NPbwpLi7nC6RlfEB9oOL15wBv3TG6qgV7eYC72uF0PsOPIrTaHYwVFabtYLXe4na7wLBdVNsD6G6IbX7Fap/hdDpByvIMk4kMQ9dgznS4toX395vI4nq5YB+v4bou4niLI5290FqSpvCDALZlwKHznmNjMY/EnpTnKQxDg+8H2Gw2mC8WBPiOM23GcQzPsjCdTjEajbDdbnE4HBDvdvDCkBKZQNM0dLtdzOdzyvwGKckyzEwqkyJZ5KzrOhzHwX6/F6DhcglVVcmZjJ4clNdNOtMfDDEcyXA9H0EQ4ng8QorJcSDLGPR76FGkfr+Peq0GWR6JiKvNGp7rwCWANEnEGvfK9OYYaTaGUxN91cBYt4uSo+UazdEU9f5YWIOs1hujM1KFo6LqqNSaeK5U8W+1gYluCEduC4Pzs3znvkvlIu4bxaZYJzsSFaLVBhw4mK+QZLlwZNCPdhZPAowPR7iLLaxohflmX0z3ShncrrCjGI2xiYE1R1v3Mfa3kJ0lDgTKwRioOP+/SYoV4rcvL/jrpYe66lFjT4Iu7CAT0N8/WniqD/EmW+jZK/StBeJDegfkCvLTGSn55czD/XoOXVXgEKcWUYgdUSKjyXPJ+90Gtmlgs1oipuEkRBn+8TTLSmpDE5++vOKp76KlB5Asx8PP/giVegt9eYIzRWPAhCbqhz66vR6mmk5c1ARlrhSI+ZkSuZlaYeAJ2kWLJTbbGNIuSWH5c1hehMUmLm4CgeV5jnW8Q0SE9X1fcHRJnOTMGYyzZJLrxM1Op4NavQ7TNCHxZC90Z89UP0+aAcuSfbo1PcqQb4lHoGc6x+v3kumsHS4hGy4GmoWZQyXzBh9gkDIq91GUTMLAUfmKLQicf3ye97mCNEtxYnACLmknleNmQCYyZ8HAvLmlXvGV9Egc2DgIT5erKcnMPhmBl3yUrkTG27UoIyd+5QScUY84okEgrU6X+tMsrNFElZ48jNsv2jAgZ3vnYZzk+LMyxOd/avg6cPFN9vHYNvCTKBAu1lDMABPLx8T0IM8cKIZH8pWJTAuZo1tyLjIWgMdTju5wiFZ3ANOPoDs+NocccUKDoQOcKf+K8o73Pp6I0IttAod8TGLIcpciJ3JLrMqsZb7nCUWxiMhlb3hA3Nv0Fy+ZLgzG2W33Kf54buPTYwVPbQ3fZapovSPFJscxCyhl2W63hVgmH2TqfvmZWh8sownbjkVqTp+F5YISMUVAie9fQBmyzDNxmR7swJlws/mdAflwSS1e59uiUyKKopB2yoL8vC4AWX1Zyj1SXv5W8MaeymUQfi95WqoLZ89VKMoEPVLtMam5G7AO7AtA3ZgJtWZQ7mVA0dar1V0XSyt0kAGvYtIqTX9Mk2frT2b0rSFA/mvLGpoDBU1S6+5YQ2swgawZ4gaUJZfP8r3MtFTskjb/AeCRUAJcR56vAAAAAElFTkSuQmCC'); background-size: cover; display: block;"></span>
<img class='gatsby-resp-image-image' alt='img.png' title='' src='/static/d27c9c172301ef1549f5e11026dc75e9/ca1dc/img.png' srcset='/static/d27c9c172301ef1549f5e11026dc75e9/e7570/img.png 170w,
/static/d27c9c172301ef1549f5e11026dc75e9/f46e7/img.png 340w,
/static/d27c9c172301ef1549f5e11026dc75e9/ca1dc/img.png 680w,
/static/d27c9c172301ef1549f5e11026dc75e9/9ae5c/img.png 751w' sizes='(max-width: 680px) 100vw, 680px' style='width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;' loading='lazy' decoding='async'>
</a>
</span>
<figcaption class='gatsby-resp-image-figcaption'>img.png</figcaption>
</figure></p>
<p>따라서 함께 가져올 연관 관계들에 대해 <code class="language-text">Fetch join</code>을 적용하는 방식으로 문제에 접근했습니다. </p>
<blockquote>
<p><code class="language-text">Fetch join</code>은 JPQL에서 성능 최적화를 위해 제공하는 기능입니다. </p>
</blockquote>
<h3>@EntityGraph</h3>
<p><code class="language-text">Fetch join</code>을 위한 방법으로는, <code class="language-text">@EntityGraph</code>를 선택했습니다.<br>
JPQL에 직접 하드코딩으로 쿼리를 작성하는 것보다, 가독성 및 유지보수성에 더 좋다고 판단했기 때문입니다.</p>
<p><code class="language-text">@EntityGraph</code>는, 객체를 로딩할 때 런타임 성능을 개선하기 위해 JPA 2.1에서 도입된 기능입니다.<br>
JPA 2.0 이전까지는 <code class="language-text">FetchType</code>으로만 로딩 전략을 지정할 수 있었지만<br>
이를 통해 객체의 연관 관계 중 그래프로 연결할 것들을 템플릿으로 정의하고, 런타임에 선택할 수 있습니다.<br>
원하는 필드들만 쉽게 Fetch join 시킬 수 있는 것이죠.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token comment">// PinRepository.class</span>
<span class="token comment">// 해당 쿼리로 데이터를 읽을 때, Topic의 필드로 존재하는 "location", "topic", "creator", "pinImages"를 Fetch join으로 불러옵니다.</span>
<span class="token annotation punctuation">@EntityGraph</span><span class="token punctuation">(</span>attributePaths <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">"location"</span><span class="token punctuation">,</span> <span class="token string">"topic"</span><span class="token punctuation">,</span> <span class="token string">"creator"</span><span class="token punctuation">,</span> <span class="token string">"pinImages"</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Pin</span><span class="token punctuation">></span></span> <span class="token function">findAllByCreatorId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<h2>문제 상황</h2>
<p><code class="language-text">@EntityGraph</code>는 동적으로 생성되므로<br>
해당 어노테이션에 작성한 설정이 잘못 되었어도, 컴파일 타임이 아니라 해당 쿼리를 사용하는 런타임 시점에 예외가 발생합니다. </p>
<p>여러 필드들을 <code class="language-text">attributePaths</code>에 넣어본 뒤, 로컬 환경에서 API를 호출하며 쿼리 개수 및 지연 시간을 확인하던 중<br>
아래와 같은 오류가 발생했습니다.</p>
<div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
[com.mapbefine.mapbefine.topic.domain.Topic.bookmarks, com.mapbefine.mapbefine.topic.domain.Topic.permissions]</code></pre></div>
<p>호출된 쿼리는 아래와 같았습니다.</p>
<div class="gatsby-highlight" data-language="java"><pre class="language-java"><code class="language-java"><span class="token comment">// TopicRepository.class</span>
<span class="token annotation punctuation">@EntityGraph</span><span class="token punctuation">(</span>attributePaths <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token string">"creator"</span><span class="token punctuation">,</span> <span class="token string">"permissions"</span><span class="token punctuation">,</span> <span class="token string">"bookmarks"</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Topic</span><span class="token punctuation">></span></span> <span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div>
<h2>MultipleBagFetchException의 발생 이유</h2>
<p>간단하게 이야기하면 <strong><code class="language-text">OneToMany</code> 관계를 1개보다 더 많이 Fetch join</strong> 하려고 했기 때문에 발생한 문제입니다.<br>
"permissons"와 "bookmarks"를 동시에 Fetch join할 수 없습니다.</p>
<h3>Fetch join과 카테시안 곱</h3>
<p>Fetch join은 <code class="language-text">*ToOne</code> 관계에는 개수 제한이 없지만, <code class="language-text">*ToMany</code> 관계를 1개만 사용할 수 있습니다.<br>
fech join을 여러 개의 컬렉션에 적용한다면, 카테시안 곱에 의해 중복 데이터가 발생하기 때문입니다.<br>