useState에 대한 정리가 아닌 어떻게 사용했었는지에 대해 정리.

 

프로젝트를 진행하면서 useState를 통해 한개의 데이터만을 관리하기도 했지만 여러개의 데이터가 들어가있는 객체 또는 배열을 관리해야 했다.

예를들어 페이징 기능을 수행하기 위해 pageNum을 관리한다고 하면 간단하게 setPageNum(2) 이런 식으로 처리하고 관리하기 쉬웠으나 객체 또는 배열을 관리하는건 다르게 처리해야 했기에 정리.

 

//pageNum. 단일 데이터 초기값을 1로 설정
const [pageNum, setPageNum] = useState(1);

//게시글 input 값 관리
//title, content를 담고 있는 객체 구조. 초기값은 둘다 '' 로 설정
const [values, setValues] = useState({
    title: '',
    content: '',
});

//게시글 리스트 값 관리
//페이지 접근 시 리스트를 담아 처리.
//기본값으로 [] 빈 배열로 설정.
const [boardData, setBoardData] = usetState([]);


//단일 데이터 set
setPageNum(2);

//객체 데이터 set
setValues({
    title: 'setTitle',
    content: 'setContent',
});

//객체 데이터 중 하나의 데이터만 set
setValues({
    ...values,
    title: 'setTitle2',
});

//위 하나의 객체 데이터를 수정하는 set 활용
//onChange가 발생할 때 state를 set
setValues({
    ...values,
    [e.target.name]: e.target.value,
});

//리스트 데이터 set
setBoardData(response.data.content);

//배열 데이터 set
const boardArray = [];
boardArray.push({data: data1, ...});
boardArray.push({data: data2, ...});
//...
setBoardData(boardArray);

//boardData에 객체를 추가하는 경우
const boardArray = [...boardData];
boardArray.push({data: data10, ...});
//...
setBoardData(boardArray);

 

프로젝트를 진행하면서 useState는 이렇게 사용했다.

가장 간단한 단일 데이터는 React를 학습하면서 배운 useState 사용법 그대로 사용.

객체까지도 학습한 내용만으로 해결이 가능했다.

 

객체의 경우 단순하게 모든 데이터를 직접 작성해 set을 해주는 방법이 있고, 그 객체 안에서 하나의 값만 수정하고자 한다면 다르게 처리해야 했다.

위 코드처럼 title, content 중 title 하나만 값을 변경하고자 한다면 content의 값은 유지되어야 한다.

이때 사용할 수 있는 방법이 전개 연산자(Spread Operator)를 사용하는 것이다.

전개연산자는 ES6 문법으로 배열 또는 객체를 넘기는 용도로 사용된다.

...values의 의미는 기존에 values에 담겨있는 값을 그대로 담아준다고 볼 수 있다.

그럼 위 코드에서 ...values를 통해 title: 'setTitle1', content: 'setContent1'이 담기게 될 것이고 그 이후 title을 setTitle2로 바꾸게 되면서 set 되는 데이터는 title: 'setTitle2', content: 'setContent2' 가 된다.

 

이걸 input에서 onChange 핸들링으로 처리하도록 하는 것으로 입력이 끝난 후 submit이 발생했을 때 values값을 확인하는 것으로 input 들의 값을 알아낼 수 있다.

...values로 다른 input 데이터는 다시 담아 유지할 수 있도록 하고 e.target.name 과 e.target.value로 input에 작성해둔 name값, 작성한 value 값을 가져와 set 해주게 된다.

그럼 입력창의 value를 좀 더 수월하게 관리할 수 있게 된다.

 

그리고 마지막 배열.

axios 요청에 대한 응답으로 리스트 데이터를 받는 경우 간단하게 res.data를 set 해주는 것으로 배열형태로 담을 수 있다.

문제는 배열을 직접 제어해야 하는 경우다.

 

배열을 직접 제어하는 것이 가장 처음 렌더링 될때만 수행하게 되는 경우도 있지만 매번 직접 제어해야 하는 경우도 있다.

예를들어 이미지 파일 업로드에서 업로드할 파일과 중간에 사용자가 삭제 버튼을 눌러 사진을 제거했을때의 경우가 있다.

 

사용자가 파일 5개를 업로드 하고자 선택을 하면 가장 먼저 배열에 파일들을 담아 setImageData 같이 처리하게 될 것이다.

근데 3번째 파일을 지우려고 삭제 버튼을 눌렀다면?

set 된 데이터들 중 3번째 파일 데이터를 삭제하고 나머지 데이터만 남겨야 한다.

하지만 useState는 readOnly로 수행된다.

그래서 imageData 라는 state에서 특정 데이터를 삭제하고 나머지를 set 해주기 위해서는 배열을 사용해야 했다.

 

이때 처리한 방법이 가장 마지막 코드와 같은 방법이다.

삭제 버튼의 onClick이 발생했을 때 handleOnClick에서는 비어있는 새로운 배열에 전개 연산자로 state 값을 그대로 담아준다.

그리고 3번째 파일이니 2번 인덱스에 대한 값을 찾는다.

그리고 그 파일을 배열에서 제거한 뒤 setState를 통해 set을 해주게 되면 사용자의 요청대로 특정 위치의 데이터만 삭제할 수 있게 된다.

 

 

 

 

마지막으로 useState를 사용하며 주의해야 할 점.

React 학습에서는 보통 초기값을 설정하지 않고 처리하는 경우가 많았다.

그리고 초기값을 잘못 설정하는 경우 재 렌더링 될때마다 의도하지 않은 값이 설정될 수 있기 때문에 잘 고민해서 초기값 설정을 해야 한다고도 들었다.

 

프로젝트를 진행하면서 문제가 발생한 부분들은 대부분 초기값에 대한 문제가 많았다.

대부분의 컴포넌트에서 서버에 데이터를 요청한 뒤 setData 에 담아주도록 하고 있는데 이 axios 요청은 useEffect에서 보내도록 해두었다.

거의 첫 렌더링에서만 수행하면 되는 요청이기 때문이라는 생각이었기 때문에 useEffect를 사용해 호출하도록 했는데 문제는 useEffect는 렌더링 후에 수행된다는 점이었다.

 

해당 컴포넌트가 하위 컴포넌트를 갖지 않고 처리한다면, 또는 해당 값이 null이라도 렌더링에서 오류가 발생하지 않는다면 초기값은 이상하게 설정하지만 않으면 문제가 되지 않았다.

하지만 리스트와 같이 하위 컴포넌트를 두고 그 하위 컴포넌트를 map을 통해 반복 하도록 했다면 얘기가 다르다.

 

반복문을 통해 처리하라고 했으면 '얘는 배열 타입이야' 라고 알려줘야 한다.

근데 state의 초기값을 아무것도 설정하지 않고 얘로 반복문 돌려서 처리해줘 라고 하면 오류가 발생하는 것.

'단일 객체를 반복문을 왜 돌림?' 이런 느낌이다..

그래서 state의 초기값을 [] 로 빈 배열로 초기화를 해둬야 한다.

혹은 배열이 아니더라도 하위 컴포넌트에서 해당 state 값으로 무언가를 처리하는데 오류가 발생할 여지가 있다면 초기값을 설정해 오류가 발생하지 않도록 해야 한다.

 

배열 state 코드가 게시글 리스트 코드를 예로 들었으니 게시글 리스트를 예로 들어본다.

리스트 페이지 접근 -> 렌더링 -> useEffect 수행 -> axios 요청, 응답 후 setData -> state가 변경되었으니 재렌더링.

이 순서로 발생하기 때문에 1차 렌더링 시에 오류가 발생해 useEffect가 제대로 수행되지 않거나 수행되더라도 재 렌더링이 수행되지 않게 된다.

주의!!

 

이 문제에 대한 참고 블로그

https://velog.io/@gyutato/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85React-useEffect-%EC%9D%98%EC%A1%B4%EC%84%B1-%EB%B0%B0%EC%97%B4%EC%97%90-props-%EB%84%A3%EA%B8%B0

 

[트러블슈팅][React] useEffect는 렌더링 '이후'에 실행된다

useEffect의 동작 및 컴포넌트 렌더링 과정에 대한 이해도가 부족해 발생한 문제였습니다. 의존성 배열을 사용하여 해결했지만, useLayoutEffect를 사용해볼 여지가 남아 있습니다.

velog.io

 

 

'Front > React' 카테고리의 다른 글

React에서 state 배열 관리 및 반복문에서의 state  (0) 2024.04.13
React 페이지 이동  (1) 2024.04.12
React useParams(), useSearchParams()  (1) 2024.04.12
React에서 .env 사용  (0) 2024.04.11
React Axios 모듈화  (0) 2024.04.11

React 학습 후에 처음으로 프로젝트를 진행해보면서 axios를 처음 사용해보게 되었다.

 

처음 axios는 아래와 같이 사용했었다.

//board list component

import axios from 'axios';
//....

function BoardPage() {
    const [params] = useSearchParams();
    const pageNum = params.get('pageNum') == null ? 1 : params.get('pageNum');
    const [data, setData] = useState([]);
    //...
    
    useEffect(() => {
        getBoardList(pageNum);
    }, [pageNum]);
    
    const getBoardList = async (pageNum) => {
        await boardAxios.get(`http://localhost:9096/board?pageNum=${pageNum}`)
            .then(res => {
                setData(res.data.content);
            }
            .catch(err => {
                console.error('boardList axios error : ', err);
            }
    }
    
    //....
}


//imageBoard list component

import axios from 'axios';
//....

function imagePage() {
    const [params] = useSearchParams();
    const pageNum = params.get('pageNum') == null ? 1 : params.get('pageNum');
    const [data, setData] = useState([]);
    //...
    
    useEffect(() => {
        getImageBoardList(pageNum);
    }, [pageNum]);
    
    const getImageBoardList = async (pageNum) => {
        await boardAxios.get(`http://localhost:9096/image-board?pageNum=${pageNum}`)
            .then(res => {
                setData(res.data.content);
            }
            .catch(err => {
                console.error('imageBoardList axios error : ', err);
            }
    }
    
    //....
}

 

 

이렇게 일단 작성해두고 테스트를 진행 해 정상적으로 동작하는 것을 확인하고 나니 좀 불편한게 눈에 들어왔다.

가장 먼저 눈에 들어온것이 axios를 요청하는 url 작성이었다.

이걸 어쩔까 하다가 생각난 방법이 .env에 기본 url과 크게 분류되어있는 기능의 url을 작성해 두고 가져다 사용하는 방법이었다.

 

//.env
REACT_APP_API_URL=http://localhost:9096
REACT_APP_API_BOARD=/board/
//...



//board list component

import axios from 'axios';
//....

const default_url = process.env.REACT_APP_API_URL;
const board_default_url = process.env.REACT_APP_API_BOARD;
function BoardPage() {
    const [params] = useSearchParams();
    const pageNum = params.get('pageNum') == null ? 1 : params.get('pageNum');
    const [data, setData] = useState([]);
    //...
    
    useEffect(() => {
        getBoardList(pageNum);
    }, [pageNum]);
    
    const getBoardList = async (pageNum) => {
        await boardAxios.get(`${default_url}${board_default_url}?pageNum=${pageNum}`)
            .then(res => {
                setData(res.data.content);
            }
            .catch(err => {
                console.error('boardList axios error : ', err);
            }
    }
    
    //....
}

 

이전보다는 url 관리도 쉽겠구나 하긴 했지만 컴포넌트 몇개 더 작성하다보니 계속 env 파일에서 불러와야 한다는 점이 너무 불편했다.

만약 env 파일에 작성한 변수명이 달라지면 그건 그거대로 일이 커질 것이라는 생각도 들었다.

그리고 가장 큰 문제는 JWT 토큰을 모두 쿠키에 담아 저장하다보니 withCredentials 옵션을 설정해야 하고 headers 역시 매번 작성해야 했다.

테스트 코드는 이런 문제를 다 배제하고 단순하게 연결만 테스트 했기 때문에 문제가 없었지만...

 

그래서 방법을 알아보다 axios를 모듈화 할 수 있다는 것을 알게 되었다.

처음에는 간단하게 기본적인 설정에 대한 인스턴스만 생성했지만 이미지 파일에 대한 처리도 해야 했고 컴포넌트에서 기능별로 크게 분리되어있는 url 역시 작성하지 않도록 하기 위해 좀 세분화해서 모듈화를 하게 되었다.

 

//customAxios.js

import axios from 'axios';

/*
* axios list
* board
* image_default
* image_multipart
* image_blob
* ...
*/

const default_url = process.env.REACT_APP_API_URL;
const board_default = process.env.REACT_APP_API_BOARD;
const image_default = process.env.REACT_APP_API_IMAGE;
const comment_default = process.env.REACT_APP_API_COMMENT;
const member_default = process.env.REACT_APP_API_MEMBER;

const default_header = {
    'Content-Type': 'application/json',
}

export const boardAxios = axios.create({
    baseURL: `${default_url}${board_default}`,
    headers: default_header,
    withCredentials: true,
});

export const imageAxios = axios.create({
    baseURL: `${default_url}${image_default}`,
    headers: default_header,
    withCredentials: true,
});

export const imageDisplayAxios = axios.create({
    baseURL: `${default_url}${image_default}`,
    headers: default_header,
    withCredentials: true,
    responseType: 'blob',
});

export const imageInsertAxios = axios.create({
    baseURL: `${default_url}${image_default}`,
    headers: {
        'Content-Type' : 'multipart/form-data',
    },
    withCredentials: true,
});

export const axiosErrorHandling = (err) => {
    const err_code = err.response.status;
    
    if(err_code === 403) {
        window.location.href = '/error';
    }//...
    
}

//...

 

header의 경우 딱히 여러가지를 사용할 만한것 없이 Content-Type 만 사용하는 것으로도 괜찮았기 때문에 해당 설정 역시 변수화해서 기본 적인 header를 갖는 axios는 그대로 가져다 사용하도록 처리했다.

그리고 image의 경우 파일을 응답받는 경우와 파일을 담아 요청하는 경우 Content-Type이 다르기 때문에 이때만 직접 작성했다.

 

axios 요청 이후 오류 코드 응답이 오는 경우도 모듈화를 하기 위해 axiosErrorHandling 을 만들어두고 특정 오류코드에 대한 핸들링을 할 수 있도록 했다.

 

//board list component

import { boardAxios, axiosErrorHandling } from '../../../modules/customAxios';
//....

function BoardPage() {
    const [params] = useSearchParams();
    const pageNum = params.get('pageNum') == null ? 1 : params.get('pageNum');
    const [data, setData] = useState([]);
    //...
    
    useEffect(() => {
        getBoardList(pageNum);
    }, [pageNum]);
    
    const getBoardList = async (pageNum) => {
        await boardAxios.get(`?pageNum=${pageNum}`)
            .then(res => {
                setData(res.data.content);
            }
            .catch(err => {
                axiosErrorHandling(err);
            }
    }
    
    //....
}

 

모듈화를 하게 되면서 컴포넌트 코드가 엄청 간결해졌고 나중에 url 변경이나 header 등등 설정의 수정에 있어서도 대응하기 굉장히 좋아졌다.

 

아쉬운점으로는 대부분의 요청 헤더가 비슷한만큼 하나의 대표적인 기본 설정을 두고 그 설정에 set 해서 url만 수정한다거나 할 수 있도록 만든다면 더 좋지 않을까 싶긴 한데 방법을 찾지도, 딱히 생각나는 것도 없어서 일단은 보류..

'Front > React' 카테고리의 다른 글

React에서 state 배열 관리 및 반복문에서의 state  (0) 2024.04.13
React 페이지 이동  (1) 2024.04.12
React useParams(), useSearchParams()  (1) 2024.04.12
React에서 .env 사용  (0) 2024.04.11
useState 사용 정리  (0) 2024.04.11

프로젝트를 리펙토링하면서 JQuery에서 꼭 좀 개선하고 싶은 부분이 있었다.

중복되는 함수에 대해 하나의 js파일에 담아두고 필요한 곳에서 호출해 사용하게 하고 싶었다.

 

예를들어 jQuery를 통해 하나의 테이블을 만들어야 된다고 가정.

function tableStr(arr) {
    let str = "<table class=\"table\">" + 
                  "<thead>" +
                      "<tr>" +
                          "<th>header1</th>" + 
                          "<th>header2</th>" + 
                          "<th>header3</th>" +
                      "</tr>" +
                  "</thead>" + 
                  "<tbody>";
    
    $(arr).each(function(i, attach) {
        str += "<tr>" +
                   "<td>" + arr.body1 + "</td>" +
                   "<td>" + arr.body2 + "</td>" +
                   "<td>" + arr.body3 + "</td>" +
               "</tr>";
    }
    
    str += "</tbody>" +
           "</table>";
    
    return str;
}

 

이렇게 데이터를 받아 테이블을 파싱하는 코드가 여러곳에 존재한다고 하면, 각각의 js 파일에 중복되어 작성되게 된다.

그럼 가장 좋은 방법은 이 tableStr 이라는 함수를 하나의 js 파일에 분리하거나 필요한 곳 중 한군데에만 작성해두고 그 함수를 호출하는 것이 가장 좋은 방법일 것.

 

처리는 js 파일을 새로 만들고 그 안에 함수를 작성한 뒤 필요한 곳에서 호출하게 했다.

 

필요한 곳을 parent1.js, parent2.js 라고 가정하고 분리해서 작성한 곳을 child1.js로 가정한다.

//parent1.js
function setTable() {
    $.getJSON("/getTableData", function(arr) {
        let str = jQuery.tableStr(arr);
        $(".formDiv").append(str);
    }
}

//parent2.js
function setTable() {
    $.getJSON("/getBoardTableData", function(arr) {
        let str = jQuery.tableStr(arr);
        $(".formDiv").append(str);
    }
}


//child1.js
jQuery.tableStr = function(arr) {
    let str = "<table class=\"table\">" + 
                  "<thead>" +
                      "<tr>" +
                          "<th>header1</th>" + 
                          "<th>header2</th>" + 
                          "<th>header3</th>" +
                      "</tr>" +
                  "</thead>" + 
                  "<tbody>";
    
    $(arr).each(function(i, attach) {
        str += "<tr>" +
                   "<td>" + arr.body1 + "</td>" +
                   "<td>" + arr.body2 + "</td>" +
                   "<td>" + arr.body3 + "</td>" +
               "</tr>";
    }
    
    str += "</tbody>" +
           "</table>";
    
    return str;
}

 

방법은 위와 같다.

호출되는 함수는 jQuery.함수명 = function() { } 이런 구조로 작성하면 되고, 호출할때는 jQuery.함수명 으로 호출하면 된다.

여기서 주의사항.

해당 html 혹은 jsp 파일에 parent, child가 모두 들어가있어야 한다.

 

<html>
....
<script type="text/javascript" src="/js/parent1.js"></script>
<script type="text/javascript" src="/js/child1.js"></script>
....

이렇게 되어있다면 정상적으로 호출해서 사용할 수 있다.

 

'Front > JQuery' 카테고리의 다른 글

JQuery(비동기방식연동 Ajax)  (0) 2020.05.26
JQuery(애니메이션 효과 제어 메소드)  (0) 2020.05.26
JQuery(효과 및 애니메이션 메소드)  (0) 2020.05.22
JQuery(그룹이벤트)  (0) 2020.05.22
JQuery(이벤트 객체)  (0) 2020.05.22

QueryDSL이란?

하이버네이트 쿼리 언어(Hibernate Query Language, HQL)의 쿼리를 타입에 안전하게 생성 및 관리해주는 Framework다.

정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있게 해준다.

JPA를 사용하면서 복잡한 쿼리, 동적 쿼리를 구현하는데 한계가 있는데 이런 문제점을 QueryDSL로 해결할 수 있다.

 

QueryDSL을 사용하지 않고 MyBatis, JPQL, Criteria등 문자열 기반으로 쿼리를 작성하는 방법에서는 컴파일시에 오류를 발견하는 것이 어려웠으나 QueryDSL은 자바 코드로 쿼리를 생성하기 때문에 컴파일시에 오류를 발생시켜 실수를 방지할 수 있다.

 

 

QueryDSL 설정

Gradle에 QueryDSL 의존성을 추가하는 것에 대해서는 좀 여러가지 방법을 볼 수 있었다.

프로젝트에서 사용한 방법만 정리.

 

개발환경

- Spring Boot 2.7.6

- Spring Data JPA

- Gradle

- MySQL

 

기본 세팅을 참고한 블로그

https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/

 

Spring Boot에 QueryDSL을 사용해보자

1. QueryDSL PostRepository.java Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL…

tecoble.techcourse.co.kr

 

가장 먼저 gradle에 의존성을 추가한다.

....

dependencies {
    ....
    
    //QueryDSL
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}

def querydslSrcDir = '/src/main/generated'

sourceSets {
    main {
        java {
            srcDirs += [ querydslSrcDir ]
        }
    }
}

compileJava {
    options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}

tasks.withType(JavaCompile) {
    options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

clean {
    delete file(querydslSrcDir)
}

...

 

대체적으로 implementation 으로 querydsl-jpa, querydsl-apt를 추가하는 것 까지는 동일했다.

하지만 annotationProcessor에서 querydsl-apt를 설정하는데서 조금씩 차이가 있고, dependencies를 제외한 다른 부분들 역시 블로그마다 조금씩 차이가 있었다.

 

gradle build를 마친 후 gradle -> tasks -> other -> compileJava를 통해 컴파일을 수행하면 Q 클래스가 생성된다.

QEntity는 프로젝트내의 build -> classes -> java -> main -> ... -> entity 안에 생성된다.

이것도 설정에 따라 차이가 발생한다.

설정에 따라 build -> generated -> sources -> ... -> entity 하위에 생성되기도 한다.

이렇게 생성된 클래스를 Q 클래스 혹은 Q(쿼리) 타입이라고 부르는데 QueryDSL로 쿼리를 작성할 때 이 Q 클래스를 사용해 작성하게 된다.

 

다음은 configuration 설정.

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QueryDSLConfig {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entitymanager);
    }
}

 

 

config라고 해서 거한 설정이 있는것은 아니고 JPAQueryFactory를 사용하기 위해 Bean을 등록해준다.

 

//Repository
public interface BoardRepository extends JpaRepository<Board, Long>, BoardRepositoryCustom {
    @Query(value = "....")
    Board findByBoardNo(long boardNo);
    ....
}


//Custom Repository
public interfact BoardRepositoryCustom {
    List<Board> findAll();
}

//repositoryImpl
import static com.example.jpa.domain.entity.QBoard.board;
import com.querydsl.jpa.impl.JPAQueryFactory;

@Repository
public class BoardRepositoryImpl implements BoardRepositoryCustom {
    
    private final JPAQueryFactory jpaQueryFactory;
    
    @Override
    public List<Board> findAll() {
        List<Board> boardList = jpaQueryFactory.selectFrom(board).fetch();
        
        return boardList;
    }
}

 

이렇게 BoardRepository에서는 JPQL로 처리하도록 하고 Custom Repository를 생성해 QueryDSL을 사용하도록 했다.

 

QueryDSL을 사용하기 위해서는 Bean으로 등록한 JPAQueryFactory가 있어야 하고 Q 클래스로 쿼리를 작성하게 된다.

findAll에 작성된 쿼리는 SELECT * FROM board; 이다.

조회할 컬럼을 따로 지정하지 않고 조회한다면 selectFrom으로 Q 클래스만 담아주면 쿼리가 실행된다.

만약 특정 컬럼만 조회하겠다고 한다면 select로 작성하면 되고 Q클래스.컬럼명 형태로 사용한다.

그리고 fetch()로 마무리 해주면 된다.

Q 클래스는 static import로 사용하게 된다.

 

쿼리를 작성해야 하기 때문에 당연하게도 orderBy, offset, limit, join, where 등을 사용해 작성할 수 있다.

 

QueryDSL로 동적쿼리 수행

프로젝트를 JPA로 구현하게 되면서 조건에 따른 동적쿼리를 처리해야 했고 QueryDSL로 처리하는 방법이 가장 깔끔하고 괜찮아 보였기 때문에 선택하게 되었다.

//boardSevice

@Override
public Page<boardDTO> getBoardList(Criteria cri) {
    
    Page<boardDTO> dto;
    
    if(cri.getKeyword == null) {
        dto = boardRepository.getBoardList(
                PageRequest.of(cri.getPageNum() - 1
                    , cri.getBoardAmount()
                    , Sort.by("boardGroupNo").descending()
                        .and(Sort.by("boardUpperNo").ascending()))
        );
    }else if(cri.getSearchType().equals("t") {
        dto = boardRepository.getSearchTitleBoardList(
                cri.getKeyword()
                , PageRequest.of(cri.getPageNum() - 1
                    , cri.getBoardAmount()
                    , Sort.by("boardGroupNo").descending()
                        .and(Sort.by("boardUpperNo").ascending()))
        );
    }else if(cri.getSearchType().equals("c") {
        dto = boardRepository.getSearchContentBoardList(
                cri.getKeyword()
                , PageRequest.of(cri.getPageNum() - 1
                    , cri.getBoardAmount()
                    , Sort.by("boardGroupNo").descending()
                        .and(Sort.by("boardUpperNo").ascending()))
        );
    }else if(cri.getSearchType().equals("tc") {
        dto = boardRepository.getSearchTitleAndContentBoardList(
                cri.getKeyword()
                , PageRequest.of(cri.getPageNum() - 1
                    , cri.getBoardAmount()
                    , Sort.by("boardGroupNo").descending()
                        .and(Sort.by("boardUpperNo").ascending()))
        );
    }else if(cri.getSearchType().equals("u") {
        dto = boardRepository.getSearchUserIdBoardList(
                cri.getKeyword()
                , PageRequest.of(cri.getPageNum() - 1
                    , cri.getBoardAmount()
                    , Sort.by("boardGroupNo").descending()
                        .and(Sort.by("boardUpperNo").ascending()))
        );
    }else{
        ...
    }
    
    return dto;
}

 

이렇게 페이징 처리를 해야 하는 게시판 리스트이고 JPQL로 작성되어있었다.

처음 이 프로젝트를 진행했을때는 JPA로 처음 해보는거였고 동적쿼리는 생각도 못했고.... 단순하게 서비스단에서 조건문을 통해 하나하나 작성해 처리하도록 했었다.

 

그러다 이전에 리펙토링하면서 대부분 동적쿼리로 수정했는데 여기는 놓치고 지나가서 수정이 안된 상태였다.

 

다른 기능에서 수행하는 동적쿼리들은 대체로 삽입, 삭제에서 발생하는 동적쿼리였기 때문에 list를 받아 처리하는 정도의 간단한 쿼리라 JPQL로도 간단하게 처리할 수 있었다.

 

하지만 이번경우는 List에 대한 검색어 처리, Pageable로 인한 count 쿼리에서의 조건을 제어할 수 있는 동적쿼리여야 했기 때문에 @Query Annotation을 붙여 사용하던 방법에서는 마땅하게 해결책을 찾을 수 없었다.

@Query Annotation을 사용하지 않으면 문자열 생성으로 처리할 수 있는 방법을 찾았지만 그 경우는 뭔가 JDBC Template으로 처리하는 느낌이라 다른 방법을 써보고 싶었다.

 

 

그럼 코드를 정리하기 전 위 코드를 대강 정리해본다.

  1. keyword가 null인 경우 검색이 아닌 기본 리스트 조회이기 때문에 검색어와 검색 타입이 없는 쿼리가 실행되어야 한다.
  2. keyword가 존재한다면 searchType값이 존재할 것이기 때문에 검색 타입에 따른 where 문이 생성되어야 한다.
    1. searchType이 ' t '라면 title을 의미하기 때문에 where boardTitle LIKE :keyword가 수행되어야 한다.
    2. searchType이 ' c '라면 content를 의미하기 때문에 where boardContent LIKE :keyword가 수행되어야 한다.
    3. searchType이 ' tc '라면 title과 content를 의미하기 때문에 where boardTitle LIKE :keyword or boardContent LIKE :keyword가 수행되어야 한다.
    4. searchType이 ' u '라면 userId를 의미하기 때문에 where userId LIKE :keyword가 수행되어야 한다.
  3. 해당 리스트는 페이징 기능이 포함되어 있기 때문에 count 쿼리를 수행해야 하고 Page 타입으로 리턴되어야 한다.
  4. Entity가 아닌 DTO에 매핑되어 반환되어야 한다.

 

//service

@Override
public Page<BoardDTO> getBoardList(Criteria cri) {
    Pageable pageable = PageRequest.of(cri.getPageNum() - 1
                                       , cri.getBoardAmount()
                                       , Sort.by("boardGroupNo").descending()
                                           .and(Sort.by("boardUpperNo").ascending()));
    
    return boardRepository.findAll(cri, pageable);
}


//boardRepository
public interface BoardRepository extends JpaRepository<Board, Long>, BoardRepositoryCustom {
    @Query(value = "....")
    Board findByBoardNo(long boardNo);
    ....
}


//custom Repository
public interface BoardRepositoryCustom {
    Page<BoardDTO> findAll(Criteria cri, Pageable pageable);
}


//custom Repository impl
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.support.PageableExecutionUtils;

import static com.example.jpa.domain.entity.QBoard.board;

@Repository
@RequiredArgsConstructor
public class BoardRepositoryCustomImpl implements BoardRepositoryCustom {
    
    private final JPAQueryFactory jpaQueryFactory;
    
    @Override
    public Page<BoardDTO> findAll(Criteria cri, Pageable pageable) {
        //1. 리스트 조회
        List<BoardDTO> list = jpaQueryFactory.select(
                       Projections.fields(
                           BoardDTO.class
                           , board.boardNo
                           , board.boardTitle
                           , board.member.userId
                           , board.boardDate
                           , board.boardIndent
                       )
                   )
                   .from(board)
                   .where(
                       searchTypeEq(cri.getSearchType(), cri.getKeyword()
                   )
                   .orderBy(board.boardGroupNodesc())
                   .orderBy(board.boardUpperNo.asc())
                   .limit(cri.getBoardAmount())
                   .fetch();
        
        //2. count 쿼리
        JPAQuery<Long> count = jpaQueryFactory.select(board.countDistinct())
                   .from(board)
                   .where(
                       searhTypeEq(cri.getSearchType(), cri.getKeyword())
                   );
        
        return PageableExecutionUtils.getPage(list, pageable, count::fetchOne);
    }
    
    private BooleanExpression searchTypeEq(String searchType, String keyword) {
        if(searchType == null)
            return null;
        else if(searchType.equals("t"))
            return hierarchicalBoard.boardTitle.like(keyword);
        else if(searchType.equals("c"))
            return hierarchicalBoard.boardContent.like(keyword);
        else if(searchType.equals("tc"))
            return hierarchicalBoard.boardTitle.like(keyword).or(hierarchicalBoard.boardContent.like(keyword));
        else if(searchType.equals("u"))
            return hierarchicalBoard.member.userId.like(keyword);
        else
            return null;
    }
}

 

Repository 구조에 대해서는 위에서 설명했으니 생략.

Criteria의 경우 페이징에 필요한 페이지 번호, 페이지당 출력할 데이터 개수, 검색어 등을 담고 있는 직접 만들어준 클래스다.

 

수행하는 과정을 간단하게 보면 list 조회 후 count 쿼리, PageableExecutionUtils.getPage를 통해 결과를 반환한다.

 

일단 list는 Entity에 담는것이 아니라 DTO에 담아야 한다고 했다.

DTO에 담기 위해서는 Projections.fields()를 사용하면 된다.

이때 fields가 아닌 bean, constructor를 통한 사용도 가능하다.

bean()을 사용하는 경우 getter, setter, constructor가 모두 있어야 한다.

fields()를 사용하는 경우에는 getter, setter가 필요없이 바로 주입되며, constructor()를 사용하는 경우에는 의미 그대로 생성자를 통해 주입하게 된다.

또 다른 방법으로 DTO역시 Q 클래스를 생성하도록 해 처리하는 방법도 있다.

DTO의 생성자에 @QueryProjection Annotation을 붙여주게 되면 해당 DTO 역시 Q 클래스가 생성되기 때문에 static import로 주입해 사용할 수 있다.

 

이 내용은 아래 블로그에서 참고해 처리했다.

https://doing7.tistory.com/m/129

 

[Querydsl] 튜플이나 DTO로 결과 반환하기

프로젝션 : select 대상지정하는 일 프로젝션 대상이 두개 이상이라면 튜플이나 DTO로 조회해야한다. 🌱 튜플 사용하기 com.querydsl.core.Tuple를 사용하고 있다. 때문에 Repository 계층을 넘어서 Service나

doing7.tistory.com

 

DTO에 결과를 주입하기 위해서는 코드에서 보이는 것과 같이 담아줄 DTO.class를 첫번째 인자로 담아줘야 하고, 그 다음부터는 DTO 필드에 주입할 필드들을 작성해주면 된다.

JPA를 쓰는거니까 Q 클래스를 그냥 써주면 알아서 해주지 않을까? 라는 기대를 하며 board 만 넣어봤지만 오류가 발생하지 않을뿐 모든 필드값이 null로 나왔다..ㅎㅎ

모든 필드를 작성해야 하는 만큼 selectFrom이 아닌 select로 작성했기 때문에 from을 붙여줘야 한다.

 

where문의 경우 searchTypeEq이라는 BooleanExpression 타입의 메소드를 호출하는 것을 볼 수 있다.

QueryDSL은 null인 경우 null로 처리하는 것이 아니라 아예 쿼리에 포함하지 않는다고 한다.

그럼 searchType이 null인 기본 조회라면 null이 반환되어 where 문이 생성되지 않고 처리되며, 그게 아닌 경우 각 타입에 맞는 like 구문이 반환되어 where ~~~ LIKE :keyword가 수행되게 된다.

 

그럼 LIKE가 아닌 where boardNo = ? 같은 것은 아래와 같이 처리할 수 있다.

......
.where(
    commentBoardEq(boardNo)
    , commentImageEq(imageNo)
)
....
}


private BooleanExpression commentBoardEq(String boardNo) {
    if(boardNo == null)
        return null;
    
    return comment.board.boardNo.eq(Long.parseLong(boardNo));
}

private BooleanExpression commentImageEq(String imageNo) {
    if(imageNo == null)
        return null;
    
    return comment.image.imageNo.eq(Long.parseLong(imageNo));
}

 

댓글 리스트를 조회하면서 board에 속하는지 image에 속하는지를 구분하는 코드다.

위 처럼 처리하게 되면 조건에 따라 where boardNo = ? 같은 형태로 처리되게 된다.

이 경우와는 다르게 둘다 존재할 수 있는 상황이라면 and 조건으로 처리하게 된다.

 

 

다음은 count 쿼리다.

count 쿼리는 Pageable이 알아서 처리하게 둘 수도 있지만 대용량 데이터의 경우 count 쿼리역시 제어해서 직접 사용하는 것이 좋다.

어느 포스팅에서 봤는지 찾을 수 없어 남기진 못하지만 count 쿼리 역시 직접 작성해 제어하는 것이 최적화 하기 더 좋다는 글도 있었다.

 

count 쿼리를 보면 whre 문까지만 처리하고 fetch는 붙이지 않은것을 볼 수 있다.

이유는 PageableExecutionUtils.getPage를 통해 count 쿼리를 수행하기 때문이다.

PageableExecutionUtils.getPage()는 리스트가 첫 페이지에 담겨야 하는 양보다 적거나 마지막 페이지인 경우 count 쿼리를 수행하지 않도록 최적화 해준다.

캐싱처리만큼 완벽한 최적화는 아니지만 간단하게 사용하기에는 좋은 것 같다.

 

 

 

간단하게 다시 정리.

쿼리 작성은 JPAQueryFactory를 통해 작성한다.

Q 클래스는 import static으로 가져온다.

동적 쿼리를 위해 BooleanExpression을 사용한다. QueryDSL의 where에서는 null을 무시하고 아예 담아주지 않는다.

PageableExecutionUtils.getPage()는 list의 size()가 한 페이지에 담겨야 하는 개수보다 작거나 마지막 페이지인 경우에는 count 쿼리를 수행하지 않도록 최적화 해준다.

DTO에 담아주기 위해서는 DTO의 생성자에 @QueryProjection이라는 Annotation을 붙여주거나 select 구문 안에 Projections.fields(DTO.class, field, ....) 로 작성한다.

이때 fields말고도 bean(), constructor()로도 가능하지만 주입되기 위한 조건이 존재한다.

 

 

마지막으로 CASE WHEN을 사용하는 방법!

댓글의 경우 delete 요청이 들어오게 되었을 때 삭제하지 않고 상태값을 변경해 삭제된것 처럼 보이도록 하고 있다.

그래서 조회시 댓글 내용을 '삭제된 댓글입니다'로 보이도록 하고 있다.

이걸 서버에서 코드로 변경하는 것이 아닌 데이터베이스부터 받을때부터 그렇게 받을 수 있도록 CASE WHEN을 사용하고 있는데 QueryDSL에서는 아래와 같이 사용한다.

 

....
Projections.fields(
commentDTO.class
, comment.commentNo
, comment.member.userId
, comment.commentDate
, new CaseBuilder()
.when(comment.commentStatus.gt(0))
.then("삭제된 댓글입니다.")
.otherwise(comment.commentContent)
.as("commentContent")
, comment.commentGroupNo
.....
)

 

이렇게 new CaseBuilder를 통해 처리하면 된다.

when에 조건문이 들어가게 되는데 commentStatus의 기본값이 0이고 삭제된 값이 1이기 때문데 gt(0)가 true인 경우 then이 수행되고 아닌경우 otherwise가 수행된다.

 

 

JPA의 동적 쿼리는 JPQL, Specification으로도 처리할 수 있지만 이래저래 찾아본 바로는 아무래도 QueryDSL이 좀 더 깔끔하고 편하게 사용할 수 있는 것 같다.

컴파일 단계에서 오류를 잡아 실수를 줄일 수 있다는 점도 큰 장점인 것 같다.

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


템플릿 메소드 패턴은 행동(행위) 패턴에 속하는 디자인 패턴으로 상위 클래스에서 처리의 뼈대를 결정하고 하위 클래스에서 구체적인 내용을 결정하는 패턴이다.

알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다.

 

예제

문자 또는 문자열을 출력하는 프로그램.

Character 타입의 경우 << >> 안에 해당 문자가 5번 반복이 되며, String 타입의 경우 +---+ +---+ 사이에 | 문자열 | 형태로 개행이 되며 5번 반복이 된다.

 

구조로는 추상클래스인 AbstractDisplay 에는 open, print, close라는 세개의 추상 메소드, open, 반복문으로 5번 수행하는 print, close 순서로 호출하고 있는 display 메소드가 존재한다.

구현체인 CharDisplay와 StringDisplay는 각각 출력할 문자 혹은 문자열 필드를 갖고 있으며, AbstractDisplay를 상속받아 추상메소드를 구현하고 있다.

이 구조에서 Template Method는 display가 된다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public abstract class AbstractDisplay {
    public abstract void open();
    
    public abstract void print();
    
    public abstract void close();
    
    public final void display() {
        open();
        for(int i = 0; i < 5; i++)
            print();
        
        close();
    }
}
public class CharDisplay extends AbstractDisplay {
    
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    @Override
    public void open() {
        System.out.println("<<");
    }
    
    @Override
    public void print() {
         System.out.print(ch);
    }
    
    @Override
    public void close() {
        System.out.println(">>");
    }
}
public class StringDisplay extends AbstractDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    @Overrdie
    public void open() {
        printLine();
    }
    
    @Overrdie
    public void print() {
        System.out.println("|" + string + "|");
    }
    
    @Override
    public void close() {
        printLine();
    }
    
    private void printLine() {
        System.out.print("+");
        for(int i = 0; i < width; i++)
            System.out.print("-");
        System.out.println("+");
    }
}
public class Main {
    public static void main(String[] args) {
        AbstractDisplay charH = new CharDisplay('H');
        
        AbstractDisplay stringHello = new StringDisplay("Hello World");
        
        charH.display();
        stringHello.display();
    }
}

 

실행결과

<<HHHHH>>

+----------+

|Hello World|

|Hello World|

|Hello World|

|Hello World|

|Hello World|

+----------+

 

예제 작성 전에 Template Method는 display() 메소드라고 했다.

display() 메소드는 동일하게 open() -> print() * 5 -> close() 를 처리하고 있다.

즉, 처리과정에 대한 뼈대가 된다.

그럼 상위 클래스인 AbstractDisplay에서 뼈대를 결정한 것이라고 볼 수 있다.

그리고 하위 클래스인 CharDisplay와 StringDisplay에서는 AbstractDisplay를 상속받아 메소드를 구현하고 있다.

각 클래스에서는 open(), print(), close()를 어떻게 처리할지 구체적인 구현을 담당하고 있다.

처음 템플릿 메소드의 설명에 빗대어 본다면,

상위 클래스(AbstractDisplay)에서 처리의 뼈대(display())를 결정하고 하위 클래스(CharDisplay, StringDisplay)에서 그 구체적인 내용(open(), print(), close())을 결정하는 패턴이다.

 

코드를 보며 재정리.

 

AbstractDisplay에는 구현해야 할 추상메소드(open(), print(), close())와 템플릿 메소드(display())가 존재한다.

템플릿 메소드는 추상 메소드들을 호출하게 된다.

실제 사용한다고 하면 여러 알고리즘 + 추상 메소드의 구조가 될 것이다.

즉, 템플릿 메소드는 해당 처리에 대한 정형화된 틀이 된다.

그리고 이 틀을 만들어 두게 됨으로써 다른 타입을 받거나 다른 결과를 내야 하는 경우 중복되는 처리 과정을 거치는 여러 메소드들을 생성하지 않고 재사용할 수 있다.

 

즉, CharDisplay와 StringDisplay의 공통점으로는 open() -> print() * 5 -> close()로 처리되고 있는데, 

이걸 각 클래스의 display() 메소드로 처리한다고 한다면 동일한 코드가 반복되거나 혹은 전혀 분리되어 있지 않은 코드가 생길것이다.

public class CharDisplay {
    private char ch;
    
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    
    public void display() {
        System.out.print("<<");
        
        for(int i = 0; i < 5; i++)
            System.out.print(ch);
        
        System.out.println(">>");
    }
}



public class StringDisplay {
    private String string;
    
    private int width;
    
    public StringDisplay(String string) {
        this.string = string;
        this.width = string.length();
    }
    
    private void open() {
        printLine();
    }
    
    private void print() {
        for(int i = 0; i < 5; i++)
            System.out.println("|" + string + "|");
    }
    
    private void close() {
        printLine();
    }
    
    public void display() {
        open();
        print();
        close();
    }
    
    private void printLine() {
        System.out.print("+");
        
        for(int i = 0; i < width; i++)
            System.out.print("-");
            
        System.out.println("+");
    }
    
    
}

 

이러한 코드 구조라면 틀만 봤을때는 같은 처리과정을 갖고 있는데, 각 클래스에서 구현하게 됨으로써 중복이 발생한다.

그리고 만약 이 문자 또는 문자열을 10번 출력하도록 수정해야 한다면 모든 클래스를 체크하면서 5번을 10으로 수정해줘야 한다.

또한 재사용성이라고는 전혀 찾아볼 수도 없다.

그래서 이 처리 과정들을 추상메소드로 구체적인 것은 하위 클래스에서 맡겨놓고 처리하는 틀은 템플릿 메소드로 처리하게 되면 코드도 간결해져 코드가 보다 간결해 질 수 있고, 틀을 수정해야 하더라도 각 하위 클래스에서 수정할 것은 없고 템플릿 메소드에서만 수정이 발생하게 된다.

 

이렇게 상위 클래스 타입의 변수에 생성한 인스턴스 중 어느것을 대입하더라도 제대로 동작할 수 있도록 하는 원칙을 리스코프 치환법칙(LSP, Liskov Substitution Principle)이라고 한다.

 

템플릿 메소드 패턴을 하위 클래스 관점에서 본다면

  • 상위 클래스에서 정의 된 메소드를 하위 클래스에서 이용할 수 있다.
  • 하위 클래스에서 약간의 메소드를 기술하는 것만으로 새로운 기능을 추가할 수 있다.
  • 하위 클래스에서 메소드를 오버라이드 하게 되면 동작을 변경할 수 있다.

상위 클래스 관점에서 본다면

  • 하위 클래스에서 그 메소드를 구현하기를 기대한다.
  • 하위 클래스에 메소드 구현을 요청한다.

즉, 하위 클래스에서는 상위 클래스에서 선언한 추상 메소드를 구현할 책임이 있다고 할 수 있는데 이것을 subclass responsibility(하위 클래스의 책임)이라고 한다.

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


전략 패턴(Strategy Pattern)은 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

전략패턴은 스위치를 전환하듯 알고리즘(전략)을 바꿔서 같은 문제를 다른 방법으로 해결하기 쉽게 만들어주는 패턴이다.

특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하여 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

 

예제

자바 언어로 배우는 디자인패턴 입문 책에서의 예제를 먼저 본다.

예제 프로그램은 가위바위보를 하는 프로그램이다.

이 프로그램에서는 두가지의 전략을 갖는다.

1. 이기면 다음에도 같은 손을 내는 다소 어리석은 방법(WinningStrategy)

2. 직전 손에서 다음 손을 확률적으로 계산하는 방법(ProbStrategy)

 

//예제 출처 - JAVA 언어로 배우는 디자인패턴 입문

public enum Hand {
    
    ROCK("바위", 0)
    , SCISSORS("가위", 1)
    , PAPER("보", 2);
    
    private String name;   //가위바위보 손 이름
    private int handValue; //가위바위보 손의 값
    
    private static Hand[] hands = {
        ROCK, SCISSORS, PAPER
    };
    
    // Constructor
    private Hand(String name, int handValue) {
        this.name = name;
        this.handValue = handValue;
    }
    
    //손의 값으로 enum 상수를 가져온다
    public static Hand getHand(int handValue) {
        return hands[handValue];
    }
    
    //this가 h 보다 강하다면 true
    public boolean isStrongerThan(Hand h) {
        return fight(h) == 1;
    }
    
    //this가 h보다 약하다면 true
    public boolean isWeakerThan(Hand h) {
        return fight(h) == -1;
    }
    
    //무승부는 0, this가 이기면 1, h가 이기면 -1
    private int fight(Hand h) {
        if(this == h)
            return 0;
        else if((this.handValue + 1) % 3 == h.handValue)
            return 1;
        else
            return -1;
    }
    
    @Override
    public String toString() {
        return name;
    }
}
public interface Strategy {
    public abstract Hand nextHand();
    
    public abstract void study(boolean win);
}
/**
* 이기면 다음에도 같은 손을 내는 전략
*/

public class WinningStrategy implements Strategy{
    private Random random;
    
    private boolean won = false;
    
    private Hand prevHand;
    
    public WinningStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        if(!won)
            prevHand = Hand.getHand(random.nextInt(3));
        
        return prevHand;
    }
    
    @Override
    public void study(boolean win) {
        won = win;
    }
}
/**
* 확률을 계산해 다음 손을 결정하는 전력
* 
* history[직전에 낸 손][이번에 낸 손]
* 배열의 value는 과거의 승리 수를 의미
*/
public class ProbStrategy implements Strategy{
    
    private Random random;
    
    private int prevHandValue = 0;
    
    private int currentHandValue = 0;
    
    private int[][] history = {
        { 1, 1, 1 },
        { 1, 1, 1 },
        { 1, 1, 1 }
    };
    
    public ProbStrategy(int seed) {
        random = new Random(seed);
    }
    
    @Override
    public Hand nextHand() {
        int bet = random.nextInt(getSum(currentHandValue));
        int handValue = 0;
        
        if(bet < history[currentHandValue][0])
            handValue = 0;
        else if(bet < history[currentHandValue][0] + history[currentHandValue][1])
            handValue = 1;
        else
            handValue = 2;
        
        prevHandValue = currentHandValue;
        currentHandValue = handValue;
        
        return Hand.getHand(handValue);
    }
    
    private int getSum(int handValue) {
        int sum = 0;
        
        for(int i = 0; i < 3; i++)
            sum += history[handValue][i];
        
        return sum;
    }
    
    @Override
    public void study(boolean win) {
        if(win)
            history[prevHandValue][currentHandValue]++;
        else{
            history[prevHandValue][(currentHandValue + 1) % 3]++;
            history[prevHandValue][(currentHandValue + 2) % 3]++;
        }
    }
}
public class Player {
    private String name;
    
    private Strategy strategy;
    
    private int winCount;
    
    private int loseCount;
    
    private int gameCount;
    
    //이름과 전략을 받아 플레이어를 생성
    public Player(String name, Strategy strategy) {
        this.name = name;
        this.strategy = strategy;
    }
    
    //전략에 따라 다음 손을 결정
    public Hand nextHand() {
        return strategy.nextHand();
    }
    
    //승리
    public void win() {
        strategy.study(true);
        winCount++;
        gameCount++;
    }
    
    //패배
    public void lose() {
        strategy.study(false);
        loseCount++;
        gameCount++;
    }
    
    //무승부
    public void even() {
        gameCount++;
    }
    
    @Override
    public String toString() {
        return "["
                + name + " : "
                + gameCount + " games, "
                + winCount + " win, "
                + loseCount + " lose]";
    }
}
public class Main{
    public static void main(String[] args) {
        int seed1 = 314;
        int seed2 = 15;
        
        Player player1 = new Player("Kim", new WinningStrategy(seed1));
        Player player2 = new Player("Lee", new ProbStrategy(seed2));
        
        for(int i = 0; i < 10000; i++) {
            Hand nextHand1 = player1.nextHand();
            Hand nextHand2 = player2.nextHand();
            
            if(nextHand1.isStrongerThan(nextHand2)) {
                System.out.println("Winner : " + player1);
                player1.win();
                player2.lose();
            }else if(nextHand2.isStrongerThan(nextHand1)) {
                System.out.println("Winner : " + player2);
                player1.lose();
                player2.win();
            }else{
                System.out.println("Even...");
                player1.even();
                player2.even();
            }
        }
        
        System.out.println("Total result : ");
        System.out.println(player1);
        System.out.println(player2);
    }
}

 

구조

Hand 가위바위보에서 손을 나타내는 클래스
Strategy 가위바위보의 '전략'을 나타내는 인터페이스
WinningStrategy 이기면 다음에도 같은 손을 내는 전략의 클래스.
Strategy의 구현체
ProbStrategy 직전 손에서 다음 손을 확률적으로 계산하는 전략의 클래스.
Strategy의 구현체
Player 가위바위보를 하는 플레이어 클래스

 

메인메소드 실행 시 10,000번의 게임을 진행하고 가장 마지막에 A와 B의 승리, 패배 횟수가 출력된다.

Hand는 enum 클래스로 가위, 바위, 보 라는 세개의 enum 상수를 갖고 있으며, 필드로 name, handValue를 갖고 있다.

또한 isStrongerThan() 메소드를 통해 A와 B의 결과를 반환 할 수 있다.

각 전략에 대한 인터페이스인 Strategy가 존재한다.

그리고 전략에 대한 구체화를 표현한 WinningStrategy, ProbStrategy가 있다.

각 플레이어는 이름과 전략을 갖고 생성된다.

그래서 게임이 진행될때마다 각 플레이어는 자신의 전략에 따라 다음에 낼 손을 결정하게 된다.

 

Strategy 패턴의 구조는 아래와 같다.

1. Strategy - 전략을 이용하기 위한 인터페이스. 예제에서 인터페이스로 처리했지만 추상 클래스로 처리하는 것도 가능하다.

2. ConcreteStrategy - Strategy의 구현체. 여러개의 ConcreteStrategy로 다양한 전략이 가능하다.

3. Context - Strategy를 이용하는 주체. ConcreteStrategy의 인스턴스를 갖고 있다가 필요에 따라 이용한다. 예제에서의 Player 클래스.

 

마무리 정리는 조금 뒤로 미루고 헤드퍼스트 디자인 패턴 책에서의 예제도 정리한다.

예제 프로그램은 오리 시뮬레이터이다.

객체로는 일반 오리와 모형오리 두가지가 존재.

 

코드 먼저 정리한다.

이 예제에서는 패키지가 나눠져 있으니 패키지 구조도 확인.

//예제 출처 - 헤드퍼스트 디자인 패턴

package strategyPattern.headfirst.fly;

public interface FlyBehavioral {
    public void fly();
}
package strategyPattern.headfist.fly;

public class FlyNoWay implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날지 못해요");
    }
}
package strategyPattern.headfist.fly;

public class FlyRocketPowered implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("로켓 추진!!!!!!!!!!!");
    }
}
package strategyPattern.headfist.fly;

public class FlyWithWings implements FlyBehavioral {
    
    @Override
    public void fly() {
        System.out.println("날고 있어요");
    }
}
package strategyPattern.headfist.quack;

public interface QuackBehavioral {
    
    public void quack();
}
package strategyPattern.headfist.quack;

public class MuteQuack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("---");
    }
}
package strategyPattern.headfist.quack;

public class Quack implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
}
package strategyPattern.headfist.quack;

public class Squeak implements QuackBehavioral {
    
    @Override
    public void quack() {
        System.out.println("삑!");
    }
}
package strategyPattern.headfirst;

public abstract class Duck {
    
    FlyBehavioral flyBehavioral;
    
    QuackBehavioral quackBehavioral;
    
    public Duck() {
    
    }
    
    public void swim() {
        System.out.println("모든 오리는 물에 뜹니다.");
    }
    
    public abstract void display();
    
    public void perfomFly() {
        flyBehavioral.fly();
    }
    
    public void performQuack() {
        quackBehavioral.quack();
    }
    
    public void setFlyBehavioral(FlyBehavioral fb) {
        flyBehavioral = fb;
    }
    
    public void setQuackBehavioral(QuackBehavioral qb) {
        quackBehavioral = qb;
    }
}
package strategyPattern.headfirst;

public class MallardDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("물오리!");
    }
    
    public MallardDuck() {
        quackBehavioral = new Quack();
        flyBehavioral = new FlyWithWings();
    }
}
package strategyPattern.headfirst;

public class ModelDuck extends Duck {
    
    @Override
    public void display() {
        System.out.println("모형오리!");
    }
    
    public ModelDuck() {
        flyBehavioral = new FlyNoWay();
        quackBehavioral = new MuteQuack();
    }
}
package strategyPattern.headfirst;

public class DuckSimulator {
    public static void main(String[] args) {
        // 물오리
        Duck mallardDuck = new MallardDuck();
        mallardDuck.display();
        mallardDuck.performFly();
        mallardDuck.performQuack();
        
        System.out.println("------------");
        
        Duck modelDuck = new ModelDuck();
        modelDuck.display();
        modelDuck.performFly();
        modelDuck.setFlyBehavioral(new FlyRocketPowered());
        modelDuck.performFly();
        modelDuck.performQuack();
    }
}

메인 메소드를 실행해보면

물오리!

날고 있어요

꽥!

----------

모형오리!

날지 못해요

로켓 추진!!!!!!!

---

 

이렇게 출력된다.

그럼 책에서의 예시를 정리해보자.

다양한 종류의 오리의 모습을 보여주고, 헤엄치고 울 수 있는 시뮬레이터를 만들었다.

 

그래서 Duck 이라는 클래스에 quack(), swim(), display() 메소드를 만들었다.

모든 오리는 꽥꽥 소리를 내고 헤엄을 칠 수 있기 때문에 display() 메소드만 추상 메소드로 만들었다.

그래서 각 오리 객체는 Duck 클래스를 상속받고, display() 메소드를 각각 구현하고 있다.

 

그런데 시뮬레이터를 서비스하다가 날아가는 것도 표현되었으면 좋겠다는 사용자 의견에 따라 날아 가는 기능을 추가하게 되었다.

그래서 Duck 클래스에 fly() 메소드를 추가하게 되었다.

시간이 조금 지나서 좀 더 다양한 객체를 추가하려고 러버덕을 추가하기로 했다.

러버덕 객체 역시 다른 오리 객체들과 마찬가지로 Duck을 상속받는 형태로 작성했다.

그랬더니??

날 수 없는 러버덕이 날아다녔다.

또한 러버덕은 꽥꽥 소리가 아닌 삑삑 이라는 소리가 나야 하는데 똑같이 꽥꽥 소리를 내며 울고 있었다.

 

그래서 러버덕 객체에서는 quack() 메소드와 fly() 메소드를 모두 오버라이드해 처리하는 방법을 생각했지만, 그 후 객체를 계속 추가하게 된다면 이것 역시 비효율적이라는 생각을 하게 된다.

 

그래서 분리를 생각하게 되었고, 공통적으로 모든 객체가 수행하는 기능인 swim()과 display() 메소드는 Duck 클래스에 남겨두고, fly()와 quack()은 각각의 인터페이스로 분리한 뒤 필요로 하는 클래스에서 인터페이스들을 확장해 구현하도록 처리한다.

 

여기까지가 책에서의 예시이다.

그럼 위와 같이 처리했을 때의 문제점으로는 무엇이 있을까?

바로 코드의 중복이다.

디자인패턴의 장점으로 유지보수성과 재사용성이 있다.

인터페이스로 분리하는것까지는 좋았지만, 인터페이스를 확장해 구현하는 모든 객체에서 동일한 기능을 하는 객체가 단 하나도 없다면 이러한 분리도 괜찮을 수 있을지도 모르겠다.

하지만, 가장 먼저 시뮬레이터에 생성한 객체인 실제 오리들의 날아가는 행동이 '날고 있어요' 라는 기능으로 동일하다면?

모든 해당 객체들에서는 인터페이스 구현으로 인해 '날고 있어요' 라는 기능의 코드가 작성되어 있을 것이다.

그럼 코드의 중복이 발생한다는 것이다. 중복이 발생한다는 것은 동일한 코드를 사용하고 있음에도 재사용을 하지 않고 새로운 코드를 사용한다고 할 수 있다.

또한, 이 '날고 있어요' 라는 기능을 '날고 있네요??' 라고 수정해야 하는 상황이 왔다면??

그리고 여기에 해당하는 객체들이 몇십 몇백개라면???

모든 객체들의 구현 코드를 수정해야 하는 상황이 발생하게 된다.

 

이 문제의 해결책이 예제이다.

분리까지는 좋은 방법이니 swim, display는 Duck에 그대로 두고 Quack과 Fly를 인터페이스로 만들어준다.

단, 이 인터페이스들을 각 객체에서 구현하는 것이 아닌 클래스 집합으로 문제를 해결한다.

예제 구조를 보면 FlyBehavioral 인터페이스를 구현하는 클래스는 FlyNoWay(날지 못해요), FlyRocketPowered(로켓 추진!!!!), FlyWithWings(날고 있어요) 이렇게 존재한다.

그리고 각 오리 객체에서는 이를 구현하지 않고 있으며, Duck 클래스에서는 각 인터페이스변수를 갖고 있게 된다.

각 객체는 Duck 클래스를 상속받고 있기 때문에 생성자를 통해 각 인터페이스 변수에 행동 클래스 인스턴스를 대입하게 된다.

그 코드가 각 객체 생성자에 있는 flyBehavioral = new FlyWithWings(); 코드이다.

그럼 객체가 생성되고 행동 처리를 하는 메소드를 호출하게 된다면 행동 클래스 인스턴스가 대입되어 있는 인터페이스 변수를 통해 기능을 수행하게 되는 것이다.

 

이렇게 수행했을때를 한번 살펴보면,

일단, 오리 객체들이 직접 fly()를 구현하고 있지 않기 때문에 코드의 중복이 발생하지 않는다.

동일하게 날아가는 모습을 표현하는 오리 객체가 200개가 존재한다고 했을 때, 

이 객체들은 생성자를 통해 flyBehavioral에 행동 클래스 인스턴스를 담아주기만 하면 될 뿐이지 직접 '날고 있어요' 라는 기능의 코드를 작성할 필요가 없게 된다.

그럼 FlyWithWings()의 구현체는 재사용이 처리되고 있는 코드라고 볼 수 있고, 그로인해 중복이 제거되며 재사용성이 높아졌다고 볼 수 있게 된다.

 

다음은 유지보수 관점에서다.

각 객체에서 구현하는 경우 '날고 있어요'를 '날고 있네요??' 라고 수정하기 위해서는 200개의 객체를 모두 수정해야 했다.

하지만 이렇게 구현을 해두고 나면 FlyWithWing() 의 구현체만 수정한다면 문제가 해결된다.

그럼 200개의 메소드를 수정할 필요없이 단 한개의 메소드 수정으로 문제가 해결되는 것이다.

 

또한, 클래스 집합에 대한 디자인은 최대한 유연하게 만드는 것이 좋다.

메인 메소드를 보면 모형오리는 날지 못하는 것이 생성자를 통한 기본값이지만, setter를 통해 FlyRocketPowered 클래스 인스턴스를 담도록 처리한다.

그리고 모형오리를 날도록 해보면 로켓을 달아놓은 모형오리가 된다.

이렇게 유연하게 만들면 다양한 기능을 사용할 수 있도록 처리할 수 있다.

 

위 정리에서의 중점은 나는 행동과 우는 행동을 Duck(슈퍼 클래스)나 그 서브클래스에서 정의한 메소드를 사용해 구현하는 것이 아닌 다른 클래스에 '위임'한다는 것이다.

 

 

정리

두 책에서의 예제는 완전 다른 스타일의 예제였다.

JAVA 언어로 배우는 디자인패턴 입문 책의 예제는 전략 패턴 그 자체에 집중한 예제였다.

가위바위보를 수행하는 프로그램을 만들것이지만, 승리했을 때 이전에 냈던 손을 그대로 낸다는 전략(WinningStrategy)와 직전에 낸 손에서 다음 손을 확률적으로 계산해 낸다는 전략(ProbStrategy)가 존재하며 플레이어는 생성될 때 어떤 전략을 사용할 것인지를 결정하게 되고, 이 전략은 Strategy 라는 인터페이스를 통해 같은 메소드의 구현체로 전략을 구현한다.

그리고 플레이어는 생성될 때 전략을 받아 Strategy 인터페이스 변수에 전략을 담아두고 게임에 임하는 내내 해당 전략을 구사한다.

 

이렇게 구현한다면 각 전략을 플레이어 객체에서 구현하느라 여러 플레이어 객체를 생성해 구현할 필요가 없고 전략 인터페이스를 구현하는 집합체로 구현 클래스를 통한 전략 설계를 하면 된다.

 

헤드퍼스트 디자인 패턴에서는 전략 패턴은 캡슐화가 중요하다! 라는 느낌이었다.

아무래도 좀 더 다양한 기능에 대한 예제라 그렇지 않나 싶다.

시뮬레이터라는 프로그램에서의 기능을 전략이라고 볼 수 있다.

직관적으로 이게 전략이다! 라는 느낌은 없었지만 이렇게 캡슐화를 해야 코드의 재사용성과 유지보수성을 높일 수 있다 라는 것에 대해 이해하기는 더 좋았다.

 

두 책에서의 공통적인 중점.

전략패턴은 알고리즘을 캡슐화 해 각각의 알고리즘 수정에 다른 클래스를 수정해야 하는 경우가 발생하지 않도록 해야 한다.

구현체인 ConcreteStrategy를 수정하거나 추가로 생성하는 형태로 처리할 수 있으며, 위임이라는 약한 결합으로 알고리즘 수정이 용이하다.

 

 

 

 

 

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


어댑터 패턴(Adapter Pattern)은 구조패턴(Structural Pattern)에 속하는 패턴이다.

현실에서 어댑터라는 것은 보통 전류 변환을 해주는 용도로 사용이 된다.

제공되는 것과 필요한 것 사이에서 그 사이를 채워주는 역할을 하는 것이 어댑터이다.

책에서는 이렇게 예시를 들고 있다.

노트북에 필요한 전류는 12볼트이지만 콘센트에서는 100볼트로 들어온다면, 그것을 변환해줘야 하는 것이 필요하다.

100볼트로 들어온 전류를 12볼트로 변환해주는 역할을 어댑터가 담당한다.

 

프로그램에서도 이미 제공된 코드를 그대로 사용할 수 없을 때, 필요한 형태로 변환 후 이용하는 경우가 발생했을 때 어댑터 패턴을 사용한다.

어댑터 패턴은 Wrapper 패턴이라고 불리기도 한다.

감싼다는 의미인데 무엇인가를 포장해서 다른 용도로 사용할 수 있도록 변환해주는것이 Wrapper이자 어댑터이다.

 

어댑터 패턴에는 두가지 종류가 있다.

1. 클래스에 의한 어댑터(상속을 사용한 패턴)

2. 인스턴스에 의한 어댑터(위임을 사용한 패턴)

 

예제

 

예제 프로그램은 주어진 문자열을 ( ) 안에 넣어 출력하거나 * * 안에 넣어 출력하도록 하는 프로그램이다.

 

1. 클래스에 의한 어댑터

메인 클래스를 제외하고 2개의 클래스와 1개의 인터페이스로 작성한다.

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Banner {
    
    private String string;
    
    public Banner(String string) {
        this.string = string;
    }
    
    public void showWithParen() {
        System.out.println("(" + string + ")");
    }
    
    public void showWithAster() {
        System.out.println("*" + string + "*");
    }
}
public interface Print {
    
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner extends Banner implements Print {
    
    public PrintBanner(String string) {
        super(string);
    }
    
    @Override
    public void printWeak() {
        showWithParen();
    }
    
    @Override
    public void printStrong() {
        showWithAster();
    }
}
public class AdapterMain {
    
    public static void main(String[] args) {
        Print p = new PrintBanner("Hello");
        
        p.printWeak();
        p.printStrong();
    }
}

처리 과정을 먼저 보면 PrintBanner 생성자의 super를 통해 Banner 의 생성자에 접근하게 된다.

그리고 생성된 인스턴스는 Print를 구현하고 있는 구현체이기 때문에 Print 타입의 p 변수에 담을 수 있게 된다.

그리고 여기서 printWeak과 printStrong을 호출하면 PrintBanner 클래스에서는 Banner를 상속받고 있기 때문에

Banner의 showWithParen()과 showWithAster()를 바로 호출해 사용할 수 있게 된다.

그래서 (Hello)와 *Hello*가 출력되게 된다.

 

여기서 Banner 클래스는 문자열을 괄호로 묶는 메소드와 * * 로 묶는 메소드가 준비되어 있다.

이 Banner 클래스를 위 전류 예시 중 콘센트에서 제공되는 100볼트라고 가정한다.

 

Print 인터페이스에서는 printWeak과 printStrong이 선언되어 있다.

이 인터페이스를 12볼트가 필요한 노트북이라고 가정한다.

 

마지막으로 PrintBanner 클래스는 Banner 클래스를 상속받으며 Print 인터페이스를 구현하고 있다.

PrintBanner에서는 부모클래스인 Banner의 showWithParen()으로 printWeak을 구현하고 있고, showWithAster()로 printStrong을 구현하고 있다.

이 PrintBanner 클래스가 어댑터의 역할을 한다.

 

정리해보자면 아래와 같다.

- 제공되는 것(100볼트) -  Banner(showWithParen, showWithAster)

- 전류 변환장치(어댑터) -  PrintBanner

- 필요한 것(12볼트)       -  Print 인터페이스(printWeak, printStrong)

 

그럼 메인메소드 코드를 이렇게 볼 수 있다.

PrintBanner 인스턴스를 Print 타입으로 생성해준다.

Print에서 사용하고자 한 printWeak, printStrong을 호출하게 되면

어댑터인 PrintBanner는 제공되는 Banner의 showWithParen, showWithAster를 Print()가 사용할 수 있게 해준다.

 

그럼 메인 메소드에서는 Print 인터페이스를 통해 요청을 처리하고 있게 되는 것이기 때문에 Print 인터페이스에만 관심이 있을 뿐, Banner클래스와 PrintBanner 클래스가 어떻게 구현되어있는지는 관심이 없게 된다.

즉, Banner와 PrintBanner에서 어떻게 처리하는지는 알 수 없고, Print 인터페이스에 printWeak(), printStrong()이 선언되어 있다는것만 알고 있는 것이다.

마치 노트북에서 12볼트로 동작을 하지만 어댑터에 들어오는 전류가 100볼트이던 200볼트이던 상관이 없다는 것이다.

이렇게 구현하게 되면 수정이 필요하게 되더라도 메인메소드의 수정이 발생하지 않고 Banner 클래스나 PrintBanner 클래스에서의 수정만 발생하게 된다.

 

 

2. 인스턴스에 의한 어댑터

클래스에 의한 어댑터와 다른 점으로는 상속이 아닌 위임을 사용한다는 점이다.

자바에서 위임은 어떤 메소드의 실제 처리를 다른 인스턴스의 메소드에 맡기는 것을 말한다.

예제는 Banner 클래스는 수정이 없고, Print 인터페이스를 추상클래스로, 그리고 PrintBanner 클래스에서 Banner 클래스를 상속받지 않도록 수정한다.

public abstract class Print2 {
    public abstract void printWeak();
    
    public abstract void printStrong();
}
public class PrintBanner2 extends Print2 {
    
    private Banner banner;
    
    public PrintBanner2(String string) {
        this.banner = new Banner(string);
    }
    
    @Override
    public void printWeak() {
        banner.showWithParen();
    }
    
    @Override
    public void printStrong() {
        banner.showWithAster();
    }
}
public class AdapterMain {
    public static void main(String[] args) {
        Print2 p2 = new PrintBanner2("Hi");
        
        p2.printWeak();
        p2.printStrong();
    }
}

1번과 다른점을 보자면 Print 인터페이스가 추상클래스인 Print2로 바뀌었다는 점.

PrintBanner2에서는 Banner를 필드로 갖고 생성자에서는 상위 클래스가 아니기 때문에 new 키워드를 통한 인스턴스를 생성해 필드에 담아주게 된다.

그리고 구현메소드 역시 상속에서 벗어났기 때문에 그냥 사용할 수 없어 banner 필드를 통해 호출한다.

 

자바에서는 다중상속이 불가능하기 때문에 Print2 클래스를 상속받은 구현체라면 Banner 클래스를 또 상속받을 수 없다.

그래서 Banner 필드를 갖게 되는 것이고 이 필드를 통한 호출로 인해 위임이 발생하게 된다.

즉, PrintBanner2에서 printWeak() 메소드가 호출되었을 때 자신이 처리하지 않고, 다른 인스턴스인 banner의 showWithParen()으로 위임하게 되는 것이다.

 

 

처음에는 이게 너무 이해가 어려웠다.

PrintBanner 클래스가 어댑터라는데 이게 왜 어댑터인지, 중간에서 뭘 변환해준다는 것인지 이해하기가 어려웠다.

그래서 헤드퍼스트 디자인 패턴에서의 예제를 봤는데 이게 좀 더 이해가 쉬웠다.

 

예제 프로그램은 아래와 같다.

Duck, Terkey 라는 객체가 존재하고, 

Duck은 꽥! 이라는 소리를 내고 날아간다.

Terkey는 골골 이라는 소리를 내고 5번을 짧게 날아간다.

 

위 예제에서와는 다르게 다른 처리과정이 존재하는 예제라고 볼 수 있다.

구조는 인터페이스로 Duck, Terkey를 갖게 되고, 클래스로 MallardDuck, WildTurkey, TurkeyAdapter가 존재한다.

그리고 메인 클래스로 DuckMain 클래스가 있다.

//예제 출처 - 헤드퍼스트 디자인 패턴

public interface Duck {
    public void quack();
    
    public void fly();
}
public class MallardDuck implements Duck {
    
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
    
    @Override
    public void fly() {
        System.out.println("날고 있어요!");
    }
}
public interface Turkey {
    public void gobble();
    
    public void fly();
}
public class WildTurkey implements Turkey {
    
    @Override
    public void gobble() {
        System.out.println("골골");
    }
    
    @Overrdie
    public void fly() {
        System.out.println("짧게 나는중!");
    }
}
public class TurkeyAdapter implements Duck {
    private Turkey turkey;
    
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    
    @Override
    public void quack() {
        turkey.gobble();
    }
    
    @Override
    public void fly() {
        for(int i = 0; i < 5; i++)
            turkey.fly();
    }
}
public class DuckMain {
    public static void main(String[] args) {
        Turkey turkey = new WildTurkey();
        
        System.out.println("칠면조가");
        turkey.gobble();
        turkey.fly();
        
        Duck duck = new MallardDuck();
        
        System.out.println();
        System.out.println("오리가");
        testDuci(duck);
        
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        System.out.println();
        System.out.println("칠면조 어댑터가");
        testDuck(turkeyAdapter);
    }
    
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

실행해보면

칠면조가 한번 울고 한번 날고, 오리가 한번 울고 한번 날고, 칠면조 어댑터가 한번 울고 5번을 나는것을 볼 수 있다.

 

Duck은 quack()과 fly()메소드를, Turkey는 gobble(), fly() 메소드를 갖고 있고

각각 MallardDuck과 WildTurkey라는 구현체를 갖고 있다.

각 구현체에서는 울음소리와 날아가는 것을 한번씩 출력하도록 되어있다.

 

그리고 칠면조 어댑터가 존재한다.

이 어댑터의 역할은 칠면조를 Duck과 같이 사용하도록 하는 것이다.

메인 클래스의 testDuck 메소드에서는 Duck 타입의 매개변수를 받아 quack()과 fly()를 호출하고 있다.

이 testDuck 메소드는 들어오는 매개변수가 오리인지 칠면조인지 전혀 구분하지 못하지만 다른 결과를 출력하게 된다.

그럼 이 결과로 gobble()과 fly()를 갖고 있는 Turkey 인터페이스가 어댑터를 통해 Duck과 같은 quack(), fly()로 변환되어 동작하고 있다고 볼 수 있고, testDuck 메소드는 타깃 인터페이스인 Duck를 통해 어댑터에게 요청을 보낼 뿐 어댑터에 연결된 반대쪽 객체가 무엇인지에 대해서는 알 수 없다.

 

정리.

어댑터 패턴은 새로운 객체를 추가하여 연결해주는 목적이라기 보다 호환이 되지 않는 서로 다른 인터페이스(오리와 칠면조)를 어댑터를 통해 연결하여 사용할 수 있도록 하는 패턴이다.

JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.

 

생성패턴(Creational Pattern)

1. 싱글톤(Singleton)

2. 빌더(Builder)

3. 팩토리 메소드(Factory Method)

4. 추상 팩토리(Abstract Factory)

5. 프로토타입(Prototype)

 

구조패턴(Structural Pattern)

1. 어댑터(Adapter)

2. 브릿지(Bridge)

3. 컴포지트(Composite)

4. 데코레이터(Decorator)

5. 퍼사드(Facade)

6. 플라이웨이트(flyweight)

7. 프록시(Proxy)

 

행동(행위) 패턴(Behavioral Pattern)

1. 책임 연쇄(Chain of Responsibility)

2. 커맨드(Command)

3. 인터프리터(Interpreter)

4. 반복자(Iterator)

5. 중재자(Mediator)

6. 메멘토(Memento)

7. 옵저버(Observer)

8. 상태(State)

9. 전략(Strategy)

10. 템플릿 메소드(Template Method)

11. 방문자(Visitor)


 

반복자(Iterator)는 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.

자바에서 배열의 모든 요소를 표현하기 위해서는 반복문을 사용해 처리하는 경우가 많다.

for문을 통해 사용하게 되는 경우가 많고 이 경우 i 라는 인덱스 변수를 사용해 i를 증가시켜가며 검색하게 되는데

이 인덱스 변수인 i 의 기능을 추상화 하여 일반화 한 것을 디자인 패턴에서는 Iterator 패턴이라고 한다.

무엇인가 많이 모여있을 때 이를 순서대로 가리키며 전체를 검색하고 처리를 반복하는 것이다.

Iterate의 의미는 '반복하다'라는 의미이기 때문에 Iterator를 반복자라고 부른다.

 

예제

예제 프로그램으로는 책장(BookShelf) 안에 책(Book)을 넣고 책 이름을 차례대로 표시하는 프로그램이다.

 

//예제 출처 - JAVA 언어로 배우는 디자인 패턴 입문

public class Book {
    private String name;
    
    public Book(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    private Book[] books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}
import java.util.Iterator;
import java.util.NoSuchElementException;

public class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    
    private int index;
    
    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }
    
    @Override
    public boolean hasNext() {
        if(index < bookShelf.getLength())
            return true;
        else
            return false;
    }
    
    @Override
    public Book next() {
        if(!hasNext())
            throw new NoSuchElementException();
        
        Book book = bookShelf.getBookAt(index);
        
        index++;
        
        return book;
    }
}
import java.util.Iterator

public class BookMain {
    
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        
        bookShelf.appendBook(new Book("Around the world in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Daddy-Long-Legs"));
        
        //명시적으로 Iterator를 사용하는 방법
        //Iterator 인스턴스를 생성
        Iterator<Book> it = bookShelf.iterator();
        
        //hasNext()가 false가 될때까지.
        //즉, 다음 객체가 존재하지 않을때까지
        //book에 next()로 값을 받아오고 책 이름을 출력한다.
        while(it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        
        System.out.println();
        
        //확장 for문을 사용하는 경우
        for(Book book : bookShelf)
            System.out.println(book.getName());
    }
}

 

 

 

구조

BookShelf

- 책장을 나타내는 클래스. Iterable<book> 인터페이스를 구현한다.

- books라는 Book 타입 배열이 존재하며, 크기는 BookShelf 인스턴스 생성 시 생성자 인수를 통해 결정된다.

- last는 appendBook() 메소드를 통해 books에 데이터가 추가될때마다 하나씩 증가하게 되며 배열의 인덱스를 담당한다.

- 그 외 메소드로는 배열의 가장 끝에 있는 데이터를 반환하는 getBookAt()과 인덱스를 반환하는 getLength()가 있다.

- 마지막으로 Iterable<Book> 인터페이스를 구현한 iterator() 메소드가 존재한다.

- 이 메소드는 BookShelf 클래스에 대응하는 Iterator로서, BookShelfIterator 클래스의 인스턴스를 생성하여 반환한다.

- 책장에 꽂혀있는 책을 반복해서 처리하고자 할 때 이 iterator() 메소드를 호출.

 

BookShelfIterator

- BookShelf 클래스의 검색을 실행하는 클래스이다.

- 생성자에서는 BookShelf 인스턴스를 인수로 받아 bookShelf 필드에 담아주고 index 필드는 0으로 초기화 한다.

- Iterator<Book> 인터페이스를 구현한다.

- hasNext() 메소든느 다음 책이 있는지 확인하고 다음 책이 있다면 true, 없다면 false를 리턴하도록 한다.

- next() 메소드는 다음 책이 존재하지 않는다면 NoSuchElementException을 발생시키고 존재한다면 해당 객체를 반환한다.

- 반환 전 index 필드 값을 하나 미리 증가시켜 다음 next() 를 준비하도록 한다.

 

Iterable<E>

- 처리를 반복할 대상을 나타내는 것으로 java.lang에 선언되어 있다.

- 이 인터페이스의 구현체는 배열같은 집합체가 된다. 예제에서는 Book을 모은 인터페이스를 사용하기 때문에 Iterable<Book>으로 선언한다.

- Iterable 인터페이스에는 iterator 메소드가 선언되어있는데 집합체에 대응하는 Iterator<E>를 만들도록 하기 위함이다.

- 집합체에 포함된 요소를 하나하나 처리해나가고자 할 때는 iterator 메소드를 사용해 Iterator<E> 인터페이스를 구현한 클래스의 인스턴스를 하나 만든다.

 

Iterator<E>

- 하나하나의 요소 처리를 반복하기 위한 것으로 루프 변수(i) 와 같은 역할을 한다.

- 선언되는 메소드로는 hasNext()와 next()가 있다.

- hasNext()는 다음 요소가 존재하는지 여부에 대한 것이고 next()는 다음 요소를 가져오는 것이다.

- next()의 경우는 보이지 않는 곳에서 다음에 다시 호출되었을 때 다음 요소를 반환할 수 있도록 사전에 미리 준비를 하게 된다.

 

 

예제 프로그램에서의 역할들

Iterator(반복자) 역 - Iterator<Book>

- 요소를 순서대로 검색하는 인터페이스를 결정한다.

- 다음 요소의 존재여부를 파악하는 hasNext()와 다음 요소를 가져오는 next()를 결정한다.

 

ConcreteIterator(구체적인 반복자) 역 - BookShelfIterator

- Iterator 인터페이스를 구현하는 구현체 역할이다.

- 검색에 필요한 정보를 가지고 잇어야 하기 때문에 BookShelf 클래스의 인스턴스를 bookShelfIterator 클래스의 bookShelf 필드에서 기억하고, 검색중인 위치를 index 필드에서 기억한다.

 

Aggregate(집합체) 역 - Iterable<Book>

- Iterator를 만들어내는 인터페이스를 결정한다.

- 이 인터페이스는 내가 가진 요소를 차례대로 검색해 주는 것을 만들어내는 메소드이다.

 

ConcreteAggregate(구체적인 집합체) 역 - BookShelf

- Aggregate가 결정한 Iterable 인터페이스를 실제로 구현하는 구현체 역할이다.

- 구체적인 Iterator 역할인 ConcreateIterator의 인스턴스를 만들어 낸다.

 

 

for 문 대신 Iterator를 사용하는 이유

Iterator도 for문처럼 반복을 통해 다음 값을 가져오게 되는데 굳이 Iterator를 써야 하는 이유가 무엇일까.

이유는 구현과 분리해서 사용할 수 있기 때문이다.

Iterator의 hasNext()와 next()는 Iterator의 메소드일 뿐이지 실질적인 BookShelf의 구현에 사용되는 것은 아니다.

또한 Iterator 인터페이스는 BookShelf의 메소드에 접근해야할 이유도 없다.

 

기존 예제는 배열을 사용하고 있지만 만약 리스트로 수정하고자 한다면?

Iterator를 사용하는 경우에는 BookShelf에서 Book[] 을 List<Book>으로 수정하고 appendBook에서 this.books.add로 수정만 해주면 된다.

import java.util.Iterator;

public BookShelf implements Iterable<Book> {
    //private Book[] books;
    private List<Book> books;
    
    private int last = 0;
    
    public BookShelf(int maxSize) {
        this.books = new Book[maxSize];
    }
    
    public Book getBookAt(int index) {
        return books[index];
    }
    
    public void appendBook(Book book) {
        //this.books[last] = book;
        this.books.add(book);
        last++;
    }
    
    public int getLength() {
        return last;
    }
    
    @Override
    public Iterator<Book> iterator() {
        return new BookShelfIterator(this);
    }
}

이렇게 BookShelf 하나만 수정하면 메인클래스 포함 다른 클래스에서는 수정이 필요없어진다.

반면, Iterator를 사용하지 않는다면 메인 메소드에서는 books[i].getName() 형태로 출력하게 되었을 것이고,

그럼 이렇게 처리되고 있는 모든 것들에 대해 books.get(i) 형태로 수정해야 했을 것이다.

Iterator를 사용하게 되면서 코드 재사용성이 높아졌다고 볼 수 있다.

또한, 자바에서는 사용하지 않는 인스턴스에 대해 GarbegeCollection을 통해 자동으로 삭제되기 때문에 iterate에 대응하는 deleteIterator 메소드는 불필요하다.

 

그리고 메인메소드에서 추가로 하단에 확장 for문을 사용한 것을 볼 수 있다.

확장 for문의 구조를 보면 for(Book book : bookShelf) 로 처리하여 간단하게 Iterator를 통한 처리와 마찬가지로 book.getName()으로 출력할 수 있는것을 볼 수 있다.

이 코드로 알 수 있는 점으로는 확장 for문은 Iterator와 동일한 처리를 하고 있는 것을 알 수 있다.

물론 bookShelf에 Book 배열이 들어가있는 것이기 때문에 동일하게 Iterator가 구현이 되어있어야 이렇게 사용할 수 있게 된다.

단지 hasNext와 next를 굳이 명시하지 않아도 된다는 편의성이 제공되는 것이다.

+ Recent posts