[Spring Boot] Thymeleaf(타임리프) layout dialect 사용하기

 

 

웹 페이지를 만들게되면 기본적으로 head, header, content, footer 등의 레이아웃으로 나누어서 만들게된다. 기존에 스프링과 jsp를 통하여 만들 때 tiles를 이용하여 각 영역들을 분할하여 작업할 수 있었는데, 타임리프에선 thymeleaf-layout-dialect를 이용하여 비스하게 사용할 수 있다.

 

 

타임리프는 프로젝트를 생성할 때 의존성을 추가하면 된다.

 

 

2021.08.13 - [Spring-Boot] - [Spring Boot] 프로젝트 생성하기(Intellij)

 

[Spring Boot] 프로젝트 생성하기(Intellij)

[Spring Boot] Spring Initializr를 이용하여 프로젝트 생성하기(Intellij) 기존에 STS(Spring Tool Suite)나 Eclipse 툴 안에서 Maven이나 Gradle로 프로젝트를 생성했던 경험이 있는데 Spring Initializr 웹 도..

dbsyys.tistory.com

 

기존에 프로젝트에 타임리프를 사용중이라면 의존성을 하나 더 추가하면 된다.

 

 

 

https://mvnrepository.com/

 

메이븐 사이트에 접속하여 "Thymeleaf layout dialect"를 검색한다.

 

 

검색 후 내용. 같은 이름이지만 2013년은 너무 오래전이니 2021년을 선택하자

 

 

Maven인지 Gradle인지는 각자 프로젝트 의존성 설정방식에 맞추면된다. 현재 프로젝트에서는 Gradle을 사용하고 있기때문에 Gradle을 사용한다.

 

 

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '2.5.3'

 

해당 코드를 Gradle 의존성에 추가했다면 하나의 웹페이지를 구성할 영역들과 동적으로 바꿀 페이지를 지정하면 된다.

 

 

 

  • layout/head.html(영역으로 쓸 페이지)
  • layout/header.html(영역으로 쓸 페이지)
  • layout/navBar.html(영역으로 쓸 페이지)
  • layout/footer.html(영역으로 쓸 페이지)
  • layout/layout.html(각 영역들을 조합한 페이지)
  • main/content.html(각 영역을 유지하고 동적으로 변환할 페이지)
  • member/saveForm.html(각 영역을 유지하고 동적으로 변환할 페이지)

 

 

- layout/layout.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
    <head th:replace="layout/head :: layout_head(~{::meta}, ~{::title}, ~{::link})">
    </head>
    <body class="nav-md">
        <div class="container body">
            <div class="main_container">

                <div th:replace="~{layout/navBar :: layout_navBar}"></div>
                <div th:replace="~{layout/header :: layout_header}"></div>
                <div layout:fragment="layout_content"></div>
<!--                <div th:replace="~{layout/content :: layout_content}"></div>-->
                <div th:replace="~{layout/footer :: layout_footer}"></div>

            </div>
        </div>

    </body>
</html>

 

기본적으로 타임리프를 쓰기위해선 xmlns:th="http://www.thymeleaf.org" 설정을 해야하며 layout-dialect를 사용하는 페이지에선 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"를 설정한다.

 

각 영역들을 지정할 땐 th:replace로 경로를 지정하며, 주된 content부분의 내용은 페이지마다 동적으로 변환할 것이기 때문에  layout:fragment를 지정한다. 또한 <head> 부분은 head.html 페이지에서 설정한 정보를 받기위하여 meta, title, link 요소를 파라미터로 받아오게 설정했다.

 

 

- layout/head.html

<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="layout_head(meta, title, link)">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <!-- Meta, title, CSS, favicons, etc. -->
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" th:href="@{/images/favicon.ico}" type="image/ico" />

    <title>Gentelella Alela! | </title>

    <!-- Bootstrap -->
    <link th:href="@{/vendors/bootstrap/dist/css/bootstrap.min.css}" rel="stylesheet">
    <!-- Font Awesome -->
    <link th:href="@{/vendors/font-awesome/css/font-awesome.min.css}" rel="stylesheet">
    <!-- NProgress -->
    <link th:href="@{/vendors/nprogress/nprogress.css}" rel="stylesheet">
    <!-- iCheck -->
    <link th:href="@{/vendors/iCheck/skins/flat/green.css}" rel="stylesheet">
    .
    .
    .
    .

 

head부분은 <head>태그의 모든 설정부분을 그대로 넘기기 위해서 meta, title, link등 의 요소를 파라미터로 넘겼다. 현재 설정한 페이지의 요소들이 layout.html 파일의 내용으로 변경된다.

 

 

- layout/header.html

<html xmlns:th="http://www.thymeleaf.org">
    <th:block th:fragment="layout_header">
        <!-- top navigation -->
            <div class="top_nav">
                <div class="nav_menu">
                    <div class="nav toggle">
                        <a id="menu_toggle"><i class="fa fa-bars"></i></a>
                    </div>
                    .
                    .
                    .
                    .

 

 

- layout/navBar.html

<html xmlns:th="http://www.thymeleaf.org">
    <th:block th:fragment="layout_navBar">
        <div class="col-md-3 left_col">
            <div class="left_col scroll-view">
                <div class="navbar nav_title" style="border: 0;">
                    <a href="index.html" class="site_title"><i class="fa fa-paw"></i> <span>Gentelella Alela!</span></a>
                </div>

                <div class="clearfix"></div>
                .
                .
                .
                .

 

 

- layout/footer.html

<html xmlns:th="http://www.thymeleaf.org">
    <th:block th:fragment="layout_footer">
        <!-- footer content -->
            <footer>
                <div class="pull-right">
                    Gentelella - Bootstrap Admin Template by <a href="https://colorlib.com">Colorlib</a>
                </div>
                <div class="clearfix"></div>
            </footer>
        <!-- /footer content -->
    </th:block>
</html>

 

layout.html 페이지에서 영역으로 쓰일 부분은 th:replace, 각 영역에 해당하는 페이지는 th:fragment로 지정하며 두 부분의 이름은 같아야 한다.

 

 

- main/content.html

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/layout">
    <th:block layout:fragment="layout_content">
        <!-- page content -->
            <div class="right_col" role="main">
                <!-- top tiles -->
                <div class="row" style="display: inline-block;" >
                    <div class="tile_count">
                        <div class="col-md-2 col-sm-4  tile_stats_count">
                            <span class="count_top"><i class="fa fa-user"></i> Total Users</span>
                            <div class="count">2500</div>
                            <span class="count_bottom"><i class="green">4% </i> From last Week</span>
                        </div>
                        .
                        .
                        .
                        .
                        .

 

layout.html 에서 동적으로 변경시킬 content가 들어갈 페이지 이기때문에 html 부분에 xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" 설정을 한다. 추가적으로 해당 페이지에서는 어떤 페이지의 fragment로 쓸지 알아야하기 때문에 layout:decorator 설정을하고 layout으로 쓸 페이지의 경로를 지정해준다.

 

해당 페이지는 페이지 레이아웃은 유지하면서 상황에따라 content 부분만 바꿀 것이기 때문에 위에서 지정했던 영역들과는 다르게 th:fragment가 아닌 layout:fragment로 설정한다. 마찬가지로 layout.html에서 지정했던 name과 동일하게 지정한다.

 

 

- member/saveForm.html

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/layout">
    <th:block layout:fragment="layout_content">

    </th:block>
</html>

 

해당 페이지에도 content.html과 같은 설정을 했다. layout.html 페이지를 레이아웃으로 유지하면서 content 부분을 content.html과 saveForm.html 부분을 동적으로 변환하며 사용할 수 있다. 이러한 설정을 통해 다른 페이지를 추가했을 때도 같은 레이아웃을 적용할 수 있다.

[Mysql] Access denied for user (using password: yes) 에러 발생

 

 

JPA 공부하면서 뭔가 혼자 해보려고 기존에 쓰던 h2데이터베이스 말고 다른 데이터베이스 뭘 쓸까 하다가 개인적인 작업이기 때문에 mysql 데이터베이스를 사용하려했는데...

 

 

하 뭐지

 

저 위에 나오는 에러에서 (using password: YES) 이 부분이 YES라는 것은 거의 비밀번호를 잘못 입력했을 때 나오는 에러라고한다...

 

계정도 방금 생성했고, 테스트하는 계정의 비밀번호는 개발 시작하고 지금까지 쭉 동일한 비밀번호를 사용했기 때문에 전혀 비밀번호가 틀릴 일이 없는데 ?

 

 

그래서 그냥 처음부터 다시 세팅했다.

 

C:\Users> mysql -u root -p

 

콘솔 창에서 mysql 루트 계정으로 접속한다.

 

mysql> use mysql

 

해당 모드로 변경한 후에

 

select host, user, authentication_string from user;

 

계정을 생성하지 않았다면 아무것도 나오지 않겠지만

 

create user 유저명@호스트명(ex.localhost) identified by '비밀번호';

 

쿼리를 입력하게 되면

 

+-----------+------------------+------------------------------------------------------------------------+
| host      | user             | authentication_string                                                  |
+-----------+------------------+------------------------------------------------------------------------+
| localhost | 유저명           | 패스워드                                                                |

 

이런식으로 나오게 된다.

 

그럼 여기서 권한을 주어야 하는데

 

 

grant all privileges on 해당 데이터베이스명.* to 유저명@호스트;

 

 

all은 모든 권한을 부여하려고 썼다. all 말고도 select 와 같은 다른 개별 권한도 줄 수 있다.

 

사실 여기서 저 계정을 입력하고 들어가면 되어야 하는거 아니야 ? 싶었는데 내 뜻대로 되지 않았기때문에...

 

 

alter user '유저명'@'호스트' identified with mysql_native_password by '비밀번호';

 

 

비밀번호 변경을 다시 해줬다.

 

 

flush privileges;

 

 

플러시는 습관처럼 작업 후 해준다.

 

 

성공...!

 

끝 !

 


* 추가 수정(2021-09-07)

 

 

이건 또 모야

 

회사 PC에서 세팅 해 놓은대로 집에와서 했는데... ? 집 컴퓨터에서 Public Key Retrieval is not allowed 라는 에러가 나버렸다 ㅎㅎ Mysql 8.0 이상의 버전은 드라이버 속성의 allowPublicKeyRetrieval=true의 설정을 해주어야 한다는데 ??

 

 

기본 커넥션 설정 옆에 Driver properties가 있다 !

 

 

성공 !

 

또 끝 !

[JPA] JPA 연관관계 매핑하기

 

 

JPA를 이용하여 기본 객체와 관계형 데이터베이스를 엔티티로 매핑시켰다면 각 엔티티간의 연관관계를 매핑 시켜야 한다.

 

2021.08.31 - [Spring-Boot/JPA] - [JPA] JPA 엔티티 매핑과(Entity Mapping) 엔티티 설계

 

[JPA] JPA 엔티티 매핑과(Entity Mapping) 엔티티 설계

[JPA] JPA 엔티티 매핑(Entity Mapping) 엔티티 설계 JPA에서 가장 기본적이지만 중요한 두 가지 개념을 뽑아보자면 영속성 컨텍스트 객체와 관계형 데이터베이스(RDB) 매핑 이 두가지가 아닐까 한다

dbsyys.tistory.com

 

객체와 테이블을 매핑시켜 엔티티로 사용할 수 있지만 객체와 테이블이 연관관계를 맺는 형식에 차이가 있기때문에 우리는 둘 중에 어느 쪽으로 모델링을 해야한다.

 

객체와 테이블의 연관관계 형식을 보자면

 

 

(객체 연관관계)

 

 

(테이블 연관관계)

 

차례로 객체 연관관계와 테이블 연관관계를 다이어그램으로 나타냈다. 회원과 주문은 1:N 관계를 가지고 있다. 회원의 아이디 값과 주문의 아이디 값으로 서로 연관관계가 정의 되어있다. 

 

언뜻보면 맞는 관계인것 같지만 위의 다이어그램에 객체 연관관계에는 문제가 있다. 테이블은 연관관계를 지정할 때 Join을 이용하여 연관관계를 지정한다. 회원 테이블의 외래키(ORDER_ID)로 주문 테이블의 기본키(ORDER_ID)를 가질 수 있는 것이다. 이렇게 되면 테이블간의 연관관계가 성립된다.

 

하지만 객체 연관관계에서는 Join을 이용 할 수 없다. 회원의 주문아이디(orderId) 값과 주문의 아이디(id) 값은 따로 존재한다.

 

 

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id; // 회원 id

    private String Mname;

    private Long orderId; // 주문 id

}

 

이렇게 되면 회원 객체의 값이 변경 되었을 때 주문 객체의 값 또한 변경되어야 하고 그 반대의 경우도 마찬가지이다. 이건 객체지향적이라고 할 수 없다. 하지만 객체는 참조를 이용하여 연관관계를 지정할 수 있다.

 

 

(올바른 객체 연관관계)

 

회원 객체는 주문객체의 아이디를 따로 생성하는 것이 아니라 주문 객체를 하나의 필드로 참조하는 것이다. 이렇게 하면 객체 지향적으로 모델링을 할 수 있다.

 

하지만 테이블과 객체의 연관관계 매핑 방식의 큰 차이가 있으므로 JPA를 이용하여 객체 연관관계와 테이블 연관관계의 연관관계를 매핑할 것이다. 


 

연관관계 매핑에 있어서 연관관계는 3가지의 큰 특징을 가진다.

 

  • 방향(Direction) : 단방향, 양방향
  • 다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N)
  • 연관관계의 주인(Owner) : 객체의 양방향 연관관계 시 연관관계 주인이 필요

 

위에서 계속 언급했듯 객체와 테이블은 관계를 맺는차이가 있다. 객체의 연관관계 매핑에 방향성이 있는 이유는 

- 객체 연관관계

  • 회원 -> 주문 연관관계 (단방향)
  • 주문 -> 회원 연관관계 (단방향)

- 테이블 연관관계

  • 회원 <-> 주문 연관관계 (양방향)

의 차이가 있기 때문이다. 객체의 양방향 관계는 객체의 서로 다른 단방향 관계가 각각 있는 것이다. 객체를 단방향 연관관계만 지정해도 연관관계 매핑은 완료된 것이나 양방향 매핑을 하게되면 연관관계의 주인으로 지정하지 않은 반대 방향으로 조회가 가능하다(객체 그래프 탐색, *역 방향은 조회만 가능하다)

 

아래는 양방향 연관관계 매핑을 한것이다.

 

 

(객체의 양방향 연관관계)

 

 

@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String Mname;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();

}
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    private String OName;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;   

}

 

@ManyToOne

  • 다대일(N:1) 관계를 매핑할 때 사용한다. (@JoinColumn과 함께 사용)

@OneToMany

  • 일대다(1:N) 관계를 매핑할 때 사용한다. mappedBy 라는 속성을 통해 다(N)객체와 연관관계를 매핑한다.

@JoinColumn

  • @ManyToOne과 함께 사용하며 name 속성을 사용하여 일(1)객체와 연관관계를 매핑한다.

 

객체의 연관관계를 양방향으로 설정했다면 연관관계의 주인(Owner)을 정해야한다. 연관관계 주인은 객체의 두 관계 중 하나를 연관관계의 주인으로 지정하고 연관관계의 주인만이 외래 키를 관리(등록, 수정) 할 수 있다. 주인이 아닌 역방향은 조회(읽기)만 가능하다. 실제로 사용하다보면 역방향으로 탐색할 일이 많기 때문에 양방향 매핑을 사용한다.

 

위에서 본 속성들 중에서 주인은 mappedBy 속성을 사용하지 않고 주인이 아닌 속성에 mappedBy 속성을 지정한다. 연관관계의 주인은 외래 키(FK)의 위치를 기준으로 정해야 한다. 양방향 연관관계에서는 양 쪽에 값을 설정하고 연관관계에 관련된 편의 메소드를 생성하는 것을 권장한다. 또한 양방향 매핑 시에 무한루프에 빠질 수 있다는것을 주의하자.

 

 

[JPA] JPA 엔티티 매핑(Entity Mapping) 엔티티 설계

 

JPA에서 가장 기본적이지만 중요한 두 가지 개념을 뽑아보자면

 

  • 영속성 컨텍스트
  • 객체와 관계형 데이터베이스(RDB) 매핑

 

이 두가지가 아닐까 한다.

 

 

영속성 컨텍스트에 관련된 내용은 아래 링크에 정리해보았다.

 

2021.08.19 - [Spring-Boot/JPA] - [JPA] 영속성(Persistence) 관리와 영속성 컨텍스트(Persistence Context)

 

[JPA] 영속성(Persistence) 관리와 영속성 컨텍스트(Persistence Context)

[JPA] 영속성(Persistence) 관리와 영속성 컨텍스트(Persistence Context) 지난번에는 JPA와 관련하여 기본적인 세팅과 EntitiyManagerFactory, EntitiyManager, EntitiyTransaction의 특징과 어떤 역할을 하..

dbsyys.tistory.com


 

이번엔 객체와 테이블 매핑을 할 때 사용하게되는 속성들을 정리해보겠다.

 

먼저 엔티티 매핑이란 객체와 테이블 사이의 관계와 속성을 지정할 수 있도록 매핑하는 것이다.

 

관련된 어노테이션을 사용해서 테이블과 클래스나 필드와 속성 등을 매핑할 수 있다.

 

@Entity
@Table(name = "TBMember")
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    
    @Column(name = "name")
    private String username; 
 	
    @Enumerated(EnumType.STRING) 
    private RoleType roleType; 
 	
    @Temporal(TemporalType.TIMESTAMP) 
    private Date createdDate; 
 	
    @Lob 
    private String description;

}

@Entity

  • 해당 어노테이션이 붙은 클래스는 JPA에 관리되어지며 엔티티라고 한다. JPA를 사용하기 위해선 필수적으로 사용해야한다. (*기본 생성자 필수)

@Table

  • 엔티티와 매핑할 테이블을 지정한다. name 속성을 통해 매핑할 테이블 이름을 지정 할 수있다. ex) 엔티티 이름을 Order이라고 한다면 Oracle에선 order by 예약어와 중복되어 테이블 명으로 설정 할 수 없기때문에 변경 해야함.

@Id

  • 해당 필드를 기본 키로 매핑한다. (직접 할당 하는 방식)

@GeneratedValue

  • 해당 필드를 기본 키로 매핑한다. (자동 생성 하는 방식, @Id 와 같이 사용)
  • strategy 속성을 이용하여 생성되는 타입을 정할 수 있다. default 값은 Auto이며 sql 방언에 따라 자동 지정된다.

@Column

  • 필드와 컬럼의 매핑 속성을 지정한다. 컬럼 이름이나 변경 여부, null 여부, unique, 길이 등의 제약조건을 설정할 수 있다.

@Enumerated

  • 자바의 enum 타입을 매핑할 때 사용한다.
  • 속성타입으로 ORDINAL과 STRING이 있는데 꼭 STRING을 사용하길 권장한다. ORDINAL은 enum 순서를 DB에 저장하고 STRING은 이름으로 값을 저장하는데 enum 타입이 변경 되었을 시 ORDINAL은 순서로 저장하기 때문에 데이터가 꼬일 수 있다.

@Temporal

  • 날짜 타입을 매핑 할 때 사용한다. 속성 타입으로 DATE(날짜), TIME(시간), TIMESTAMP(날짜와 시간) 등 이 있다.
  • Date 객체를 쓴 이유는 하이버네이트가 최신버전이라면 LocalDate나 LocalDateTime 객체를 사용 시 해당 어노테이션을 생략 할 수있다.

@Lob

  • 데이터베이스의 BLOB, CLOB 타입과 매핑할 때 사용한다. (속성은 없으며 문자타입은 CLOB이고  나머지는 BLOB이다.)
  • CLOB: String, char[] / BLOB: byte[]

 

JPA는 데이터베이스의 스키마(DDL)를 애플리케이션 실행 시점에 자동으로 생성할 수 있다. 기존의 테이블 중심의 설계에서 객체 중심의 설계로 바꾸는 것이다. 각 데이터베이스의 특징이 있는 방언별로 적절한 DDL을 생성한다. 하지만 이렇게 생성한 DDL은 개발 서버에서만 사용하거나 초기 세팅 시에만 사용할 것을 권장한다. 운영서버에서는 적절히 수정한 후에 사용해야 한다.

 

데이터베이스 스키마를 자동생성은 설정파일인 프로퍼티(application.properties)나 야믈(application.yml)에서 설정 하면 된다.

 

* application.properties

spring.jpa.hibernate.ddl-auto=create

 

* application.yml

jpa:
  hibernate:
    ddl-auto: create

 

해당 설정파일 create부분에 들어갈 속성들로 5가지가 있다.

 

  • create : 기존 테이블 삭제 후 다시 생성(DROP 후 CREATE)
  • create-drop : create와 같으나 종료 시점에 모든 테이블 DROP
  • update : 변경분만 반영
  • validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
  • none : 사용 안함

 

위에서 언급했듯이 운영서버에는 create, create-drop, update는 사용하지 않을것을 권장한다.

 

- 개발 초기 : create 또는 update

- 테스트 서버 : update 또는 validate

- 스테이징 서버나 운영 서버 : validate 또는 none

 

[Spring Boot] IoC(제어의 역전)와 DI(의존주입) 스프링 컨테이너

 

 

우리는 자바란 프로그래밍 언어를 이용해서 프로젝트를 개발 시에 스프링이라는 프레임워크를 이용해서 개발한다. 스프링 프레임워크를 사용하는 이유는 여러가지가 있겠지만 가장 주된 이유로는 자바가 객체지향프로그래밍 언어라는 이유일 것이다.

 

객체지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성을 가지며 이 특징들로 SOLID라는 5가지의 원칙을 만족하는 프로그래밍 방식을 의미한다. 이러한 특징들을 가진 프로그래밍 방식으로 여러개의 독립된 단위로 나누며 각각의 객체들의 역할과 구현방식으로 구분하여 프로그램을 유연하고 변경이 용이 하도록 만들 수 있다. 그러므로 확장이나 재 사용에 이점을 보인다.

 

설계를 할 때, 인터페이스와 구현체를 가지고 다형성을 만족하는 객체 지향 설계를 할 수있다. 하지만 다형성 만으로는 완전한 객체 지향 설계를 했다고 보기는 힘들다.

 

예를 들어

public class Member {

    private Long id;
    private String name;
    private boolean check; // 회원 여부
    private int count = 10000; // 가진 금액

    public Member(Long id, String name, boolean check) {
        this.id = id;
        this.name = name;
        this.check = check;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isCheck() {
        return check;
    }

    public void setCheck(boolean check) {
        this.check = check;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

}

Member라는 도메인과

 

public class MemberMain {

    public static void main(String[] args) {

        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

로직을 수행 할 Main

 

public interface MemberService {

    void enter(Member member);

}
public class MemberServiceImpl implements MemberService {

    private GradeMember gradeMember = new CommonGradeMember();

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

회원 여부 별 입장에 관한 로직을 수행할 서비스 인터페이스와 서비스 구현체를 나눴다.

 

package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public interface GradeMember {

    void countInfo(Member member);

}
package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public class CommonGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 4000;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {
        if (member.isCheck()){
            System.out.println("결제 될 금액은 " + MEMBER_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - MEMBER_PRICE) + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }
    }
}

지불해야 할 금액에 관한 상세 인터페이스와 구현체로 나눴다.

 

 

Member의 정보 별로 입장에 관한 인터페이스와 구현체, Member의 회원 여부에 따른 금액 측정에 관한 인터페이스와 구현체로 나눈 것이다. main 함수를 실행하면

------------------------
memberA님, 회원 여부 : true
결제 될 금액은 4000원 입니다
남은 금액: 6000원
------------------------

의 형태로 출력된다.

 

 

인터페이스(역할)과 구현체(구현)을 분리하여 개발했고 다형성(polymorphism)을 통해 하나의 객체 타입으로 여러가지 타입을 가질 수 있게, 즉 인터페이스를 통해 해당 인터페이스를 구현한 여러가지 구현체를 가질 수 있도록 만들었다.

 

하지만 만약에 여기서 회원정보 별로 결제될 금액이 고정 값이 아니라 할인율을 통해서 할인이 된다고 바뀐다고하면 위에서 말한 SOLID의 5원칙을 지키지 못하게 된다.

 

public class VIPGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 10;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {

        if (member.isCheck()){
            int discount = (COMMON_PRICE / MEMBER_PRICE);
            int resultCount = (member.getCount() - discount);

            System.out.println("할인 될 금액은 " + discount + "원 입니다");
            System.out.println("남은 금액: " + resultCount + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }

    }
}

할인을 고정 값이 아닌 할인율을 통해 지정하는 구현체이다.

 

 

MemberServiceImpl 구현체가 GradeMember 인터페이스를 의존하면서 CommonGradeMember 구현체까지 의존하고 있다.

 

 

위의 VIPGradeMember 구현체로 변경하게되면 MemberServiceImpl 구현체에서 아래와 같은 작업을 해야한다.

// private GradeMember gradeMember = new CommonGradeMember();
private GradeMember gradeMember = new VIPGradeMember();

 

 

SOLID 5원칙 중 DIP(Dependency inversion principle)의 원칙에 따라 구현체에 의존하지 않으며 인터페이스에 의존해야 하기때문에 설계를 변경해야한다.

 

// private GradeMember gradeMember = new CommonGradeMember();
private final GradeMember gradeMember;

인터페이스에만 의존하도록 설계를 변경하게되어 실제로 구현체가 없어 예외가 발생한다. 그렇기 때문에 다른 곳에서 GradeMember의 구현 객체를 대신 생성하여 주입해야한다.

 

public class MemberServiceImpl implements MemberService {

    /*private GradeMember gradeMember = new CommonGradeMember();*/
    private final GradeMember gradeMember;

    public MemberServiceImpl(GradeMember gradeMember) {
        this.gradeMember = gradeMember;
    }

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

MemberServiceImpl에서는 생성자를 통해 주입된 구현체의 인터페이스만 의존하여 처리하도록 변경했기때문에 GradeMember를 의존하는 구현체는 어떠한 것이든 받을 수 있고 구현체가 바뀌어도 따로 구조를 변경하지 않아도 된다.

 

public class MemberConfig {

    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        MemberConfig memberConfig = new MemberConfig();
        MemberService memberService = memberConfig.memberServiceImpl();
//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

위의 MemberConfig라는 클래스를 생성했다. 해당 클래스는 앞에서 할인율을 어떻게 적용할지 변경 할 때, 인터페이스와 구현체의 로직 변경은 일어나지 않고 단순히 구성 부분만 변경함으로 할인율을 변경 할 수 있다.

 

MemberConfig를 통해서 전체 동작 방식을 구성할 수 있다. 해당 클래스에서선 구현체를 생성하고 연결하는 역할을 하게 된다. 이로써 각 구현 객체들은 어떤 객체가 들어오게 되던 자신의 로직을 실행하는 역할만을 담당하고 전체 프로그램의 제어의 흐름은 MemberConfig 객체가 하게된다. 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다.

 

 

프로그램의 의존관계를 보게되면 MemberServiceImpl은 GradeMember에 의존하지만 어떤 구현체가 사용 될지는 모른다. 의존 관계는 정적 클래스 의존관계와 동적 객체 인스턴스 의존 관계로 나누어진다.

 

- 정적 클래스 의존관계

  • 클래스가 사용하는 코드만 보고 의존관계를 판단 할 수 있다. MemberServiceImpl은 GradeMember에 의존한다.

- 동적 객체 인스턴스 의존관계

  • 애플리케이션 실행 시점에서 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다. CommonGradeMember객체를 구현체로 쓸건지 VIPGradeMember 구현체인지는 실행 시에 결정된다. 런타임 시에 객체를 생성하고 클라이언트와 서버의 실제 구현체와의 의존관계가 연결 되는 것을 의존관계 주입(DI)이라 한다. 

MemberConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 

 


 

지금까지 작성한 IoC, DI는 순수하게 자바로만 구현한 것이다. 추가되어야 할 부분이 많지만 기본 틀은 이러한 방식을 사용했다. 스프링을 이용하게되면 IoC와 DI 컨테이너와 함께 그에 관련된 수 많은 추가 기능들을 제공한다. 앞에선 개발자가 MemberConfig와 같은 제어하는 객체를 직접 만들고 의존관계를 주입했지만 스프링에선 스프링이 의존관계를 주입해준다. 그것을 스프링 컨테이너라고 하고 여기에 등록된 객체를 스프링 빈이라고 한다.

 

@Configuration
public class MemberConfig {

    @Bean
    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        ApplicationContext ac = new AnnotationConfigApplicationContext(MemberConfig.class);
        MemberService memberService = ac.getBean("memberServiceImpl", MemberService.class);

//        MemberConfig memberConfig = new MemberConfig();
//        MemberService memberService = memberConfig.memberServiceImpl();

//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

기존에 DI 컨테이너로 사용하던 MemberConfig와 데이터를 입력하는 MemberMain을 스프링에서 지원하는 어노테이션 방식으로 사용을 변경했다. main함수에서 사용된 ApplicationContext 객체는 스프링 컨테이너이며 스프링컨테이너는 @Configuration이 붙은 MemberConfig를 설정 정보로 사용한다. 그리고 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체로 스프링 컨테이너에 등록한다. 다시 말해서 @Bean이 붙은 객체는 스프링 컨테이너에 등록되어 있으며 그것을 스프링 빈이라고 한다.

 

이처럼 스프링은 제어의역전(IoC)과 의존주입(DI)에 대해 기본적인 설정들을 편리하게 제공하며, 공통적으로 누구나 사용하면서 고려해볼 수 있는 무수히 많은 설정들을 제공한다.

[WEB] HTTP와 서블릿(Servlet) 요청(Request), 응답(Response)

 

 

클라이언트와 서버간에 인터넷 통신을 하는 것을 웹 통신이라고 하며, 이러한 웹은 HTTP기반으로 이루어진다. HTTP는 HTML, TEXT 등과 IMAGE, 음성, 영상 파일 등의 다양한 형태의 데이터를 전송할 수 있다.

 

이러한 HTTP를 기반으로 하는 서버에는 WEB서버와 WAS(Web Application Server)서버가 있는데 이 서버의 특징은 아래와 같다.

 

 

- 웹 서버(Web Server)

  • HTTP 기반으로 동작한다.
  • 정적 리소스(HTML, CSS, JS, 이미지, 사진, 영상 등) 제공
  • 예로 APACHE, NGINX 등이 있다.

 

- 웹 애플리케이션 서버(WAS - Web Application Server)

  • HTTP 기반으로 동작한다.
  • 웹 서버 기능을 포함한다. (정적 리소스 제공 가능)
  • 프로그래밍 코드를 실행해서 애플리케이션 로직을 수행한다.
  • 동적 리소스(HTML, HTTP API, 서블릿, JSP 등) 수행
  • 예로 Tomcat, Jetty, Undertow 등이 있다.

 

웹 시스템의 구성

 

기본적으로 웹 시스템의 구성은 정적 리소스는 WEB서버에서 동적(애플리케이션) 리소스는 WAS에서 관리하며 로직 등을 통해 DB 서버와 데이터를 송, 수신 한다.

 

여기서 Client가 WAS와 통신을 할 때, HTTP 메세지를 기반으로 통신한다. HTTP의 메세지는 기본적으로 시작 라인(start-line)과 헤더(header), 메세지 바디(message body), 헤더와 메세지 바디 사이에 공백라인(empty line, CRLF)로 구성된다.

 

- HTTP 요청메세지 시작 라인

  • HTTP 메서드(GET, POST, PUT, PATCH, DELETE)
  • 요청 대상 (/path[?query&parameter=value])
  • HTTP 버전

 

- HTTP 응답메세지 시작 라인

  • HTTP 버전
  • HTTP 상태코드 (200: 성공, 400: 클라이언트 오류, 500: 서버 오류)

 

- HTTP 메세지 헤더

  • HTTP 전송에 필요한 모든 부가정보 (메세지 바디의 내용, 크기, 압축, 인증, 요청 클라이언트 정보 등)

 

- HTTP 메세지 바디

  • 실제 전송할 데이터 (HTML, 이미지, 영상, JSON 등 byte로 표현 할 수 있는 모든 데이터 전송)

 

 

Client와 Server가 통신하면서 수행하려고 했던 의미있는 비즈니스 로직을 제외한 TCP / 소켓 연결, HTTP 메세지 파싱, 서버 기능 프로세스 실행 등 서버해서 설정해야 할 처리들이 있는데 이 설정들은 비슷하게 반복적이고 양도 많기 때문에 서블릿(Servlet)을 사용한다.

 

서블릿(Servlet)은 Client가 Server에 HTTP를 통한 요청과 응답 정보를 편리하게 사용하고 제공 받을 수 있게 해준다. 서블릿을 이용하면 Request와 Response 객체를 사용 할 수 있는데 이 객체를 통하여 HTTP 요청, HTTP 응답 메세지를 편하게 가져오고 입력할 수 있다.

 

 

@WebServlet(name = "testServlet", urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       // 비즈니스 로직
    }

위와 같은 서블릿을 사용하게 되면 Client와 Server의 통신간에 설정해야 할 처리들을 알아서 설정해주고 의미있는 비즈니스 로직 부분만을 구현 할 수 있다.

 

@WebServlet에서 해당 urlPattern의 URL이 호출되면 service메서드의 비즈니스 로직 부분이 실행되고 HttpServletRequest 객체를 통해 HTTP 요청 정보를 사용 할 수 있으며 HttpServletResponse 객체를 통해 HTTP 응답 정보를 제공 할 수 있다.

 

[JPA] 영속성(Persistence) 관리와 영속성 컨텍스트(Persistence Context)

 

지난번에는 JPA와 관련하여 기본적인 세팅과 EntitiyManagerFactory, EntitiyManager, EntitiyTransaction의 특징과 어떤 역할을 하는지 알아봤고 이번에는 본격적으로 JPA를 사용해보려고 한다.

 

 

2021.08.17 - [Spring-Boot/JPA] - [JPA] JPA? JPA 세팅 및 시작하기

 

[JPA] JPA? JPA 세팅 및 시작하기

[JPA] JPA? JPA 세팅 및 시작하기 기존에 MyBatis 방식만을 사용하다가 ORM(Object-Relation Mapping)기반인 JPA(Java Persistence Interface) 프로그래밍에 관심이 생겨서 알아보게 되었다. 자바 퍼시스턴스 API..

dbsyys.tistory.com

 

 

JPA에는 영속성 컨텍스트(Persistence Context) 라는 용어가 등장한다. 영속성 컨텍스트란 엔티티를 영구 저장하는 환경을 의미한다. 관계형 데이터베이스(RDB)는 데이터나 데이터의 집합을 테이블에 저장, 관리하며  객체지향 프로그래밍에서는 그 테이블을 엔티티라고한다.

 

영속성 컨텍스트는 논리적인 개념이며, 우리가 앞에서 선언했던 EntitiyManager를 통해서 영속성 컨텍스트에 접근이 가능하다. EntityManager에게 관리되는 엔티티는 생명주기를 가지게 되는데 영속성 컨텍스트를 기준으로 아래의 상태를 가진다.

- 비영속(new/transient)

  • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

- 영속(managed)

  • 영속성 컨텍스트에 관리되는 상태

- 준영속(detached)

  • 영속성 컨텍스트에 저장되었다가 분리된 상태

- 삭제(removed)

  • 삭제된 상태

 

 

각각의 상태가 영속성 컨텍스트에서 어떻게 관리되는지 알아보겠다.


import javax.persistence.Entity; 
import javax.persistence.Id; 

@Entity 
@Getter 
@Setter
public class Member { 
	
	@Id 
	private String id; 
	private String name; 
 
}

Member 엔티티를 생성했다.

 

public class JpaTest {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
        	// 객체만 생성한 상태(비영속)
            Member member = new Member();
            member.setId("memberId");
            member.setName("memberName");
			
            // 객체를 저장한 상태(영속)
            em.persist(member);
            
            // 엔티티를 영속성 컨텍스트에서 분리(준영속)
            em.detach(member);
            
            // 객체를 삭제한 상태(삭제)
            em.remove(member);
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();

    }
}

 

엔티티 생명주기별 각각의 상태.

 

JPA에서는 영속성 컨텍스트에 관하여 각각의 상태를 오가며, 영속성 컨텍스트에 관리되는 영속 상태인 엔티티에서 저장, 수정, 조회, 삭제 처리한다.

 

그렇다면 왜 ? 굳이

 

비영속, 영속, 준영속, 삭제의 4가지 상태로 구분되며, 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 데이터를 어떻게 관리하고 처리하는지에 대해 알아보겠다.

 


 

영속성 컨텍스트의 특징으로 아래 항목이 있다.

  • 1차 캐시
  • 동일성(identity)
  • 트랜잭션을 지원하는 쓰기 지연(transactional write-begind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

 

- 1차 캐시

1차 캐시는 엔티티가 영속성 컨텍스트에 저장되어 영속 상태로 된다면 key와 value 형태로 1차 캐시라는 곳에 저장이 된다. key는 엔티티에서 @Id로 지정한 pk값 (=memberId), value는 해당 엔티티 자체가 된다(=Member).

Member member = new Member();
member.setId("memberId");
member.setName("memberName");

// 영속 상태 (1차 캐시에 저장됨)
em.persist(member);

//1차 캐시에서 조회
Member findMember = em.find(Member.class, "memberId");

member 객체가 1차 캐시에 저장된 상태에서 JPA는 member 객체를 조회하면 데이터베이스에 접근해서 데이터를 가져오는 것이 아닌 1차 캐시를 먼저 확인 한 후에 해당하는 데이터가 없다면 그 때 데이터베이스를 조회한다.

 

만약 memberId2라는 키를 가진 데이터가 데이터베이스에선 존재하고 영속성 컨텍스트의 1차 캐시 안에는 없다고 가정하고 조회를 해보면

Member findMember2 = em.find(Member.class, "memberId2");

영속성 컨텍스트는 데이터베이스에서 해당하는 데이터를 가져온 후에 그 값을 1차 캐시에 저장하고 반환한다.

 

 

- 동일성(identity)

JPA는 같은 트랜잭션 안에서 1차 캐시를 통해 데이터베이스가 아닌 애플리케이션 차원으로 영속된 엔티티의 동일성을 보장해준다.

Member member1 = em.find(Member.class, "memberId");
Member member2 = em.find(Member.class, "memberId");

System.out.println(member1 == member2); // true

 

 

- 트랜잭션을 지원하는 쓰기 지연(transactional write-begind)

JPA는 영속성컨텍스트의 트랜잭션안에서 데이터가 변경된다. member1과 member2를 차례대로 저장 할 때 1차 캐시에 엔티티의 정보가 저장됨과 동시에 저장하는 insert 쿼리가 생성되지만 영속성 컨텍스트 안에만 저장되고 데이터베이스로 쿼리가 전송되지 않는다.

EntityManager em = emf.createEntityManager();

EntityTransaction transaction = em.getTransaction();

// 데이터 변경 시
transaction.begin(); // 트랜잭션 시작

em.persist(member1);
em.persist(member2);
// 영속성 컨텍스트에 등록 관리 -> query를 보내지 않음

// 커밋하는 순간 데이터베이스에 query를 보냄
transaction.commit(); // 트랜잭션 커밋

영속성 컨텍스트에 저장 되어있는 쿼리는 트랜잭션에서 commit()이라는 메서드가 실행되어야 비로소 데이터베이스로 저장 되어있던 쿼리가 보내진다.

 

 

 

- 변경 감지(Dirty Checking)

영속성 컨텍스트는 최초의 데이터 조회나 저장할 때 1차 캐시에 @Id로 지정한 pk값과 엔티티말고도 엔티티에 관련된 스냅샷을 저장한다. 스냅샷에는 최초 데이터가 조회 또는 저장된 시간이나 그 시점의 데이터가 저장되는데,  JPA에서 트랜잭션이 commit() 하는 시점에 엔티티와 스냅샷을 비교한다.

EntityManager em = emf.createEntityManager();

EntityTransaction transaction = em.getTransaction();

transaction.begin(); // 트랜잭션 시작

// 데이터 조회(영속)
Member member1 = em.find(Member.class, "memberId");

// 영속된 엔티티 데이터 수정
member1.setId("memberIdTest");
member1.setName("memberNameTest");

transaction.commit(); // 트랜잭션 커밋

비교 후에 기존 값과 스냅샷의 데이터가 다르면 영속성 컨텍스트에 update 쿼리를 만들어 놓게되고 flush라는 작업을 거치면서 데이터베이스에 반영되고 데이터베이스 commit가 실행된다. 이러한 매커니즘으로 데이터 수정 시에는 저장(persist), 조회(find), 삭제(remove)와 같은 처리가 따로 필요없다.

 

*flush

플러시는 영속성 컨텍스트에 변경된 내용을 데이터베이스에 반영(동기화)하는 것이다. 플러시가 발생하면 변경감지와 쓰기 지연 SQL 저장소에 수정된 엔티티가 등록되고 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 등록한다.(저장, 수정, 삭제)

 

플러시 후에 데이터베이스 트랜잭션 커밋이 일어나는 것은 아니다. 데이터베이스 트랜잭션 커밋은 JPA의 트랜잭션에서 commit()을 실행했을 때 일어난다.

 

flush를 수행했을 시 쓰기 지연 SQL저장소에 있거나 변경감지로 인해 만들어진 쿼리들이 데이터베이스에 반영이 되지만 1차 캐시는 그대로 남아있고 사라지지 않는다.


'JPA' 카테고리의 다른 글

[JPA] JPA 엔티티 매핑과(Entity Mapping) 엔티티 설계  (1) 2021.08.31
[JPA] JPA? JPA 세팅 및 시작하기  (1) 2021.08.17

 

[Spring Boot] 프로젝트 빌드하고 실행하기(.jar)

 

 

평소 지금까지 해오던 방식이나 회사에서는 운영서버에 웹 서버를 설치하고 WAS서버를 설치한 후, 설치한 서버를 통해서 해당 웹 어플리케이션을 가동했지만 스프링부트의 내장 서버를 가지고 프로젝트 파일을 빌드하여 서버를 올려보려고 한다.(.jar)

 

 

프로젝트를 아직 만들지 않았다면 툴을 사용하여 프로젝트를 생성하거나, Spring Initializr을 이용하여 프로젝트를 생성하도록 하자.

 

2021.08.13 - [Spring-Boot] - [Spring Boot] 프로젝트 생성하기(Intellij)

 

[Spring Boot] 프로젝트 생성하기(Intellij)

[Spring Boot] Spring Initializr를 이용하여 프로젝트 생성하기(Intellij) 기존에 STS(Spring Tool Suite)나 Eclipse 툴 안에서 Maven이나 Gradle로 프로젝트를 생성했던 경험이 있는데 Spring Initializr 웹 도..

dbsyys.tistory.com

 

위 링크에보면 Spring Initializr을 이용하여 프로젝트를 생성할 수 있다.

 

 

먼저, 프로젝트를 만든 폴더의 Root경로로 가자.

프로젝트 구성파일

콘솔창을 열고 해당 루트 경로로 들어간다. (복붙이 가능하면 복붙 추천)

 

C:\Users\{사용자폴더}> cd {프로젝트 경로}

 

해당 경로에서 "ls" 입력하게되면 프로젝트 구성파일 이미지와 같은 폴더와 파일 목록이 출력된다.

 

 

해당 목록을 확인했다면 gradlew.bat 파일을 실행시켜주자.

 

C:\Users\{프로젝트경로}> ./gradlew.bat build

(*Mac의 경우는 ./gradlew build)

 

 

문제없이 빌드 성공메세지가 나오게되면 build 폴더가 생성된다.

 

(*build폴더나 build폴더 안에 libs 파일이 생성되지 않았다면 ./gradlew.bat clean build로 재실행)

 

폴더가 생성되면

 

build - libs - {프로젝트명}-0.0.1-SNAPSHOT.jar 파일이 있는지 확인한다.

 

C:\Users\{프로젝트경로}> cd build\libs

 

위의 경로에 jar파일이 생성되었다면 콘솔창에서 해당 경로로 들어가서

 

C:\Users\{프로젝트경로}\build\libs> java -jar {프로젝트명}-0.0.1-SNAPSHOT.jar

 

명령어를 실행시켜준다.

 

성공 !

 

간혹 8080번 포트가 이미 실행되어 있으면 서버가 올라가지 않을 수도 있다. 그렇다면 netstat 명령어나 작업관리자 서비스에서 8080번 포트를 죽이고 올리자

(이클립스나 인텔리제이에서 실행중이어도 마찬가지)


 

- 스프링부트를 사용하여 내장 서버를 통해 서버를 가동했는데 이런 방식이라면 운영서버에 빌드하고 배포하는 일이 많이 수월해질것같다..! (지금 회사에서 운영서버 관리할 때 서버 설치하는건 둘째치고...기존에 class, jsp 등 리소스 파일이랑....sql쿼리랑...일일이 하나하나 날짜보면서 옮겼던거 생각하면...옮기는건 그렇다쳐도 파일이 하나라도 빠지면...에러.....반복작업....후....)

 

아무튼..

 

끝 !

+ Recent posts