1. ERD 수정

  • 이전에 작성했던 ERD에서 member_role 엔티티의 pk를 빼는 방향으로 수정

 

2. 클래스 다이어그램 작성

  • 모델 구현만을 위한 클래스 다이어그램이기 때문에 dto와 service, dao(repository)를 비롯한 기타 클래스까지는 클래스 다이어그램에 포함하지 않음

  • Member
    • 회원 정보를 나타내기 위한 클래스
    • 자신이 작성한 게시글, 댓글과 자신이 추천한 게시글 정보를 Set으로 관리 (추가, 삭제 기능 포함)

  • Role
    • 회원의 권한 정보를 나타내기 위한 열거형 클래스
    • ADMIN(관리자) / USER(일반 유저) 의 두 가지 값을 가짐
    • Member의 Role 객체들은 Member 객체와 수명을 함께하기 때문에 합성(composition)관계로 표기

  • Post
    • 게시글 정보를 나타내기 위한 클래스
    • 게시글을 작성한 유저의 정보를 가짐
    • 게시글에 달린 댓글과 게시글의 추천 정보를 Set으로 관리(추가, 삭제 기능 포함)
    • 조회수를 나타내기 위한 viewCount 필드와 이를 증가시키는 기능 포함

  • Comment
    • 댓글 정보를 나타내기 위한 클래스
    • 댓글을 작성한 유저의 정보를 가짐

  • VoteInfo
    • 추천 정보를 나타내기 위한 클래스
    • 추천을 받은 게시글과 추천한 유저의 정보를 가짐

 

 이후 추가하게될 기능 등으로 인해 클래스가 추가될 수 있지만 이 프로젝트는 Spring 프레임워크 학습을 병행하며 단계적으로 발전시켜나가는 프로젝트이기 때문에 기본적인 사항만을 설계하고 개발에 들어가기로 한다. 다음에는 실제로 Spring 프로젝트에서 엔티티 클래스를 작성하는 작업을 진행해볼 것이다.

'프레임워크 > Spring' 카테고리의 다른 글

#13 모델 설계 1  (0) 2022.05.13
#12 Thymeleaf 3  (0) 2022.05.10
#11 Thymeleaf 2  (0) 2022.05.08
#10 Thymeleaf 1  (0) 2022.05.06
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05

1. ERD 작성

  • 본격적으로 게시판 개발에 들어가기 위해 데이터 모델을 설계할 필요가 있다. 위 그림은 이번 게시판 프로젝트를 위해 간단하게 작성한 ERD이다. (ERD 작성을 위한 툴은 매우 다양하지만 이번에 사용한 QuickDBD는 텍스트 코드를 작성하면 이를 파싱하여 위 처럼 깔끔하게 ERD를 그려주기 때문에 굉장히 편리했다. 앞으로도 ERD 작성은 이 사이트를 이용하게 될 것 같다.)

  • 각 엔티티들과 그 사이의 관계를 어떻게 구성했는지 하나씩 살펴보며 정리해보자(PK로 사용할 식별자(id)에 대해서는 별도로 설명하지 않기로 한다).

 

2. 회원 정보

  • member
    • 회원 정보를 저장하기 위한 엔티티
    • name
      • 회원 ID. 여기서의 ID는 PK로서의 ID가 아닌 로그인시에 입력하는 계정이름을 의미(최대 20자)

    • password
      • 회원 비밀번호. 회원 가입 시에 입력받아 로그인 시에 사용
      • 암호화된 패스워드를 저장해야 하기 때문에 넉넉하게 최대 크기를 100자로 잡음

    • email
      • 회원의 email 주소(최대 50자)

    • register_datetime
      • 회원 가입 일시

 

  • role
    • 역할(권한)을 나타내기 위한 엔티티 

    • name
      • 역할의 이름(최대 20자)

 

  • member_role
    • member와 role 을 연결해주기 위한 엔티티

    • 한 명의 member는 여러 개의 role을 가질 수 있고 하나의 role 또한 여러 멤버에게 주어질 수 있기에 두 엔티티의 관계는 다대다(N:M) 관계이다. 관계형 데이터베이스에서 다대다 관계를 나타내기 위해서는 사이를 이어줄 join table이 필요한데, 이 역할을 해주기 위해 필요한 엔티티이다.

    • member_id
      • role을 가질 member의 PK
    • role_id 
      • member가 가질 role의 PK

 

 

3. 게시글 관련 정보

  • post
    • 게시글의 정보를 저장하기 위한 엔티티

    • title
      • 게시글 제목(최대 40자)

    • content
      • 게시글 내용
      • 게시글 내용은 매우 길어질 수 있기 때문에 TEXT 타입을 사용

    • create_datetime
      • 작성 일자

    • modify_datetime
      • 최근 수정 일자

    • author_id
      • 작성자인 member의 PK

 

  • comment
    • 댓글의 정보를 저장하기 위한 엔티티

    • content
      • 댓글 내용(최대 255자 - VARCHAR 최대크기)

    • create_datetime
      • 작성 일자

    • modify_datetime
      • 수정 일자

    • author_id
      • 작성자인 member의 PK

    • post_id
      • 댓글이 달린 post의 PK

 

  • vote_info
    • 게시글의 추천(혹은 좋아요) 정보를 관리하기 위한 엔티티
    • 하나의 게시글을 여러 회원이 추천할 수도 있으며 한 명의 회원이 여러 게시글을 추천할 수도 있는 다대다 관계
    • member : role 관계때와 마찬가지로 post와 member를 연결해줄 엔티티로 사용됨

    • post_id
      • 추천받은 post의 PK

    • voter_id
      • 추천한 member의 PK

 

 

 이정도면 게시판을 개발하기 위해 필요한 기본적인 데이터 모델의 설계가 끝났다고 볼 수 있다. 다음에는 실제 프로그램상에서 사용될 클래스를 설계하기 위해 클래스 다이어그램을 작성해볼 것이다.

 

 

+ ERD 수정)

  • 기존에는 member와 role을 다대다 관계로 매핑하려 했지만 굳이 조인테이블을 두는 것보다 role은 enum 타입을 정의하여 입력을 한정하고 하나의 member가 여러개의 member_role 테이블의 레코드에 매핑되는 방식이 나을 것 같아서 구조를 조금 바꿨다. role 엔티티가 없어지고 member_role에 role의 이름이 바로 들어가도록 수정되었다.

  • post에 조회수를 나타내기 위해 view_count 속성을 추가했다.
  • member의 name에 index를 추가했다.

'프레임워크 > Spring' 카테고리의 다른 글

#14 모델 설계 2  (0) 2022.05.29
#12 Thymeleaf 3  (0) 2022.05.10
#11 Thymeleaf 2  (0) 2022.05.08
#10 Thymeleaf 1  (0) 2022.05.06
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05
  • 프론트엔드 개발을 하다보면 여러 페이지에서 공통적으로 사용하게 되는 코드가 다수 존재한다.  예를 들면 직접 구현한 select box라던가 header와 footer, 공통 css파일이나 js파일 등을 추가하는 코드 등이 그렇다. 이런 중복 코드를 한 번만 작성하여 재사용 가능하도록 thymeleaf에서는 fragment 기능을 제공한다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <style>
        ul, li {
            list-style: none;
        }
        .select-box {
            display: inline-block;
        }
        .select-box-selected {
            border: 1px solid black;
            width: 100%;
            cursor: pointer;
        }
        .select-box-items {
            display: none;
            border: 1px solid black;
            width: 100%;
        }
        .select-box-open + .select-box-items {
            display: inline-block;
        }
    </style>
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div class="select-box" style="display: inline-block">
        <div class="select-box-selected" onclick="this.classList.toggle('select-box-open')" style="border: 1px solid black;">
            항목을 선택해주세요
        </div>
        <div class="select-box-items">
            <ul>
                <li>항목 1</li>
                <li>항목 1</li>
                <li>항목 1</li>
                <li>항목 1</li>
            </ul>
        </div>
    </div>

</body>
</html>

 

 

  • index.html에 위와 같이 간단하게 클릭하면 드롭다운 메뉴가 나타나는 기능을 구현해두고 여러 템플릿에서 반복적으로 사용한다고 해보자. 저 기능이 사용되는 모든 페이지에 위의 코드를 작성하는 것은 굉장히 비효율적일 것이다. 이 기능을 fragment화 하여 간단하게 재사용할 수 있도록 만들어보자.

 

  • 먼저 만들었던 기능의 html 코드를 별도의 html 파일에 옮긴 후 최상위 태그에 th:fragment="<fragment 이름>" 의 형태로 th:fragment 속성을 추가해준다. 만약 최상위 태그가 여러개인 경우 이전 글에서 배웠던 th:block 태그로 한번 감싼 뒤 th:block 태그에 th:fragment 속성을 추가해주면 된다. 

  • 여기서는 templates/fragment/simple_fragment.html 에 fragment를 작성하였다.

 

  • 다음으로 style 태그에 작성했던 stylesheet의 내용을 static/css/style.css 에 옮겨주었다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment"></div>
</body>
</html>
  • 남은건 index.html에서 fragment를 사용하는 것 뿐이다. head태그에서 css/style.css 를 추가해준 뒤 th:replace 속성을 사용하여 fragment를 사용해준다. th:replace="<fragment 경로>::<fragment 이름>" 의 형태로 사용할 수 있다.

 

  • 처음에 직접 index.html에 코드를 작성했을 때와 동일하게 동작하는 것을 볼 수 있다. 이제 이 기능은 어느 템플릿에서든 css추가와 th:replace 코드 한줄만 작성하면 재사용이 가능하다.  th:fragment 와 th:replace 를 사용하면 이처럼 자주 사용되는 코드를 손쉽게 재사용할 수 있다. 

 

 

2. th:fragment에서 변수 사용하기

  • 그런데 위에서 만든 fragment에는 조금 문제가 있다. 완벽하게 동일한 코드가 필요할 때만 사용 가능하다는 점이다. 실제로 개발을 하다보면 형식과 기능은 동일하지만 내용은 조금씩 바뀌어야하는 경우도 굉장히 많다.  이번에 만든 fragment의 경우 "항목을 선택해주세요" 부분이나 항목 1~4 부분을 다른 값으로 바꾸어 사용하고 싶을 수도 있을 것이다.  그렇게 내용에만 조금씩 차이가 있는 경우에도 매번 새로운 fragment를 만들어야한다면 굉장히 불편할 것이다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <div th:fragment="simple-fragment(defaultText, itemList)" class="select-box" style="display: inline-block">
        <div class="select-box-selected"
             onclick="this.classList.toggle('select-box-open')"
             style="border: 1px solid black;"
             th:text="${ defaultText }">
        </div>
        <div class="select-box-items">
            <ul>
                <li th:each="item: ${ itemList }" th:text="${ item }"></li>
            </ul>
        </div>
    </div>
</html>
  • thymeleaf의 fragment 기능은 fragment 사용시 매개변수를 넘겨주는 것으로 fragment의 내용을 어느정도 컨트롤하는 것이 가능하다. 먼저 simple_fragment.html에서 th:fragment="<fragment 이름>(<매개변수명 ...>)" 의 형태로 th:fragment 속성을 다시 지정해보자. 

  • 그리고나서 지금까지 배웠던 thymeleaf의 기능을 사용하여 "항목을 선택해주세요" 대신 defaultText 값을, 항목 1~4 대신 itemList 값을 사용하여 select-box-items 내의 리스트를 표현하도록 코드를 작성해주면 fragment의 수정은 끝이다.

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("myItemList", List.of("myitem1", "myitem2", "myitem3", "myitem4"));
        return "index";
    }
}

 

  • 다음으로 Controller에서 itemList로 사용할 문자열 리스트를 myItemList라는 이름으로 model에 추가해준다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment(defaultText='please select item', itemList=${ myItemList })"></div>
</body>
</html>
  • 마지막으로 위와 같이 index.html의 th:replace를 사용한 부분에서 fragment가 요구하는 매개변수 값을 넘겨준다.

 

  • 서버를 구동하고 접속해보면 기대한 대로 fragment의 내용물이 설정된 것을 볼 수 있다.

 

 

3. 입력하지 않아도 되는 매개변수 - th:with

  • 그런데 th:fragment에서 사용할 변수값을 th:fragment="<fragment 이름>(<매개변수명 ...>)" 의 형태로 받을 경우 fragment 사용시 반드시 매개변수 값을 넣어줘야만 fragment가 정상적으로 동작한다. 매개변수를 전달해줄 수도 있지만 전달하지 않을 경우에도 default 값을 사용하여 정상적으로 작동하게 할 수는 없을까?

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <div th:fragment="simple-fragment(itemList)"
         th:with="defaultText=${ defaultText } ?: '항목을 선택해주세요.'"
         class="select-box"
         style="display: inline-block">
        <div class="select-box-selected"
             onclick="this.classList.toggle('select-box-open')"
             style="border: 1px solid black;"
             th:text="${ defaultText }">
        </div>
        <div class="select-box-items">
            <ul>
                <li th:each="item: ${ itemList }" th:text="${ item }"></li>
            </ul>
        </div>
    </div>
</html>
  • th:with 속성을 사용하여 이 문제를 해결할 수 있다. th:with="<매개변수 이름>=${ <매개변수 이름> } ?: '항목을 선택해주세요.'" 의 형태로 이전에 알아본 thymeleaf의 연산자중 하나인 default 조건 연산자를 사용하여 만약 매개변수를 입력받았다면 그 매개변수의 값을, 입력받지 않았다면 default 값을 사용하도록 할 수 있다. 

  • 여기서 왼쪽의 <매개변수 이름>은 fragment 내에서 사용하기 위한 변수 이름이며 오른쪽의 <매개변수 이름>은 th:replace에서 매개변수를 전달하기 위해 사용할 이름이기 때문에 꼭 같아야할 필요는 없다.

  • simple-fragment의 defaultText 를 default 값을 가지는 변수로 바꿔보자. 

 

  • 그리고나서 다시 서버를 구동하고 접속해보면 아직은 index.html이 defulatText 매개변수를 전달하고있기 때문에 이전처럼 select-box-selected의 텍스트가 "please select item" 이라고 표시되어 있는 것을 볼 수 있다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:replace="fragment/simple_fragment::simple-fragment(itemList=${ myItemList })"></div>
</body>
</html>
  • 이번엔 index.html에서 fragment 사용시 defaultText 매개변수를 넘기지 않도록 해보자.

 

  • 의도한 대로 default 값인 "항목을 선택해주세요"가 나타나는 것을 볼 수 있다. 이제 fragment에서 사용할 변수를 선택적으로 매개변수로 받아올 수 있게 되었다.

 

 

4. th:insert

  • th:replace는 이름대로 해당 속성이 적용된 태그 전체를 fragment로 대체하는 형태로 fragment를 적용시켜준다. 그런데 속성이 적용된 태그 자체는 내버려둔 채로 내부에 fragment를 삽입할 수는 없을까?

  • 이 문제를 해결하려면 th:insert를 사용할 수 있다. th:insert는 이름에서도 유추할 수 있듯이 해당 속성이 적용된 태그 내부에 fragment를 삽입하는 형태로 fragment를 적용시켜준다. 

  • 3.0 이전에 사용되던 th:include 라는 속성도 있지만 이 속성의 사용은 thymeleaf 3.0 이후로 권장되고 있지 않기 때문에(th:insert로 충분히 대체 가능하기 때문으로 보인다.) 굳이 알아둬야할 필요는 없을 듯 하다.

 

  • th:replace 를 사용하여 fragment를 사용하던 기존 방식의 경우 위와 같이 div 태그 전체가 fragment로 대체된 것을 볼 수 있다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:insert="fragment/simple_fragment::simple-fragment(itemList=${ myItemList })"></div>
</body>
</html>

 

  • th:replace를 th:insert로 바꾸고 다시 서버를 구동하여 접속해보면 th:insert 속성이 적용된 최상위 div 태그가 그대로 남아있고 내부에 fragment가 삽입된 것을 볼 수 있다.

 

 

5. fragment 표현식 ~{ ... }

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="simple-fragment(itemList)"
     th:with="defaultText=${ defaultText } ?: '항목을 선택해주세요.'"
     class="select-box">
  <div class="select-box-selected"
       onclick="this.classList.toggle('select-box-open')"
       th:text="${ defaultText }">
  </div>
  <div class="select-box-items" th:insert="${ itemList }">
  </div>
</div>
</html>

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <h1>This is Index Page via Controller</h1>
    <div th:insert="fragment/simple_fragment::simple-fragment(itemList=~{ ::#simple-my-list })">
        <ul id="simple-my-list">
            <li th:each="item: ${ myItemList }" th:text="${ item }"></li>
        </ul>
    </div>
</body>
</html>
  • fragment 표현식은 fragment를 객체(object)처럼 사용할 수 있도록 하기 위한 표현식이다. 조건에 따라 특정 fragment를 삽입하는 등의 방식으로도 사용 가능하고 fragment의 매개변수로 fragment를 넘겨주는 경우에도 사용할 수 있다.
  • simple-fragment의 select-box-items가 항목 문자열 리스트를 받는 대신 항목 리스트 프래그먼트를 받아 insert 하도록 수정한 후 index.html에서 항목 리스트 프래그먼트를 만들어 fragment 표현식을 사용하여 simple-fragment에 넘겨주자.

 

  • 서버를 재구동하고 접속해보면 이전과 동일하게 동작하는 것을 확인할 수 있다.  위에서 설명했던 프래그먼트의 내용을 커스터마이징하는 경우에 이 방식을 자주 사용하게되니 잘 알아둘 필요가 있다.

 

 

 Thymeleaf에는 이 외에도 다양한 기능이 있지만 지금까지 알아본 Thymeleaf의 문법과 기능만 잘 숙지해둔다면 이후 프로젝트를 진행하는데 전혀 무리가 없을 것이다. Thymeleaf 에 대한 내용은 여기까지로 끝내고 다음 포스팅부터는 다시 본 목적으로 돌아가서 게시판 개발을 진행하기로 하자.

'프레임워크 > Spring' 카테고리의 다른 글

#14 모델 설계 2  (0) 2022.05.29
#13 모델 설계 1  (0) 2022.05.13
#11 Thymeleaf 2  (0) 2022.05.08
#10 Thymeleaf 1  (0) 2022.05.06
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05

1. 기존 HTML 속성의 대체

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <!-- 1. 기존 속성의 대체 -->
    <button th:id="${ idName }" th:value="${ btnName }"></button>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("idName", "my-id");
        model.addAttribute("btnName", "my-button");
        return "index";
    }
}
  • HTML 태그에 기본적으로 존재하는 속성들은 앞에 th: 를 붙이는 것으로 thymeleaf 문법을 사용할 수 있게 된다.

  • button 태그에 th:id, th:value 를 사용하여 id, value 속성을 정의하고 Controller에서 변수 값을 추가해보자

 

  • 그러고나서 서버를 구동하고 다시 접속해보면 button 태그의 id와 value가 작성한 thymeleaf 문법대로 들어가있는 것을 확인할 수 있다. 이처럼 th:<속성이름> 을 사용하면 thymeleaf 문법을 사용하여 기존 html의 속성값도 동적으로 설정하는 것이 가능하다. 

 

 

2. 반복문(순회)

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <!-- 2. 반복문 -->
    <div th:each="num: ${ numList }">
        <p th:text="${ num }"></p>
    </div>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("numList", List.of(1, 6, 2, 7, 3, 9, 0, 13));
        return "index";
    }
}
  • thymeleaf 에서는 th:each 속성을 통해 순회 가능한 객체의 요소에 순차적으로 접근하는 기능을 제공한다.

  • th:each="<변수 이름>: ${ <순회할 객체 이름> }" 의 형태로 th:each 속성을 지정해주면 속성이 지정된 태그 내부에서는 변수 이름을 통해서 그 값에 접근할 수 있다.  위 코드에서는 Controller가 등록한 numList 의 각 요소를
    p 태그의 text로 사용하고있다.

 

  • 그 상태로 서버를 구동하고 접속해보면 위처럼 리스트에 넣어준 정수값들이 순차적으로 출력된 것을 볼 수 있다.

 

 

3. th:if / th:unless

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <!-- 3. if문 -->
    <div th:if="${ score == 100 }">
        만점입니다!
    </div>
    <div th:unless="${ score == 100 }">
        만점이아닙니다.
    </div>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("score", 100);
        return "index";
    }
}
  • th:ifth:unless 속성을 사용하면 조건식을 만족할 경우나 만족하지 않을 경우에만 해당 태그를 나타내도록 할 수 있다.

  • th:if=${ <조건문> }, th:unless=${ <조건문> } 의 형태로 조건문을 설정한 뒤 Controller에서 scroe를 100으로 설정해보자.

 

  • 기대한대로 조건문이 잘 동작하는 것을 확인할 수 있다.

  • 주의할 점은 일반적인 프로그래밍 언어의 if/else 관계와는 달리 th:unless 에도 조건문을 써줘야한다는 점이다.

  • 즉, th:unless 는 엄밀히 말하면 else 보다는 if not ~ 에 가까운 속성이다.

 

 

4. th:switch / th:case

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <!-- 4. switch 문 -->
    <div th:each="input: ${ inputs }">
        <div th:switch="${ input }">
            <p th:case="1">입력된 숫자는 1입니다.</p>
            <p th:case="2">입력된 숫자는 2입니다.</p>
            <p th:case="3">입력된 숫자는 3입니다.</p>
            <p th:case="*">입력된 숫자는 1, 2, 3 중 어느것도 아닙니다.</p>
        </div>
    </div>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("inputs", List.of(3, 2, 6, 1, 9));
        return "index";
    }
}
  • th:switch / th:case 속성을 사용하면 switch 문을 사용할 수 있다. 

  • th:switch="${ <조건 변수 이름> }" 의 형태로 조건값을 지정하고 태그 내부에서 th:case="<케이스값>" 의 형태로 케이스를 지정해주는 것으로 케이스값이 매칭되는 태그만이 보여지도록 할 수 있다.

  • case 값으로 "*" 을 줄 경우 default 케이스의 역할을 한다.

 

  • 서버를 구동하고 접속해보면 정상적으로 switch문이 동작하는 것을 확인할 수 있다.

 

 

5. th:block

  • 크롬 개발자도구에서 위에서 사용했던 반복문과 조건문을 사용한 태그들이 실제 html로 어떻게 변환되고있는지 살펴보면 한가지 사실을 확인할 수 있다.  바로 속성이 적용된 태그 내부가 아닌 속성이 적용된 태그 자체가 반복되거나 조건에 따라 표시되고 있다는 것이다. 

  • 물론 div를 쓰지 않고 내부의 태그들마다 th:each나 th:if 등을 사용하는 방법도 있지만 내부의 태그가 많아지면 굉장히 번거로운 작업인데다 switch문의 경우 조건값을 넣어줄 바깥 태그의 존재가 필수적이다. 그렇다면 지금의 방식을 그대로 사용하면서 내부의 내용만을 반복하거나 조건에 따라 표시하고싶다면 어떻게 해야할까?

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <!-- 2. 반복문 -->
    <th:block th:each="num: ${ numList }">
        <p th:text="${ num }"></p>
    </th:block>

    <!-- 3. if문 -->
    <th:block th:if="${ score == 100 }">
        만점입니다!
    </th:block>
    <th:block th:unless="${ score == 100 }">
        만점이아닙니다.
    </th:block>

    <!-- 4. switch 문 -->
    <th:block th:each="input: ${ inputs }">
        <th:block th:switch="${ input }">
            <p th:case="1">입력된 숫자는 1입니다.</p>
            <p th:case="2">입력된 숫자는 2입니다.</p>
            <p th:case="3">입력된 숫자는 3입니다.</p>
            <p th:case="*">입력된 숫자는 1, 2, 3 중 어느것도 아닙니다.</p>
        </th:block>
    </th:block>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("numList", List.of(1, 6, 2, 7, 3, 9, 0, 13));
        model.addAttribute("score", 100);
        model.addAttribute("inputs", List.of(3, 2, 6, 1, 9));
        return "index";
    }
}
  • thymeleaf는 th:block이라는 태그를 지원한다.(thymeleaf가 지원하는 유일한 태그이다)

  • th:block은 thymeleaf 문법상으론 일반적인 태그처럼 동작하지만 html 생성시 자기 자신은 존재하지 않는 태그로 취급되기 때문에 th:block 태그에 th:each 나 th:if, th:switch 등을 사용하면 블록 내부의 내용만을 반복하거나 조건에 따라 표시할 수 있다.

  • 위와 같이 지금까지의 모든 예제에서 바깥의 div 태그를 th:block으로 대체해보자.

 

  • 깔끔하게 내부의 내용만이 반복되거나 조건에 따라 표시된 것을 볼 수 있다.

 

6. th:object

  • 태그에 th:object 속성을 사용하여 해당 태그 내에서 사용할 객체를 지정할 수 있다. 
  • 이 기능은 주로 데이터를 객체단위로 표시하거나 form 태그의 전송값을 객체에 매핑시키기 위해 사용된다.

 

package com.scala.personal.mycomm;

import org.springframework.stereotype.Component;

public class FormData {
    String attr1;
    int attr2;
    String attr3;

    public String getAttr1() {
        return attr1;
    }
    public int getAttr2() {
        return attr2;
    }
    public String getAttr3() {
        return attr3;
    }
    public void setAttr1(String attr1) {
        this.attr1 = attr1;
    }
    public void setAttr2(int attr2) {
        this.attr2 = attr2;
    }
    public void setAttr3(String attr3) {
        this.attr3 = attr3;
    }
}
  • 먼저 FormData 클래스를 정의한다.  FormData 클래스는 attr1, attr2, attr3의 3개의 필드를 가진다.
  • 필드값을 가져오거나 설정하기 위해 getter, setter를 만들어줘야한다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller

    <!-- 5. object -->
    <form method="post" action="" th:object="${formData}">
        <input type="text" th:field="*{attr1}">
        <input type="number" th:field="*{attr2}">
        <select th:field="*{attr3}">
            <option value="a">A</option>
            <option value="b">B</option>
            <option value="c">C</option>
        </select>
        <input type="submit" value="제출">
    </form>

</body>
</html>
  • index.html에는 th:object를 사용한 form 태그를 작성하자. 주의할 점은 input 값마다 th:field 속성을 사용하여 formData의 각 속성과 매핑시켜줘야한다는 점이다. 여기서 앞서 알아봤던 thymeleaf의 표현식중 하나인 선택 변수 표현식을 사용한다. 

  • 다른 thymeleaf 속성들이 변수 표현식 내부에서 변수 이름의 앞뒤로 공백이 들어가도 변수를 인식하는데 문제가 없었던 것과 달리 th:object나 th:field에서 사용되는 변수 표현식이나 선택 변수 표현식에서는 객체명, 필드명 앞뒤로 공백이 들어가선 안된다는 점에도 주의하자.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    <!-- 5. object -->
    <table th:object="${formData}">
        <tr>
            <th>attr1</th>
            <th>attr2</th>
            <th>attr3</th>
        </tr>
        <tr>
            <td th:text="*{attr1}"></td>
            <td th:text="*{attr2}"></td>
            <td th:text="*{attr3}"></td>
        </tr>
    </table>

</body>
</html>
  • form_result.html에서는 마찬가지로 th:object를 사용하여 formData를 사용할 객체로 지정하고 각 속성값을 테이블 형태로 표시한다. 여기서는 객체의 속성을 참조할 뿐이기 때문에 th:field 는 사용할 필요가 없다.

 

package com.scala.personal.mycomm.controller;

import com.scala.personal.mycomm.FormData;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(@ModelAttribute FormData formData, Model model) {
        return "index";
    }

    @PostMapping("")
    public String formResult(@ModelAttribute FormData formData, Model model) {
        return "form_result";
    }
}
  • Controller에서는 formData 객체를 model에 추가해준다.  model.addAttribute("formData", new FormData()); 와 같이 직접 객체를 생성하여 추가하는 방법도 있지만 위와 같이 메소드의 매개변수에 formData를 추가하고 @ModelAttribute 어노테이션을 붙여주면 Spring이 자동으로 의존성을 주입하고 model에 추가까지 해준다.

  • @ModelAttribute(<속성이름>) 의 형태로 속성의 이름도 지정할 수 있다. 지정하지 않을 경우 매개변수 이름을 속성 이름으로 사용한다.

  • 물론 form의 post타입 요청에 매핑될 메소드도 정의해주고 이 메소드에서도 formData를 model에 추가해줘야한다.

 

 

  • 서버를 구동하고 접속해보면 attr1, attr2, attr3를 입력하는 form을 볼 수 있으며 이를 입력하고 제출버튼을 클릭하면 제출한 값이 정상적으로 테이블 형태로 표시되는 것을 볼 수 있다.
  • 구글링을 통해 알아본 @ModelAttribute 어노테이션에 대한 정보글에는 대부분 @ModelAttribute를 사용할 대상 클래스는 Bean 으로 등록되어있어야 한다고 되어있었는데, 코드를 보면 알다시피 FormData는 Bean으로 등록되어있는 상태가 아니다. @ModelAttribute가 내부적으로 인스턴스를 Bean으로 등록하여 사용하는것인지 애초에 Bean일 필요가 없는 것인지는 아직 모르겠다.

 

 

7. th:inline

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller

    <!-- 6. inline -->
    <p th:text="${ name }">My name is </p>

</body>
</html>

 

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("name", "David");
        return "index";
    }
}

 

  • th:text 나 th:utext 등을 사용하여 서버로부터 전달받은 값을 표시할 수 는 있지만 이 방식은 태그 내부에 작성한 값을 무시해버린다. 위의 코드의 경우에도 p태그 내부의 My name is 는 사라지고 David만이 출력된다.

  • 물론 이전에 배운 Literal Substitution 문법을 사용해서 th:text="|My name is ${ name }|" 와 같이 사용할 수는 있지만 보기에 썩 좋지는 않다. thymeleaf로 참조한 변수의 값을 HTML상의 텍스트와 함께 사용할 방법은 없을까?

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller

    <!-- 6. inline -->
    <p th:inline="text">My name is [[${ name }]]</p>

</body>
</html>
  • 이런 경우에 사용 가능한 것이 th:inline 속성이다. th:inline 속성값을 "text"로 지정하면 HTML상의 텍스트와 함께 thymeleaf 변수값을 사용할 수 있다.  [[${<변수이름>}]] 의 형태로 사용 가능하다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller

    <!-- 6. inline -->
    <script th:inline="javascript">
        let myName = [[${ name }]];
        console.log(myName);
    </script>

</body>
</html>

 

  • th:inline은 script 타입의 사용 또한 가능하다. th:inline 속성값을 "javascript"로 하면 script 태그 내에서 js 코드를 작성할 때도 thymeleaf 변수 값을 사용할 수 있다. 코드를 위와 같이 작성하고 서버를 구동하여 접속해보면 변수 name이 정상적으로 javascript에서 사용되고 있는 것을 볼 수 있다.

  • 이 기능은 dart 라는 구글이 javascript의 대체재로 개발한 스크립트언어에도 사용 가능하지만(th:inline="dart") 여기서 굳이 예시를 들지는 않기로 한다.

 

 

 이번 포스팅으로 thymeleaf를 사용하여 템플릿을 작성할 준비는 거의 끝났다. 다음 포스팅에서는 마지막으로 thymeleaf가 지원하는 fragment 기능에 대해서 알아보기로 하자. 

'프레임워크 > Spring' 카테고리의 다른 글

#13 모델 설계 1  (0) 2022.05.13
#12 Thymeleaf 3  (0) 2022.05.10
#10 Thymeleaf 1  (0) 2022.05.06
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05
#8 Spring Boot 프로젝트  (0) 2022.05.04

0. Thymeleaf

 본격적으로 동적으로 뷰를 구성하기 위해 먼저 템플릿 엔진인 Thymeleaf 문법에 대한 이해가 필요하다. 이번에는 Thymeleaf에서 유용하고 자주 사용되는 문법들을 사용해보며 정리하고 넘어갈 것이다. 물론 아직 배우는 중이기에 빠진 내용도 많겠지만 추후에 알게되면 별도의 포스팅으로 보충해나기로 한다.

 

 

1. XML name space 추가

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
</body>
</html>
  • 위와 같이 html 태그에 xmlns:th="http://thymeleaf.org" 를 추가해준다. 이것은 th 라는 xml 네임 스페이스를 추가한다는 의미이며 이 네임스페이스가 추가된 태그 안에서는 th가 지원하는 속성이나 태그 등을 사용할 수 있게 된다.

 

 

2. th:text

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <div th:text="${ myName }"></div>
</body>
</html>
  • html 태그에 th:text 속성을 지정해주면 해당 태그 내의 텍스트를 해당하는 변수의 값으로 치환해준다.
  • thymeleaf에서 변수값을 사용할 때는 문자열 내에서 ${ <변수명> } 을 사용하면 된다.
  • 위와 같이 myName 변수를 내부 텍스트로 가질 div 태그를 작성해준뒤 서버를 구동, 접속해보자.

 

  • 아무 내용도 나타나지 않는다. 아직 myName이라는 변수가 존재하지 않기 때문에 당연한 일이다.

 

 

3. Model 객체에 속성 추가

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("myName", "Scala");
        return "index";
    }
}
  • thymeleaf가 참조할 변수는 Controller가 이전 글에서 설명했던 Model 객체에 추가시켜줘야한다.
  • 위와 같이 model 객체에 addAttribute 메소드를 호출하여 "Scala" 라는 문자열을 저장한 속성 myName을 추가한다.

 

  • 서버를 재구동한 뒤 접속해보면 정상적으로 th:text 속성이 작동하는 것을 볼 수 있다.

 

4. th:utext

package com.scala.personal.mycomm.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {
    @GetMapping("")
    public String index(Model model) {
        model.addAttribute("myName", "<strong>Scala</strong>");
        return "index";
    }
}
  • Controller에서 myName의 값에 <strong>태그를 씌워서 속성에 추가하고  서버를 재구동한 뒤 다시 접속해보자.

 

  • 태그가 html 태그로 인식이 되지 않고 그대로 문자열로 출력된다.

 

  • Chrome 개발자 도구에서 해당 라인을 우클릭하여 Edit as HTML을 선택하면 위와 같이 태그를 나타내는 특수문자(<, >)가 이스케이프 처리 되어있는 것을 확인할 수 있다. th: text 는 이처럼 변수값을 이스케이프 처리하여 전달받은 형태 그대로 출력되도록 한다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>My Community</title>
</head>
<body>
    This is Index Page via Controller
    <div th:utext="${ myName }"></div>
</body>
</html>
  • 이번에는 th:text 속성을 th: utext로 바꾸고 다시한번 서버에 접속해보자

 

  • 이제 <strong> 태그가 정상적으로 적용되어 텍스트가 강조되어 표시되는 것을 볼 수 있다.
  • th:utext 의 u는 unescaped, 즉 이스케이프 처리하지 않고 텍스트를 삽입한다는 의미이다.

 

 

5. Thymeleaf 표현식(Expression)

  • 위에서는 thymeleaf에서 변수(variable)를 표현하기 위한 표현식( ${  } ) 만을 사용했지만 thymeleaf 는 그 외에도 다양한 표현식을 지원한다. thymeleaf의 더 많은 기능을 알아보기 전에 간단하게 짚고넘어가보자.

  • 변수 표현식(Variable Expression) : ${ ... } 
    • 위에서도 사용했던 변수 표현식은 말그대로 thymeleaf에서 변수를 참조하기 위해 사용된다.
    • 참조할 변수가 객체(object)인 경우 객체의 속성에도 접근이 가능하지만 속성에 직접 접근하는 것이 아닌 getter를 호출하는 방식이기 때문에 getter를 정의하지 않은 속성에는 접근할 수 없다.

  • 선택 변수 표현식(Selection Variable Expression) : *{  }
    • 변수 표현식은 변수를 참조하는 범위가 전체 컨텍스트였던 것과 달리 선택 변수 표현식은 이전에 선택했던 변수 내에서 해당 값을 참조한다. 실제 사용 예시는 나중에 th:object 속성과 함께 알아볼 것이다.

  • 메시지 표현식(Message Expression) : #{ ... }
    • .properties 등의 외부 파일에 정의된 값을 참조하기 위한 표현식
    • Spring에서는 resources/messages.properties 에 정의된 메시지값을 참조
    • messages_<국가표시> 로 브라우저의 국가 설정에 따라 다른 메시지를 표시 가능

  • 링크 url 표현식(Link URL Expression) : @{ ... }
    • src나 href 등 url 주소를 대입할 속성에서 사용되는 표현식
    • 기존의 절대경로은 물론 상대경로 또한 사용이 가능하다. 예를들어 서버의 메인 경로가 http://127.0.0.1:8080 일 경우 @{ /card } 를 사용하여 http://127.0.0.1:8080/card를 표현할 수 있다.
    • 이 외에 [http | https]://www 를 생략해주는 //static 등의 특수한 문법도 존재
  • 프래그먼트 표현식(Fragment Expression) ~{ ... }
    • tymeleaf에서는 뷰를 재사용하기 위한 프래그먼트 기능을 제공
    • 프래그먼트 표현식은 미리 정의해둔 프래그먼트를 참조하기 위한 표현식이다. 이에 대해서는 나중에 th:fragment 와 함께 알아보게 될 것이다.

 

6. 리터럴과 연산자

  • thymeleaf는 또한 문자열이나 숫자, 논리값 등을 나타내기 위한 리터럴 표현과 다양한 연산자들을 지원한다. 여기까지 숙지해두고 나면 이후에 알아볼 thymeleaf의 기능들을 활용하기에 부족함이 없을 것이다.

  • 리터럴 표현
    • 문자열
      • '<문자열 내용>' 과 같이 작은 따옴표로 감싸서 표현 가능
      • ex) 'abc', 'Hello World', ...
    • 숫자
      • 정수든 소수든 프로그래밍 언어에서처럼 평범하게 사용할 수 있다.
      • ex) 1, 27, 31.0, 12.7 ...
    • 논리형
      • true / false 로 나타낼 수 있다.

    • 널(Null) 값
      • null 로 나타낼 수 있다.

    • 리터럴 토큰
      • 리터럴 토큰은 기본적으로 문자열 리터럴과 동일하게 동작
      • 리터럴 토큰을 사용할 때는 작은 따옴표로 값을 감쌀 필요가 없다. (편의성)
      • 단, 리터럴 토큰에는 오직 알파벳과 숫자, 대괄호( [, ] )와 점(.), 하이픈(-), 언더바(_) 만을 사용할 수 있다.

  • 연산자
    • 문자열 연산자
      • + : 문자열을 합칠 수 있다(Concatenation)
      • |<표현식>| : 표현식의 결과를 문자열 리터럴로 치환할 수 있다(Literal Substitution)

    • 산술 연산자
      • +, -, *, /, % 등 기본적인 사칙연산
      • - 단항연산자(부호)

    • 논리 연산자
      • and, or 사용 가능
      • not 의 경우 !로도 사용 가능

    • 비교 연산자
      • >, <, >=, <=, ==, != 등의 기본 비교 연산자 모두 사용 가능
      • 위의 연산자 대신 gt, lt, ge, le, eq, ne 를 사용할 수도 있다.

    • 조건 연산자
      • if 조건문
        • <조건> ? <조건이 참일 경우>
        • <조건> ? <조건이 참일 경우> : <조건이 참이 아닐 경우>
      • defulat 조건문
        • <값> ?: <Default 값>
          => 해당 값이 없을 경우 Default값으로 대체

    • No-operation
      • _(언더바) : 아무 연산도 하지 않음
  • 이것으로 thymeleaf의 기능들을 알아보기 위한 밑준비가 끝났다. 다음부터는 thymeleaf가 지원하는 여러 속성값에 대해 알아볼 것이다. 

'프레임워크 > Spring' 카테고리의 다른 글

#12 Thymeleaf 3  (0) 2022.05.10
#11 Thymeleaf 2  (0) 2022.05.08
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05
#8 Spring Boot 프로젝트  (0) 2022.05.04
#7 Spring 의 구조  (0) 2022.05.03

1. index.html 추가

  • Dispatcher Servlet은 핸들러를 찾지 못할 경우 정적 리소스 경로(resouces)를 탐색한다. 그리고 만약 접속시에 호스트와 포트번호만을 입력하여 접속했을 때 resources/static 디렉토리에 index.html 파일이 있다면 별도의 매핑 없이도 해당 파일을 클라이언트에게 반환해준다.  즉, static의 index.html은 별도의 설정이 없이도 메인 페이지로 매핑이 되어있다.

  • 위와 같이 resources/static 디렉토리에 index.html을 작성하고 서버를 구동한 뒤 서버에 접속해보자

 

  • 메인 페이지가 정상적으로 출력되는 모습을 확인할 수 있다.

 

 

2. Controller 구현

  • mycomm 패키지 하위에 Controller가 위치할 controller 디렉토리를 생성

  • controller 패키지 하위에 메인 화면에 관한 요청을 담당할 HomeController를 생성

  • HomeController 클래스에 @Controller 어노테이션 적용
    => Controller로 사용할 것임을 표시
    => @Controller는 내부적으로 @Component를 사용하기에 Controller 클래스는 Bean으로 등록됨

  • 요청을 처리할 메소드 정의
    • @GetMapping은 HTTP 메소드중 GET 방식으로 들어온 요청을 처리할 메소드임을 표시

    • 괄호 안에는 매핑될 url을 적어줘야한다.  여기서는 메인 페이지를 매핑할 것이기 때문에 "" 를 넣어줬다.

    • 인자로 받는 Model 객체는 Controller로부터 View로 데이터를 전달하기 위한 객체
      => MVC 패턴의 Model 과는 상관이 없으니 혼동해서는 안된다.

    • 문자열 "index"를 반환
      => Dispatcher Servlet에게 view name 반환
      => Dispatcher Servlet은 view resolver를 통해 view name에 해당하는 view 객체를 받아와 렌더링
      => 렌더링된 view를 Client에게 반환
      => 이 때, view resolver가 view name에 해당하는 view 를 찾는 곳은 resources/templates 디렉토리이다.

 

 

3. View 구현

  • resources/templates 디렉토리에 Controller에서 반환해줬던 view name으로 html파일을 생성(index.html)
  • 위와 같이 html 파일을 작성하면 이제 view resolver가 index.html을 찾아 view 객체를 반환하고 view가 렌더링되어 Client에게 전달될 것이다.

 

  • 그런데 이상하게도 실제로 서버를 구동한 후 접속해보면 페이지를 찾을 수 없다는 에러 페이지가 보인다
  • 동적으로 view를 렌더링해줄 템플릿 엔진이 없기 때문이다.

 

 

4. 템플릿 엔진 의존성 추가

  • 여기서는 Spring Boot가 자동 설정을 지원하는 네 가지 템플릿 엔진(FreeMarker, Groovy, Thymeleaf, Mustache) 중 Thymeleaf 를 사용할 것이다. Thymeleaf는 공식 문서도 잘 갖춰져있고 문법도 배우기 쉬우며 확장성또한 뛰어나다.

  • 먼저 build.graddle 파일에 아래 내용을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  • 그리고 화면 우상단의 graddle 마크가 달린 새로고침 버튼을 눌러 다시한번 build를 해주면 프로젝트에서 thymeleaf 엔진을 사용할 수 있게 된다.

  • 템플릿 엔진도 추가했으니 다시한번 서버를 구동하고 메인 페이지에 접속해보자

 

  • 이제 정상적으로 templates에 만들어둔 index.html의 내용을 볼 수 있다. static에 index.html 이 있더라도 Dispatcher Servlet이 정적 리소스를 탐색하는 것은 핸들러를 찾지 못했을 때 뿐인데, 요청을 처리할 수 있는 Controller 가 존재하기 때문에 static의 index.html은 참조되지 않는다.

  • thymeleaf를 사용하여 동적으로 뷰를 렌더링하기는 했지만 아직까진 정적 리소스 반환과 별다른 차이가 없다. 다음에는 Controller에서 Model 객체를 통해 데이터를 전달하고 그 데이터에 thymeleaf를 통해 접근하여 진정한 의미로 동적으로 view를 구성해볼 것이다.

'프레임워크 > Spring' 카테고리의 다른 글

#11 Thymeleaf 2  (0) 2022.05.08
#10 Thymeleaf 1  (0) 2022.05.06
#8 Spring Boot 프로젝트  (0) 2022.05.04
#7 Spring 의 구조  (0) 2022.05.03
#6 PSA(Portable Service Abstraction)  (0) 2022.05.02

1. Spring Initializr

  • Spring Boot 프로젝트를 쉽게 생성할 수 있게 도와주는 서비스
  • Spring Initializr 웹 페이지에 접속하면 아래와 같은 모습을 볼 수 있다.

  • Project 에서는 Maven 과 Gradle 중 원하는 빌드 도구를 선택 가능
  • Language 에서는 Java, Kotlin, Goovy 중 원하는 언어를 선택 가능
  • Spring Boot 버전 선택 가능
    • SNAPSHOT : 개발 도중인 daily build 버전. 변경이 잦기 때문에 안정적으로 사용하기는 어려움
    • M(Milestone) : 새로 개발중인 기능이 포함된 버전. 기능이 완성되면 바로 릴리즈 가능한 상태
    • RC(Release Candidate) : 정식 릴리즈될 후보 버전. 치명적인 결함이 발견되지 않는다면 릴리즈해도 되는 상태
    • GA(General Availability) : 정식 릴리즈된 버전. 여기서는 아무 명칭도 붙어있지 않은 버전이 GA 버전
  • Project Metadata 작성
    • Group
      • 프로젝트의 패키지명
      • 일반적으로 com.<회사명>.<프로젝트명> 등의 형태로 구성 (도메인을 뒤집어놓은 형태)
      • 특수문자나 대문자 등의 사용은 쓰지 않을 것이 권장됨 (가독성을 위해 hyphen 정도는 사용 가능)
    • Artifact
      • version을 붙이지 않은 배포용 jar 파일의 이름
      • 빌드 산출물의 이름을 미리 정하는 것
    • Name
      • 어플리케이션의 이름
      • 일반적으로 Artifact와 동일하게 사용
    • Description
      • 프로젝트에 관한 간략한 설명

    • Packaging
      • 배포 방식 선택 가능
      • JAR
        • 빌드 결과 실행가능한 jar파일을 얻을 수 있음
        • jar 파일 내에 톰캣이 내장되어있어 별도의 WAS를 설치하지 않고도 바로 서버 구동 가능
          => webflux 를 사용할 경우 톰캣 대신 Netty를 사용

      • WAR
        • NginX 와 같은 별도의 WAS를 사용하고자 하는 경우 선택해야할 방식
        • WAR 파일을 WAS에 웹 어플리케이션으로 등록하는 식으로 배포 가능
    • Java 버전 선택가능
      • Java 8부터 Stream이나 Lambda 표현식, 인터페이스의 default method, Optional 클래스 등
        다양한 기능이 업데이트되었기 때문에 일반적으로 최소 Java 8 이상의 버전을 사용

      • 오라클 웹 사이트에서 각 버전별 차이를 숙지하고 자신의 프로젝트에 적합한 버전을 골라
        사용하는것이 좋음.

 

 

2. Dependencies

  • Spring Initializr 페이지 우측의 Dependencies 에서 ADD 를 클릭하면 위와 같은 모습을 볼 수 있다.
  • 원래 Spring에서 다양한 라이브러리나 보조 프레임워크 등을 사용하기 위해서는 복잡한 의존성 설정이 필요
  • Spring Boot에서는 이러한 의존성 관리를 xml이나 그래들 빌드 파일 등에서 쉽게 관리할 수 있음
  • Spring Initializr의 Dependencies는 프로젝트를 생성하면서부터 미리 필요한 의존성을 추가할 수 있도록 해줌
  • 위 사진에서 선택된 Spring Web 의존성만 추가해도 Spring MVC 구조의 웹 개발을 위한 최소한의 환경이 갖춰짐

 

 

3. 프로젝트 시작

  • Spring Web 의존성만 추가해준 뒤 GENERATE 버튼을 누르면 프로젝트가 압축파일 형태로 다운로드 된다.
  • 압축을 풀고 디렉토리 내의 build.gradle 파일을 프로젝트로 불러오기만 하면 완성이다.

 

 

  • IntelliJ
    • Open > 프로젝트 디렉토리 내의 build.gradle 선택 > OK > Open as Project 선택
    • 이후 이 글에서는 IntelliJ를 사용하여 프로젝트를 진행
  • Eclipse
    • File > Import > Existing Gradle Project 에서 마찬가지로 build.gradle 파일을 선택하여 불러오기
    • Eclipse의 경우에는 별도로 Gradle 플러그인을 설치해줄 필요가 있음

 

 

4. 프로젝트 구조

  • 프로젝트를 불러오고 Gradle이 빌드를 완료하기까지 기다리면 위와 같이 프로젝트가 구성된다.

  • src
    • 웹 어플리케이션을 구성할 Java 코드와 리소스(이미지, 템플릿, css, js 파일 등)가 위치할 디렉토리

    • main
      • java
        • 실제로 웹 어플리케이션을 구성하는 Java 코드가 위치
        • 앱을 구동할 메인 어플리케이션 클래스는 물론 이전에 설명한 Controller, Service, Repository나
          Entity(모델), DTO, 개발자가 임의로 정의한 클래스나 Java Configuration 파일도 여기에 위치
      • resources
        • css, js, 이미지 파일, 템플릿(html) 등의 정적 리소스들이 위치하는 디렉토리
        • Dispatcher Servlet이 요청을 처리한 핸들러를 찾지 못했을 경우 탐색하는 정적 리소스 경로

    • test
      • JUnit Test를 위한 코드들이 위치하는 디렉토리
      • 일반적으로 실제 java 디렉토리와 같은 구조로 구성하여 테스트를 진행한다.

    • .gitignore
      • git 저장소에 commit 시에 staging 하지 않을 파일들을 명시한 파일
      • 원래 직접 작성해야하지만 Spring Initializr로 Spring Boot 프로젝트 생성시 자동으로
        .gitignore에 추가해야할 파일들을 작성해준다. 물론 추가적으로 명시해야할 파일이
        생기게 될 경우에는 직접 작성해줘야한다.

    • build.gradle
      • Gradle을 사용하여 프로젝트를 빌드하기 위한 설정 파일
      • 이 파일을 통해 프로젝트의 의존성을 간편하게 관리할 수 있다.

 

 

5. 메인 어플리케이션 실행

  • src/main/java/<Group 이름> 디렉토리 하위의 MycommApplication 을 실행하면 웹 서버를 구동할 수 있다.

  • 정상적으로 실행되었다면 콘솔창에 위와 같이 출력되며 웹 서버에 접속 가능한 상태가 된다.
  • 현재 서버는 로컬에서 구동중이며 톰캣의 기본 포트는 8080이므로 127.0.0.1:8080 으로 웹 서버에 접속 가능하다.

 

  • 접속해보면 위와 같이 Whitelabel Error Page가 뜨는 것을 확인할 수 있다. 아직 요청을 처리할
    컨트롤러도, 넘겨줄 정적 리소스도 없기 때문이다. 

  • 다음 포스팅에서는 간단한 index 페이지와 컨트롤러, 템플릿을 작성하여 원하는 웹 페이지를 띄워볼 것이다.

'프레임워크 > Spring' 카테고리의 다른 글

#10 Thymeleaf 1  (0) 2022.05.06
#9 MVC 구조 갖추기 - Controller/View 구현 1  (0) 2022.05.05
#7 Spring 의 구조  (0) 2022.05.03
#6 PSA(Portable Service Abstraction)  (0) 2022.05.02
#5 Spring AOP(Aspect Oriented Programming)  (0) 2022.04.30

 

1. Dispatcher Servlet

  • Client로부터 요청이 들어왔을 때 가장 먼저 요청을 받아들이는 서블릿

     

  • 동작 절차

    1. 받은 요청에 적합한 핸들러(Controller)를 찾아 요청을 매핑
    1. 정적 리소스에 대한 요청은 핸들러를 찾지 못했을 때 정적 리소스 경로를 탐색하여 처리

    2. 핸들러를 찾았다면 호출하여 요청을 처리하고 결과를 반환받음

    3. View Name을 반환받은 경우 View Resolver를 통해 해당 View 객체를 얻어 렌더링

    4. 클라이언트에게 렌더링된 View나 응답 데이터 등을 반환

       

  • 가장 먼저 요청을 처리하기 시작하는 서블릿이기에 Front Controller 라고도 부름

  • 기존 Spring 프레임워크에서는 복잡한 설정이 필요했지만 Spring Boot를 사용하면
    Dispatcher Servlet의 설정이 자동화되어있어 별도의 설정 없이도 사용 가능

 

 

2. Controller

  • Dispatcher Servlet에 의해 매핑된 요청을 실제로 처리하는 핸들러
  • 처리해야할 비즈니스 로직이 있다면 Service를 호출하여 처리
  • 요청 처리 결과를 Dispatcher Servlet에게 반환
  • 처리 결과는 View Name이나 View 객체일 수도 있고 다른 타입의 데이터(JSON 등)가 될 수도 있음

 

 

3. Service

  • 비즈니스 로직(유저의 요청을 실제로 처리하기 위한 내부 로직)이 구현되는 영역
  • DAO 인터페이스를 호출하여 DB에 CRUD작업을 수행
  • 비즈니스 로직의 처리 결과를 Controller에 반환

 

 

4. Repository(DAO - Data Access Object)

  • JDBC 등의 API를 사용하여 직접 DB에 접근, CRUD 작업을 수행하는 영역
  • JDBC를 직접 사용하는 것 외에도 MyBatis등의 보조 프레임워크나 JPA 등의 ORM을 사용할 수도 있음
  • DB 작업의 처리 결과를 Service에 반환

1. 서비스 추상화(Service Abstraction)

  • 어떤 서비스의 구현부에 관계없이 추상화된 하나의 인터페이스로 같은 기능을 수행 가능
  • 코드가 서비스의 구현부에 독립적이기 때문에 재사용성이 높은 코드를 작성 가능
  • 복잡한 기능의 구현을 내부로 숨기고 사용하기 편한 추상화 인터페이스를 통해 편의성 제공
  • DBMS에 독립적으로 동일하게 사용 가능한 JDBC 등이 서비스 추상화의 대표적 예시
  • 서비스 추상화를 통해 특정 서비스 계층의 구현부를 교체하더라도 이를 사용하는 코드에는
    변화가 없도록 하는 것을 PSA라 함
  • 객체지향의 5원칙중 하나인 개방-폐쇄 원칙(Open-Closed Principle)의 사례

 

 

2. Spring의 PSA

  • Spring은 사용자에게 제공할 추상화된 인터페이스와 실제 구현부의 API를 이어줄 다양한
    어댑터(Adapter)를 제공하여 PSA를 구현

     

  • 예시

    • Spring Boot로 프로젝트를 시작할 경우 기본 내장 서블릿 컨테이너로 Tomcat을 사용
      => 코드를 거의 수정하지 않고 전혀 다른 서블릿 컨테이너를 사용하도록 할 수 있음

       

    • 원래 DB 작업에서 트랜잭션을 사용하기 위해서는 JDBC등의 라이브러리를 통해 복잡한 처리가 필요
      => Spring에서는 @Transactional 어노테이션을 사용하여 간편하게 트랜잭션 적용 가능(AOP)
      => JDBC가 아닌 JPA 등이 지원하는 Transaction Manager를 사용하더라도 코드 변경이 필요없음(PSA)

       

    • 캐싱 기능 또한 원래라면 사용할 캐시 라이브러리에 따라 (Redis, Memcached, EhCache 등)
      다른 코드를 작성하여 캐싱을 적용해야하지만 Spring은 각 캐시 라이브러리의 CacheManager를
      관리할 Manager를 Bean으로 등록하면 @Cacheable, @CacheEvict, @CachePut 등의 어노테이션을
      통해 추상화된 캐시 인터페이스를 사용한 간편한 캐싱이 가능

 

'프레임워크 > Spring' 카테고리의 다른 글

#8 Spring Boot 프로젝트  (0) 2022.05.04
#7 Spring 의 구조  (0) 2022.05.03
#5 Spring AOP(Aspect Oriented Programming)  (0) 2022.04.30
#4 Spring 의존성 주입 방법  (0) 2022.04.27
#3 Spring에 적용된 디자인 패턴  (0) 2022.04.24

1. AOP(Aspect Oriented Programming)

  • 횡단 관심사(Cross-cutting Concerns)

    • 어플리케이션 전반에 걸쳐 공통적으로 나타나는 로직
    • 동일한 기능을 하는 코드가 여러 모듈에서 반복적으로 나타나기에 흩어진 관심사라고도 함
    • 횡단 관심사를 통합하는 것으로 중복 코드를 제거하고 각 모듈이 핵심 로직에 집중 가능
      => 단일 책임 원칙(Single Responsibility Principle) 을 자연스럽게 적용 가능
  • 횡단 관심사를 상속 등의 기본적인 객체 지향 기법을 사용하여 분리하는 데는 한계가 있음

  • AOP는 보다 효율적으로 공통 로직을 여러 모듈에 적용하기 위한 프로그래밍 기법

 

 

2. Spring AOP의 특성

  • 대상 객체나 메소드를 감싸는 동적 프록시를 사용하여 구현
  • 프록시의 특성상 프록시 내부에서 메소드를 직접 호출할 경우 부가기능이 동작하지 않음
  • AOP는 Bean으로 등록된 객체에만 사용 가능
  • @Transactional, @Cacheable 등이 Spring이 지원하는 대표적인 AOP 서비스
  • 기본적으로 지원하는 AOP 서비스 외에도 사용자가 직접 AOP를 구현할 수도 있음

 

 

3. @Aspect

  • Spring에서 직접 AOP를 구현하기 위해 사용하는 어노테이션

  • 대상 클래스를 AOP 모듈로 사용할 것임을 명시

  • AOP 모듈로 사용하기 위해서는 Bean으로 등록되어야함(@Component 등 사용)

  • 공통 로직을 구현한 AOP 모듈 내의 메소드는 Advice라 함

     

  • Advice 실행 시점 지정

    • @Before : 대상 메소드의 실행 이전

    • @After : 대상 메소드의 실행 이후

    • @AfterReturning : 대상 메소드가 정상적으로 실행되어 반환된 이후

    • @AfterThrowing : 대상 메소드 실행중 Exception이 발생한 시점

    • @Around : 메소드 호출을 intercept하여 로직 실행 전후로 공통 로직을 수행 가능

       

  • AOP 적용 대상 지정

    • @Pointcut

      • @Pointcut("")의 형태로 AOP 적용 대상을 지정할 수 있음
        => Pointcut에서 사용되는 표현식을 PCD(Point Cut Designator)라 함

       

      • execution

        • 메소드의 반환 타입, 이름, 파라미터 등을 기준으로 대상을 지정

        • example

          • execution(pubilc * *(..)) : 모든 public 메소드
          • execution(int *(..)) : 반환 타입이 int인 모든 메소드
          • execution(* * (Long, int)) : 파라미터가 Long, int 인 모든 메소드

       

      • within

        • 특정 패키지, 클래스 내의 모든 메소드를 대상으로 지정

        • example

          • within(com.example.SampleProject.service.TestService)

       

      • this / target

        • 타입명을 사용하여 해당 클래스 또는 그 하위 클래스의 메소드를 대상으로 지정

        • this는 프록시 객체의 타입과, target은 실제 객체의 타입과 매칭

        • example

          • this(com.example.SampleProject.service.TestInterface)
          • target(com.example.SampleProject.service.TestInterfaceImpl)

       

      • args

        • 특정 이름의 매개변수를 받는 메소드를 대상으로 지정

        • Pointcut 메소드 또는 Advice 메소드에서 해당 변수를 매개변수로 받아야함

        • example

          • args(name)
          • public Object advice(ProceedingJoinPoint pj, String name) { ... }
          • String 타입의 name 변수를 가지는 메소드가 대상이 됨
          • 해당 변수의 값을 참조하는 것도 가능

       

      • @target

        • 특정 어노테이션을 가지는 모든 클래스와 그 하위 클래스의 메소드를 대상으로 지정

        • example

          • @target(org.springframework.stereotype.Repository)
            => @Repository가 붙은 모든 클래스와 그 하위 클래스의 메소드

       

      • @args

        • 특정 어노테이션을 가지는 객체를 파라미터로 받는 메소드를 대상으로 지정

        • 어노테이션은 Target ElementType이 Type이어야 함(클래스가 대상)

        • example

          • @args(javax.persistence.Entity)
            => @Entity 가 붙은 객체를 파라미터로 받는 메소드

       

      • @within

        • 특정 어노테이션을 가지는 클래스의 모든 메소드를 대상으로 지정

        • example

          • @within(org.springframework.stereotype.Service)
            => @Service 가 붙은 모든 클래스의 메소드를 대상으로 지정 (하위 클래스는 대상 외)

       

      • @annotation

        • 특정 어노테이션을 가지는 메소드를 대상으로 지정

        • example

          • @annotation(com.example.SampleProject.annotation.MyAnnotation)
            => @MyAnnotation이 붙은 모든 메소드를 대상으로 지정

     

    • PCD 는 @Pointcut 대신 Adivce 실행 시점 어노테이션(@Before, @After 등)에서도 사용 가능

     

  • 구현 절차

    • AOP 모듈로 사용할 클래스에 @Aspect, @Component 어노테이션 적용
    • Advice 메소드를 구현한 후 @Before, @After, @Around 등의 어노테이션을 사용하여
      실행 시점을 지정하고 PCD를 사용하여 Advice가 적용될 대상을 지정

     

  • 예시

    package com.example.SampleProject.service;
    
    import com.example.SampleProject.annotation.MyClassAnnotation;
    import com.example.SampleProject.annotation.MyMethodAnnotation;
    import com.example.SampleProject.domain.Category;
    import org.springframework.stereotype.Service;
    
    /* Test Service */
    @Service
    @MyClassAnnotation
    public class TestService implements TestInterface {
    
        // target 테스트
        public void doSomething(int i) {
            System.out.println("TargetTest: Do Something...");
        }
    
        // args 테스트
        public void doSomething(String name) {
            System.out.println("ArgsTest: Do Something...");
        }
    
        // @within 테스트
        public void doSomething(Long a, Long b) {
            System.out.println("@WithinTest: Do Something...");
        }
    
        // @args 테스트
        public void doSomething(Category category) {
            System.out.println("@ArgsTest: Do Something...");
        }
    
        // @annotation 테스트
        @MyMethodAnnotation
        public void doSomething(Boolean a) {
            System.out.println("@AnnotationTest: Do Something...");
        }
    }
    

     

    package com.example.SampleProject.aop;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    /* Test AOP */
    @Aspect
    @Component
    public class MyLogger {
        // Execution 테스트: 이름이 doSomething 인 모든 메소드가 각각 실행된 후에 실행
        @After("execution(* doSomething(..)) && within(com.example.SampleProject.service.TestService)")
        public void logAfterDoSomething() {
            System.out.println(">>> LogAfterDoSomething");
        }
    
        // Target 테스트:
        @Around("target(com.example.SampleProject.service.TestInterface)")
        public Object logging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            try {
                System.out.println("AOP 시작");
                return proceedingJoinPoint.proceed();
            } finally {
                System.out.println("AOP 끝\n");
            }
        }
    
        // Args 테스트: 인자로 String name 을 받는 메소드가 실행되기 전에 실행
        @Before("args(name) && within(com.example.SampleProject.service.TestService)")
        public void logBeforeArgs(String name) {
            System.out.println(">>> LogBeforeArgs: Parameter = " + name);
        }
    
        // @target 테스트: @MyClassAnnotation 이 붙은 클래스와 그 하위 클래스의 메소드가 실행된 후에 실행
        @After("@target(com.example.SampleProject.annotation.MyClassAnnotation) && within(com.example.SampleProject.service.TestService)")
        public void logAfterTargetAnnotation() {
            System.out.println(">>> LogAfterTargetAnnotation");
        }
    
        // @within 테스트: @MyClassAnnotation 이 붙은 클래스의 메소드 실행 전에 실행
        @Before("@within(com.example.SampleProject.annotation.MyClassAnnotation)")
        public void logBeforeWithinAnnotation() {
            System.out.println(">>> LogBeforeWithinAnnotation");
        }
    
        // @args 테스트: 인자로 @Entity 어노테이션이 붙은 클래스의 객체를 받는 메소드가 정상적으로 반환되는 시점에 실행
        @AfterReturning("@args(javax.persistence.Entity) && within(com.example.SampleProject.service.TestService)")
        public void logAfterReturingArgs() {
            System.out.println(">>> logAfterReturingArgs");
        }
    
        // @annotation 테스트: @MyMethodAnnotation 이 붙은 메소드가 실행되기 전에 실행
        @Before("@annotation(com.example.SampleProject.annotation.MyMethodAnnotation)")
        public void logBeforeAnnotation() {
            System.out.println(">>> LogBeforeAnnotation");
        }
    
    }
    
    

     

    package com.example.SampleProject;
    
    import com.example.SampleProject.domain.Category;
    import com.example.SampleProject.service.TestService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    /* Test Main */
    @SpringBootTest
    class SampleProjectApplicationTests {
    	private final TestService testService;
    
    	@Autowired
    	public SampleProjectApplicationTests(
    			TestService testService
    	) {
    		this.testService = testService;
    	}
    
    	@Test
    	void contextLoads() {
    
    		// target 테스트
    		testService.doSomething(0);
    
    		// args 테스트
    		testService.doSomething("name");
    
    		// @args 테스트
    		testService.doSomething(new Category());
    
    		// @within 테스트
    		testService.doSomething(0L, 0L);
    
    		// @annotation 테스트
    		testService.doSomething(true);
    
    	}
    }
    
    

     

    === 실행결과 ===
    AOP 시작
    >>> LogBeforeWithinAnnotation
    TargetTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    >>> LogBeforeArgs: Parameter = name
    ArgsTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    @ArgsTest: Do Something...
    >>> logAfterReturingArgs
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    @WithinTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    >>> LogBeforeAnnotation
    @AnnotationTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    • within(com.example.SampleProject.service.TestService) 와 결합하여 범위 한정
      => AOP의 대상을 너무 넓게 잡을 경우 의도치 않은 대상에게도 AOP를 적용하게됨

       

  • 하나의 대상에 적용되는 Advice 의 실행 순서

    1. @Around 실행 전 코드

    2. @Before 코드

    3. === 메소드 실행 ===

    4. @AfterReturning 코드

    5. @After 코드

    6. @Around 실행 후 코드

       

  • @Order를 사용한 실행순서 제어

    • 하나의 Advice 내에서의 실행 순서는 제어가 불가능(기본 우선순위를 따름)
    • 같은 대상에 적용될 Advice간에 임의의 순서를 지정해야한다면 다른 Aspect로 분리해야함
    • 다른 Aspect간에는 @Order를 사용하여 우선순위를 지정해줄 수 있음
    • Order안의 값이 높을수록 메소드 실행 이전에는 먼저 실행되고 실행 이후에는 나중에 실행됨
      => Order의 우선순위가 높을수록 보다 바깥의 Proxy가 되기 때문

 

  • AOP의 작동 방식

    • JDK Dynamic Proxy

      • 코드상에는 Proxy가 나타나지 않지만 Runtime에 동적으로 프록시를 생성

      • 대상과 같은 인터페이스를 구현한 Proxy 클래스를 사용

      • 인터페이스를 대상으로만 사용 가능한 방식

         

    • CGLib(Code Generation Library)

      • JDK Dynamic Proxy와 마찬가지로 Runtime에 동적으로 Advice를 적용

      • 대상을 상속하여 오버라이딩한 Proxy 클래스를 사용

      • 인터페이스가 아닌 클래스를 대상으로도 사용 가능

      • final, private 클래스는 상속이 불가능하기에 대상으로 할 수 없음

         

    • Spring에서는 기본적으로 타겟이 되는 클래스가 인터페이스를 구현하고있다면
      JDK Dynamic Proxy 방식을, 구현하고 있지 않다면 CGLib를 사용하도록 설정되어있음

       

    • aop config 파일을 작성하여 특정 방식을 강제할 수도 있음

 

'프레임워크 > Spring' 카테고리의 다른 글

#7 Spring 의 구조  (0) 2022.05.03
#6 PSA(Portable Service Abstraction)  (0) 2022.05.02
#4 Spring 의존성 주입 방법  (0) 2022.04.27
#3 Spring에 적용된 디자인 패턴  (0) 2022.04.24
#2 Spring Bean  (0) 2022.04.22

+ Recent posts