pynecone 공식 사이트 첫 번째 예제 만들어보기

https://pynecone.io/docs/getting-started/introduction

 

1. counter app을 위한 새로운 폴더 만들기

 

2. counter 폴더에 pynecone 프로젝트 생성해주기

cd counter
pc init

  - 터미널에서 해당 명령어로 counter 폴더에 접근해준뒤 "pc init" 명령어로 pynecone 프로젝트 생성

  - counter 폴더 안에 메인 파일인 counter.py가 생성됨

 

3. counter.py 작성

  - 원래의 기본 코드는 다 지우고 counter app을 만들기 위해 기본 틀만 남김

from pcconfig import config
import pynecone as pc

# 각종 상태값을 정의하고 변경하기 위한 State 클래스
class State(pc.State):
    pass

# 앱의 메인
def index():
    return 

# 앱 인스턴스 생성
# 페이지 추가
# 컴파일
app = pc.App(state=State)
app.add_page(index)
app.compile()

 

  - pynecone 홈페이지의 코드 작성

from pcconfig import config
import pynecone as pc

# 각종 상태값을 정의하고 변경하기 위한 State 클래스
class State(pc.State):

    # 변수는 모두 State에서 정의
    count = 0

    def increment(self):
        self.count += 1
    
    def decrement(self):
        self.count -= 1

# 앱의 메인
def index():
    return pc.hstack(
            # 버튼을 클릭했을 때, State에서 정의한 decrement 함수가 실행
            pc.button("desc - 1", on_click = State.decrement, color_scheme = "red", border_radius = "1em"),
            # State에서 정의한 count 변수
            pc.text(State.count),
            # 버튼을 클릭했을 때, State에서 정의한 increment 함수가 실행
            pc.button("asc + 1", on_click = State.increment, color_scheme = "green", border_radius = "1em"),
        )
    )

# 앱 인스턴스 생성
# 페이지 추가
# 컴파일
app = pc.App(state=State)
app.add_page(index)
app.compile()

 

  - 해당 페이지로 실행(pc run을 통해 한 번만 연결해두면 ctrl + s로 저장만 하면 자동으로 서버 재시작됨)

 

  - 추가로 페이지의 중앙에 놓기, ±10 버튼 만들기 연습

from pcconfig import config
import pynecone as pc

# 각종 상태값을 정의하고 변경하기 위한 State 클래스
class State(pc.State):

    # 변수는 모두 State에서 정의
    count = 0

    def increment(self):
        self.count += 1

    def increment_10(self):
        self.count += 10   
    
    def decrement(self):
        self.count -= 1

    def decrement_10(self):
        self.count -= 10

# 앱의 본체에 해당하는 함수(index)
def index():
    return pc.center(
        pc.hstack(
            pc.button("desc - 10", on_click = State.decrement_10, color_scheme = "red", border_radius = "1em"),
            pc.button("desc - 1", on_click = State.decrement, color_scheme = "red", border_radius = "1em"),
            pc.text(State.count),
            pc.button("asc + 1", on_click = State.increment, color_scheme = "green", border_radius = "1em"),
            pc.button("asc + 10", on_click = State.increment_10, color_scheme = "green", border_radius = "1em"),
        ), padding = "50px"
    )

# 앱의 인스턴스 생성
# 페이지 추가
# 컴파일
app = pc.App(state=State)
app.add_page(index)
app.compile()

'Python > pynecone' 카테고리의 다른 글

[pynecone] 03. pynecone으로 시계 만들기  (0) 2023.01.19
[pynecone] 01. 설치  (0) 2023.01.18

1. 터미널에 "pip install pynecone-io"를 입력하여 설치

2. node js 설치

https://nodejs.org/ko/

 

Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

3. pynecone을 연습해볼 폴더 생성

  - 커맨드 창에 다음 코드를 입력하여 가상환경 생성(git bash 이용)

# my_app이라는 폴더 생성
$ mkdir my_app
# 생성한 my_app 폴더로 현재 위치 이동
$ cd my_app
# 파이썬 가상환경 생성
$ python -m venv venv
# 가상환경 활성화
$ source venv/Scripts/activate

  - pynecone 설치, node js 설치

pip install pynecone-io
# node js를 가상환경 별로 사용할 수 있게 해주는 패키지
pip install nodeenv
# 현재 가상환경에 독립된 nodejs 환경 추가
nodeenv -p
# nodejs 버전 잘 나오는지 확인
node -v

  - 여기까지 만들어진 폴더

 

  - 터미널에 "pc init" 명령어로 pynecone 프로젝트를 초기화 시켜주면 다음과 같은 폴더 구조가 생성됨

pc init

  - 여기서 my_app.py가 메인 실행 파일

# my_app.py
"""Welcome to Pynecone! This file outlines the steps to create a basic app."""
from pcconfig import config

import pynecone as pc

docs_url = "https://pynecone.io/docs/getting-started/introduction"
filename = f"{config.app_name}/{config.app_name}.py"


class State(pc.State):
    """The app state."""

    pass


def index():
    return pc.center(
        pc.vstack(
            pc.heading("Welcome to Pynecone!", font_size="2em"),
            pc.box("Get started by editing ", pc.code(filename, font_size="1em")),
            pc.link(
                "Check out our docs!",
                href=docs_url,
                border="0.1em solid",
                padding="0.5em",
                border_radius="0.5em",
                _hover={
                    "color": "rgb(107,99,246)",
                },
            ),
            spacing="1.5em",
            font_size="2em",
        ),
        padding_top="10%",
    )


# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index)
app.compile()

 

  - 터미널에 "pc run" 명령어 실행하면 서버 열림

pc run

  - localhost:3000을 통해 접속(오류가 뜨기는 하지만 정상)

 

  - my_app.py 파일을 다음과 같이 변경하여 다시 "pc run"을 실행해보면 아래의 페이지가 정상적으로 출력됨

from pcconfig import config
import pynecone as pc

class State(pc.State):
   pass


def index():
    return pc.text("Hello World")

app = pc.App(state=State)
app.add_page(index)
app.compile()

● 결정 트리(Decision Tree)

  • 분류와 회귀에 사용되는 지도 학습 방법
  • 데이터 특성으로부터 추론된 결정 규칙을 통해 값을 예측
  • if-then-else 결정 규칙을 통해 데이터 학습
  • 트리의 깊이가 깊을 수록 복잡한 모델
  • 결정 트리의 장점
    • 이해와 해석이 쉬움
    • 시각화 용이
    • 많은 데이터 전처리 필요 x
    • 수치형, 범주형 모두 다룰 수 있음
    • ...
  • 결정 트리에 필요한 라이브러리
# 필요 라이브러리
import pandas as pd
import numpy as np
import graphviz
import multiprocessing
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, load_wine, load_breast_cancer, load_diabetes
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

 

1. 결정 트리를 활용한 분류(DecisionTreeClassifier())

  • DecisionTreeClassifier는 분류를 위한 결정 트리 모델
  • 두 개의 배열 X, y를 입력받음
    • X는 [n_samples, n_features] 크기의 데이터 특성 배열
    • y는 [n_samples] 크기의 정답 배열
X = [[0, 0], [1, 1]]
y = [0, 1]

# X가 [0, 0]일 때는 y가 0, X가 [1, 1]일 때는 y가 1 과 같이 분류
model = tree.DecisionTreeClassifier()
model = model.fit(X, y)

# X에 [2, 2]를 줬을 때 0과 1 중 어디로 분류될 지
model.predict([[2., 2.]])

# 출력 결과
array([1]) # 1로 분류됨


# X에 [2, 2]를 줬을 때 0과 1에 각각 분류될 확률
model.predict_proba([[2., 2.]])

# 출력 결과
array([[0., 1.]]) # 1이 선택될 확률이 100%로 나옴

 

 1) 붓꽃 데이터 분류(전처리 x)

model = DecisionTreeClassifier()
cross_val_score(
    estimator = model,
    X = iris.data, y = iris.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.96666667, 0.96666667, 0.9       , 1.        , 1.        ])

 

 2) 붓꽃 데이터 분류(전처리 o)

model = make_pipeline(
    StandardScaler(),
    DecisionTreeClassifier()
)

cross_val_score(
    estimator = model,
    X = iris.data, y = iris.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.96666667, 0.96666667, 0.9       , 1.        , 1.        ])

  - 전처리 한 것과 하지 않은 것의 결과에 차이가 없는데, 결정 트리는 규칙을 학습하기 때문에 전처리에 큰 영향을 받지 않음

 

 3) 학습된 결정 트리 시각화

  - 텍스트 형태로 시각화

# 트리를 텍스트로 추출
r = tree.export_text(decision_tree = model, feature_names = iris.feature_names)
print(r)

# 출력 결과
|--- petal length (cm) <= 2.45
|   |--- class: 0
|--- petal length (cm) >  2.45
|   |--- petal width (cm) <= 1.75
|   |   |--- petal length (cm) <= 4.95
|   |   |   |--- petal width (cm) <= 1.65
|   |   |   |   |--- class: 1
|   |   |   |--- petal width (cm) >  1.65
|   |   |   |   |--- class: 2
|   |   |--- petal length (cm) >  4.95
|   |   |   |--- petal width (cm) <= 1.55
|   |   |   |   |--- class: 2
|   |   |   |--- petal width (cm) >  1.55
|   |   |   |   |--- petal length (cm) <= 5.45
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- petal length (cm) >  5.45
|   |   |   |   |   |--- class: 2
|   |--- petal width (cm) >  1.75
|   |   |--- petal length (cm) <= 4.85
|   |   |   |--- sepal width (cm) <= 3.10
|   |   |   |   |--- class: 2
|   |   |   |--- sepal width (cm) >  3.10
|   |   |   |   |--- class: 1
|   |   |--- petal length (cm) >  4.85
|   |   |   |--- class: 2

 

  - plot 형태로 시각화

tree.plot_tree(model)

  - graphviz 시각화

dot_data = tree.export_graphviz(decision_tree = model,
                                feature_names = iris.feature_names,
                                class_names = iris.target_names,
                                filled = True, rounded = True,
                                special_characters = True)
# filled는 상자에 색깔을 칠할 것인지
# rounded는 상자 모서리를 둥글게 할 것인지
# special_characters는 특수문자

graph = graphviz.Source(dot_data)
graph

 

  - 결정 경계 시각화

n_classes = 3
plot_colors = 'ryb'
plot_step = 0.02

# 결정경계 시각화
plt.figure(figsize = (16, 8))
# pairidx는 자동으로 1씩 늘어나는 숫자 변수
# pair는 그래프의 x축과 y축에 각각 [0번째 변수, 1번째 변수], [0번째 변수, 2번째 변수],...를 넣음을 의미
for pairidx, pair in enumerate([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]):
    X = iris.data[:, pair]
    y = iris.target

    model = DecisionTreeClassifier()
    model = model.fit(X, y)

    # 그래프는 2행 3열의 형태로 배치
    plt.subplot(2, 3, pairidx + 1)

    # meshgrid로 각 분류 영역 색깔로 구분
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
                         np.arange(y_min, y_max, plot_step))
    plt.tight_layout(h_pad = 0.5, w_pad = 0.5, pad = 2.5)

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)

    plt.xlabel(iris.feature_names[pair[0]])
    plt.ylabel(iris.feature_names[pair[1]])

    # 전체 분류 개수(3개의 클래스)에 각각 plot_colors('r(red)y(yello)b(blue)')를 부여
    for i, color in zip(range(n_classes), plot_colors):
        idx = np.where(y == i)
        plt.scatter(X[idx, 0], X[idx, 1], c = color, label = iris.target_names[i],
                    cmap = plt.cm.RdYlBu, edgecolor = 'b', s = 15)

plt.suptitle("Decision surface")
plt.legend(loc = 'lower right', borderpad = 0, handletextpad = 0)
# loc: 범례의 위치는 오른쪽 아래
# borderpad: 범례 경계에 부분적인 빈 공간 입력
# handletextpad: 범례의 handle과 text 사이의 공간
plt.axis('tight')

  - max_depth를 2로 줬을 때

 

 

 4) 와인 데이터 분류(전처리 x)

model = DecisionTreeClassifier()
cross_val_score(
    estimator = model,
    X = wine.data, y = wine.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.94444444, 0.86111111, 0.88888889, 0.91428571, 0.85714286])

 

 5) 와인 데이터 분류(전처리 o)

model = make_pipeline(StandardScaler(), DecisionTreeClassifier())

cross_val_score(
    estimator = model,
    X = wine.data, y = wine.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.94444444, 0.80555556, 0.91666667, 0.91428571, 0.85714286])

 

 6) 학습된 결정 트리 시각화

  - 텍스트 형식으로 시각화

model = DecisionTreeClassifier()
model.fit(wine.data, wine.target)

r = tree.export_text(decision_tree = model, feature_names = wine.feature_names)
print(r)

# 출력 결과
|--- proline <= 755.00
|   |--- od280/od315_of_diluted_wines <= 2.11
|   |   |--- hue <= 0.94
|   |   |   |--- flavanoids <= 1.58
|   |   |   |   |--- class: 2
|   |   |   |--- flavanoids >  1.58
|   |   |   |   |--- class: 1
|   |   |--- hue >  0.94
|   |   |   |--- ash <= 2.45
|   |   |   |   |--- class: 1
|   |   |   |--- ash >  2.45
|   |   |   |   |--- class: 2
|   |--- od280/od315_of_diluted_wines >  2.11
|   |   |--- flavanoids <= 0.80
|   |   |   |--- class: 2
|   |   |--- flavanoids >  0.80
|   |   |   |--- alcohol <= 13.17
|   |   |   |   |--- class: 1
|   |   |   |--- alcohol >  13.17
|   |   |   |   |--- magnesium <= 98.50
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- magnesium >  98.50
|   |   |   |   |   |--- class: 0
|--- proline >  755.00
|   |--- flavanoids <= 2.17
|   |   |--- hue <= 0.80
|   |   |   |--- class: 2
|   |   |--- hue >  0.80
|   |   |   |--- class: 1
|   |--- flavanoids >  2.17
|   |   |--- magnesium <= 135.50
|   |   |   |--- class: 0
|   |   |--- magnesium >  135.50
|   |   |   |--- class: 1

 

  - plot 형태로 시각화

tree.plot_tree(model)

 

  - graphviz로 시각화

 

  - 결정 경계 시각화

n_classes = 3
plot_colors = 'ryb'
plot_step = 0.02

# 결정경계 시각화
plt.figure(figsize = (16, 8))
for pairidx, pair in enumerate([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]):
    X = wine.data[:, pair]
    y = wine.target

    model = DecisionTreeClassifier()
    model = model.fit(X, y)

    plt.subplot(2, 3, pairidx + 1)

    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
                         np.arange(y_min, y_max, plot_step))
    plt.tight_layout(h_pad = 0.5, w_pad = 0.5, pad = 2.5)

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)

    plt.xlabel(iris.feature_names[pair[0]])
    plt.ylabel(iris.feature_names[pair[1]])

    for i, color in zip(range(n_classes), plot_colors):
        idx = np.where(y == i)
        plt.scatter(X[idx, 0], X[idx, 1], c = color, label = iris.target_names[i],
                    cmap = plt.cm.RdYlBu, edgecolor = 'b', s = 15)

plt.suptitle("Decision surface")
plt.legend(loc = 'lower right', borderpad = 0, handletextpad = 0)
plt.axis('tight')

 

  - max_depth를 2로 줬을 때

 

 7) 유방암 데이터 분류(전처리 x)

model = DecisionTreeClassifier()
cross_val_score(
    estimator = model,
    X = cancer.data, y = cancer.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.9122807 , 0.90350877, 0.92982456, 0.93859649, 0.89380531])

 

 8) 유방암 데이터 분류(전처리 o)

model = make_pipeline(StandardScaler(), DecisionTreeClassifier())

cross_val_score(
    estimator = model,
    X = cancer.data, y = cancer.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([0.90350877, 0.9122807 , 0.92105263, 0.93859649, 0.89380531])

 

 9) 학습된 결정 트리 시각화

  - 텍스트 형식으로 시각화

model = DecisionTreeClassifier()
model.fit(cancer.data, cancer.target)

r = tree.export_text(decision_tree = model)
print(r)

# 출력 결과
|--- feature_20 <= 16.80
|   |--- feature_27 <= 0.14
|   |   |--- feature_29 <= 0.06
|   |   |   |--- class: 0
|   |   |--- feature_29 >  0.06
|   |   |   |--- feature_13 <= 38.60
|   |   |   |   |--- feature_14 <= 0.00
|   |   |   |   |   |--- feature_26 <= 0.19
|   |   |   |   |   |   |--- class: 1
|   |   |   |   |   |--- feature_26 >  0.19
|   |   |   |   |   |   |--- class: 0
|   |   |   |   |--- feature_14 >  0.00
|   |   |   |   |   |--- feature_21 <= 33.27
|   |   |   |   |   |   |--- class: 1
|   |   |   |   |   |--- feature_21 >  33.27
|   |   |   |   |   |   |--- feature_21 <= 33.56
|   |   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |   |--- feature_21 >  33.56
|   |   |   |   |   |   |   |--- class: 1
|   |   |   |--- feature_13 >  38.60
|   |   |   |   |--- feature_5 <= 0.06
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- feature_5 >  0.06
|   |   |   |   |   |--- feature_13 <= 39.15
|   |   |   |   |   |   |--- class: 0
|   |   |   |   |   |--- feature_13 >  39.15
|   |   |   |   |   |   |--- class: 1
|   |--- feature_27 >  0.14
|   |   |--- feature_21 <= 25.67
|   |   |   |--- feature_23 <= 810.30
|   |   |   |   |--- feature_4 <= 0.12
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- feature_4 >  0.12
|   |   |   |   |   |--- class: 0
|   |   |   |--- feature_23 >  810.30
|   |   |   |   |--- feature_3 <= 621.80
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- feature_3 >  621.80
|   |   |   |   |   |--- class: 1
|   |   |--- feature_21 >  25.67
|   |   |   |--- feature_6 <= 0.10
|   |   |   |   |--- feature_1 <= 19.44
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- feature_1 >  19.44
|   |   |   |   |   |--- class: 0
|   |   |   |--- feature_6 >  0.10
|   |   |   |   |--- class: 0
|--- feature_20 >  16.80
|   |--- feature_1 <= 16.11
|   |   |--- feature_27 <= 0.15
|   |   |   |--- class: 1
|   |   |--- feature_27 >  0.15
|   |   |   |--- class: 0
|   |--- feature_1 >  16.11
|   |   |--- feature_24 <= 0.09
|   |   |   |--- class: 1
|   |   |--- feature_24 >  0.09
|   |   |   |--- feature_26 <= 0.18
|   |   |   |   |--- feature_13 <= 37.05
|   |   |   |   |   |--- class: 1
|   |   |   |   |--- feature_13 >  37.05
|   |   |   |   |   |--- class: 0
|   |   |   |--- feature_26 >  0.18
|   |   |   |   |--- class: 0

 

  - plot 형태로 시각화

tree.plot_tree(model)

  - graphviz로 시각화

dot_data = tree.export_graphviz(decision_tree = model,
                                feature_names = cancer.feature_names,
                                class_names = cancer.target_names,
                                filled = True, rounded = True,
                                special_characters = True)

graph = graphviz.Source(dot_data)
graph

  - 결정 경계 시각화

n_classes = 2
plot_colors = 'ryb'
plot_step = 0.02

# 결정경계 시각화
plt.figure(figsize = (16, 8))
# 경계에 레이블이 두 개 밖에 없으므로 레이블의 개수를 수정해야함
for pairidx, pair in enumerate([[0, 1], [0, 2], [0, 3]]):
    X = cancer.data[:, pair]
    y = cancer.target

    model = DecisionTreeClassifier()
    model = model.fit(X, y)

    plt.subplot(2, 3, pairidx + 1)

    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step),
                         np.arange(y_min, y_max, plot_step))
    plt.tight_layout(h_pad = 0.5, w_pad = 0.5, pad = 2.5)

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)

    plt.xlabel(iris.feature_names[pair[0]])
    plt.ylabel(iris.feature_names[pair[1]])

    for i, color in zip(range(n_classes), plot_colors):
        idx = np.where(y == i)
        plt.scatter(X[idx, 0], X[idx, 1], c = color, label = iris.target_names[i],
                    cmap = plt.cm.RdYlBu, edgecolor = 'b', s = 15)

plt.suptitle("Decision surface")
plt.legend(loc = 'lower right', borderpad = 0, handletextpad = 0)
plt.axis('tight')

  - max_depth를 2로 줬을 때

 

 

1. 결정 트리를 활용한 회귀(DecisionTreeRegressor())

 1) 당뇨병 데이터 분류(전처리 x)

diabetes = load_diabetes()
model = DecisionTreeRegressor()

cross_val_score(
    estimator = model,
    X = diabetes.data, y = diabetes.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([-0.38141731, -0.05774227, -0.16941196, -0.01293649, -0.21747911])

 

 2) 붓꽃 데이터 분류(전처리 o)

model = make_pipeline(StandardScaler(), DecisionTreeRegressor())

cross_val_score(
    estimator = model,
    X = diabetes.data, y = diabetes.target,
    cv = 5,
    n_jobs = multiprocessing.cpu_count()
)

# 출력 결과
array([-0.26382084, -0.04666825, -0.24234995, -0.11229066, -0.1568879 ])

 

 

 3) 학습된 결정 트리 시각화

  - 텍스트 형태로 시각화

# 트리를 텍스트로 추출
model = DecisionTreeRegressor()
model.fit(diabetes.data, diabetes.target)

r = tree.export_text(decision_tree = model)
print(r)

 

  - plot 형태로 시각화

tree.plot_tree(model)

 

  - graphviz 시각화

dot_data = tree.export_graphviz(decision_tree = model,
                                feature_names = iris.feature_names,
                                class_names = iris.target_names,
                                filled = True, rounded = True,
                                special_characters = True)
graph = graphviz.Source(dot_data)
graph

 

  - 회귀식 시각화

plt.figure(figsize = (16, 8))

for pairidx, pair in enumerate([0, 1, 2]):
    X = diabetes.data[:, pair].reshape(-1, 1)
    y = diabetes.target

    model = DecisionTreeRegressor()
    model.fit(X, y)

    X_test = np.arange(min(X), max(X), 0.1)[:, np.newaxis]
    predict = model.predict(X_test)

    plt.subplot(1, 3, pairidx + 1)
    plt.scatter(X, y, s = 20, edgecolors = 'k', c = 'darkorange', label = 'data')
    plt.plot(X_test, predict, color = 'royalblue', linewidth = 2)
    plt.xlabel(diabetes.feature_names[pair])
    plt.ylabel('Target')

  - max_depth를 3으로 줬을 때

 

1. 스프링 Web MVC의 특징

  • 스프링 Web MVC는 웹 MVC 패턴으로 구현된 구조
  • 기본적인 흐름과 Controller, View, Model 등의 용어도 그대로 사용
  • 스프링 MVC가 기존 구조와 다른 부분
    • Front-Controller패턴을 이용해서 모든 흐름의 사전 / 사후 처리 가능
    • 어노테이션을 적극 활용, 최소한의 코드로 많은 처리 가능
    • HttpServletRequest / HttpServletResponse 이용하지 않아도 될 만큼 추상화된 방식으로 개발 가능
  • 스프링 MVC의 전체 흐름

 

 1) DispatcherServlet과 Front Controller

  • 스프링 MVC의 모든 요청은 반드시 DispatcherServlet이라는 존재를 통해서 실행됨
  • Front-Controller 패턴을 이용하면 모든 요청이 반드시 하나의 객체를 지나서 처리되어 모든 공통적인 처리를 Front-Controller에서 처리 가능
  • 스프링 MVC에서 DispatcherServlet이라는 객체가 Front-Controller 역할 수행
  • Front-Controller가 사전 / 사후에 대한 처리를 하게 되면 중간에 매번 다른 처리를 하는 부분만 별도로 처리하는 구조를 만들게 됨(이 부분이 Controller이고 @Controller를 이용해서 처리)

 


실습

 1) 스프링 MVC 사용하기

  - 프로젝트의 webapp 폴더 > resources 폴더 생성: 이미지나 html 파일 같은 정적인 파일을 서비스하기 위한 경로

  - webapp 폴더 > WEB-INF > servlet-context.xml 생성

<!-- servlet-context.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc = "http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 스프링 MVC 설정을 어노테이션 기반으로 처리한다는 의미, 스프링 MVC의 여러 객체들을 자동을 ㅗ스프링의 Bean으로 등록하게 하는 기능 -->
    <mvc:annotation-driven></mvc:annotation-driven>

    <!-- 이미지나 html 파일 같은 정적인 파일 경로 지정 -->
    <!-- "/resources" 경로로 들어오는 요청은 정적인 파일을 요구하는 것으로 판단하고 스프링 MVC에서 처리하지 않는다는 의미 -->
    <mvc:resources mapping = "/resources/**" location = "/resources/"></mvc:resources>

    <!-- InternalResourceViewResolver는 스프링 MVC에서 제공하는 View를 어떻게 결정하는지에 대한 설정 담당 -->
    <bean class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name = "prefix" value = "/WEB-INF/views/"></property>
        <property name = "suffix" value = ".jsp"></property>
    </bean>

</beans>

 

 2) web.xml의 DispatcherServlet 설정

  • 스프링 MVC 실행을 위해 Front-Controller 역할을 하는 DispatcherServlet 설정
<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    
    ...
    
    <!-- DispatcherServlet 로딩 시 servlet-context.xml을 이용하도록 설정 -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/servlet-context.xml</param-value>
        </init-param>

        <!-- Tomcat 로딩 시 클래스를 미리 로딩해두기 위한 설정 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- DispatcherServlet이 모든 경로의 요청에 대한 처리를 담당하기 때문에 '/'fh wlwjd -->
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

 


실습

 2) 스프링 MVC Controller

  - 스프링 MVC Controller의 다른 점

  • 상속이나 인터페이스를 구현하는 방식을 사용하지 않고 어노테이션만으로 처리 가능
  • 오버라이드 없이 필요한 메서드 정의
  • 메서드의 파라미터를 기본 자료형이나 객체 자료형을 마음대로 지정
  • 메서드의 리턴타입도 void, String, 객체 등 다양한 타입 사용 가능

  - org.zerock.springex 프로젝트 내에 controller 패키지 추가 > SampleController 클래스 추가

// SampleController
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {
  
  // GET 방식으로 들어오는 요청을 처리하기 위해 사용("/hello"라는 경로를 호출할 때 동작)
  @GetMapping("/hello")
  public void hello() {
    log.info("hello........" );
  }
  
}

 

 3) servlet-context.xml의 component-scan

  • controller 패키지에 존재하는 Controller 클래스들을 스프링으로 인식하기 위해 @Controller 어노테이션이 추가된 클래스들의 객체들이 스프링의 Bean으로 설정되게 만들어야 함
  • servelt-context.xml의 component-scan을 다음과 같이 적용
<!-- servlet-context.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc = "http://www.springframework.org/schema/mvc"
       xmlns:context = "http://www.springframework.org/schema/context"
       xsi:schemaLocation=
               "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

   ...
   
   <context:component-scan base-package = "org.zerock.springex.controller" />

</beans>
  • "/hello" 경로의 화면을 간단하게 작성(webapp > WEB-INF > views 폴더 생성 > hello.jsp 파일 생성)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Hello JSP</h1>
</body>
</html>

 

 4) @RequestMapping과 파생 어노테이션들

  • @RequestMapping은 특정 경로의 요청을 지정하기 위해 사용
  • 클래스 선언부에도 사용할 수 있고, Controller의 메서드에도 사용할 수 있음
  • Servlet 중심의 MVC에서는 Servlet을 상속받아서 doGet() / doPost()와 같은 제한적인 메서드를 오버라이드해서 사용했지만, 스프링 MVC의 경우 하나의 Controller를 이용해서 여러 경로의 호출 처리 가능
  • org.zerock.springex > controller 패키지 > TodoController 작성
// TodoController
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {

  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

  @RequestMapping(value = "/register", method = RequestMethod.GET)
  public void register() {
    log.info("todo register");
  }
  
}

/todo/list 경로의 화면을 만들지 않아서 화면을 띄우는 것은 오류가 나지만, 로그에 list() 메서드가 실행되어 메세지가 뜬 것을 확인

  • @RequestMapping을 이용하는 것만으로 여러 Controller를 하나의 클래스로 묶을 수 있고, 각 기능마다 메서드 단위로 설계할 수 있게 되어 많은 양의 코드 줄일 수 있음

 

  • 스프링 4버전 이후에는 @GetMapping / @PostMapping 어노테이션으로 GET/ POST 방식 구분해서 처리 가능
  • 예를 들어, "/todo/register"는 GET 방식으로 화면을 보여주고, POST 방식으로 처리하므로 다음과 같이 설계
// TodoController
package org.zerock.springex.controller;

import ...

@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {

  // 클래스 선언부에서 RequestMapping의 value가 "/todo"이고 list() 메서드에서 ReuestMapping의 value가 "/list"이므로
  // 최종 경로는 "/todo/list"가 됨
  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

  // @RequestMapping(value = "/register", method = RequestMethod.GET)
  @GetMapping("/register")
  public void registerGET() {
    log.info("GET todo register...............");
  }
  
  @PostMapping("/register")
  public void registerPOST() {
    log.info("POST todo register...............");
  }
  
}

 

 

2. 파라미터 자동 수집과 변환

  • 파라미터 자동 수집은 DTO, VO 등을 메서드의 파라미터로 설정하면 자동으로 전달되는 HttpServletRequest의 파라미터들을 수집해주는 기능
  • 단순 문자열만이 아니라 숫자, 배열, 리스트, 첨부 파일도 가능
  • 파라미터 수집 동작 기준
    • 기본 자료형의 경우 자동으로 형 변환처리 가능
    • 객체 자료형의 경루 setXXX()를 통해 처리
    • 객체 자료형의 경우 생성자가 없거나 파라미터가 없는 생성자가 필요(Bean)

 


실습

 3) 단순 파라미터의 자동 수집

  - SampleController에서의 예시

// SampleController
package org.zerock.springex.controller;

import ...

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {

  ...

  @GetMapping("/ex1")
  public void ex1(String name, int age) {
    log.info("ex1.......");
    log.info("name: " + name);
    log.info("age: " + age);
  }

}

  - 주소를 "http://localhost:8080/ex1?name=AAA&age=16"로 설정하면 자동으로 name은 문자열 AAA로, age는 숫자 16으로 파라미터를 수집해와서 로그에 출력

 

  - @RequestParam

  - 요청에 전달된 파라미터 이름을 기준으로 동작하지만, 간혹 파라미터가 전달되지 않으면 문제 발생할 수 있음

  - 이 때 @RequestParam이라는 어노테이션 고려

  - @RequestParam은 defaultValue라는 속성이 있어서 '기본값'을 지정할 수 있음

package org.zerock.springex.controller;

import ...

// 해당 클래스가 MVC에서 Controller 역할을 한다는 의미, 스프링의 Bean으로 처리되기 위해 사용
@Controller
@Log4j2
public class SampleController {

  ...

  @GetMapping("/ex2")
  public void ex2(@RequestParam(name = "name", defaultValue = "AAA") String name, 
                  @RequestParam(name = "age", defaultValue = "20") int age) {
    log.info("ex2.......");
    log.info("name: " + name);
    log.info("age: " + age);
  }

}

  - 주소에 "http://localhost:8080/ex2"만 입력하고 파라미터를 주지 않아도 기본값으로 파라미터를 받아서 로그로 출력함

 

  - Formatter를 이용한 파라미터의 커스텀 처리

  - 기본적으로 HTTP는 문자열로 데이터를 전달하기 때문에 Controller는 문자열을 기준으로 특정 클래스의 객체로 처리하는 작업이 진행

  - 이때 날짜 등 특정 타입을 처리하는 Formatter 이용 가능

  - Formatter는 문자열을 포맷을 이용해서 특정 객체로 변환하는 경우 사용

  - controller 패키지 > formatter 패키지 작성 > LocalDateFormatter 클래스 작성

  - Formatter에는 parse()와 print() 메서드 존재

// LocalDateFormatter
package org.zerock.springex.controller.formatter;

import org.springframework.format.Formatter;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class LocalDateFormatter implements Formatter<LocalDate> {

  @Override
  public LocalDate parse(String text, Locale locale) {
    return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  }

  @Override
  public String print(LocalDate object, Locale locale) {
    return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
  }
}

  - Formatter를 servlet-context.xml에 적용하기 위해 FormattingConversionServiceFactoryBean 객체를 스프링의 Bean으로 등록하고 이 안에 작성한 LocalDateFormatter를 추가해야 함

<!-- servlet-context.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<beans ...>

    <!-- conversionService라는 Bean을 등록한 후에 스프링 MVC를 처리할 때 <mvc:annotation-driven에 이를 이용한다는 것을 명시해야 함 -->
    <mvc:annotation-driven conversion-service = "conversionService"></mvc:annotation-driven>
    
    ...
    
    <bean id = "conversionService" class = "org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name = "Formatters">
            <set>
                <bean class = "org.zerock.springex.controller.formatter.LocalDateFormatter"/>
            </set>
        </property>
    </bean>

</beans>

  - ex3 경로에 날짜를 파라미터로 받는 메서드 추가후 날짜 형변환 정상 작동 여부 확인

// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

 ...

  @GetMapping("/ex3")
  public void ex3(LocalDate dueDate) {
    log.info("ex3.......");
    log.info("dueDate: " + dueDate);
  }

}

  - 주소로 "http://localhost:8080/ex3?dueDate=2020-10-10"를 작성하면 날짜형태로 잘 받아서 로그에 출력함을 확인


 

 1) 객체 자료형의 파라미터 수집

  • 기본 자료형과 달리 객체 자료형을 파라미터로 처리하기 위해서는 객체가 생성되고 setXXX()을 이용해서 처리
  • Lombok을 활용하면 @Setter나 @Data를 이용하는 것이 간단
  • 프로젝트에 dto 패키지 추가 > TodoDTO 클래스 추가
// TodoDTO
package org.zerock.springex.dto;

import lombok.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;

@ToString
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {

  private Long tno;
  private String title;
  private LocalDate dueDate;
  private boolean finished;
  private String writer;

}

 

※ javax.validation을 사용하려면 build.gradle에 다음을 의존성으로 추가해야함

implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'

 

  • TodoController의 '/todo/register'를 POST 방식으로 처리하는 메서드에 TodoDTO를 파라미터로 적용
package org.zerock.springex.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.zerock.springex.dto.TodoDTO;

@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {

  @RequestMapping("/list")
  public void list() {
    log.info("tood list..........");
  }

  // @RequestMapping(value = "/register", method = RequestMethod.GET)
  @GetMapping("/register")
  public void registerGET() {
    log.info("GET todo register...............");
  }

  // TodoDTO를 파라미터로 적용
  // 자동으로 형변환되기 때문에 다양한 타입의 멤버 변수들의 처리가 자동으로 이루어짐
  @PostMapping("/register")
  public void registerPOST(TodoDTO todoDTO) {
    log.info("POST todo register...............");
    log.info(todoDTO);
  }

}

 

  • WEB-INF/views에 todo 폴더 생성 > register.jsp 파일 추가
<!-- register.jsp -->
<%@ taglib prefix ="form" uri = "http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action = "/todo/register" method = "post">
        <div>
            Title: <input type = "text" name = "title">
        </div>
        <div>
            DueDate: <input type = "date" name = "dueDate" value = "2023-01-17">
        </div>
        <div>
            Writer: <input type = "text" name = "writer">
        </div>
        <div>
            Finished: <input type = "checkbox" name = "finished">
        </div>
        <div>
            <button type = "submit">Register</button>
        </div>
    </form>
</body>
</html>

  • 'localhost:8080/todo/register'에 들어가면 위와 같은 입력 화면이 나오고, 각 입력창에 데이터를 입력한 뒤, Register 버튼을 누르면 로그에 해당 데이터가 출력됨

 

 2) Model이라는 특별한 파라미터

  • 웹 MVC와 스프링 MVC 모두 Model이라고 부르는 데이터를 JSP에 전달해야함
  • Servlet 방식에서는 request.setAttribute()를 이용해서 데이터를 담아 JSP에 전달
  • 스프링 MVC 방식에서는 Model이라는 객체를 이용해서 처리 가능
  • Model에서 addAttribute() 메서드를 이용해서 View에 전달할 이름과 값을 지정
// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

  ...

  @GetMapping("/ex4")
  public void ex4(Model model) {
    log.info("----------------");
    model.addAttribute("message", "Hello World");
  }

}

 

  • 데이터를 받을 ex4.jsp 파일도 WEB-INF/views 폴더에 생성
<!-- ex4.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri ="http://java.sun.com/jsp/jstl/core" prefix = "c" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>${message}</h1>
    <h1><c:out value = "${message}"></c:out></h1>
</body>
</html>

 

  • Model에 담긴 데이터는 HttpServletRequest의 setAttribute()와 동일한 동작을 수행하여 JSP에서 EL을 이용해 별다른 처리없이 사용 가능

 

  • 스프링 MVC의 Controller는 파라미터로 getter / setter를 이용하는 Java Beans의 형식의 사용자 정의 클래스가 파라미터인 경우 자동으로 화면까지 객체 전달
  • 예를 들어, 파라미터로 TodoDTO를 받는 경우, 다음과 같이 작성하고, JSP에서 ${todoDTO}를 통해 이용 가능
@GetMapping("/ex4_1")
public void ex4Extra(TodoDTO todoDTO, Model model) {
	log.info(todoDTO);
}
  • 이때 자동으로 생성된 todoDTO라는 이름 외에 다른 이름을 사용하고 싶으면 @ModelAttribute()를 사용하여 지정가능
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
	log.info(todoDTO);
}
  • 이렇게 하면 ${dto}로 사용가능

 

 3) RedirectAttributes와 리다이렉션

  • POST 방식으로 어떤 처리를 하고 Redirect 해서 GET 방식으로 특정 페이지 이동하는 PRG 패턴 처리를 위해 스프링 MVC에서는 RedirectAttributes라는 타입 사용
  • RedirectAttributes의 중요 메서드
    • addAttribute(키, 값): Redirect할 때 쿼리 스트링 값 적용
    • addFlashAttribute(키, 값): 일회용으로 데이터만 전달하고 삭제되는 값 지정
  • SampleController에서 ex5()는 RedirectAttributes를 파라미터로 추가하고 addAttribute와 addFlashAttribute를 사용해서 name과 result라는 이름을 가진 값들을 전달
// SampleController
package org.zerock.springex.controller;

import ...

@Controller
@Log4j2
public class SampleController {

  ...
  
  @GetMapping("/ex5")
  public String ex5(RedirectAttributes redirectAttributes) {
    redirectAttributes.addAttribute("name", "ABC");
    redirectAttributes.addFlashAttribute("result", "success");
    
    // redirect를 위해 "redirect:"라는 접두어를 붙여 문자열로 반환
    return "redirect:/ex6";
  }
  
  @GetMapping("/ex6")
  public void ex6() {
    
  }

}
  • ex5에서 넘겨준 값을 받은 ex6의 jsp 화면 작성
<!-- ex6.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>ADD FLASH ATTRIBUTE: ${result}</h1>
</body>
</html>

 

  • ex5 호출하면 name과 result라는 이름을 가진 값들이 생성되어 ex6으로 redirect됨

  • ex6의 화면이 나오고 쿼리 스트링으로 준 name의 값 "ABC"가 주소창에 전달되어 있고, 화면에 ${result}로 출력한 result의 값인 "success"가 출력되어 있음

  • addFlashAttribute는 일회용으로 전달하고 사라지므로, 해당 페이지를 새로고침하면, "success"가 사라짐을 확인

 

 4) 다양한 리턴 타입

  • 스프링 MVC에서 Controller내에 선언하는 메서드의 리턴 타입을 다양하게 사용 가능
    • void: 화면이 따로 있는 경우, @RequestMapping값 과 @GetMapping 등 메서드에서 선언된 값을 그대로 View의 이름으로 사용, 주로 상황에 관계없이 동일한 화면을 보여줄 때 사용
    • 문자열: 화면이 따로 있는 경우, 상황에 따라 다른 화면 보여줄 때 사용, 다음과 같은 특별한 접두어 사용가능
      • redirect: 리다이렉션을 이용하는 경우, 주로 forward 대신 redirect 이용
      • forward: 브라우저의 URL은 고정하고 내부적으로 다른 URL로 처리하는 경우
    • 객체나 배열, 기본 자료형: JSON 타입 활용 시
    • ResponseEntity: JSON 타입 활용 시

 

 5) 스프링 MVC에서 주로 사용하는 어노테이션

  • Controller 선언부에 사용하는 어노테이션
    • @Controller: 스프링 Bean의 처리됨을 명시
    • @RestController: REST 방식의 처리를 위한 Controller임을 명시
    • @RequestMapping: 특정한 URL 패턴에 맞는 Controller인지를 명시
  • 메서드 선언부에 사용하는 어노테이션
    • @GetMapping / @PostMapping / @DeleteMapping / @PutMapping ...: HTTP 전송방식에 따라 해당 메서드를 지정하는 경우 사용, 일반적으로 @GetMapping과 @PostMapping을 주로 사용
    • @RequestMapping: GET / POST 방식 모두 지원하는 경우 사용
    • @ResponseBody: REST 방식에서 사용
  • 메서드의 파라미터에 사용하는 어노테이션
    • @RequestParam: Request에 있는 특정한 이름의 데이터를 파라미터로 받아서 처리하는 경우 사용
    • @PathVariable: URL 경로의 일부를 변수로 삼아서 처리하기 위해 사용
    • @ModelAttribute: 해당 파라미터는 반드시 Model에 포함되어 다시 View로 전달됨을 명시(주로 기본 자료형이나 Wrapper 클래스, 문자열에 사용)
    • 기타: @SessionAttribute, @Valid, @RequestBody 등

 

 

 3. 스프링 MVC의 예외 처리

  • 스프링 MVC에서 예외를 처리하는 가장 일반적인 방법은 @ControllerAdvice를 이용하는 것
  • @ControllerAdvice는 Controller에서 발생하는 예외에 맞게 처리할 수 있는 기능 제공
  • @ControllerAdvice가 선언된 클래스 역시 스프링의 Bean으로 처리됨
  • 실습을 위해 controller 패키지 > exception 패키지 > CommonExceptionAdvice 클래스 작성
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
}

 

 1) @ExceptionHandler

  • @ControllerAdvice의 메서드들에는 특별하게 @ExceptionHandler라는 어노테이션 사용 가능
  • 이를 이용해서 전달되는 Exception 객체들을 지정하고 메서드의 파라미터에서 이를 이용 가능
  • 고의로 예외를 발생시키는 코드 작성하여 실험
// SampleController
package org.zerock.springex.controller;

import ...
@Controller
@Log4j2
public class SampleController {

  ...

  // p1에는 문자열이, p2에는 숫자가 전달되어야 함
  @GetMapping("/ex7")
  public void ex7(String p1, int p2) {
    log.info("p1........" + p1);
    log.info("p2........" + p2);
  }

}
  • 해당 코드를 작성하고, p2에 숫자가 아닌 문자열을 쿼리 스트링으로 전달해주면 예외가 발생
  • "localhost:8080/ex7?p1=AAA&p2=BBB"를 주면 BBB는 int형이 아니므로 에러 코드 400이 발생

  • 해결을 위해 CommonExceptionAdvice에 NumberFormatException을 처리하도록 지정
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  // 문자열이나 JSON 데이터를 그대로 전송할 때 사용되는 어노테이션
  @ResponseBody
  // @ExceptionHandler를 가진 모든 메서드는 해당 타입의 예외를 파라미터로 전달받을 수 있음
  @ExceptionHandler(NumberFormatException.class)
  // exceptNumber()는 @ResponseBody를 이용해서 만들어진 문자열을 그대로 브라우저에 전송하는 방식 이용
  public String exceptNumber(NumberFormatException numberFormatException) {
    log.error("--------------------------------");
    log.error(numberFormatException.getMessage());

    return "NUMBER FORMAT EXCEPTION";
  }
}

 

 2) 범용적인 예외처리

  • 예외 처리의 상위 타입인 Excpetion 타입을 처리하도록 구성
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Arrays;

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  // 문자열이나 JSON 데이터를 그대로 전송할 때 사용되는 어노테이션
  @ResponseBody
  // @ExceptionHandler를 가진 모든 메서드는 해당 타입의 예외를 파라미터로 전달받을 수 있음
  @ExceptionHandler(Exception.class)
  // exceptCommon은 Exception 타입을 처리하여 사실상 거의 모든 예외를 처리하는 용도로 사용 가능
  public String exceptCommon(Exception exception) {
    log.error("--------------------------------");
    log.error(exception.getMessage());

    // <ul>로 시작하는 buffer 문자열 작성
    StringBuffer buffer = new StringBuffer("<ul>");

    // 예외 처리 메세지가 발생할 때마다 <li>와 함께 <ul></ul> 안에 리스트 형태로 해당 메세지를 추가
    buffer.append("<li>" + exception.getMessage() + "</li>");

    // 에러가 났을 때, 현재의 함수나 메서드 명도 같이 출력하여 더 자세히 디버깅할 수 있도록 함 
    Arrays.stream(exception.getStackTrace()).forEach(stackTrackElement -> {
      buffer.append("<li>" + stackTrackElement + "</li>");
    });
    
    // 마지막에는 리스트 형식을 끝내도록 </ul> 추가
    buffer.append("</ul>");

    return buffer.toString();
  }
}

 

 3) 404 에러 페이지와 @ResponseStatus

  • 서버 내부가 아닌 시작부터 잘못된 URL을 호출할 때 404 예외 발생
  • @ControllerAdvice에 작성하는 메서드에 @ResponseStatus를 이용하면 404상태에 맞는 화면을 별도로 작성 가능
// CommonExceptionAdvice
package org.zerock.springex.controller.exception;

import ...

@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {

  ...

  // 404 에러 대비
  @ExceptionHandler(NoHandlerFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public String notFound() {
    return "custom404";
  }
}
  • custom404의 페이지를 jsp 파일로 작성
<!-- custom404 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>Oops! 페이지를 찾을 수 없습니다!</h1>
</body>
</html>
  • web.xml에서는 DispatcherServlet의 설정을 조정해야함
  • <servlet> 태그 내에 <init-param>을 추가하고 throwExceptionIfNoHandlerFound라는 파라미터 설정 추가
<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

    ...
    
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/servlet-context.xml</param-value>
        </init-param>

        <!-- 404에러 처리용으로 추가 -->
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
		<!-- 여기까지 -->
        
        <load-on-startup>1</load-on-startup>
    </servlet>

    ...
    
    
</web-app>

설정하지 않은 주소를 쳤을 때, 404에러 대신 직접 만든 페이지가 나오는 것을 확인

1. MyBatis

  • MyBatis: Sql Mapping Framework라고 표현됨, Sql Mapping이란 SQL의 실행 결과를 객체 지향으로 매핑해준다는 뜻
  • MyBatis 사용 시, 기존 SQL을 그대로 사용할 수 있고 다음의 장점이 존재
    • PreparedStatement / ResultSet의 처리: 기존 프로그램 작성 시 하나씩 처리해야 하는 파라미터나 ResultSet의 getXXX()를 MyBatis가 알아서 처리해 주어 많은 양의 코드를 줄일 수 있음
    • Connection / PreparedStatement / ResultSet의 close() 처리: MyBatis와 스프링을 연동해서 사용하는 방식을 이용하면 자동으로 close() 처리 가능
    • SQL의 분리: MyBatis를 사용하면 별도의 파일이나 어노테이션 등을 이용해서 SQL을 선언, 파일을 이용하는 경우 SQL을 별도의 파일로 분리해서 운영 가능

 

 1) MyBatis와 스프링의 연동 방식

  • MyBatis는 단독으로 실행이 가능하지만, 스프링 프레임워크는 MyBatis와 연동을 쉽게 처리할 수 있는 라이브러리와 API를 제공
    • MyBatis를 단독으로 개발하고 스프링에서 DAO를 작성해서 처리하는 방식
      • 기존 DAO에서 SQL 처리를 MyBatis를 이용하는 구조
      • 완전히 MyBatis와 스프링 프레임워크를 독립적인 존재로 바라보고 개발
    • MyBatis와 스프링을 연동하고 Mapper 인터페이스만 이용하는 방식
      • 스프링과 MyBatis 사이에 'mybatis-spring'이라는 라이브러리 이용
      • 스프링이 데이터베이스 전체를 처리
      • MyBatis는 일부 기능 개발에만 활용하는 방식
      • 개발 시에는 Mapper 인터페이스라는 방식을 이용해서 인터페이스만으로 모든 개발이 가능한 방식

 

  • MyBatis를 위한 라이브러리들
    • 스프링 관련: spring-jdbc, spring-tx
    • MyBatis 관련: mybatis, mybatis-spring
dependencies {
    
    ...

    // spring 관련 라이브러리
    implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.3.19'
    implementation group: 'org.springframework', name: 'spring-tx', version: '5.3.19'

    // MyBatis 관련 라이브러리
    implementation group: 'org.mybatis', name: 'mybatis', version: '3.5.6'
    implementation group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.6'
}

 

  • MyBatis를 위한 스프링의 설정 - SqlSessionFactory
    • MyBatis를 이용하기 위해 스프링에서 설정해둔 HikariDataSource를 이용해서 SqlSessionFactory라는 Bean을 설정
    •  
<!-- root-context.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context = "http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

    <context:component-scan base-package = "org.zerock.springex.sample"></context:component-scan>

    ...

    <!-- 이 부분 추가 -->
    <bean id = "sqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean">
        <property name = "dataSource" ref = "dataSource" />
    </bean>
    <!-- 여기까지 -->
    
</beans>

 


실습

 1) Mapper 인터페이스 활용하기

  - MyBatis는 SQL 파일을 별도로 처리할 수 있지만, 인터페이스와 어노테이션만으로도 처리 가능

  - 프로젝트에 mapper라는 패키지 구성 > 현재 시간을 처리하는 TimeMapper 인터페이스 선언

// TimeMapper
package org.zerock.springex.mapper;

import org.apache.ibatis.annotations.Select;

public interface TimeMapper {

  @Select("select now()")
  String getTime();
  
}

  - 데이터베이스의 현재 시각을 문자열로 처리하도록 구성

  - @Select 어노테이션으로 쿼리 작성 가능(';' 사용 x)

  - 작성된 인터페이스를 매퍼 인터페이스라고 하며, root-context.xml에 어떤 매퍼 인터페이스 설정했는지 등록해야함

<!-- root-context.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans ...

       xmlns:mybatis = "http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

   ...

    <mybatis:scan base-package = "org.zerock.springex.mapper"></mybatis:scan>
</beans>

 

  - 테스트 코드를 통해 확인

  - Test > java > org.zerock.springex > mapper 패키지 > TimeMapperTests

// TimeMapperTests
package org.zerock.springex.sample.mapper;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.mapper.TimeMapper;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TimeMapperTests {

  // required가 fales이면 해당 객체를 주입 받지 못하더라도 예외가 발생 x
  @Autowired(required = false)
  private TimeMapper timeMapper;

  @Test
  public void testGetTime() {
    log.info(timeMapper.getTime());
  }
}

정상적으로 현재 시간이 출력됨

  - MyBatis와 스프링을 연동하고 매퍼 인터페이스를 활용하는 방식은 개발자가 실제 동작하는 클래스와 객체를 생성하지 않고, 스프링에서 자동 생성되는 방식 이용

  - 스프링에서 자동 생성되어 개발자가 직접 코드를 수정할 수 없다는 단점이 있지만, 인터페이스만으로 개발을 완료할 수 있다는 장점

 

 2) XML로 SQL 분리하기

  - SQL을 @Select 어노테이션으로 써도 되지만, SQL이 길어지면 처리가 복잡해지고, 어노테이션이 변경되면 프로젝트 전체를 다시 빌드하는 작업이 필요하기 때문에 단순 파일(XML)로 사용하는 것이 편리

  - XML과 매퍼 인터페이스 결합 과정

  • 매퍼 인터페이스 정의하고 메서드 선언
  • 해당 XML 파일 작성(파일이름과 매퍼 인터페이스 이름 같게), <select>와 같은 태그 이용해서 SQL 작성
  • <select>, <insert> 등 태그에 id 속성 값을 매퍼 인터페이스의 메서드 이름과 같에 작성

 

  - main > org.zerock.springex > mapper > TimeMapper2 파일 생성

// TimeMapper2
package org.zerock.springex.mapper;

public interface TimeMapper2 {

  String getNow();

}

  - main > source > mappers 패키지 생성 > TimeMapper2.xml 파일 생성(파일 이름을 매퍼 인터페이스와 같게)

<!-- TimeMapper2.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "org.zerock.springex.mapper.TimeMapper2">

    <!-- select의 id 속성값을 매터 인터페이스의 메서드 이름과 같게 설정 -->
    <!-- select 태그는 반드시 resultType이나 resultMap 속성을 지정해야 함 -->
    <!-- resultType은 select문이 결과를 어떤 타입으로 처리할 지에 대한 설정 -->
    <select id = "getNow" resultType = "string">
        select now()
    </select>

</mapper>

  - 마지막으로 root-context.xml 파일의 MyBatis 설정에 XML 파일들을 인식하도록 설정을 추가

<!-- root-context.xml -->
...

<bean id = "sqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean">
        <property name = "dataSource" ref = "dataSource" />
        <!-- mapperLocations는 XML 매퍼 파일의 위치 -->
        <property name = "mapperLocations" value = "classpath:/mappers/**/*.xml"></property>
    </bean>
    
...

 

  - 테스트 코드로 정상적으로 동작하는지 확인

// TimeMapperTests
package org.zerock.springex.sample.mapper;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.zerock.springex.mapper.TimeMapper2;

@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TimeMapperTests {

  @Autowired(required = false)
  private TimeMapper2 timeMapper2;

  @Test
  public void testNow() {
    log.info(timeMapper2.getNow());
  }
}

정상적으로 현재 시간이 출력됨

● 서포트 벡터 머신(Support Vector Machine)

  • 회귀, 분류, 이상치 탐지 등에 사용되는 지도 학습 방법
  • 서포트 벡터: 클래스 사이의 경계에 위치한 데이터 포인트
  • 각 서포트 벡터가 클래스 사이의 경계를 구분하는데 얼마나 중요한지 학습
  • 각 서포트 벡터 사이의 margin이 가장 큰 방향으로 학습
  • 서포트 벡터까지의 거리와 서포트 벡터의 중요도를 기반으로 예측 수행

 

  • SVM을 사용한 회귀 모델(SVR)
X, y = load_diabetes(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

model = SVR()
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.14990303611569455
평가 데이터 점수: 0.18406447674692128
  • SVM을 사용한 분류 모델(SVC)
X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

model = SVC()
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.9107981220657277
평가 데이터 점수: 0.951048951048951

 

1. 커널 기법

  • 입력 데이터를 고차원 공간에 사상(Mapping)하여 비선형 특징을 학습할 수 있도록 확장
  • scikit-learn에서는 Linear, Polynomial, RBF(Radial Basis Function) 등 다양한 커널 기법 지원

    • 위의 두 개는 Linear Kernel로, 직선으로 분류
    • 아래는 RBF Kernel과 Polynomial Kernel로 비선형적으로 분류
# SVR에 대해 Linear, Polynomial, RBF Kernel 각각 적용
X, y = load_diabetes(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

linear_svr = SVR(kernel = 'linear')
linear_svr.fit(X_train, y_train)

print("Linear SVR 학습 데이터 점수: {}".format(linear_svr.score(X_train, y_train)))
print("Linear SVR 평가 데이터 점수: {}".format(linear_svr.score(X_train, y_train)))

polynomial_svr = SVR(kernel = 'poly')
polynomial_svr.fit(X_train, y_train)

print("Polynomial SVR 학습 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))
print("Polynomial SVR 평가 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))

rbf_svr = SVR(kernel = 'rbf')
rbf_svr.fit(X_train, y_train)

print("RBF SVR 학습 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))
print("RBF SVR 평가 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))

# 출력 결과
Linear SVR 학습 데이터 점수: -0.0029544543016808422
Linear SVR 평가 데이터 점수: -0.0029544543016808422
Polynomial SVR 학습 데이터 점수: 0.26863144203680633
Polynomial SVR 평가 데이터 점수: 0.26863144203680633
RBF SVR 학습 데이터 점수: 0.14990303611569455
RBF SVR 평가 데이터 점수: 0.14990303611569455
# SVC에 대해 Linear, Polynomial, RBF Kernel 각각 적용
X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

linear_svr = SVC(kernel = 'linear')
linear_svr.fit(X_train, y_train)

print("Linear SVC 학습 데이터 점수: {}".format(linear_svr.score(X_train, y_train)))
print("Linear SVC 평가 데이터 점수: {}".format(linear_svr.score(X_train, y_train)))

polynomial_svr = SVC(kernel = 'poly')
polynomial_svr.fit(X_train, y_train)

print("Polynomial SVC 학습 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))
print("Polynomial SVC 평가 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))

rbf_svr = SVC(kernel = 'rbf')
rbf_svr.fit(X_train, y_train)

print("RBF SVC 학습 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))
print("RBF SVC 평가 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))

# 출력 결과
Linear SVC 학습 데이터 점수: 0.9694835680751174
Linear SVC 평가 데이터 점수: 0.9694835680751174
Polynomial SVC 학습 데이터 점수: 0.8990610328638498
Polynomial SVC 평가 데이터 점수: 0.8990610328638498
RBF SVC 학습 데이터 점수: 0.9107981220657277
RBF SVC 평가 데이터 점수: 0.9107981220657277

 

2. 매개변수 튜닝

  • SVM은 사용하는 Kernel에 따라 다양한 매개변수 설정 가능
  • 매개변수를 변경하면서 성능 변화를 관찰
# SVC의 Polynomial Kernel 설정에서 매개변수를 더 변경
X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

polynomial_svr = SVC(kernel = 'poly', degree = 2, C = 0.1, gamma = 'auto')
polynomial_svr.fit(X_train, y_train)

print("Polynomial SVC 학습 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))
print("Polynomial SVC 평가 데이터 점수: {}".format(polynomial_svr.score(X_train, y_train)))

# 출력 결과
Polynomial SVC 학습 데이터 점수: 0.9765258215962441
Polynomial SVC 평가 데이터 점수: 0.9765258215962441


# SVC의 RBF Kernel 설정에서 매개변수를 더 변경
rbf_svr = SVC(kernel = 'rbf', C = 2.0, gamma = 'scale')
rbf_svr.fit(X_train, y_train)

print("RBF SVC 학습 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))
print("RBF SVC 평가 데이터 점수: {}".format(rbf_svr.score(X_train, y_train)))

# 출력 결과
RBF SVC 학습 데이터 점수: 0.9131455399061033
RBF SVC 평가 데이터 점수: 0.9131455399061033
  • 위에서 기본 매개변수로만 돌렸을 때 0.89정도로 나왔던 점수보다 더 높은 점수로, 매개변수를 조작함에 따라 성능이 더 높아짐을 확인

 

 

3. 데이터 전처리

  • SVM은 입력 데이터가 정규화 되어야 좋은 성능을 보임
  • 주로 모든 특성 값의 범위를 [0, 1]로 맞추는 방법 사용
  • scikit-learn의 StandardScaler 또는 MinMaxScaler 사용해 정규화
X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)

# 스케일링 이전 데이터
model = SVC()
model.fit(X_train, y_train)

print("SVC 학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("SVC 평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
SVC 학습 데이터 점수: 0.9107981220657277
SVC 평가 데이터 점수: 0.951048951048951


# 스케일링 이후 데이터
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

model.fit(X_train, y_train)

print("SVC 학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("SVC 평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
SVC 학습 데이터 점수: 0.9882629107981221
SVC 평가 데이터 점수: 0.972027972027972

 

 

4. 당뇨병 데이터로 SVR(커널은 기본값('linear')로) 실습

  - 가장 기본 모델

# 데이터 불러오기
X, y = load_diabetes(return_X_y = True)

# 학습 / 평가 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

# 범위 [0, 1]로 정규화
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# 선형 SVR 모델에 피팅
model = SVR(kernel = 'linear')
model.fit(X_train, y_train)

# 점수 출력
print("SVR 학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("SVR 평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
SVR 학습 데이터 점수: 0.5114667038352527
SVR 평가 데이터 점수: 0.45041670810045853

 

  - 차원 변환

X_comp = TSNE(n_components = 1).fit_transform(X)
plt.scatter(X_comp, y)

저차원 변환된 당뇨병 데이터의 특성변수와 목적변수

model.fit(X_comp, y)

# 선형으로 예측한 값
predict = model.predict(X_comp)

plt.scatter(X_comp, y)
plt.scatter(X_comp, predict, color = 'r')

 

  - 파이프라인으로 하나로 묶기

estimator = make_pipeline(StandardScaler(), SVR(kernel = 'linear'))

cross_validate(
    estimator = estimator,
    X = X, y = y,
    cv = 5,
    n_jobs = multiprocessing.cpu_count(),
    verbose = True
)

# 출력 결과
[Parallel(n_jobs=8)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=8)]: Done   2 out of   5 | elapsed:    0.0s remaining:    0.0s
[Parallel(n_jobs=8)]: Done   5 out of   5 | elapsed:    0.5s finished
{'fit_time': array([0.00697613, 0.00797367, 0.00599027, 0.00797415, 0.00897002]),
 'score_time': array([0.00099683, 0.00099564, 0.00099587, 0.00099683, 0.00099707]),
 'test_score': array([0.43039087, 0.51655019, 0.48275821, 0.4224617 , 0.53077081])}

 

  - GridSearch까지 포함

pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVR(kernel = 'linear'))])

param_grid = [{'model__gamma': ['scale', 'auto'],
               'model__C': [1.0, 0.1, 0.01],
               'model__epsilon': [1.0, 0.1, 0.01]}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)

gs.best_estimator_

# epsilon이 1.0일 때 가장 좋은 예측

 

5. 당뇨병 데이터로 SVR(커널 변경) 실습

# 하이퍼 파라미터에 kernel을 넣어 어떤 kernel이 가장 좋은 지 판단
pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVR(kernel = 'rbf'))])

param_grid = [{'model__kernel': ['rbf', 'poly', 'sigmoid']}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)
gs.best_estimator_
# sigmoid가 가장 좋은 예측을 하는 것으로 나옴

# 커널은 sigmoid로 하고 나머지 하이퍼 파라미터 중 어떤 게 좋은지 GridSearch 실행
pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVR(kernel = 'sigmoid'))])

param_grid = [{'model__gamma': ['scale', 'auto'],
               'model__C': [1.0, 0.1, 0.01],
               'model__epsilon': [1.0, 0.1, 0.01]}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)
gs.best_estimator_

# epsilon이 1.0, gamma는 auto일 때 가장 좋음

  - 최종 sigmoid를 사용한 점수

model = gs.best_estimator_
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.3649291208855052
평가 데이터 점수: 0.39002165443861103

 

 

6. 유방암 데이터로 SVC(커널은 기본값('linear')로) 실습

X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

model = SVC(kernel = 'linear')
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.9846153846153847
평가 데이터 점수: 1.0

  - 이미 잘 나오긴 함

  - 시각화

def make_meshgrid(x, y, h = 0.02):
    x_min, x_max = x.min()-1, x.max()+1
    y_min, y_max = y.min()-1, y.max()+1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))

    return xx, yy

def plot_contour(clf, xx, yy, **params):
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    out = plt.contourf(xx, yy, Z, **params)
    return out

X_comp = TSNE(n_components = 2).fit_transform(X)
X0, X1 = X_comp[:, 0], X_comp[:, 1]
xx, yy = make_meshgrid(X0, X1)

model.fit(X_comp, y)

plot_contour(model, xx, yy, cmap = plt.cm.coolwarm, alpha = 0.7)
plt.scatter(X0, X1, c = y, cmap = plt.cm.coolwarm, s = 20, edgecolors = 'k')

  - 거의 대부분 같은 색으로 맞춘 모습

# 교차 검증
estimator = make_pipeline(StandardScaler(), SVC(kernel = 'linear'))

cross_validate(
    estimator = estimator,
    X = X, y = y,
    cv = 5,
    n_jobs = multiprocessing.cpu_count(),
    verbose = True
)

# 출력 결과
[Parallel(n_jobs=8)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=8)]: Done   2 out of   5 | elapsed:    1.5s remaining:    2.3s
[Parallel(n_jobs=8)]: Done   5 out of   5 | elapsed:    1.5s finished
{'fit_time': array([0.00498796, 0.00498223, 0.00598669, 0.00300407, 0.00298858]),
 'score_time': array([0.00199056, 0.00099754, 0.00099754, 0.00091267, 0.00099921]),
 'test_score': array([0.96491228, 0.98245614, 0.96491228, 0.96491228, 0.98230088])}
# GridSearch
pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVC(kernel = 'linear'))])

param_grid = [{'model__gamma': ['scale', 'auto'],
               'model__C': [1.0, 0.1, 0.01]}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)
gs.best_estimator_

# C가 0.1일 때 최적

  - 해당 예측기로 최종 점수 출력

model = gs.best_estimator_
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.978021978021978
평가 데이터 점수: 1.0

 

 

7. 유방암 데이터로 SVC(커널 변경) 실습

X, y = load_breast_cancer(return_X_y = True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)

scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

model = SVC(kernel = 'rbf')
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.989010989010989
평가 데이터 점수: 0.9385964912280702
def make_meshgrid(x, y, h = 0.02):
    x_min, x_max = x.min()-1, x.max()+1
    y_min, y_max = y.min()-1, y.max()+1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))

    return xx, yy

def plot_contour(clf, xx, yy, **params):
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    out = plt.contourf(xx, yy, Z, **params)
    return out

X_comp = TSNE(n_components = 2).fit_transform(X)
X0, X1 = X_comp[:, 0], X_comp[:, 1]
xx, yy = make_meshgrid(X0, X1)

model.fit(X_comp, y)

plot_contour(model, xx, yy, cmap = plt.cm.coolwarm, alpha = 0.7)
plt.scatter(X0, X1, c = y, cmap = plt.cm.coolwarm, s = 20, edgecolors = 'k')

  - 커널이 linear일 때처럼 선형으로 나눠지지 않고 곡선으로 나눠짐

# 교차 검증
estimator = make_pipeline(StandardScaler(), SVC(kernel = 'rbf'))

cross_validate(
    estimator = estimator,
    X = X, y = y,
    cv = 5,
    n_jobs = multiprocessing.cpu_count(),
    verbose = True
)

# 출력 결과
[Parallel(n_jobs=8)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=8)]: Done   2 out of   5 | elapsed:    1.6s remaining:    2.5s
[Parallel(n_jobs=8)]: Done   5 out of   5 | elapsed:    1.6s finished
{'fit_time': array([0.0039866 , 0.00697613, 0.00698733, 0.0039866 , 0.00399065]),
 'score_time': array([0.00299096, 0.00299168, 0.00397682, 0.00300074, 0.0019927 ]),
 'test_score': array([0.97368421, 0.95614035, 1.        , 0.96491228, 0.97345133])}
# GridSearch
pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVC())])

param_grid = [{'model__kernel': ['rbf', 'poly', 'sigmoid']}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)
gs.best_params_

# 출력 결과
{'model__kernel': 'rbf'}
# kernel이 rbf일 때 가장 좋은 성능
# kernel은 rbf로 하고 나머지 하이퍼 파라미터 조정
pipe = Pipeline([('scaler', StandardScaler()),
                 ('model', SVR(kernel = 'rbf'))])

param_grid = [{'model__gamma': ['scale', 'auto'],
               'model__C': [1.0, 0.1, 0.01],
               'model__epsilon': [1.0, 0.1, 0.01]}]

gs = GridSearchCV(
    estimator = pipe,
    param_grid = param_grid,
    n_jobs = multiprocessing.cpu_count(),
    cv = 5,
    verbose = True
)
gs.fit(X, y)
gs.best_estimator_

  - 최적 하이퍼 파라미터를 가진 예측기로 최종 점수 출력

model = gs.best_estimator_
model.fit(X_train, y_train)

print("학습 데이터 점수: {}".format(model.score(X_train, y_train)))
print("평가 데이터 점수: {}".format(model.score(X_test, y_test)))

# 출력 결과
학습 데이터 점수: 0.9372487007252107
평가 데이터 점수: 0.8063011852114969

1. 스프링

 1) 의존성 주입

  • 어떻게 하면 객체와 객체 간의 관계를 더 유연하게 유지할 것인가(객체의 생성과 관계를 효과적으로 분리할 수 있는가)
  • 예를 들어, Todo 웹 애플리케이션을 만들 때, 모든 Controller는 TodoService 혹은 MemberService같은 서비스 객체 이용 → Controller는 서비스 객체에 의존적이다
  • 즉, 의존성이란 하나의 객체가 자신이 해야하는 일을 하기 위해 다른 개체의 도움이 필수적인 관계
  • 의존성을 해결하기 위해 스프링 프레임워크 사용

 2) 스프링 라이브러리 추가

  • maven spring 검색하여 Spring Core 라이브러리를 찾아, Gradle 메뉴의 코드 복사

 

  • 프로젝트의 build gradle 파일에 해당 코드 복사, spring-core에 더해서 spring-context, spring-test도 추가
  • lombok 라이브러리, Log4j2 라이브러리, JSTL 라이브러리 추가
dependencies {
    compileOnly('javax.servlet:javax.servlet-api:4.0.1')

    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")

    // spring 관련 라이브러리
    implementation group: 'org.springframework', name: 'spring-core', version: '5.3.20'
    implementation group: 'org.springframework', name: 'spring-context', version: '5.3.20'
    implementation group: 'org.springframework', name: 'spring-test', version: '5.3.20'

    // lombok 라이브러리
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'
    testCompileOnly 'org.projectlombok:lombok:1.18.24'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'

    // Log4j2 라이브러리
    implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2'
    implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2'
    implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.2'

    // JSTL 라이브러리
    implementation group: 'jstl', name: 'jstl', version: '1.2'
}

 

  • main > resource 폴더 > log4j2.xml 추가
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status = "INFO">
    <Appenders>
        <!-- 콘솔 -->
        <Console name = "Console" target = "SYSTEM_OUT">
            <PatternLayout charset = "UTF-8" pattern = "%d{HH:mm:ss.SSS} %5p [%c] %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <logger name = "org.springframework" level = "INFO" additivity = 'false'>
            <appender-ref ref = "console" />
        </logger>
        <logger name = "org.zerock" level = "INFO" additivity = "false">
            <appender-ref ref = "console" />
        </logger>
        <root level = "INFO" additivity = "false">
            <AppenderRef ref = "console" />
        </root>
    </Loggers>
</Configuration>

 


실습

 1) 의존성 주입하기

  - 프로젝트에 sample 패키지 > SampleService와 SampleDAO 클래스 생성

  - 스프링 프레임워크는 자체적으로 객체를 생성하고 관리하면서 필요한 곳에 객체를 주입시키는 역할(설정파일이나 어노테이션 이용)

  - 스프링이 관리하는 객체들은 빈(Bean)이라는 이름으로 불림, 프로젝트 내에 어떤 빈들을 어떻게 관리할 것인지 설정하는 설정 파일 작성

  - XML 설정이나 별도의 클래스를 이용한 자바 설정이 가능

  - XML 설정을 위해 WEB-INF 폴더 > New > XML Configuration File > Spring config > root-context.xml 파일 생성

  - Configuration application context 설정에서 인텔리제이가 현재 프로젝트를 스프링 프레임워크로 인식하고 필요한 기능을 지원하도록 설정

  - root-context.xml 내부에 <bean>이라는 태그를 이용해서 SampleService와 SampleDAO를 다음과 같이 설정

 

  - 스프링의 빈 설정 테스트

  - test 폴더 > org.zerock.springex.sample 패키지 생성 > SampleTest 클래스 추가

package org.zerock.springex.sample;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@Log4j2
// ExtendWith은 JUnit5 버전에서 spring-test를 이용하기 위한 설정
// ContextConfiguration은 스프링의 설정 정보를 로딩하기 위해 사용
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTest {

  // SampleService를 멤버 변수로 선언
  // Autowired는 스프링에서 의존성 주입 관련 어노테이션(만약 해당 타입의 빈이 존재하면 여기에 주입하기를 원한다)
  @Autowired
  private SampleService sampleService;

  @Test
  public void testService1() {
    log.info(sampleService);
    Assertions.assertNotNull(sampleService);
  }
}

테스트 성공 시 메세지(스프링이 생성하고 관리하고 있는 객체를 확인할 수 있음)


 

2. ApplicationContext와 빈(Bean)

  • Servlet이 Servlet Context 안에 존재한 것처럼, 스프링은 Bean을 관리하기 위해 ApplicationContext라는 존재 활용
  • root-context.xml에서 SampleService와 SampleDAO를 <bean>으로 설정하여 다음과 같이 저장됨

 

 1) @Autowired의 의미와 필드 주입

  • Test 코드에서 SampleService 타비의 변수가 선언될 때, @Autowired로 처리됨
  • Test 실행 시 @Autowired가 처리된 부분에 맞는 타입의 Bean이 존재하는지 확인하고 Test 실행시 주입
  • 멤버 변수에 직접 @Autowired를 선언하는 방식을 '필드 주입'이라고 함

 


실습

 2) SampleDAO 주입하기

  - @Autowired를 이용하면 필요한 타입을 주입받을 수 있다는 사실을 이용, SampleService에 SampleDAO를 주입

// SampleService
package org.zerock.springex.sample;

import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;

@ToString
public class SampleService {
  @Autowired
  private SampleDAO sampleDAO;
}

  - Test 실행 시 SampleService안에 sampleDAO 객체가 주입된 것을 확인

SampleDAO의 의존성이 주입된 SampleService


 

 2) <context:component-scan>

  • 스프링 이용시, 클래스를 작성하거나 객체를 직접 생성하는 역할은 스프링 내부에서 이루어지며 applicationContext가 생성된 객체들을 관리하게 됨

 


실습

 2) @Service, @Repository

  - 스프링 프레임워크 사용을 위한 어노테이션

  • @Controller: MVC의 Controller를 위한 어노테이션
  • @Service: 서비스 계층의 객체를 위한 어노테이션
  • @Repository: DAO와 같은 객체를 위한 어노테이션
  • @Component: 일반 객체나 유틸리티 객체를 위한 어노테이션

  - 웹 영역 뿐 아닌 애플리케이션 전체에 사용할 수 있는 객체들을 전부 포함

  - 어노테이션을 이용하면 스프링 설정은 '해당 패키지를 조사해서 클래스의 어노테이션들을 이용'하는 설정으로 변경됨

  - root-context.xml의 설정을 다음과 같이 변경

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context = "http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
    <!-- component-scan의 속성값으로 패키지를 지정 -->
    <!-- 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식함 -->
    <context:component-scan base-package = "org.zerock.springex.sample"/>
</beans>

 

  - SampleDAO는 해당 클래스의 객체가 스프링의 Bean으로 관리될 수 있도록 @Repository 어노테이션 추가

// SampleDAO
package org.zerock.springex.sample;

import org.springframework.stereotype.Repository;

@Repository
public class SampleDAO {
}

 

  - SampleService는 @Service 어노테이션 추가

// SampleService
package org.zerock.springex.sample;

import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@ToString
public class SampleService {
  @Autowired
  private SampleDAO sampleDAO;
}

테스트 코드 정상 작동


 

 3) 생성자 주입 방식

  • 초기 스프링에서는 @Autowired를 멤버 변수에 할당하거나 Setter를 작성하는 방식을 많이 이용
  • 스프링3 이후, 생성자 주입 방식 이용
  • 생성자 주입 방식 규칙
    • 주입 받아야 하는 객체의 변수는 final로 작성
    • 생성자를 이용해서 해당 변수를 생성자의 파라미터로 지정
  • 생성자 주입 방식은 객체를 생성 시 문제가 발생하는지 미리 확인할 수 있어, 필드 주입이나 Setter 주입 방식보다 선호
  • Lombok에서 @RequiredArgsConstructor를 이용해 필요한 생성자 함수를 자동으로 작성 가능
  • SampleService를 다음과 같이 변경하면 @Autowired로 필드 주입한 것과 같이 SampleDAO를 주입 가능
// SampleService
package org.zerock.springex.sample;

import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@ToString
@RequiredArgsConstructor
public class SampleService {

  private final SampleDAO sampleDAO;
}

 

 

3. 인터페이스를 이용한 느슨한 결합

  • 스프링이 의존성 주입을 가능하게 하지만 더 근본적으로 유연한 프로그램 설계를 위해 인터페이스를 이용
  • 인터페이스 이용 시, 나중에 다른 클래스의 객체로 쉽게 변경할 수 있도록 함
  • 앞에서 SampleDAO를 다른 객체로 변경하면 SampleService의 코드도 수정해야했지만, 인터페이스를 이용하면 실제 객체를 모르고 타입만을 이용해서 코드를 작성하는 일이 가능

 

 


실습

 4) SampleDAO를 인터페이스로 변경하기

  - 클래스로 작성된 SampleDAO를 인터페이스 타입으로 수정

// SampleDAO
package org.zerock.springex.sample;

import org.springframework.stereotype.Repository;

@Repository
public interface SampleDAO {
}

  - SampleDAO 인터페이스는 실체가 없기 때문에 SampleDAO 인터페이스를 구현한 클래스를 SampleDAOImpl이라는 이름으로 선언

// SampleDAOImpl
package org.zerock.springex.sample;

import org.springframework.stereotype.Repository;

// @Repository를 이용해서 해당 클래스의 객체를 스프링의 Bean으로 처리하도록 구성
@Repository
public class SampleDAOImpl implements SampleDAO{
}

 

  - SampleService 입장에서는 인터페이스만 바라보고 있기 때문에 실제 객체가 SampleDAOImpl의 인스턴스인지 알 수 없지만, 코드 작성에 문제 x

  - 느슨한 결합: 객체와 객체의 의존 관계의 실제 객체를 몰라도 가능하게 하는 방식

  - 느슨한 결합을 이용하면 나중에 SampleDAO 타입의 객체를 다른 객체로 변경해도 SampleService 타입을 이용하는 코드를 수정할 일이 없어 더 유연한 구조임

 

  - 다른 SampleDAO 객체로 변경해보기(특정 기간에만 SampleDAO를 다른 객체로 변경해야 하는 경우 생각)

  - EventSampleDAOImpl 클래스 작성

// EventSampleDAOImpl
package org.zerock.springex.sample;

import org.springframework.stereotype.Repository;

@Repository
public class EventSampleDAOImpl implements SampleDAO{
}

  - SampleService에 필요한 SampleDAO 타입의 Bean이 두 개(SampleDAOImpl, EventSampleDAOImpl)이므로, 어떤걸 주입해야 할 지 모르게 됨

  - Test 실행 시, 스프링이 SampleDAO 타입의 객체가 하나이길 기대했지만 2개가 발견됐다는 오류 출력

  - 해결 방법으로 두 클래스 중 하나를 @Primary라는 어노테이션으로 지정(지금 사용하고 싶은 것에 지정)

// EventSampleDAO
package org.zerock.springex.sample;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

@Repository
@Primary
public class EventSampleDAOImpl implements SampleDAO{
}

  - Test 실행 시, 정상적으로 EventSampleDAOImpl이 주입된 것을 확인

 

  - Qualifier 이용하기

  - @Primary 이용하는 방법 이외에 @Qualifier를 이용하여 이름을 지정해서 특정한 이름의 객체 주입

  - Lombok과 @Qualifier를 같이 이용하기 위해 src/main/java 폴더에 lombok.config 파일 생성

// lombok.config
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
// SampleDAOImpl
@Repository
// @Qualifier를 이용해서 SampleDAOImpl에 'normal'이라는 이름 지정
@Qualifier("normal")
public class SampleDAOImpl implements SampleDAO{
}


// EventSampleDAOImpl
@Repository
// @Qualifier를 이용해서 EventSampleDAOImpl에 'event'라는 이름 지정
@Qualifier("event")
public class EventSampleDAOImpl implements SampleDAO{
}

  - SampleService에서 @Qualifier를 이용해 이름을 지정하면 해당 이름의 SampleDAO 객체를 사용

// SampleService
package org.zerock.springex.sample;

import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
@ToString
@RequiredArgsConstructor
public class SampleService {

  @Qualifier("normal")
  private final SampleDAO sampleDAO;
}

SampleService에서 @Qualifier의 이름을 normal로 지정했을 때 SampleDAOImpl을 사용

// SampleService
package org.zerock.springex.sample;

import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
@ToString
@RequiredArgsConstructor
public class SampleService {

  @Qualifier("event")
  private final SampleDAO sampleDAO;
}

 

SampleService에서 @Qualifier의 이름을 event로 지정했을 때 EventSampleDAOImpl을 사용

 

  - 스프링의 Bean으로 지정되는 객체들

  - 스프링의 모든 클래스의 객체가 Bean으로 처리되는 것은 x

  - 스프링의 Bean으로 등록되는 객체들은 '핵심 배역'을 하는 객체(오랜 시간 프로그램에 상주하며 중요한 역할을 하는 '역할 중심' 객체

  - DTO나 VO 등 '역할'보다 '데이터' 중심으로 설계된 객체들은 스프링의 Bean으로 등록되지 않음(특히 DTO는 생명주기가 짧고 데이터 보관이 주된 역할이어서 Bean으로 처리 x)

 

  - XML이나 어노테이션으로 처리하는 객체

  - Bean으로 처리할 때 XML 설정을 이용할 수도 있고 어노테이션을 처리할 수도 있음

  - 판단 기준은 '코드를 수정할 수 있는가'

  - jar 파일로 추가되는 클래스의 객체를 Bean으로 처리해야 하면, 해당 코드가 존재하지 ㅇ낳아 어노테이션을 추가할 수 없어, XML에서 <bean>을 사용해 처리

  - 직접 작성되는 클래스는 어노테이션을 이용


 

4. 웹 프로젝트를 위한 스프링 준비

  • Bean을 담은 ApplicationContext가 웹 애플리케이션에서 동작하려면, 웹 애플리케이션이 실행될 때 스프링을 로딩해서 해당 웹 애플리케이션 내부에 스프링의 ApplicationContext를 생성하는 작업 필요
  • web.xml을 이용해서 리스너를 설정
  • web.xml 설정 이전에, 스프링 프레임워크의 웹 관련 작업은 spring-webmvc 라이브러리를 추가해야 설정 가능
// build.gradle 파일에 spring-webmvc 라이브러리 추가
dependencies {

    ...

    implementation group: 'org.springframework', name: 'spring-core', version: '5.3.20'
    implementation group: 'org.springframework', name: 'spring-context', version: '5.3.20'
    implementation group: 'org.springframework', name: 'spring-test', version: '5.3.20'
    implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.20'

    ...


}
  • web.xml에 <listener> 설정과 이에 필요한 <context-param> 추가
<!-- web.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>
  • 위의 설정을 한 뒤에 톰캣 실행 시, 스프링 관련 로그가 기록되며 실행

 


실습

 5) DataSource 구성

  - 톰캣과 스프링이 연동되는 구조를 완성하면, 웹 애플리케이션에서 필수인 데이터베이스 관련 설정 추가 필요

  - build.gradle에 MariaDB 드라이버와 HikariCP 관련 라이브러리 추가

// build.gradle
dependencies {

    ...

    implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'
    implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.0.1'
}

 

  - root-context.xml에 HikariCP 설정하기

  - 스프링에서는 HikariCP 사용을 위해 HikariConfig 객체와 HikariDataSource 초기화 설정을 Bean으로 처리(이전에는 ConnectUtil 클래스 사용)

<!-- root-context.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context = "http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- component-scan의 속성값으로 패키지를 지정 -->
    <!-- 해당 패키지를 스캔해서 스프링의 어노테이션들을 인식함 -->
    <context:component-scan base-package = "org.zerock.springex.sample"></context:component-scan>

    <!-- db와 connection pool 형성을 위한 hikariCP를 구성 -->
    <!-- hikariConfig를 이용해서 HikariDataSource를 구성 -->
    <bean id = "hikariConfig" class = "com.zaxxer.hikari.HikariConfig">
        <property name = "driverClassName" value = "org.mariadb.jdbc.Driver"></property>
        <property name = "jdbcUrl" value = "jdbc:mariadb://localhost:3306/webdb"></property>
        <property name = "username" value = "webuser"></property>
        <property name = "password" value = "비밀번호"></property>
        <property name = "dataSourceProperties">
            <props>
                <!-- cache 사용 여부에 대한 설정 -->
                <prop key = "cachePrepStmts">true</prop>
                <!-- 서버 연결 당 cache할 statement의 수에 관한 설정(기본값 25, 권장 250~500 -->
                <prop key = "prepStmtCacheSize">250</prop>
                <!-- 드라이버가 cache할 SQL문의 최대 길이(기본값 256, 권장 2048 -->
                <prop key = "prepStmtCacheSqlLimit">2048</prop>
            </props>
        </property>
    </bean>

    <!-- HikariDataSource는 <constructor-arg ref='hikariConfig" />로 id값을 참조해서 사용 -->
    <bean id = "dataSource" class = "com.zaxxer.hikari.HikariDataSource" destroy-method = "close">
        <constructor-arg ref = "hikariConfig" />
    </bean>
</beans>

 

  - SampleTest에는 root-context.xml에 선언된 HikariCPp를 주입받기 위해 DataSource 타입의 변수를 선언, @Autowired를 통해 주입 받음

  - testConnection()을 작성하여 데이터베이스와의 연결을 test

// SampleTest
package org.zerock.springex.sample;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.sql.DataSource;
import java.sql.Connection;

@Log4j2
// ExtendWith은 JUnit5 버전에서 spring-test를 이용하기 위한 설정
// ContextConfiguration은 스프링의 설정 정보를 로딩하기 위해 사용
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class SampleTest {

  ...

  @Autowired
  private DataSource dataSource;

  ...

  @Test
  public void testConnection() throws Exception {
    Connection connection = dataSource.getConnection();
    log.info(connection);
    Assertions.assertNotNull(connection);

    connection.close();
  }
}

testConnection의 결과, 데이터베이스와 정상적으로 연결되었다고 출력된 로그

  - 테스트에서 보이듯, 스프링은 필요한 객체를 스프링에 주입해 주기 때문에 개별적으로 클래스를 작성하여 Bean으로 등록해두면 원하는 곳에서 쉽게 다른 객체 사용 가능

1. 리스너의 개념과 용도

  • 옵저버 패턴: 특정한 변화를 구독(subscribe)하는 객체들을 보관하고 있다가 변화가 발생(public)하면 구독 객체들을 실행
  • Servlet API는 여러 이벤트(Event)에 맞는 리스너들을 인터페이스로 정의해두었고 이를 통해 다음 작업 가능
    • 해당 웹 애플리케이션이 시작되거나 종료될 때 특정 작업 수행
    • HttpSession에 특정 작업에 대한 감시와 처리
    • HttpServletRequest에 특정 작업에 대한 감시와 처리

 


실습

 1) ServletContextListener

  - 해당 프로젝트가 실행되자 마자 실행시키고 싶은 작업

  - 프로젝트 내에 listener 패키지 생성 > 패키지 내에 W2AppListener 클래스 생성

// W2AppListener
package org.zerock.w2.listener;

import lombok.extern.log4j.Log4j2;

import javax.servlet.ServletContextEvent
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
@Log4j2
public class W2AppListener implements ServletContextListener {
  
  // 시작할 때
  @Override
  public void contextInitialized(ServletContextEvent sce) {
    log.info("-----------------init-----------------");
    log.info("-----------------init-----------------");
    log.info("-----------------init-----------------");
  }
  
  // 끝낼 때
  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    log.info("----------------destroy----------------");
    log.info("----------------destroy----------------");
    log.info("----------------destroy----------------");
  }
}

시작할 때 메세지 출력
끝낼 때 메세지 출력


 

 

  • ServletContextEvent와 ServletContext
    • contextInitialized()와 contextDestroyed()에는 파라미터로 ServletContextEvent라는 객체가 전달됨
    • ServletContextEvent를 이용하면 현재 애플리케이션이 실행되는 공간인 ServletContext에 접근 가능
    • ServletContext는 현재 웹 애플레케이션 내 모든 자원들을 같이 사용하는 공간
    • 이 공간에 무언가를 저장하면 모든 Controller나 JSP 등에서 이를 활용 가능
    • ServletContext에는 setAttribute()를 이용해서 원하는 이름으로 객체를 보관할 수 있음
    • 다음과 같이 'appName'이라는 이름으로 'W2'라는 이름을 지정하면, EL에서 ${appName}으로 이를 이용 가능

// W2AppListener에서 setAttribute()를 통해 객체를 생성
package org.zerock.w2.listener;

import lombok.extern.log4j.Log4j2;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
@Log4j2
public class W2AppListener implements ServletContextListener {

  @Override
  public void contextInitialized(ServletContextEvent sce) {
    log.info("-----------------init-----------------");
    log.info("-----------------init-----------------");
    log.info("-----------------init-----------------");

    // 시작과 함께 객체 생성
    ServletContext servletContext = sce.getServletContext();
    servletContext.setAttribute("appName", "W2");
  }

  ...
  
}
// TodoListController에서 다음과 같이 객체 활용 가능
package org.zerock.w2.controller;

...

@WebServlet(name = "todoListController", value = "/todo/list")
@Log4j2
public class TodoListController extends HttpServlet {
  private TodoService todoService = TodoService.INSTANCE;

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    log.info("todo list.................");

    // getServletContext() 메서드를 이용해 ServletContext를 이용할 수 있음
    ServletContext servletContext = req.getServletContext();
    log.info("appName: " + servletContext.getAttribute("appName"));
    
    ...
    
  }
}
<!-- list.jsp에 다음과 같이 EL 구문을 추가하면 화면에서 나타내는 용도로 사용가능 -->
<body>
<h1>Todo List</h1>
<!-- appName이라는 이름을 가진 객체(W2)를 호출 -->
<h2>${appName}</h2>
<h2>${loginInfo}</h2>
<h3>${loginInfo.mname}</h3>

...

</body>

${appName}을 입력하여, appName을 이름으로 하는 객체 "W2"를 출력

 

  • ServletContextListener와 스프링 프레임워크
    • ServletContextListener와 ServletContext를 이용하면 프로젝트 실행 시 필요한 객체들을 준비하는 작업 처리 가능
    • 커넥션 풀 초기화 또는 TodoService 같은 객체 미리 생성해서 보관 등
    • 특히, 스프링 프레임워크에서 웹 프로젝트를 미리 로딩하는 작업을 처리할 때 ServletContextListener 이용

 


실습

 2) 세션 관리 리스너

  - Servlet의 리스너 중 HttpSession 관련 작업을 감시하는 리스너 등록 가능

              (HttpSessionListener나 HttpSessionAttributeListener 등)

  - 이를 이용해서 HttpSession이 생성되거나 setAttribute() 등의 작업이 이루어질 때를 감지 가능

  - listener 패키지에 LoginListener 클래스 추가

// LoginListener
package org.zerock.w2.listener;

import lombok.extern.log4j.Log4j2;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

@WebListener
@Log4j2
public class LoginListener implements HttpSessionAttributeListener {

  @Override
  public void attributeAdded(HttpSessionBindingEvent event) {
    // 이름과 객체의 값들을 받아옴
    String name = event.getName();
    Object obj = event.getValue();

    if(name.equals("loginIndo")) {
      log.info("A user logined.........");
      log.info(obj);
    }
  }
}

로그인 실행 시 로그에 위의 메세지가 출력됨

  - LoginListener는 HttpSessionAttributeListener 인터페이스를 구현

  - HttpSessionAttributeListener 인터페이스는 attributeAdded(), attributeRemoved(), attributeReplaced() 를 이용해서, HttpSession에 setAttribute() / removeAttribute() 등의 작업을 감지

+ Recent posts