Devhyun

메뉴

현재 화면 위치

데브현 메인 블로그 포스트

타이틀

SpringBoot 토이 프로젝트로 배운 내용

2020.01.18
핫한 길다란

1년간 서비스를 했었던 JABIS 웹 애플리케이션을 정리하였습니다.
토이 프로젝트로 진행했던 애플리케이션인 만큼 개발과정이 정말 재미있었고 누군가 사용하는게 참 신기했었는데, 시간이 지나면서 미흡한 점이 한두개가 아니였네요.. 😨

사용하시는 분들의 많은 요청이 있었음에도 꾸준한 업데이트와 유지보수가 진행되지 않았고 실질적으로 서버비가 부과되는 시점에서 비용문제로 정리하게 되었습니다.
이 프로젝트를 한번 되돌아보며 학습한 내용을 정리해보려고 합니다.

개발 스택

당시 프로젝트를 기획 준비하는 과정에서 제가 개발할 수 있는 언어는 Java + Springboot였고 당시 Vue에 대해 관심있게 공부하고 있었습니다.

Java로 프로젝트를 시작한다면 Spring + JSP 혹은 SpringBoot + Template Engin을 사용하는 형태입니다.
물론 SpringBoot로 REST API를 구축하고 Vue로 프론트를 개발하여 SPA를 활용하는 방법도 있었지만, 아직 학습량이 부족했고 자신이 없어 SpringBoot와 Thymleaf를 사용하였습니다.

프론트엔드

Thymleaf

Thymleaf는 SpringBoot와 가장 잘 어울리는 템플릿 엔진입니다.
JSP의 include나 태그라이브러리를 사용할 수 있어 쉽게 적응하여 개발할 수 있었습니다.

다만, layout + fragment의 사용이 코드의 가독성을 안 좋게 하는 인상을 받았습니다.
fragment는 하나의 파편이며 layout은 사전에 fragment를 모아 정의해놓은 일종의 페이지 템플릿이라 할 수 있습니다.

아래는 로그인 페이지입니다.
login_layout.html이라는 사전에 정의한 layout에 현재 페이지에서 선언한 fragment를 삽입하여 페이지를 랜더링 하는 형태입니다.

📃 login.html

<!DOCTYPE html>
<html class="no-js css-menubar" lang="ko" 
xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 
layout:decorate="~{layout/login_layout.html}" 
th:with="
title='JABIS', 
bodyClass='animsition page-login-v2 layout-full page-dark'
">
<th:block layout:fragment="page">
<div id="loginVue" class="page" data-animsition-in="fade-in" data-animsition-out="fade-out">
    <div class="page-content">
        <div class="page-brand-info">
            <div class="brand">
                <img class="brand-img" src="/images/logo@2x.png" alt="...">
                <h2 class="brand-text font-size-40" th:text="${title}">Application</h2>
            </div>
            <p class="font-size-20">
                A computerized system, process, or business is one in which the work is done by Network.
            </p>
        </div>
        <div class="page-login-main animation-slide-right animation-duration-1">
            <div class="brand hidden-md-up">
                <img class="brand-img" src="/images/logo-colored@2x.png" alt="...">
                <h3 class="brand-text font-size-40" th:text="${title}">Application</h3>
            </div>
            <h3 class="font-size-24">로그인</h3>
            <p>
                제일학원 학생/강사 통합관리 프로그램
            </p>
            <form id="loginForm" method="post" autocomplete="off" role="form">
                <div class="form-group">
                    <label class="sr-only" for="inputEmail">Email</label>
                    <input type="text" class="form-control" id="email" name="email" placeholder="이메일" data-fv-field="email" v-model="email" th:value="${email}">
                </div>
                <div class="form-group">
                    <label class="sr-only" for="inputPassword">Password</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="비밀번호" data-fv-field="password" v-model="password">
                </div>
                <div class="form-group clearfix">
                    <div class="checkbox-custom checkbox-inline checkbox-primary float-left">
                        <input type="checkbox" id="rememberMe" name="rememberMe" v-model="rememberMe" th:checked="${rememberMe}">
                        <label for="rememberMe">이메일 저장하기</label>
                    </div>
                    <a class="float-right" href="javascript:void(0);" v-on:click="restorePassword()">비밀번호 찾기</a>
                </div>
                <button id="loginButton" type="submit" class="btn btn-primary btn-block">로그인</button>
            </form>
            <p>
                계정이 없으신가요?<br>





                <a href="/register/">회원가입</a>
            </p>
            <footer class="page-copyright">
            <p>
                WEBSITE BY OPZYRA
            </p>
            <p>
                © 2018. All RIGHT RESERVED.
            </p>
            <div class="social">
                <a class="btn btn-icon btn-round social-twitter mx-5" href="javascript:void(0)">
                <i class="icon fa-github" aria-hidden="true"></i>
                </a>
                <a class="btn btn-icon btn-round social-facebook mx-5" href="javascript:void(0)">
                <i class="icon fa-linkedin" aria-hidden="true"></i>
                </a>
                <a class="btn btn-icon btn-round social-google-plus mx-5" href="javascript:void(0)">
                <i class="icon fa-google-plus" aria-hidden="true"></i>
                </a>
            </div>
            </footer>
        </div>
    </div>
</div>
</th:block>

<th:block layout:fragment="vuejs">
    <script th:attr="src=@{'/js/vue/loginVue.js?ver=' + ${@environment.getProperty('app.version')}}"></script>
</th:block>

<th:block layout:fragment="customScript">
    <script src="/vendor/jquery-placeholder/jquery.placeholder.js"></script>
    <script src="/js/common/Plugin/jquery-placeholder.js"></script>
    <script th:if="${rememberMe}==true">
            $('#rememberMe').prop('checked', true);
    </script>
</th:block>

<th:block layout:fragment="customStyle">
    <link rel="stylesheet" th:attr="href=@{'/css/custom/login-v2.css?ver=' + ${@environment.getProperty('app.version')}}">
    <style>
           body {
             background: transparent;
           }
    </style>
</th:block>

</html>

📃 login_layout.html

<!DOCTYPE html>
<html class="no-js css-menubar" lang="ko" xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="utf-8">
    <th:block th:replace="fragments/common/meta::metaFragment"></th:block>

    <title th:text="${title}">Application</title>

    <link rel="apple-touch-icon" href="/images/apple-touch-icon.png">
    <link rel="shortcut icon" href="/images/favicon.ico">

    <th:block th:replace="fragments/common/css::cssFragment"></th:block>

    <!-- Scripts -->
    <script src="/vendor/breakpoints/breakpoints.js"></script>
    <script>
      Breakpoints();
    </script>
</head>
<body th:class="${bodyClass}">
    <th:block th:replace="fragments/common/page::pageFragment"></th:block>
    <th:block th:replace="fragments/common/script_error::scriptFragment"></th:block>
</body>

</html>

코드의 중복을 최소화하자는 취지로 도입하여 적용해봤는데, 아래와 같은 문제점이 있었습니다.

  1. login.html을 보고 페이지가 어떻게 랜더링되는지 알 수 없다. => layout을 참고하거나 실제 랜더링된 페이지를 확인해야 한다.
  2. layout에 사전에 정의되있는 CSS나 JS로 인해 페이지에서 import 되는 순서를 제어하기 어렵다.
  3. CSS와 Script파일을 각 페이지별로 만들어 관리했는데, 너무 많고 복잡해진다. 애플리케이션 규모에 비해 파일이 너무 많아진다.

Vue CDN

Vue가 추구하는 데이터 바인딩을 사용해보고 싶어 CDN으로 프로젝트에 적용해보았습니다.
대부분 비동기 통신을 통해 데이터를 화면에 랜더링하는 형태로 개발되었고 SPA를 흉내 낸 MPA가 되어 버렸습니다.

사실 Vue를 적용하면서 가장 큰 걸림돌은 jQuery였습니다.
jQuery의 다양한 라이브러리로 그리드나 테이블과 같은 리스트형 데이터를 표현했었는데 여기에 Vue를 적용하니 새로운 데이터가 들어올 때 DOM이 새롭게 그려지고 달려있던 이벤트가 모두 사라지는 이슈를 많이 겪었습니다. 그 때마다 다시 해당 라이브러리를 init하였는데 점점 스파게티가 되어가는 코드를 볼 수 있었습니다.

퍼블리싱

이 프로젝트를 할 때만 해도 부트스트랩 없이는 화면 개발이 불가능하였습니다.
부트스트랩 기반의 관리자 템플릿을 활용하여 빠르게 화면을 그렸지만 여러 가지로 커스터마이징이 어려웠고 크로스브라우징 이슈도 많이 발생했었습니다.

결국 IE에서는 예상과는 다르게 화면이 랜더링되어 사용성에 문제점이 많았고 크롬과 사파리를 권장하였습니다. 다행이 대부분 모바일에서 애플리케이션에 접속했기 때문에 많은 이슈가 발생하진 않았지만, 퍼블리싱에 대한 이해가 부족한 상태로 단순히 부트스트랩에만 의존하여 진행하여 CSS코드가 중구난방으로 정형화되어있지 않아 지속적인 유지보수가 어려운 상태로 기울어져 가고 있었습니다.

특히, 관리자 템플릿에 있는 수많은 JS코드들로 인해 애플리케이션이 무거워지고 Script도 꼬여 여러 가지 에러가 발생하는 경우가 많았습니다.

백엔드

권한

학원 내부의 직급별로 제어할 수 있는 권한이 다양했습니다.
SpringSecurity에 ROLE에 따라 비지니스 로직에서 판단하여 예외를 떨구는 형태로 개발하였지만, 너무 다양하고 복잡한 권한을 통제하다보니 관리가 어려웠습니다.

조금 더 공부하면서 @preauthorize와 같은 어노테이션을 알게 되었고 필터링을 활용하는 방법을 검토하였습니다.

통계

학원 관리 프로그램이다 보니 학생 혹은 강사에 대한 통계자료를 표현해야 하는 경우가 많았습니다.
초기에 통계를 설계할 때 했던 실수는 실제 저장된 데이터를 참고하여 통계를 산출하는 방식이었습니다.
통계는 해당 시점에 원하는 정보를 보여주는 개념입니다. 즉 불변성을 지니고 있어야 합니다.

이후 스케줄러를 통해 해당 시점의 데이터를 가공하여 테이블에 쌓는 형태로 급하게 수정하였습니다.
초기 설계의 중요성을 새삼 느꼈던 수정작업이었습니다.

마치며

첫 릴리즈 토이프로젝트인 JABIS는 기술부채? 가 쌓이면서 점점 다시 보고 싶지 않은 레거시 코드의 애플리케이션이 돼버렸고 발생하는 비용문제로 서비스를 중단하게 되었습니다. 아쉽지만 제 성장에 있어 많은 도움을 준 애플리케이션입니다.

현재 유지보수중인 Devhyun도 Node로 개발하면서 비슷한 이슈의 개선해야할 포인트들이 많이있는데, 천천히 하나하나 수정하면서 조금 더 아름다운 코드로 발돋움 하기 위해 노력해보려고 합니다.

그리고 앞으로도 꾸준히 좋은 코드란 무엇인가 라는 질문에 정답을 찾기 위해 공부하려고 합니다 😊

토이프로젝트가 일회성이 아닌 유지보수의 과정을 거친다면 코드에 대해 한번 되돌아보고 장단점을 몸소 느낄 수 있는 좋은 기회라고 생각합니다. 어떤 아이디어라도 좋으니 지금바로 시작해보시는 건 어떨까요? 🚀

0개의 댓글

로그인을 하시면 댓글을 작성할 수 있어요 !
목록으로 가기