ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

๋ฐ˜์‘ํ˜•

๋ชฉ์ฐจ

    MyBatis์˜ ResultHandler๋ฅผ ํ†ตํ•ด POI SXSSF๋กœ ๋Œ€์šฉ๋Ÿ‰ ์—‘์…€ํŒŒ์ผ ๋งŒ๋“ค๊ธฐ

    ๊ฐœ๋ฐœํ™˜๊ฒฝ

    • SpringBoot 2.6.2
    • JAVA 1.8
    • Gradle 7.3.2
    • IntelliJ
    ์ •์‚ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์—‘์…€ํŒŒ์ผ๋กœ ๋งŒ๋“œ๋Š” ๊ณผ์ •์—์„œ ์ ์€ ๊ฑด์ˆ˜๋ฅผ ๋งŒ๋“ค๋•Œ๋Š” ์ด์Šˆ๊ฐ€ ์—†์—ˆ์ง€๋งŒ ๋ช‡์‹ญ๋งŒ ๊ฑด ๋˜๋Š” ๋ช‡๋ฐฑ๋งŒ๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด์„œ OOM์„ ๋งŒ๋‚˜๊ฒŒ ๋˜์–ด ์—‘์…€ํŒŒ์ผ๋กœ ์ €์žฅ์„ ํ•  ์ˆ˜๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. MyBatis๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด MyBatis์—์„œ ์ œ๊ณตํ•˜๋Š” ResultHandler๋ฅผ ์ด์šฉํ•˜๋ฉด ๋ช‡๋ฐฑ๋งŒ๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  ๋ ˆ์ฝ”๋“œ๋ณ„๋กœ ๋‹ค๋ฃฐ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    https://mybatis.org/mybatis-3/ko/java-api.html#SqlSession (์•„๋ž˜ ResultHandler ์ฐธ๊ณ )
    ๋‹ค์Œ์€ ๊ฐ€์ง€๊ณ  ์˜จ ๋ฐ์ดํ„ฐ์˜ ์ €์žฅ์€ Apache POI SXSSF ๋ฐฉ์‹์„ ์ด์šฉํ•˜์—ฌ ์—‘์…€ํŒŒ์ผ์— ์ €์žฅํ•˜์˜€์Šต๋‹ˆ๋‹ค. SXSSF๋Š” Streaming๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๊ณ„์† ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๊ณ  ์ž„์‹œ ํŒŒ์ผ์— ๊ธฐ๋กํ•œ ํ›„ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋น„์›Œ๋‚ด๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ž„์‹œํŒŒ์ผ์€ ๊ธฐ๋ณธ ๊ฒฝ๋กœ๋Š” ๋ฆฌ๋ˆ…์Šค ์„œ๋ฒ„ ๊ธฐ์ค€์œผ๋กœ /var/tmp/poi??? ๋งฅ๋ถ๊ธฐ์ค€ ํ„ฐ๋ฏธ๋„์—์„œ echo $TMPDIR๋ฅผ ์ณ์„œ ์ž„์‹œ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋กœ ๋“ค์–ด๊ฐ€์„œ poi??? ๋””๋ ‰ํ† ๋ฆฌ ๋“ค์–ด๊ฐ€์„œ ํ™•์ธํ•ด๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ž„์‹œํŒŒ์ผ ์šฉ๋Ÿ‰์€ ๋ฐ์ดํ„ฐ ์••์ถ•์˜ต์…˜์„ ์•ˆ์ฃผ์—ˆ์„ ๊ฒฝ์šฐ 100๋งŒ๊ฑด๋‹น 2G๊ฐ€ ์ •๋„์˜€๊ณ  ์••์ถ•์˜ต์…˜์„ ์ฃผ์—ˆ์„ ๊ฒฝ์šฐ 256M๊ฐ€ ์ •๋„์˜€์Šต๋‹ˆ๋‹ค. ์••์ถ•ํ•˜๋ฉด ์—‘์…€๋งŒ๋“ค์–ด์ง€๋Š” ์†๋„๊ฐ€ ๋Š๋ฆด๊ฒƒ ๊ฒƒ๊ฐ™์•˜์ง€๋งŒ ๊ฑฐ์˜ ์ฐจ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๊ธฐ์ค€์œผ๋กœ ์šฉ๋Ÿ‰ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ• ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž„์‹œํŒŒ์ผ์ด ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒฝ๋กœ๋„ ์ง€์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    https://poi.apache.org/components/spreadsheet/

    POI ์“ฐ๊ธฐ ํ™•์ธ

    HSSF ์—‘์…€ ์ด์ „๋ฒ„์ ผ(.xls) 65535๋ผ์ธ๊นŒ์ง€ ๊ฐ€๋Šฅ
    XSSF ์—‘์…€ 2007 ์ด์ƒ๋ฒ„์ ผ(.xls)  65535๋ผ์ธ ์ด์ƒ ๊ฐ€๋Šฅ
    SXSSF XSSF์˜ Streaming Usermodel API 65535๋ผ์ธ ์ด์ƒ ๊ฐ€๋Šฅ

    ์—‘์…€ ์‹œํŠธ ์ตœ๋Œ€ ํ–‰์ˆ˜ ํ™•์ธ

    xls 65535 ํ–‰
    xlsx 1,048,576 ํ–‰

    MyBatis์˜ ResultHandler ์…‹ํŒ…

    TestDao

    @RequiredArgsConstructor
    @Repository
    public class TestDAOImpl implements TestDAO {
    
        private final SqlSessionTemplate sqlSessionTemplate;
    
        @Override
        public boolean createExcelByTmpTableMap(TmpTableSchCmd tmpTableSchCmd, TestExcelHandler testExcelHandler) {
            sqlSessionTemplate.select("com.sample.batch.dao.TestDAO.selectMapOfTmpTable", tmpTableSchCmd, testExcelHandler);
            testExcelHandler.createExcelByTmpFile();
            return testExcelHandler.isSuccess();
        }
    }

    mybatis sqlsession

    ์šฐ์„  MyBatis์—์„œ ResultHandler๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ sqlsessionTemplate์˜ selectํ•จ์ˆ˜์— ๋“ค์–ด๊ฐ€ ์‚ดํŽด๋ณด๋ฉด ์œ„์™€ ๊ฐ™์ด ResultHandler๋ผ๋Š” ๊ฒƒ์„ ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐ›๋Š” ๊ฒƒ์„ ํ™•์ธ ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” ResultHandler๋ฅผ ๊ตฌํ˜„ํ•œ ๊ฐ์ฒด๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. (์นœ์ ˆํžˆ ์ฃผ์„์œผ๋กœ ResultHandler๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ‚ค์™€ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฌธ์— ๋งคํ•‘๋œ ๋‹จ์ผ ํ–‰์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค) ์—ฌ๊ธฐ์„œ ํ•œ๋ฒˆ๋” ResultHandler์˜ ํด๋ž˜์Šค๋„ ๋”ฐ๋ผ ๋“ค์–ด๊ฐ€ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    interface๋กœ ์„ ์–ธ๋˜์–ด์žˆ๊ณ  generic ํƒ€์ž…์œผ๋กœ ๋˜์–ด์žˆ๋Š”๊ฑธ ๋ณด๋‹ˆ MyBatis์— resultType์— ์…‹ํŒ… ๋œ ๊ฐ์ฒด๊ฐ€ ๋ฆฌํ„ด๋  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ResultHandler๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด MyBatis์—์„œ ResultHandler๋ถ€๋ถ„์˜ ์—ญํ• ์€ ๋์ž…๋‹ˆ๋‹ค

    ์ฆ‰ selectํ•จ์ˆ˜์— ResultHandler๋ฅผ ๊ตฌํ˜„ํ•œ class๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด handlerResult์— resultType์˜ object๊ฐ€ ๋ฆฌํ„ด๋ฉ๋‹ˆ๋‹ค

    @Slf4j
    public abstract class ExcelResultHandler implements ResultHandler {
    
        protected SXSSFWorkbook sxssfWorkbook;
        protected SXSSFSheet sxssfSheet;
    
        // row = xls:65,535 xlsx:1,048,576
        protected final int MAX_SHEET_ROW = 1000000;
    
        private FlushControl flushControl;
        protected int sheetRowCnt = 1;
    
        protected ExcelFileInfo excelFileInfo;
    
    
        public ExcelResultHandler(ExcelFileInfo excelFileInfo, FlushControl flushControl) {
            this.excelFileInfo = excelFileInfo;
            this.flushControl = flushControl;
            // 0. ์›Œํฌ๋ถ ์ƒ์„ฑ
            this.sxssfWorkbook = new SXSSFWorkbook(flushControl.getRowAccessWindowSize()); // rowAccessWindowSize๊ฐœ์˜ ๋กœ์šฐ๋งŒ ๋ฉ”๋ชจ๋ฆฌ ๋ณด์œ , ๋‚˜๋จธ์ง€ ๋””์Šคํฌ๋กœ ๋‚ด๋ณด๋‚ธ๋‹ค.
            sxssfWorkbook.setCompressTempFiles(true);                    // ์ž„์‹œํŒŒ์ผ ์••์ถ•์—ฌ๋ถ€
        }
     ... ์ค‘๊ฐ„ ์ƒ๋žต
     
        /**
         * rowCnt๋Š” 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•จ
         */
        public abstract void createExcelBody(int rowCnt, Object dbData);
    
        @Override
        public void handleResult(ResultContext resultContext) {
            createExcelBody(resultContext.getResultCount(), resultContext.getResultObject());
        }

    ResultHandler๋ฅผ ๋ฐ”๋กœ class์— ๋ฐ›์•„์„œ ๊ตฌํ˜„ํ•ด์ค˜๋„ ๋˜์ง€๋งŒ abstract๋กœ ๋ฐ›์•„์„œ ๊ธฐ๋ณธ์ ์ธ ๊ณตํ†ต ๋ถ€๋ถ„ (POI ์›Œํฌ๋ถ์ƒ์„ฑ, ์‹œํŠธ์ƒ์„ฑ, ํ—ค๋”์ƒ์„ฑ) ๋“ฑ์„ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ• ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๊ณ  @Override๋˜์–ด์žˆ๋Š” handlerResult์— createExcelBody์˜ ํ•จ์ˆ˜๋งŒ ์ถ”์ƒํด๋ž˜์Šค๋กœ ํ•˜์—ฌ ์ƒ์†๋ฐ›๋Š” class๊ฐ€ ๊ตฌํ˜„ํ•˜๊ฒŒ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    @Slf4j
    public class TestExcelHandler extends ExcelResultHandler {
    
        public TestExcelHandler(ExcelFileInfo excelFileInfo, FlushControl flushControl) {
            super(excelFileInfo, flushControl);
        }
    
        @Override
        public void createExcelBody(int rowCnt, Object dbData) {
            //์›Œํฌ๋ถ์ƒ์„ฑ -> ์‹œํŠธ์ƒ์„ฑ -> ์‰˜ ํƒ€์ดํ‹€์ƒ์„ฑ
            if(rowCnt % MAX_SHEET_ROW == 1) {
                createSXSSFSheet((rowCnt/MAX_SHEET_ROW)+1);
                //HashMap ์š”๋ถ€๋ถ„์€ resultType์œผ๋กœ castingํ•˜์„ธ์š” ์ €๋Š” map
                createCellTitle((HashMap<String, Object>)dbData);
                //์ƒˆ๋กœ์šด ์‹œํŠธ์˜ ROW๋Š” 1๋ถ€ํ„ฐ ์‹œ์ž‘
                sheetRowCnt = 1;
            }
            //ROW ์ƒ์„ฑ
            createDataCell((HashMap<String, Object>)dbData);
    
            //...์ค‘๊ฐ„์ค‘๊ฐ„์— flush
            flushMem(rowCnt);
        }
    
    }

    POI ์ž„์‹œํŒŒ์ผ ์šฉ๋Ÿ‰ ์••์ถ• ๋ฐ ๊ฒฝ๋กœ ์ˆ˜์ •

    // 0. ์›Œํฌ๋ถ ์ƒ์„ฑ
    this.sxssfWorkbook = new SXSSFWorkbook(flushControl.getRowAccessWindowSize()); // rowAccessWindowSize๊ฐœ์˜ ๋กœ์šฐ๋งŒ ๋ฉ”๋ชจ๋ฆฌ ๋ณด์œ , ๋‚˜๋จธ์ง€ ๋””์Šคํฌ๋กœ ๋‚ด๋ณด๋‚ธ๋‹ค.
    sxssfWorkbook.setCompressTempFiles(true);                    // ์ž„์‹œํŒŒ์ผ ์••์ถ•์—ฌ๋ถ€

    POI ์ž„์‹œํŒŒ์ผ์˜ ์••์ถ•์€ setCompressTempFiles(true)๋กœ ์„ค์ •ํ•˜๋ฉด gz์œผ๋กœ ์••์ถ•์ด ๋ฉ๋‹ˆ๋‹ค

    ๋ญ ์ด๊ฒƒ๋„  ํ•จ์ˆ˜ ๋”ฐ๋ผ๊ฐ€๋‹ค ๋ณด๋ฉด ๊ทธ๋ ‡๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค ^^;;

    ์ž„์‹œํŒŒ์ผ๊ฒฝ๋กœ๋Š” new SXSSFWorkbook์˜ ํด๋ž˜์Šค๋ฅผ ๋”ฐ๋ผ์„œ ์ถ”์ ํ•˜๋‹ค๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด TempFIle.createTempFile ์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    TempFIle.createTempFile์„ ๋˜ ์ซ“์•„๊ฐ€๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์œผ๋ฉฐ

    strategy๋ฅผ ์ซ“์•„๊ฐ€๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๊ธฐ๋ณธ์œผ๋กœ DefaultTempFileCreationStrategy๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค๋„ค์š”

    DefaultTempFileCreationStrategy๋ฅผ ๋ณด๋ฉด ์ƒ์„ฑ์ž์— ํŒŒ์ผ์˜ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค

    TempFile์˜ ๊ทธ ์•„๋ž˜๋ฅผ ๋ณด๋ฉด strategy๋ฅผ setTempFileCreationStrategy(TempFileCreationStrategy strategy)๊ฐ€ ์žˆ๋„ค์š”

    ์ด์ œ ํผ์ฆ์„ ๋งž์ถฐ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

    TempFile์˜ .setTempFileCreationStrategy ํ•จ์ˆ˜์— TempFileCreationStrategy๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค DefaultTempFileCreationStrategy๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋˜๊ฒ ๋„ค์š” ๋”ฐ๋ผ์„œ DefaultTempFileCreationStrategy๋ฅผ ์ƒ์„ฑํ• ๋•Œ File Dir๋ฅผ ๋‚ด๊ฐ€ ์ƒ์„ฑํ•ด ์ง€์ •ํ•ด์ฃผ๊ณ  DefaultTempFileCreationStrategy๋ฅผ ์ƒ์„ฑํ•ด์„œ setํ•ด์ฃผ๋ฉด ํ•ด๊ฒฐ ๋˜๊ฒ ๋„ค์š”

    /**
    * org.apache.poi tmpํŒŒ์ผ ๊ฒฝ๋กœ ์ˆ˜์ •
    */
    private void setTmpPath() {
    	TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(new File(excelProperties.getTmpDirPath())));
    }

    ์ด์ œ setTmpPathํ•จ์ˆ˜๋ฅผ POI ์›Œํฌ๋ถ์„ ์ƒ์„ฑํ•˜๊ธฐ์ „์— ๋ฏธ๋ฆฌ ์‹คํ–‰์„ ํ•˜๋ฉด ์ž„์‹œํŒŒ์ผ์€ ๋‚ด๊ฐ€ ์ง€์ •ํ•œ ๊ฒฝ๋กœ์— ๋งŒ๋“ค์–ด์ง‘๋‹ˆ๋‹ค

    POI SXSSF ์ž๋™ or ์ˆ˜๋™ flush

    ์„œ๋‘์—์„œ ๋งํ–ˆ๋“ฏ์ด SXSSF๋Š” Streaming๋ฐฉ์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๊ณ„์† ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๊ณ  ์„ค์ •๋œ ๊ฐ’์„ ๋„˜์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ์— ๋“ค๊ณ ์žˆ๋˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ž„์‹œ ํŒŒ์ผ์— ๊ธฐ๋กํ•œ ํ›„ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋น„์›Œ๋‚ด๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ๋ฐ˜๋ณตํ•˜์—ฌ ๋งˆ์ง€๋ง‰๊นŒ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์ž„์‹œํŒŒ์ผ์„ ๋งˆ์ง€๋ง‰์—๋Š” ์„ค์ •ํ•œ ์—‘์…€ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋น„์›Œ๋‚ผ๋•Œ ์ž๋™์œผ๋กœ ์…‹ํŒ…์„ ํ• ์ˆ˜ ์žˆ๊ณ  ์•„๋‹ˆ๋ฉด ์ˆ˜๋™์œผ๋กœ ์„ค์ •ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    	//์ž๋™ ์ƒ์„ฑํ• ๋•Œ ์ง€์ •ํ•œ ๊ฐ’(100)์— ๋„๋‹ฌํ•˜๋ฉด flush
    	this.sxssfWorkbook = new SXSSFWorkbook(100);
    
    
    	//์ˆ˜๋™์ผ๋•Œ๋Š” -1์ด๋ฉด ๋ฌด์ œํ•œ ์•ก์„ธ์Šค์ด๊ณ  ์ค‘๊ฐ„์ค‘๊ฐ„์— ์ˆ˜๋™์œผ๋กœ flushํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
    	this.sxssfWorkbook = new SXSSFWorkbook(-1);
    
        /**
         *  rowAccessWindowSize -1์ด๋ฉด ์ˆ˜๋™ flush์ž‘๋™
         */
        protected void flushMem(int rowCnt) {
            if(flushControl.getRowAccessWindowSize() == -1 && rowCnt % flushControl.getManualFlushSize() == 1) {
                try {
                	//100 ์Œ“์ด๋ฉด flush
                    ((SXSSFSheet)sxssfSheet).flushRows(100);
                    log.info("[Excel] flushMem {}", rowCnt);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    ์œ„์˜ ๋‚ด์šฉ์€ https://poi.apache.org/components/spreadsheet/how-to.html#sxssf ์—ฌ๊ธฐ์— ์ž˜ ๋‚˜์™€์žˆ์Šต๋‹ˆ๋‹ค

     

    ํ…Œ์ŠคํŠธ๋กœ ๋งŒ๋“ค์–ด๋ณธ ์†Œ์Šค

    ๊นƒํ—™ : https://github.com/moyanada/bigsize_excel_sample

    ๋ฐ˜์‘ํ˜•
    ๋ฐ˜์‘ํ˜•
    ๊ณต์ง€์‚ฌํ•ญ
    ์ตœ๊ทผ์— ์˜ฌ๋ผ์˜จ ๊ธ€
    ์ตœ๊ทผ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€
    Total
    Today
    Yesterday
    ๋งํฌ
    ยซ   2024/07   ยป
    ์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
    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
    ๊ธ€ ๋ณด๊ด€ํ•จ
    ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (56)
    ๐Ÿ“บ Develop (34)
    ๐ŸŒ‹ Error Fixed (3)
    ๐Ÿ— Tool (3)
    ๐Ÿ’ป MacBook M1 (15)
    ๐Ÿ“ฆ ETC (1)

    ์ด ํฌ์ŠคํŒ…์€ ์ฟ ํŒก ํŒŒํŠธ๋„ˆ์Šค ํ™œ๋™์˜ ์ผํ™˜์œผ๋กœ, ์ด์— ๋”ฐ๋ฅธ ์ผ์ •์•ก์˜ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ์ œ๊ณต๋ฐ›์Šต๋‹ˆ๋‹ค.