구현하게 된 배경

회사에서 웹뷰이지만 모바일에서 PDF가 이미지로 보여야 한다는 요구사항을 받았습니다.

여기서 제약사항은 HTML, CSS, JavaScript만 사용해야 한다는 것입니다.

결과물은 페이지가 한 개만 있는 pdf 파일을 웹뷰로 보여주는 게 끝이었습니다(정적인 페이지).

 

모바일에서 웹뷰를 띄우고 거기다가 PDF를 이미지로 변환하는 것이기 때문에 너무 무거우면 안 된다고 생각했습니다.

그래서 바닐라 자바스크립트를 사용해서만 구현하게 되었습니다.

 

 

PDF.js란?

웹 표준 호환 HTML5 Canvas를 사용해 PDF 파일은 렌더링 하는 자바스크립트의 라이브러리입니다.

모질라 제단의 Andreas Gal가 2011년에 런칭 했습니다.

웹 브라우저에서 기본적으로 PDF 문서를 볼 수 있는 방법을 제공하기 위해 만들어졌으며 문서를 표시하기 위한 코드가

브라우저에서 샌드박스 처리되기 때문에 브라우저 외부에서 PDF 문서를 열 때 보안 위험을 방지합니다.

HTML5의 Canvas를 사용하기 때문에 빠른 렌더링 속도를 보여줍니다.

* 문서를 이미지화하기 때문에 PDF 내 텍스트를 복사하는 것을 방지할 수 있습니다.

 

공식 사이트에 보면 promise 를 사용하는데 자바스크립트 promise에 대한 개념을 모른다면 해당 내용을 이해하고

라이브러리를 적용 해보는것을 권장하고 있습니다.

(하지만 해당 라이브러리를 활용해 방대한 프로젝트를 만들고 있는게 아니라 우선 사용해 봤습니다.)

 

 

개발에 참고한 링크들

1. PDF.JS 공식 사이트

https://mozilla.github.io/pdf.js/

 

 

2. PDF.JS 공식 예제 코드

https://mozilla.github.io/pdf.js/examples/

 

 

3. PDF.JS 라이브러리 docs

https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html

 

 

구현한 코드

 

더보기
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="style.css">
    <link rel="stylesheet" href="https://fonts.sandbox.google.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
    <script src="//mozilla.github.io/pdf.js/build/pdf.js"></script>
    <script defer src="main.js"></script>
    <title>Title</title>
</head>
<body>
    <main class="main-notice">
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-1</h3>
            </div>
            <canvas id="jsSampleOneCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn" >
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-2</h3>
            </div>
            <canvas id="jsSampleTwoCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-3</h3>
            </div>
            <canvas id="jsSampleThreeCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
        <div class="notice">
            <div class="notice__title">
                <h3>sample 1-4</h3>
            </div>
            <canvas id="jsSampleFourCanvas" class="notice__canvas"></canvas>
            <div class="notice__btn">
                <span class="material-symbols-outlined">
                    expand_more
                </span>
            </div>
        </div>
    </main>
</body>
</html>

 

더보기
let noticeBtnAll = document.querySelectorAll(".notice__btn");

const SAMPLE_ONE_CANVAS = document.getElementById("jsSampleOneCanvas");
const SAMPLE_TWO_CANVAS = document.getElementById("jsSampleTwoCanvas");
const SAMPLE_THREE_CANVAS = document.getElementById("jsSampleThreeCanvas")
const SAMPLE_FOUR_CANVAS = document.getElementById("jsSampleFourCanvas")

const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.5.207/cmaps/';

const CANVAS_ARRAY = [SAMPLE_ONE_CANVAS,
                      SAMPLE_TWO_CANVAS,
                      SAMPLE_THREE_CANVAS,
                      SAMPLE_FOUR_CANVAS];

// PDF.JS (pdf to image convertor)
let pdfJsLib = window['pdfjs-dist/build/pdf'];

pdfJsLib.GlobalWorkerOptions.workerSrc = '//mozilla.github.io/pdf.js/build/pdf.worker.js';

function imgOpenClose(event) {
    let btn = event.target;
    let btnDiv = btn.parentNode;
    let canvas = btnDiv.parentNode.querySelector(".notice__canvas");

    if(!canvas.style.display || canvas.style.display === "none") {
        canvas.style.display = "block";
        btn.innerHTML = "expand_less";
        btnDiv.style.borderBottom = "1px solid rgba(128, 128, 128, 0.3)";
    } else {
        canvas.style.display = "none";
        btn.innerHTML = "expand_more";
        btnDiv.style.borderBottom = "none";
    }
}

function pdfToImg(urlArr) {

    let pdfUrl = ["/static/pdf/1-1.pdf",
                  "/static/pdf/1-2.pdf",
                  "/static/pdf/1-3.pdf",
                  "/static/pdf/1-4.pdf"];

    for (let i = 0; i < pdfUrl.length; i++) {
        pdfRender(pdfUrl[i], CANVAS_ARRAY[i]);
    }
}

function pdfRender(url, canvas) {
    let loadingTask = pdfJsLib.getDocument({
        cMapPacked: true,
        disableFontFace: true,
        cMapUrl: CMAP_URL,
        url: url
    });

    loadingTask.promise.then(function (pdf) {
        // Fetch the first page
        let pageNumber = 1;
        pdf.getPage(pageNumber).then(function (page) {

            let scale = 1.5;
            let viewport = page.getViewport({scale: scale});

            // Prepare canvas using PDF page dimensions
            let context = canvas.getContext('2d');
            canvas.height = viewport.height;
            canvas.width = viewport.width;

            // Render PDF page into canvas context
            let renderContext = {
                canvasContext: context,
                viewport: viewport
            };
            let renderTask = page.render(renderContext);
            renderTask.promise.then(function () {
            });
        });
    }, function (reason) {
        console.error(reason);
    });
}

function init() {
    for(const btn of noticeBtnAll) {
        btn.childNodes[1].addEventListener("click", imgOpenClose);
    }
    pdfToImg();
}

init();

 

더보기
body {
    display: flex;
    align-items: center;
    flex-direction: column;
    margin: 0;
}

.notice {
    width: 100vw;
    margin-bottom: 10px;
}

.notice__title {
    display: flex;
    align-items: center;
    background-color: rgba(128, 128, 128, 0.1);
    border-bottom: 1px solid rgba(0, 0, 0, 0.3);
    border-top: 1px solid rgba(0, 0, 0, 0.3);
    border-top-left-radius: 15px;
    border-top-right-radius: 15px;
}

.notice__title h3 {
    margin-left: 13px;
}

.notice__btn {
    width: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
}


.material-symbols-outlined {
    font-variation-settings:
            'FILL' 0,
            'wght' 800,
            'GRAD' 0,
            'opsz' 48;
    opacity: 0.5;
}

.notice-question {
    margin-top: 9vh;
    background-color: rgba(128, 128, 128, 0.05);
    border: 2px solid rgba(0, 0, 0, 0.2);
    border-radius: 12px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.notice-question__btn {
    border: none;
    background-color: rgba(128, 128, 128, 0.01);
    cursor: pointer;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 5px;
}

.notice-question__arrow span {
    background-color: rgba(128, 128, 128, 0.05);
}

.notice-question__title {
    display: flex;
    align-items: center;
    margin-left: 15px;
}

.notice-question__title h3 {
    display: flex;
    align-items: center;
    margin-left: 7px;
}

.notice-question__title img {
    width: 25px;
}

.notice__canvas {
    display: none;
    width: 100vw;
}

.modal-wrap {
    position:fixed;
    top:0;
    left:0;
    right:0;
    bottom:0;
    background:rgba(0,0,0,.5);
    font-size:0;
    text-align:center;
}

.modal-wrap:after {
    display:inline-block;
    height:100%;
    vertical-align:middle;
    content:'';
}

.modal-wrap .modal-inner  {
    display:inline-block;
    padding: 8px 8px;
    background:#fff;
    width:95vw;
    vertical-align:middle;
    font-size:15px;
}

.modal-inner img {
    width: 90vw;
}
  • HTML, CSS는 제외하고 자바스크립트 소스만 보셔도 됩니다.
  • 예제코드와 거의 똑같아서 주석이나 설명은 달아놓지 않았습니다.

 

트러블 슈팅) 한국어가 글자가 렌더링 되지 않는 문제

 

구현을 하면서 한국어를 입력한 부분의 글자가 깨지는 문제가 발생했습니다.

해당 내용을 해결하기 위해 찾아보다가 중국 개발자들이 해당 문제를 해결한 것을 봤습니다.

 

 

문제를 해결하는데 큰 도움이 된 링크 => https://github.com/mozilla/pdf.js/issues/12629

 

Part of the Chinese character cannot be rendered · Issue #12629 · mozilla/pdf.js

Attach (recommended) or Link to PDF file here: pdf link Configuration: Web browser and its version: chrome 86.0.4240.193 Operating system and its version: macOS 10.13.6 PDF.js version: 2.5.207 Is a...

github.com

 

확인해보니 CDN이 형식이 잘못된 데이터를 반환하는 것이라고 합니다.

bCmap 형식을 리턴해야 하는데 잘못된 형식이 반환되는 것입니다.

그래서 cMapUrl을 변경하니까 문제가 바로 해결되었습니다.

 

 

bCmap == binary cmap

* bCmap이란? => 링크

 

cMap(Character Maps)는 PostScript 나 기타 Adobe 제품에서 문자 코드를 CID 글꼴의 문자를 매핑하는데 사용되는 텍스트 파일

(CID 폰트에 대한 설명은 오른쪽 링크 참조(영문 사이트) => 링크)

주로 동아이사 문자 체계를 다룰 때 사용되고 이 기술은 레거시 기술이므로 최신 도구로 만든 pdf에서는 사용이 안된다고 합니다.

 

pdf.js는 이런 CID 폰트를 화면상에 나타내기 위해서는 CMap 파일이 필요하다고 합니다.

그래서 같은 아시아권인 중국 개발자가 해결한 방법으로 해결이 가능했던 것 같습니다.

 

 

구현하면서 의문점

  • pdf가 이미지로 변환된 파일을 다운로드 못하게 해야 하지 않는지?
    • 이 부분은 해당 PDF 파일을 따로 암호화를 진행 해야 한다는 것을 알게 되었습니다.
      그래서 민감한 정보가 들어있는 PDF파일은 암호화 토큰 값을 주입해서 위조 및 변형이 힘들도록 하는 방식을 적용합니다.
      * 제가 구현한 방식은 단순히 텍스트를 복사 붙여넣기만 못하도록 막는 방식입니다.
  • html <head> 태그 안에 PDF.js 에 필요한 스크립트 태그 하나 추가했는데 라이브러리가 쉽게 적용되는 이유는 무엇인지?
    • CDN에 대한 개념 이해 필요

 

 

반응형

 

Entity에 게시글 수정 로직을  작성

@Getter
@NoArgsConstructor
@Entity
public class Board extends Timestamped {

   ...

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }

   ...
   
}
  • 비즈니스 로직을 엔티티에 구현하는 것은 도메인 주도 개발 방식이다.
  • 위와 같은 방식은 서비스 부분을 좀 더 가볍게 만들 수 있습니다.
    반대로 엔티티에 비즈니스 로직을 구현하지 않으면 서비스 부분이 코드가 많이 길어집니다.
  • 엔티티에 비즈니스 로직을 구현하는 것은 엔티티를 객체로 사용
    엔티티를 단순히 데이터 전달 역할을 하게 만드는건 엔티티를 자료구조로 사용
  • 클린코드 '6. 객체와 자료구조' 에 둘의 차이가 자세히 설명 되어있음

 

 

서비스단에 게시글 수정 로직 작성(JPQL 사용)

   public void updateBoard(Long id, BoardRequestDto requestDto, UserDetailsImpl userDetails) {

        loginCheck(userDetails);

        Optional<Board> findBoard = boardRepository.findById(id);

        Board board = boardValidCheck(findBoard);

        Long boardId = board.getPostId();
        String boardTitle = board.getTitle();
        String boardContent = board.getContent();

        boardRepository.updateBoard(boardId, boardTitle, boardContent);
    };
  • 아직은 간단한 프로젝트라서 서비스 로직이 단순하고 짧습니다.
    그런데 확실히 로직이 복잡해지고 기능이 많이 추가되면 서비스 부분이 많이 무거워질 것 같습니다.
  • 서비스 부분에 비즈니스 로직을 모아서 작성하면 역할을 확실하게 분담 할 수 있습니다.
  • 경험상으로 비즈니스 로직만 구현되어 있기 때문에 정리가 훨씬 편했습니다.

 

 

 

느낀점

  • 리팩토링을 하기 전에는 엔티티에 비즈니스 로직 코드가 있으면 절대 안되는 걸로만 알고 있었습니다.
    하지만 각각 장단점이 있다는 것을 자료를 찾아보면서 알게 되었습니다.
  • 각각 장단점을 알기 위해서는 우선 공부와 경험이 필요하다고 뼈저리게 느꼈습니다.
  • JPA 관련 공부는 DB 공부를 하면서 같이 진행
    • 우선 JPA를 배우는데 욕심 부리지 않고 DB 관련 공부에 더 집중
  • 엔티티에 @Setter 어노테이션은 달면 안된다고 생각했는데 그게 꼭 맞지는 않는다는 걸 알게되었습니다.

 


참고자료

  • 깃허브 블로그 [joont92] JPQL => 링크
  • 인프런 비즈니스 로직구현 Entity VS Service => 링크

 

 

 

반응형

모바일 기준으로 이미지 터치 슬라이더를 구현해봤습니다.

터치 슬라이더는 이미 다양한 블로그에서 정리를 해놔서 구현하기 쉬었습니다.

 

그런데 여기서 사람이 직접 슬라이드를 넘기는 것이 아닌 자동으로 시간이 지나면 슬라이드 되는 기능을 추가해봤습니다.

 

자동으로 슬라이드가 되는 기능은 setInterval을 사용하면 쉽게 구현 할 수 있지만,

터치를 한 순간에는 setInterval이 멈춰야 하기 때문에 그 부분에서 고민을 많이 했습니다.

 

 

오토 슬라이드 시연 gif

 

터치 슬라이드 시연 gif

 

 

HTML 구현 코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="style.css">
    <title>slider</title>
</head>
<body>
    <div class="slideshow-container">
        <div class="images">
            <img src="image/1.jpg">
            <img src="image/2.jpg">
            <img src="image/3.jpg">
            <img src="image/4.jpg">
        </div>
    </div>
<script src="slide.js"></script>
</body>
</html>

HTML은 위의 코드를 참고하셔도 괜찮고 직접 작성하셔도 됩니다.

여기서 class 값으로 JS에서 querySelector를 사용하기 때문에 그 부분만 유의하시면 될 것 같습니다.

 

 

CSS 구현코드

body {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.slideshow-container {
    overflow: hidden;
    width: 100vw;
    position: relative;
    overflow: hidden;
    display: flex;
}

.images {
    position: relative;
    display: flex;
    transition: transform 0.5s;
}

.images img {
    vertical-align: middle;
    width: 100vw;
}

CSS는 반응형으로 디자인 하기 위해서 flex 컨테이너를 사용했습니다.

이미지 혹은 div의 widh 값도 반응형으로 만들기 위해 'vw' 단위를 사용했습니다.

 

 

 

 

자바스크립트 구현코드

const IMAGE_VW = 100;
const IMAGES = document.querySelector(".images");
const INTERVAL_TIME = 5000;
const IMG_COUNT = IMAGES.childElementCount;
const LAST_INDEX = IMG_COUNT - 1;

let curIndex = 0;
let curImgVw = 0;
let startX, endX;
let interval;

function prevImg() {
    if(curIndex > 0) {
        curImgVw += IMAGE_VW;
        IMAGES.style.transform = `translateX(${curImgVw}vw)`;
        curIndex--;
    }
}

function nextImg() {
    if(curIndex < LAST_INDEX) {
        curImgVw -= IMAGE_VW;
        IMAGES.style.transform = `translateX(${curImgVw}vw)`;
        curIndex++;
    }
}

function touchStart(event) {
    stopInterval();

    startX = event.touches[0].pageX;
}

function touchEnd(event) {
    startInterval();

    endX = event.changedTouches[0].pageX;
    if(startX > endX) {
        nextImg();
    } else {
        prevImg();
    }
}

function autoSlide() {
    if(curIndex === LAST_INDEX) {
        stopInterval();
        curImgVw = 0;
        IMAGES.style.transform = `translateX(${curImgVw}vw)`;
        curIndex = 0;
        startInterval();
    } else if(curIndex < LAST_INDEX) {
        curImgVw -= IMAGE_VW;
        IMAGES.style.transform = `translateX(${curImgVw}vw)`;
        curIndex++;
    }
}

function startInterval() {
    if(interval === null) {
        interval = setInterval(autoSlide, INTERVAL_TIME);
    }
}

function stopInterval() {
    clearInterval(interval);
    interval = null;
}

function init() {
    IMAGES.addEventListener("touchstart", touchStart);
    IMAGES.addEventListener("touchend", touchEnd);
    startInterval();
}

init();
  • 고정적인 값을 나타내는 변수는 const(상수)와  전역변수로 선언 했습니다.
    ex) 인터벌 시간, 이미지의 가로 길이
  • 자동으로 슬라이드가 되는 함수(autoSlide)는 브라우저가 렌더링 되면 바로 실행이 되도록 init 함수에 넣어놨습니다.
  • 이미지를 클릭하면 touchstart 함수가 실행되고 autoSlide를 일정 간격으로 실행시키는 인터벌을 정지하도록 했습니다.
  • startInterval() 함수를 만들어서 하나의 인터벌만 실행되도록 코드를 작성했습니다.
    => 동일하게 stopInterval() 실행중인 하나의 인터벌을 clearInterval() 함수를 사용해서 종료하도록 했습니다.
    => 실행 할 하나의 interval은 미리 전역변수로 설정을 해놨습니다.
  • 터치 슬라이드에서 각 이미지에 인덱스 값이 있다는 가정하에 curIndex, LAST_INDEX 변수 선언
    => curIndex는 유저가 현재 보고있는 이미지의 인덱스입니다.
    => LAST_INDEX는 말그대로 마지막 이미지의 인덱스입니다.

 

 

 

 


참고자료

  • 블로그 [탐구소년] 자바스크립트 모바일 터치 슬라이더 구현하기 => 링크

 

 

 

 

반응형

 

Jasypt란?

개발자가 암호화 작동 방식에 대한 깊은 지식 없이도 최소한의 노력으로 자신의 프로젝트에 기본 암호화 기능을

추가할 수 있도록 하는 Java 라이브러리입니다.

 

자바 외에도 Java + Spring, Java + Hibernate 등 여러 프레임 워크에서 암복호화를 지원해주는 오픈소스입니다.

 

저는 이 Jasypt를 활용해서 yaml 파일에 있는 민감한 정보(DB 계정 비밀번호 등)을 암호화 하는데 사용했습니다.

 

 

 

1. build.gradle에 dependency 추가

    // Jasypt
    implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'

 

 

 

 

2.  config 폴더에 JasyptConfig 파일 생성 및 (아래의)코드 추가

@Configuration
public class JasyptConfig {

    @Value("${jasypt.encryptor.password}")
    private String encryptKey;

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor(){
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(encryptKey);
        config.setPoolSize("1");
        config.setAlgorithm("PBEWithMD5AndDES");
        config.setStringOutputType("base64");
        config.setKeyObtentionIterations("1000");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        encryptor.setConfig(config);
        return encryptor;
    }
}

 

 

 

3.  테스트 코드로 Jasypt를 사용해 암호화

public class JasyptTest extends JasyptConfig {

    @Test
    public void jasypt_encrypt_decrypt_test() {
        String plainText = "plainText";

        StandardPBEStringEncryptor jasypt = new StandardPBEStringEncryptor();
        jasypt.setPassword("password");

        String encryptedText = jasypt.encrypt(plainText);
        String decryptedText = jasypt.decrypt(encryptedText);

        System.out.println(encryptedText);

        assertThat(plainText).isEqualTo(decryptedText);
    }
}
  • Jasypt를 사용해 각 값들을 암호화 합니다.
  • 테스트를 실행해서 나온 암호화된 데이터들을 yml에 입력합니다.

* 테스트 코드 없이 아래의 사이트에서도 암호화 및 복호화가 가능합니다.

https://www.devglan.com/online-tools/jasypt-online-encryption-decryption

 

Programming Blog Article Feeds as per your Interest | DevGlan

Best programming article feeds as per your Interest on different technologies. Subscribe to any technology and explore the best articles from around the web.

www.devglan.com

 

 

 

4. 암호화된 값 yml에 입력

  • 암호화된 값을 입력할 때는 ENC(암호화된 값) 형식으로 입력합니다.
  • jasypt에 대한 yml 값도 위 사진처럼 입력합니다.

 

5. jasypt 키값 VM option에 추가

키값을 yml에 그냥 추가하는 방법도 있지만 그러면 암호화한 값을 복호화 하는 키가 노출 되는 것이기 때문에 VM option을 통해
환경변수로 키 값을 입력하는 방식을 사용했습니다.

 

인텔리제이 상단 메뉴에서

Run -> Edit Configurations

 

 

위에 해당하는 부분에 

-Djasypt.encryptor.password=1234

의 형식으로 비밀번호(키 값)을 입력해줍니다.

 

 

그리고 build.gradle의 test 부분에 아래의 문구를 추가해줍니다.

systemProperty "jasypt.encryptor.password", System.getProperties().get("jasypt.encryptor.password")

 

이렇게 하면 모든 세팅이 완료되었습니다.

고생하셨습니다.

 


궁금한 점

  • Jasypt를 활용하는 다른 방법은 없을지?
    암호화를 하는 라이브러리이기 때문에 다른 곳에도 사용할 수 있지만
    스프링에서는 스프링 시큐리티의 PasswordEncoder가 있어서 굳이 사용할 이유는 없는 것 같습니다.

  • PBEWithMD5AndDES 알고리즘 이란?
    -> 문자 그대로 풀어 보면 "MD5와 DES를 이용한 패스워드 기반 암호화" 정도로 해석될 수 있습니다.
    일반적으로 취약하다고 알려져 있는 MD5와 DES 알고리즘을 사용한다
    PBE(Password Based Encryption, 암호 기반 암호화) 알고리즘은 암호를 기반으로 하는 암호화 알고리즘으로
    암호는 사용자가 스스로 파악하고 무작위 수를 섞어 다중 암호화하는 방법으로 데이터의 안정성을 확보하는 것이 특징이다.

 

 

참고한 자료들

  • (벨로그 sixhustle) Jasypt => 링크
  • (벨로그 haeny-dev) [Jasypt] yaml 파일의 암호화 => 링크

 

 

 

 

 

반응형

JPA

 

 

 

간단한 게시판 프로젝트에서 게시글 수정을 하는 메서드를 호출 하는데 위와 같은 에러가 발생했습니다.

* JPQL을 사용해서 update 쿼리문을 작성했습니다.

 

 

 

구글링을 한 결과 해당 메서드에 @Transactional 어노테이션을 달거나,

Repository에 작성한 JPQL문 위에 @Transactional 어노테이션을 다는 방식으로 해결이 가능했습니다.

 

해당 메서드 위 혹은 JPQL문 위에 트랜잭셔널 어노테이션 기입

 

여기서 왜 @Transactional 어노테이션 의문이 생겼습니다.

구글링을 했지만 해당 내용에대해 명쾌하게 정리되어 있는 글이 없었습니다.

 

그래서 update 쿼리문에서 Transaction 이 필요한 이유를 검색 했습니다.

그 결과 update 문은 해당 테이블의 컬럼의 모든 데이터를 변하게 할 수 있는 강력한 쿼리문이기 때문에,

실수로 작성을 하게되면 엄청난 문제를 초래한다는 내용을 찾게 되었습니다.

 

https://woogie-db.tistory.com/13

 

[MSSQL] 트랜잭션 설명 및 사용법

오늘은 MS-SQL의 UPDATE문을 전체적으로 설명드리겠습니다. UPDATE문 역시 주의해야하는 명령어이므로 꼭 트랜잭션 안에서 사용해야 합니다...... (SELECT문을 제외하고는 대부분 주의가 필요합니다....

woogie-db.tistory.com

 

위 글을 보면 update 발생하는 문제를 방지하기 위해 select문을 먼저 실행 후에 update 쿼리문을 사용하는 방식을 권합니다.

여기서 select 문과 update문을 트랜잭션으로 묶어서 해당 쿼리문을 실행하다 문제가 생기면 롤백하는 것입니다.

 

JPA에서도 이런 문제를 방지하기 위해 update문을 실행하기 전에 select문이 먼저 실행되는 것을 볼 수 있었습니다.

결국 JPA가 우리가 주의해야 할 점들을 알아서 처리해준다는 것을 알게 되었습니다.

 

위 에러를 겪고 고민하고 해결하면서 코딩을 하면서 내가 개선하고 해야할 것들에 대해 정리해봤습니다.

개선할점

  • 결국 JPA를 사용하기 전에 SQL에 대한 공부가 선행 되어야 한다는 것을 느꼈습니다.
  • JPA 사용 시 show-sql 설정을 활용해서 호출하는 쿼리문을 로그에서 확인
    • 호출되는 쿼리문을 보고 하이버네이트의 내부동작에 대해서 공부 필요

 

 

반응형

 

참고자료

  • 우아한 테크코스 블로그 [Tecoble] 3기 케빈님의 글 => 링크
  • 작성자의 게시판 프로젝트 커밋 메시지 => 링크 

QueryDSL을 교육기관을 들으면서 사용했던 적이 있습니다.

그때는 너무 바쁘게 적용하고 QueryDSL을 사용하는 이점과 기존 쿼리문을 작성하는 것과의 차이를 정확하게 알지 못했습니다.

그런데 이번에 혼자 게시판 프로젝트를 리팩토링하면서 이유를 알게 되었습니다.

 

 

 

😱 문제가 발생한 부분(위: 수정전, 아래: 수정 후)

 

정말 단순한 쿼리문이지만 잘못 작성해서 문제가 발생했습니다.

 

여기서 큰 문제는 직접 해당 프로그램을 돌려보는 상황에서 문제를 발견 했다는 것입니다.

우선적으로 프로그램을 직접 돌려보면서 버그를 찾고 해결하는것은 상당히 비효율적입니다.

그리고 만약에 로컬에서 돌려보지 않고 그냥 git push를 한다면 해당 버그를 팀원에게 공유 하는 문제를 초래할 수도 있다고 생각했습니다.

 

 

여기서 QueryDSL을 사용하면 자바 코드로 쿼리문을 작성하기 때문에 IDE가 버그를 잡아낼 수 있게 됩니다.

쿼리문을 직접 작성하면 숙련된 프로그래머도 오타가 발생하면 버그가 생기게 된다고 생각합니다.

그런 문제를 거의 완벽하게 잡아 줄 수 있는 이점이 있다고 생각합니다.

 

지금 제가 작성한 쿼리문은 아주 적지만 더 많이 작성한다고 하면 버그가 한개만 생긴다는 보장은 없을 것 같습니다.

 


😀 결론

  • 쿼리DSL을 통해 반복적인 쿼리문에서 나오는 실수를 IDE가 잡아내도록 하자
  • 숙련된 프로그래머도 오타로 인한 버그가 발생하는 경우가 있다고 한다. 이런 문제를 IDE가 잡아내도록 해보자
  • 쿼리DSL을 사용할 수 없는 상황에서 쿼리문에서 버그를 잡아내는 방법을 고안해보자

 

반응형

 

 

📚 참고자료

  • [10분 테코톡] 🌊 바다의 JUnit5 => 링크

 

1️⃣ JUnit이란?

  • 자바 개발자의 93%가 사용하는 단위 테스트 프레임워크
  • JUnit5는 2017년 10월 공개
  • 스프링 부트 2.2버전 이상부터 기본 제공

 

 

  • Platform : 테스트를 실행해주는 런처 제공. TestEngine API 제공
  • Jupiter : JUnit 5를 지원하는 TestEngine API 구현체
  • Vintage : Junit 4와 3을 지원하는 TestEngine 구현체

 

셋 다 JUnit5 세부 모듈이다

 

 

 

2️⃣ JUnit5 시작하기

 

  • 스프링 부트 프로젝트
    => 스프링 부트 2.2버전 이상부터는 기본적으로 JUnit5 의존성이 추가된다.

  • 스프링 부트 프로젝트가 아닐 경우
    => 다음과 같이 의존성을 추가해주면 된다.
 <dependency>
 	<groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.6.2</version>
    <scope>test</scope>
 </dependency>

 

 

 

3️⃣ 어노테이션들(Annotaions)

 

@Test

  • 테스트 메서드라는 것을 나타내는 어노테이션
  • JUnit4와 다르게 어떠한 속성도 선언하지 않는다.
// JUnit4
@Test(expected = Exception.class)
void create() throws Exception {
	...
}

// JUnit5
@Test
void create() {
	...
}

 

생명주기(LifeCycle) 어노테이션

  • @BeforeAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 딱 한번 실행되는 메서드
  • @AfterAll : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 딱 한번 실행되는 메서드
  • JUnit4의 @BeforeClass / @AfterClass 와 유사

  • @BeforeEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 전에 실행되는 메서드
  • @AfterEach : 해당 클래스에 위치한 모든 테스트 메서드 실행 후에 실행되는 메서드
  • JUnit4의 @Before / @After와 유사
  • 매 테스트 메서드마다 새로운 클래스를 생성(new)하여 실행 (비효율적)

 

 

@Disabled

  • 테스트를 하고 싶지 않은 클래스나 메서드에 붙이는 어노테이션
  • JUnit4의 @Ignore 과 유사
class DisabledExample {
	@Test
    @Disabled("문제가 해결될 때까지 테스트 중단")
    void test() {
    	System.out.println("테스트");
    }
    
    @Test
    void test2() {
    	System.out.println("테스트2");
    }
}

 

 

@DisplayName

  • 어떤 테스트인지 쉽게 표현할 수 있도록 해주는 어노테이션
  • 공백, Emoji, 특수문자 등을 모두 지원
@DisplayName("특수 테스트\uD83D\uDE00")
class DisplayNameExample {
	@Test
    @DisplayName("굉장한 테스트입니다.")
    void test() {
    }
}

 

 

 

@RepeatedTest

  • 특정 테스트를 반복시키고 싶을 때 사용하는 어노테이션
  • 반복 횟수와 반복 테스트 이름을 설정가능
@RepeatedTest(10)
@DisplayName("반복 테스트")
void repeatedTest() {
	...
}

@RepeatedTest(value = 10, name = "{displayName} 중 {currentRepetition} of {totalRepetitions})
@DisplayName("반복 테스트")
void repeatedTest2() {
	...
}

 

 

@ParameterizedTest

  • 테스트에 여러 다른 매개변수를 대입해가며 반복 실행할 때 사용하는 어노테이션
@ParameterizedTest
@CsvSource(value = {"ACE,ACE:12", "ACE,ACE,ACE:13", "ACE,ACE,TEN:12"}, delimiter = ':')
@DisplayName("에이스 카드가 여러 개일 때 합 구하기")
void calculateCardSumWhenAceIsTwo(final String input, final int expected) {
	final String[] inputs = input.split(",");
    for (final String number : inputs) {
    	final CardNumber cardNumber = CardNumber.valueOf(number);
        dealer.receiveOneCard(new Card(cardNumber, CardType.CLOVER));
    }
    assertThat(dealer.calculateScore()).isEqualTo(expected);
}

 

 

 

 

@Nested

  • 테스트 클래스 안에서 내부 클래스 정의해 테스트를 계층화 할 때 사용
  • 내부클래스는 부모클래스의 멤버 필드에 접근가능
  • Before / After와 같은 테스트 생명주기에 관계된 메소드들도 계층에 맞춰 동작

 

 

Assertions

  • 사전적 의미: 주장, 행사, 단정문
  • 테스트 케이스의 수행 결과를 판별하는 메서드
  • 모든 Junit Jupiter Assertions는 static 메서드

 

 

Assertions - assertAll(executables...)

  • 매개변수로 받는 모든 테스트코드를 한 번에 실행
  • 오류가 나도 끝까지 샐행한 뒤 한 번에 모아서 출력
@Test
public void create_study() {
	Study study = new Study();
    assertNotNull(study);
    assertEquals(Status.STARTED, study.getStatus(), "처음 상태값이 DRAFT");
    assertTrue(study.getLimit() > 0, () -> "최대 인원은 0보다 커야한다.");
}

@Test
public void create_study() {
	Study study = new Study();
    assertAll(
    	() -> assertNotNull(study),
		() -> assertEquals(Status.STARTED, study.getStatus(), "처음 상태값 DRAFT"),
        () -> assertTrue(study.getLimit() > 0, "최대 인원은 0보다 커야한다.")
    );
}

 

 

 

Assertions - assertThrows(expectedType, executable)

  • 예외 발생을 확인하는 테스트
  • executable의 로직이 실행하는 도중 expectedType의 에러를 발생시키는지 확인
// JUnit4
@Test(expected = Exception.class)
void create() throws Exception {
	...
}

// JUnit5
@Test
void exceptionThrow() {
	Exception e = assertThrows(Exception.class, () -> new Test(-10));
    assertDoesNotThrow(() -> System.out.println("Do Something"));
}

 

 

Assertions - assertTimeout(duration, executable)

  • 특정 시간 안에 실행이 완료되는지 확인
  • Duration : 원하는 시간
  • Executable : 테스트할 로직
@Rule
public Timeout timeout = Timeout.seconds(5);

class TimeoutExample {
	@Test
    @DisplayName("타임아웃 준수")
    void timeoutNotExceeded() {
    	assertTimeout(ofMinutes(2), () -> Thread.sleep(10));
	}

	@Test
	@DisplayName("타임아웃 초과")
	void timeoutExceeded() {
		assertTimeout(ofMillis(10), () -> Thread.sleep(100));
	}
}

 

 

Assumption

  • 전제문이 true라면 실행, false라면 종료
  • assumeTrue : false일 때 이후 테스트 전체가 실행되지 않음
  • assumingThat : 파라미터로 전달된 코드블럭만 실행되지 않음
void dev_env_only() {
	assumeTrue("DEV".equals(System.getenv("ENV")), () -> "개발 환경이 아닙니다.");
    assertEquals("A", "A");	 // 단정문이 실행되지 않음
}

void some_test() {
	assumingThat("DEV".equals(System.getenv("ENV")),
    	() -> {
        	assertEquals("A", "B");  // 단정문이 실행되지 않음
        });
	assertEquals("A", "A");  // 단정문이 실행됨
}

 


느낀 점

  • 어노테이션을 단순히 적용하는 것이 아닌 좀 더 깊게 파고 들어가 공부 할 수 있도록 진행
  • 이론만 공부하면 확실히 해당 내용을 익힐 수 없다는 느낌을 받았습니다.
    => 정리를 하고 다음에 JUnit5를 사용할 때 이 글을 참고하면서 공부
반응형

📚 참고자료

  • [10분 테코톡] 샐리의 트랜잭션 => 링크

0️⃣ 트랜잭션 개념 공부를 하게 된 계기

스프링을 사용하면서 @Transactional 이라는 어노테이션을 적용해본 적이 있습니다.

메서드 위에 해당 어노테이션을 적용하면 메서드 안에 있는 쿼리들이 하나의 단위로 묶인다는 대략적인 용도만 고 있습니다. 그래서 @Transactional 어노테이션의 정확한 개념을 모르고 사용했기 때문에 의문점이 많이 들었습니다.

  • 트랜잭션이란?
  • 트랜잭션을 사용해야 하는 상황은?
  • 스프링에서는 트랜잭션을 어떻게 지원하는지?

 

 

 

 

1️⃣ 트랜잭션이란?

  • 더이상 나눌 수 없는 가장 작은 하나의 단위를 의미
  • 데이터베이스에서는 트랜잭션을 조작함으로써 사용자가 데이터베이스에 대한 완전성을 신뢰할 수 있도록 함
  • 모든 데이터베이스는 자체적으로 트랜잭션을 지원
  • 하나의 명령을 실행했을 때 데이터베이스가 온전히 그 명령을 실행해주는 것
    => 성공하면 커밋(Commit)
    => 실패하면 롤백(Rollback)
  • 데이터베이스는 기본적으로 트랜잭션을 관리하기 위한 설정을 갖고 있습니다.
    • 명령을 끝마칠 때까지 수행 내역을 로그에 저장해둡니다.
      - 데이터베이스에 반영된 내용을 재반영하기 위한 redo log
      - 수행을 실패해 이전의 상태로 되돌리는 undo log
      * 두 개의 log를 이용해 트랜잭션을 지원합니다.

 

 

 

2️⃣ 트랜잭션을 사용하는 상황(예시)과 성질

트랜잭션에서 가장 흔히 볼 수 있는 입출금을 예시로 들어보겠습니다.

 

  1. A가 B에게 만원을 송금하려고 합니다.
  2. A의 계좌에 만원보다 많은 금액이 있는지 확인합니다.
  3. A의 계좌에 만원보다 많은 금액이 있다면 만원을 차감합니다.
  4. 차감한 만원의 금액을 B의 계좌에 더해줍니다.

 

위 업무는 순서를 나눠놨지만 절대로 분리되거나 일부만 실행되면 안 되는 하나의 작업입니다.

이렇게 절대로 깨져서는 안되는 하나의 작업을 트랜잭션이라고 합니다.

 

 

트랜잭션의 네가지 성질

  • 원자성(Atomicity)
    - A가 B에게 송금을 하는데 A의 계좌에서 만원이 차감만 되고 작업이 종료되면 문제가 발생합니다.
    - 이렇듯 일부만 실행되는 경우는 없다는 원자성을 지닙니다.
    - [롤백]1번부터 4번까지 연결된 하나의 작업에서 어느 하나의 명령이라도 실패하면 롤백을 하게 됩니다.
    - [트랜잭션 커밋]반대로 1번부터 4번까지 작업이 성공적으로 수행하면 수정된 내용 데이터베이스에 반영
    - 롤백이나 커밋이 실행되어야 트랜잭션이 종료됩니다.

  • 일관성(Consistency)
    - 데이터베이스 내의 상태 혹은 계층 관계
    - 컬럼의 속성이 항상 일관되게 유지되어야 함
    - 컬럼의 속성이 수정되었다면 trigger를 통해 일괄적으로 모든 데이터베이스에 적용해야 합니다.
  • 지속성(Durability)
    - 트랜잭션이 성공적으로 수행되어 커밋되었다면 어떠한 문제가 발생하더라도 데이터베이스에 그 내용이
    영원히 지속되어야한다는 지속성이 있습니다.
    - 이를 위해 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 합니다.

  • 독립성(Isolation)
    - 트랜잭션 수행 시 다른 트랜잭션이 끼어들 수 없고 각각 독립적으로 실행된다는 성질
    DB 독립성의 문제점(더보기 클릭)
    더보기
    하지만 데이터베이스에 작업이 들어왔을 때 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면, CPU는 DBMS보다 인풋, 아웃풋이 빈번히 수행되기 때문에 트랜잭션을 순차적으로 시행하면 CPU는 점점
    응답을 기다리는 시간이 길어지게 됩니다. 그 결과 프로그램이 비효율적으로 동작하는 문제가 발생합니다.

 

이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요한 것입니다.

여러 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션 명시

 

 

 

 

3️⃣ 스프링에서 지원하는 트랜잭션

 

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
    void commit(TransactionStatus status);
    void rollback(TransactionStatus status);
}

 

  • 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 제공
    • getTransaction 메서드
      - 파라미터로 전달되는 TransactionDefinition에 따라 트랜잭션을 시작
      - 트랜잭션을 문제없이 마치면 commit을, 문제가 발생하면 Rollback을 호출
    • getTransaction부터 commit이나 rollback을 하는 부분까지가 트랜잭션의 경계설정입니다.
  • 다양한 DataSource에 맞게 트랜잭션을 관리

 

 

스프링이 제공하는 다양한 트랜잭션 매니저 구현체

 

  1. DataSourceTransactionManager
    - JDBC에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  2. JpaTransactionManager
    - JPA에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  3. JtaTransactionManager
    - 하나 이상의 데이터베이스가 참여하는 경우 글로벌 트랜잭션에 사용되는 JtaTransactionManager 사용
    - 여러 개의 데이터베이스에 대한 작업을 하나의 트랜잭션에 묶을 수 있고, 다른 서버에 분산된 것도 묶을 수 있음

 

 

🍃 어노테이션을 기반으로 트랜잭션을 설정하는 방법

 

  @Transactional
    public void addRestaurantFoods(
            Long restaurantId,
            List<FoodRequestDto> requestDtoList
    ) {
        Optional<Restaurant> foundRestaurant = restaurantRepository.findById(restaurantId);

        checkRestaurant(foundRestaurant);
        Restaurant restaurant = foundRestaurant.get();

        for (FoodRequestDto requestDto : requestDtoList) {
            String foodName = requestDto.getName();
            int foodPrice = requestDto.getPrice();

            checkDuplicateRestaurantFood(restaurant, foodName);

            checkFoodPrice(foodPrice);

            Food food = Food.builder()
                    .name(foodName)
                    .price(foodPrice)
                    .restaurant(restaurant)
                    .build();

            foodRepository.save(food);
        }
    }

 

  • 어노테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작
    => 메서드를 성공적으로 끝마치면 트랜잭션 커밋
    => 도중에 문제가 발생하면 롤백
  • 어노테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙인다.

 

 

 

반응형

 

참고자료

  • 유튜브 노마드코더 채널 [반드시 "함수형 프로그래밍"을 알아야 할까?] => 링크

 

 


 

 

 

😎 개발자는 반드시 함수형 프로그래밍을 배워야 한다!

  • '함수형 프로그래밍'은 코드 작성 '스타일'이라고 할 수 있다.
  • 함수형 프로그래밍을 배우고 컨셉과 아이디어를 좋아한다면, 해당 개념을 좋아하는 언어에 적용할 수 있다.
  • Clojure와 Scala는 함수형 프로그래밍으로 아예 설계된 언어이다.
  • 배워두면 무슨 언어로 코드를 작성하든 도움이 된다!
  • 이미 좋은 기술과 경력을 갖춘 시니어 개발자들은 점진적으로 함수형 언어를 사용하게 된다.
    => 점차 커리어가 발전함에 따라서 함수형 언어를 사용
  • 함수형 방식에서는 버그 자체가 발생하기 어렵다.
    => 함수형 코드에서는 버그가 쉽게 숨겨지지도 않는다.

 

 

 

😀 함수형 프로그래밍은 일종의 새로운 사고방식을 배우는 것이다.

  • 코드를 쓰고 설계하는 새로운 관점
  • 배워두면 더 나은 개발자가 될 것이다.
  • 니꼬쌤은 시작부터 함수형 프로그래밍에 따라 코딩을 쓰는 것이 더 깔끔하다고 생각

 

 

 

😶 명령형(imperative) 코드와 선언형(declarative) 코드의 차이

 

선언형 코드

Make em a sandwich 🥪

 

  • 원하는 결과를 표현하기 위해 코드가 작성된다.
  • 선언형 언어의 예시로 CSS가 있다.
  • 선언형 코드는 원하는 결괏값을 선언하는 것이다.
  • 필요한 것을 달성하는 과정을 하나하나 기술하는 것보다 필요한 것이 어떤 것인지 기술하는 데 방점을 두고
    애플리케이션의 구조를 세워 나가는 프로그래밍 구조입니다.

 

 

선언형 코드 예시(JS)

function spaceToHeart(text) {
	return text.replaceAll(" ", "💜");
}
  • 명령 함수와 동일한 결과를 갖지만, 읽기가 더 쉽다
  • 함수가 무슨 역할을 하는지 이해하기 편하다.

 

 

 

 

 

명령형 코드

1. 🍞 toast
2. 🧀  + 🍅 + 🥓 + 🥗
3. combine
4. done!!! 🥪
  • 원하는 결과를 얻기 위해 필요한 지침에 따라 코드가 작성된다.
  • 요구사항을 충족하는데 필요한 모든 단계를 하나씩 적어야 함.
  • 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 가깝다.
  • 코드로 원하는 결과를 달성해 나가는 과정에만 관심을 두는 프로그래밍 스타일

 

 

명령형 코드 예시(JS)

function spaceToHeart(text) {
	let result = "";			 // 1. 최종 결과를 보유할 변수 선언 및 초기화
    for (let i = 0; i < text.length; I++) {	 // 2. 지우려는 텍스트의 각 문자 내부의 살펴보기
    	if (text[i] === " ") {			 // 3. 문자가 공백인지 여부 체크
        	result += "💜";                 // 4. 문자에 공백이 있다면 하트를 추가
        } else {
        	result += text[i];		// 4. 문자에 공백이 없다면 text의 i번째 문자 추가
        }
    }
    return result;				// 5. 완료 후 결과 리턴
}

 

명령형 코드의 단점

  • 코드가 길어진다.
  • 요구사항의 구현이 개발자인 우리에게 달려있기 때문에 버그나 실수가 더 생길 수 있다.
  • 팀 동료들이 이해하기 어려울 수 있다.
  • 함수가 정확히 무엇을 하려고 하는지 한 줄 한줄 자세히 읽어야 한다.

 

 

 

🤔 느낀 점 혹은 생각

  • 선언적 접근 방식이 읽기 쉽고 더 직관적이라고 느꼈습니다.
  • 함수형 프로그래밍에 대해 1도 모르고 있었다는 것을 알게 되었습니다.
  • 둘 다 공부를 하면서 작성했던 코드들이라는 것을 알게 되었습니다.

 

반응형

 

 

📚 참고자료

  • 유튜브 채널 [우아한 Tect] - 10분 테코톡 아마찌의 ORM vs SQL Mapper vs JDBC

https://www.youtube.com/watch?v=VTqqZSuSdOk&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=66 

 

 


🤔 영속성(Persistence)이란?

  • 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터의 특성
  • 객체지향 프로그램에서 객체의 상태는 단지 메모리에서만 존재하고 프로그램이 종료되면 휘발하는 형태
    => 영속성을 가지지 않은 상태
    => 객체의 상태를 DB에 저장하면서 객체에게 영속성을 부여함

 

 

  • 위 사진은 Mark Richards의 소프트웨어 아키텍처 패턴 => 이미지링크
  • Persistence 계층에서 Domain Model 계층에 영속성을 부여하는 역할을 합니다.
  • JDBC, SQL Mapper로 Persistence Layer를 구현할 수 있습니다.

 

 

 

 

 

⏺ JDBC만을 이용하여 Persistence Layer 구현하기

 

 

JDBC란?

  • Java Database Connectivity의 약자
  • 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API
  • 자바 애플리케이션에서 DBMS의 종류에 상관없이, 하나의 JDBC API를 이용해 DB 작업을 처리
  • 각각의 DBMS는 이를 구현한 JDBC 드라이버를 제공한다.

 

 

JDBC의 단점

  • 간단한 SQL을 실행하는 데도 중복된 코드를 반복적으로 사용
  • Connection과 같은 공유 자원을 제대로 릴리즈(반환) 해주지 않으면 시스템의 자원이 바닥나는 버그 발생
  • DB에 따라 일관성 없는 정보를 가진 채로 Cheked Exception (SQLException)처리
    • Cheked Exception이란?
      => Checked Exception은 컴파일 단계에서 확인 가능한 예외이다.
      => 다른 말로는 "Compiletime Exception"이라고 한다.
      => Checked Exception은 try/catch로 감싸거나 throw로 던지는 처리를 반드시 해주어야 한다.
      => 예외처리를 컴파일러가 강제하는 것이다.

 

 

 

 

 

 

 

 

 

⏺ Persistence Framework로 Persistence Layer 구현하기

 

SQL Mapper란?

  • 자바 Persistence Framework 중 하나입니다.
  • SQL을 직접 작성합니다.
  • SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객체화

 

 

SQL Mapper의 장점

  • 쿼리 수행 결과와 객체의 필드를 맵핑 & RowMapper 재활용
  • JdbcTemplate가 JDBC에서 반복적으로 해야 하는 많은 작업들을 대신해줌

 

 

SQL Mapper에 속하는 대표적인 프레임워크로 MyBatis가 있습니다.

 

 

 

 

 MyBatis란?

  • 반복적인 JDBC 프로그래밍을 단순화합니다.
  • SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL을 분리하여 관리합니다.
  • JDBC만 사용하면 결과를 가져와서 객체의 인스턴스에 매핑하기 위한 많은 코드가 필요하겠지만,
    마이바티스는 그 코드들을 작성하지 않아도 되게 해 줍니다.
  • 동적 쿼리를 작성할 수 있습니다.
    • 동적 쿼리란?
      => 

 

 

 

  • 위 이미지는 마이바티스가 데이터를 Access 하는 순서를 나타내는 것입니다.
  • 스프링 부트에서 구현할 때 보라색 부분인 Mapper Interface와 Mapping File을 구현하게 되면
    자연스럽게 객체의 필드와 SQL문이 매핑되게 됩니다.
  • Mapper Interface에 대한 구현체를 우리가 구현하지 않아도 마이바티스에서 자동으로 생성하게 됩니다.

 

 

 

마이바티스의 장점

  • 자동으로 Connection 관리를 해주면서 JDBC를 사용할 때의 중복 작업 대부분을 없애준다.
  • 복잡한 쿼리나 다이나믹하게 변경되는 쿼리 작성이 쉽다.
  • 관심사 분리
    • DAO로부터 SQL문을 분리하여 코드의 간결성 및 유지보수성을 향상할 수 있습니다.

 

 

 

😓 JDBC와 마이바티스의 단점

  • SQL을 직접 다룸으로 문제점이 생기게 된다.
  • 특정 DB에 종속적으로 사용하기 쉽습니다.
  • 테이블 마다 비슷한 CRUD SQL 작성은 DAO 개발이 매우 반복되는 작업입니다.
  • 테이블 필드가 변경될 시 이와 관련된 모든 DAO의 SQL문, 객체의 필드 등을 수정해야 합니다.
  • 코드상으로 SQL과 JDBC API를 분리했다 하더라도 논리적으로 강한 의존성을 가지고 있습니다.
  • SQL문을 직접 작성하면 SQL에 의존적인 개발을 하게 됩니다.
    • 데이터 베이스에 항상 의존 관계를 가지게 됩니다.

 

 

 

 

 

ORM이란?

  • 객체와 관계형 데이터베이스를 매핑하는 것
  • 객체간의 관계를 바탕으로 SQL문을 자동으로 생성하고 직관적인 코드(메서드)로 데이터를 조작 할 수 있습니다.'
    • SELECT * FROM user; -> user.findAll();
  • ORM의 대표적인 예로 JPA가 있습니다.

 

 

ORM의 장점

  • 패러다임 불일치 문제 해결
    => 객체지향 언어가 가진 장점을 활용할 수 있습니다.

  • 생산성
    => 지루하고 반복적인 CRUD 용 SQL을 개발자가 작성하지 않아도 됩니다.

  • 데이터 접근 추상화, 벤더 독립성
    => 데이터베이스 벤더마다 미묘하게 다른 데이터 타입, SQL을 손쉽게 해결
  • 유지보수
    => 필드 추가, 삭제 시 관련된 CRUD 쿼리를 직접 수정하지 않고, 엔티티를 수정하면 됩니다.

 

 

ORM의 단점

  • 복잡한 쿼리 사용이 어렵습니다.
    • JPA에서는 SQL과 유사한 기술인 JPQL을 지원합니다.
    • SQL 자체 쿼리를 작성할 수 있도록 지원합니다.
    • SQL Mapper와 혼용해서 사용도 가능합니다.

 

 


 

😃 느낀점

  • 현재(작성일) 사이드 프로젝트에서 SQL Mapper인 마이바티스를 사용하고 있습니다.
    • 직접 사용해보면서 장점과 단점을 생각해볼 예정입니다.
  • SQL Mapper를 사용하는 프로젝트를 끝낸 후에 JPA를 사용하는 프로젝트 진행
    • 부트캠프에서 Spring Data JPA를 사용했지만 너무 얕게 공부했다는걸 영상을 보고 알게되었습니다.
  • JPA를 김영한님 책과 인프런 강의를 보면서 공부할 예정입니다.

 

 

 

 

 

📝 해야할 것!

  • 사이드 프로젝트를 통해 SQL Mapper(마이바티스)의 장점과 단점을 사용하면서 알아가기
  • 사이드 프로젝트가 끝난 후에 JPA에 대한 공부를 진행
    • JPA 공부를 통해 ORM에 대한 개념을 이해하고 사용하는 이유와 장단점을 알아가기
    • JPA에서 SQL을 처리하는 구조를 공부하면서 사용해보기

 

 

 

 

반응형

+ Recent posts