1. ViewHolder의 필요성

 

앞선 글에서 설명했듯 많은 데이터를 

항목화하여 표현하는 AdapterView 에서는

동작할 때마다 각 항목

(xml형식의 레이아웃)을

View객체로 가져와야 한다

 

그리고 이렇게 생성한 View에 

포함된 뷰를 찾아

( Java 에서는 findViewById)

접근하는 일도 아주 빈번하게 일어난다.

 

문제는 이 작업은 

많은 리소스를 필요로 한다는 점이다. 

 

*참고

 

 

이를 해결하기 위해 사용되는 것이 ViewHolder이다.

한번 inflate 된 View를 객체에 담아 두어 

재사용하는 방식이다.

 

즉, View를 생성하고 활용하는 과정에서

리소스를 효율적으로 사용하기 위한

패턴이라고 볼 수 있다.

 

 

 

2. ListView에 ViewHolder 적용하기

 


이러한 ViewHolder 패턴은

꼭 RecyclerView에서만 사용되는 건 아니다.

오히려 ListView에

ViewHolder를 적용해보면
어떤 이점이 있는지 이해하기 편할 것 같다.

1) 기본 구성 (ViewHolder 없이 구현)

(1) activity_main.xml

먼저 기본적인 Main Activity의 화면 구성은 
다음과 같이 단순하게 해 놓았다

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

	<ListView
      android:id="@+id/listView"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:layout_marginStart="1dp"
      android:layout_marginLeft="1dp"
      android:layout_marginTop="1dp"
      android:layout_marginEnd="1dp"
      android:layout_marginRight="1dp"
      android:layout_marginBottom="1dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

(2) item.xml

ListView에 요소로 들어갈 

item 레이아웃을 생성한다.

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/textName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="MacBook Pro" />

    <TextView
        android:id="@+id/textColor"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="sliver" />

    <TextView
        android:id="@+id/textOs"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="OS X" />
</LinearLayout>

 

 

(3) NoteBook.kt

Data class를 선언한다.

하나의 항목에 들어갈 데이터를

저장하는 객체라고 보면 된다.

 

data class NoteBook(val name:String, val color:String, val os:String)

 

 

 

 

 

 

 

 

 

(4) CustomBaseAdapter.kt

일반적으로 구현하는 어댑터이다.

getView를 자세히 보면 

Item.xml을 inflate 하고 inflate 한 View에서 

textView를 찾아서 데이터를 바인딩해준다.

 

그리고 마지막으로 inflate한 View전체에 

click이벤트를 지정한다.

 

class CustomBaseAdapter(private val context:Context, private val dataList: List<NoteBook>) : BaseAdapter() {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // Inflating
        val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)

        val getData = dataList[position]

        // findViewById
        view.textName.text = getData.name
        view.textOs.text = getData.os
        view.textColor.text = getData.color

        // 이벤트 생성
        view.setOnClickListener(View.OnClickListener {
            Toast.makeText(context,getData.toString(),Toast.LENGTH_SHORT).show()
        })
        return view
    }

    override fun getItem(position: Int): Any {
        return dataList[position]
    }

    override fun getItemId(position: Int): Long {
        return 0
    }

    override fun getCount(): Int {
        return dataList.size
    }
}

 

 

** 이전 글에서 Adapter에 대한 내용과 일치하는지 확인하기

더보기

Adapter가 알고 있어야 하는 것


(1) Data가 무엇인지 알고 있어야 한다.
(2) 어떤 형태로 item을 구성할지 layout을 알고 있어야 한다.
(3) Data와 layout을 매핑할 수 있어야 한다.
(4) item도 화면에 표현하는 내용이기 때문에 Context를 알고 있어야 한다.

 

 

 

[Android] RecyclerView 이해하기 (1) - Adapter는 무엇인가

RecyclerView를 이해하기에 앞서 AdapterView를 알고 있어야 한다. 왜냐하면 나는 RecyclerView를 AdapterView의 확장판쯤으로 이해했기 때문이다. 1. AdapterView AdapterView는 많은 데이터를 하나의 뷰로 보여..

kyome.tistory.com

 

 

 

(5) MainActivity.kt

Main Activity에서는 지금까지 작성한 내용을 조합한다고 보면 된다.

Data를 list로 선언해서 Adapter에 초기화 값으로 선언한 후 

Adapter 를 ListView에 연결하면 정상적으로 동작한다.

 

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Data
        val noteBookList = listOf<NoteBook>(
            NoteBook("MacBook Pro","Sliver","OS X"),
            NoteBook("MacBook Pro","Gray","OS X"),
            NoteBook("Galaxy Book","Sliver","Windows")
        )

        //CustomAdapter를 View에 추가
        var arrayAdapter = CustomBaseAdapter(this,noteBookList)
        listView.adapter =arrayAdapter
}

 

2) 문제점

getView메서드 내에

Log.d를 찍어보면 알겠지만

화면에 Item이 보여야 할 때마다 

계속 반복적으로 호출된다.

 

앞에서 반복적으로 이야기했듯

문제는 여기서 생긴다.

 

getView 안을 살펴보면 아래와 같은 구문들이

아무 조건 없이 반복적으로 실행되기 때문이다.

 

...

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    
    /* 리소스 사용!! */
    // Inflating
    val view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)
    
    ...
    
    /* 리소스 사용!! */
    // findViewById
    view.textName.text = getData.name
    view.textOs.text = getData.os
    view.textColor.text = getData.color
    
    ...
    
    return view
}

 

 

3) 해결방법 : ViewHolder

해결방법은 간단하다.

View를 한번 만들었으면

어딘가에 담아 두었다가

필요할 때 재사용하는 것이다.

 

(1) VIewHodler class 생성

그 '어딘가'가 ViewHolder 클래스이다.

Item.xml에서 사용한 구성요소에 맞추어

변수를 선언하고 setter를 생성해두면 된다.

 

class ViewHolder() {
    // item을 구성하고 있는 View에 맞게 변수 생성
    
    var textName:TextView? = null
    var textOs:TextView? = null
    var textColor:TextView? = null

    // setter 생성
    fun setNoteBook(noteBook: NoteBook){
        textName?.text = noteBook.name
        textOs?.text = noteBook.os
        textColor?.text = noteBook.color
    }

}

 

 

(2) ViewHolder 사용하기

 

이렇게 생성된 ViewHolder는 

getView 메서드 내에서 사용된다.

 

이전에 View로 inflate 한 xml을

다시 inflate 하지 않기 위해
getView의 두 번째 매개변수를 활용한다.

 

두 번째 매개변수 convertView는 

이전에 inflate 한 View를 그대로 전달한다.

 

 

 

 

 

 

 

즉, convertView가 null이라면 최초 생성이라는

말이기 때문에 Inflate를 수행한다.

 

위에서 만들어 놓은 ViewHolder class를 

객체로 생성하고

이 객체에 Inflate 된 View를 매핑한다.

 

ViewHolder객체를 잘 생성했다면 

Inflate 된 View에 tag의 값으로

저장하고 반환한다.

 

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
    // return 
    val view:View
    val getData = dataList[position]

    val vh:ViewHolder

    if (convertView == null){
        // Inflating
        view = LayoutInflater.from(parent?.context).inflate(R.layout.item,parent,false)

        // 매핑해놓음
        vh = ViewHolder()
        vh.textName = view.textName
        vh.textOs = view.textOs
        vh.textColor =view.textColor

        // view에 viewholder 저장
        view.tag = vh

        // 이벤트 생성
        view?.setOnClickListener{
            Toast.makeText(context,getData.toString(),Toast.LENGTH_SHORT).show()
        }

    }else {
        view = convertView
        vh = convertView.tag as ViewHolder
    }
    //값 매칭
    vh.setNoteBook(getData)

    return view
}

이렇게 반환된 View는 화면에서 보이고

이후에 다시 getView가 실행될 때 convertView로 재사용된다.

convertView로 재사용되었다면

convertView안에 tag를 꺼내어 

ViewHolder로 활용한다.

 

ViewHolder에 Inflate 된 View가 다 매핑이 되었다면

ViewHolder의 setter를 통해

데이터를 세팅해주면 손쉽게 

값이 바인딩된다.

 

 

 

4. RecyclerView와 다른 점

RecyclerView는 ListView와 달리

앞에 설명한 내용을 

반드시 구현하도록 되어있다.

 

덕분에 자연스럽게 각 요소가 재활용될 수 있으며

추가적으로 배치 방법과 방향등을

LayoutManager를 통해 조작할 수 있어 

더 편하게 구현이 가능하다.

 

 

 

RecyclerView를 이해하기에 앞서

AdapterView를 알고 있어야 한다.

왜냐하면 나는 RecyclerView를

AdapterView의 확장판쯤으로 이해했기 때문이다.

 

 

1. AdapterView

AdapterView는 많은 데이터를

하나의 뷰로 보여주기 위해 만들어진 뷰이다.

즉, 여러 항목을 보여주는 뷰이다.

 

많은 데이터를 View로 보여주기 위해

Adapter라는 것을 사용하기 때문에

지어진 이름이지 않을까 싶다.

 

 

1) 구성요소

 

 

AdapterView에 대한 정의를 내린것에서

구성요소를 추출할 수 있다.

 

"항목을 보여준다"

 

항목을 보여준다는 표현에서 알 수 있듯

 

데이터로 구성되어 있는 하나의 요소가 존재하고

이러한 요소를 각자의 방식대로 보여주는 것이다.

 

즉, (1) 데이터를 (2)하나의 요소로 구성하고

여러 개의 요소들을 (3) View에 나열한다는 것이다.

 

 

 

 

 

 

 

위의 정의에서 볼 수 있듯 크게 3가지 구성요소로 정리할 수 있다.

 

(1) Data

뷰의 내용이 되는 정보

데이터 저장소

 

(2) Adapter

Data를 요소(Item)로 구성하여

View에서 활용 가능하도록 만드는 객체

 

(3) View

나름의 표현방식으로 Item을 보여주는 View

 

 

2) 여전히 정의되지 않은 Adapter

 

 

위의 내용처럼 정리를 해보니

Data와 View의 역할을 뚜렷한데

Adpater는 그래서 뭘 하겠다는 건지

여전히 정리가 잘 안 돼서

다시 세부적으로 정리했다.

 

Adapter는 정보를 가공하여

하나의 View(item)를 만든다.

 

Adpater가 만든 View는

(3) View의 하나의 항목이 된다.

 

 

- Adapter가 알고 있어야 하는 것

(1) Data가 무엇인지 알고 있어야 한다.

(2) 어떤 형태로 item을 구성할지 layout을 알고 있어야 한다.

(3) Data와 layout을 매핑할 수 있어야 한다.

(4) item도 화면에 표현하는 내용이기 때문에 Context를 알고 있어야 한다.

 

( 이 내용은 제가 이해하기 위해 추측을 더하여 작성한 부분이 기입니다.

실제와 다를 수 있습니다. 더 공부가 필요한 부분입니다. )

 

 

3) 예시

대표적인 AdpaterView 인 Spinner를 통해

정리한 내용이 맞는지 확인해보았다.

 

val spinner: Spinner = findViewById(R.id.spinner)
// Create an ArrayAdapter using the string array and a default spinner layout
// Spinner 생성

// Adapterter 생성 
// (1) Context 넣음
// (2) 리소스에서 R.array.planets_array 배열을 데이터로 넣음
// (3) item layout 넣음

ArrayAdapter.createFromResource(
        this,
        R.array.planets_array,
        android.R.layout.simple_spinner_item
).also { adapter ->

    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
    
    // Spinner에 adapter 연결
    spinner.adapter = adapter
}

출처:  공식문서 [developer.android.com/guide/topics/ui/controls/spinner?hl=ko]

 

 

 

 

 

 

ArrayAdapter의 생성 파라미터를

통해 확인할 수 있듯

Adapter는 컨텍스트가 무엇인지,

데이터가 무엇인지,

item은 어떤 레이아웃을 사용할지를 알고 있다.

ArrayAdapter는 배열 형식의 데이터를

안드로이드 기본 layout에 매칭 할 수 있다.

 

 

 

 

4) 기존 AdapterView의 한계는?

Spinner나 ListView (Legacy)와

같은 AdapterView에서는

매번 Item을 만들 때마다 Inflating을 해야 한다.

xml을 객체로 만드는 과정인데 수많은 요소들을
반복해서 View를 만드는 것은 리소스 낭비이며

부하를 일으킨다고 한다.

 

 

 

 

5) RecyclerView는 한계를 해결했는가?

화면에 보이는 View는 어차피 한정되어있다

예를 들어 500개의 항목을

보여주는 View가 있다고 하더라도

한 화면에서 볼 수 있는 항목이 10개라고 하면

Item을 10개만 생성해놓고

Item안의 데이터만 변경하는 방식으로

재사용하면 되는 것이다.

 

이러한 컨셉으로 도입된 것이 ViewHolder이고

이를 강제화한 것이 RecyclerView이다.

 

 

 

 

이어지는 글 : [Android] RecyclerView 이해하기 (2) - ViewHolder는 무엇인가

 

[Android] RecyclerView 이해하기 (2) - ViewHolder는 무엇인가

1. ViewHolder의 필요성 앞선 글에서 설명했듯 많은 데이터를 항목화하여 표현하는 AdapterView 에서는 동작할 때마다 각 항목 (xml형식의 레이아웃)을 View객체로 가져와야 한다 그리고 이렇게 생성한

kyome.tistory.com

 

 

 

 

도움이 되었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

쿼리를 짜다보면 여러 행을

하나의 행으로 묶어서 보여줘야 할 때가 있다. 

 

여러 행을 하나로 합치는 건 기본적으로 집계이다.

그렇기 때문에 GROUP BY를 사용해 기준을 주어서 묶고 

함수를 사용해서 결과를 만들어야 한다.

 

이때, 여러 행을 ', '로 구분해서 합쳐주는 함수가

WM_CONCAT 이다. 

 

 

 

 

 

 

 

예시

다음의 예시를 통해 사용방법을

쉽게 익힐 수 있을 것이다.

 

 

 

 

개인별로 지원해본 회사명을 

입력한 테이블이 있다고 가정할 때,

 

이 테이블의 내용을 개인당 한 행으로

합쳐서 보이도록 하는 예시이다.

 

 

SELECT 이름,WM_CONCAT(회사명) AS "회사명"
FROM (
    SELECT 이름,회사명
    FROM TB_COM 
    )
GROUP BY 이름

 

 

 

 

도움이 되었다면

로그인이 필요없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

WITH 문

WITH [테이블명] AS (
    SELECT ... 
    FROM ...
    -- [임시테이블 SQL]
)
SELECT ..
FROM ... 
-- [사용 테이블]

 

 

 

쿼리를 만들다 보면 하나의 SQL문에서

가공한 테이블을 계속해서 사용해야 할 때가 있다.

 

가공한 테이블이 쿼리 안에서 계속 쓰다 보면 

가독성이 떨어져 혼란스럽고 개발하기 복잡해지는 경우가 종종 있다.

 

 

예시 

다음의 쿼리는 별로 복잡하지 않지만
엄청 복잡한 쿼리라고 가정해보자

-- 자주 조회할 테이블

SELECT T1.키,T1.컬럼1, T2.컬럼2
FROM T1 
    LEFT OUTER JOIN T2 
    ON T1.키 = T2.키

 

 

 

 

이제 이 쿼리는 복잡한 쿼리를 가져와서 

조건, 조인 등으로 사용하면서 더더욱 복잡해질 것이다.

 

-- 복잡해진 쿼리

SELECT A.키, TEST1.컬럼2-A, TEST2.컬럼2-B
FROM A
    LEFT OUTER JOIN 
    (
    SELECT T1.키,T1.컬럼1, T2.컬럼2
    FROM T1 
        LEFT OUTER JOIN T2 
        ON T1.키 = T2.키
    ) TEST1
    ON A.키 = TEST1.키
    AND TEST1.구분코드 = 'A'
    
    LEFT OUTER JOIN 
    (
    SELECT T2.키,T1.컬럼1, T2.컬럼2
    FROM T1 
        LEFT OUTER JOIN T2 
        ON T1.키 = T2.키
    ) TEST2
    ON A.키 = TEST2.키
    AND TEST2.구분코드 = 'B'

 

 

 

복잡해서 잘 안읽히는 코드는 유지보수에도 어려움이 있기 때문에 
최대한 보기 좋게 짜는 것도 요령인듯하다
이런 점에서 WITH 문을 활용할 수 있다

 

-- 간결해진 쿼리

-- 자주 조회할 테이블
WITH TEMP AS(
    SELECT T1.키,T1.컬럼1, T2.컬럼2
    FROM T1 
        LEFT OUTER JOIN T2 
        ON T1.키 = T2.키
)
-- 메인 쿼리
SELECT A.키, TEST1.컬럼2-A, TEST2.컬럼2-B
FROM A

    LEFT OUTER JOIN TEMP TEST1
    ON A.키 = TEST1.키
    AND TEST1.구분코드 = 'A'
    
    LEFT OUTER JOIN TEMP TEST2
    ON A.키 = TEST2.키
    AND TEST2.구분코드 = 'B'

 

 

 

 

 

도움이 된 정보였다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

 

[SPRING/Mybatis] POI를 이용한 대용량 데이터 추출 (엑셀 다운) -1

오랜만에 티스토리에 개발관련 기록을 남기는 것같다. 디비의 값을 엑셀로 다운로드을 수 있도록 해달라는 요청이 있었다. (요즘은 Android를 파는 중이지만...) 요청은 하나의 대상 테이블에 대한

kyome.tistory.com

 

이전 게시글에 이어서,

이제 ResultHandler 를 작성한다.

 

0. ResultHandler (ExcelHandler.java)

 

ResultHandler는 handleResult라는 메서드를

Override 하기만 하면 쉽게 사용할 수 있다.

handleResult는 데이터가 들어오는대로

건마다 호출되는 걸로 보인다.

 

 

1. 변수 및 생성자 선언

 

ResultHandler는 Generic으로 선언할 수 있고 

내가 만들 ExcelHandler는 Map으로 받아져야하기 때문에 

Generic 변수 T는 Map을 상속받는 변수야한다는 조건을 걸었다.

 

ResultHandler를 통해 다운로드를 받을 수 있도록 할 계획이기 때문에

생성자로 Response를 받으며 파일명이나

컬럼 순서를 받을 수 있도록 생성자를 추가했다.

 

import ...

public class ExcelHandler<T extends Map<String,Object>> implements ResultHandler<T> {
	public static final Logger LOGGER = LoggerFactory.getLogger(ExcelHandler.class);
	
	private T result;
	private String title;
	private String filename;
	private SXSSFWorkbook  workbook;
	private SXSSFSheet  sheet;
	private HttpServletResponse response;
	private ResultContext<? extends T> context;
	private List<String> columnTitleList;
	private int rownum;
	
	final int TITLE = 0;
	final int BODY = 1;

	
	private ExcelHandler() {
		super();
		rownum = 0;
	}
	
	public ExcelHandler(HttpServletResponse response, String filename) {
		this();
		this.response = response;
		this.title = filename;
		try {
			this.filename = URLEncoder.encode(filename.replace(" ", "_"),"UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		};
		workbook = new SXSSFWorkbook(10000);
		sheet = workbook.createSheet(title);
	}
	
	public ExcelHandler(HttpServletResponse response, 
    	String filename,List<String> orderedColumnTitleList) {
		this(response,filename);
		this.columnTitleList = orderedColumnTitleList;
	}

	...
    

 

 

 

 

 

 

 

 

2.  엑셀에 쓰기 메서드 구현

 

DB 에서 조회한 결과의 Column이 몇 개이고 이름이 무엇이든 상관없이

독립적으로 실행이 되어야 재사용이 가능해지기 때문에 

 

엑셀에 입력하는 기능에만 집중했고 

나머지는 파라미터로 받도록 했다.

 

private void write(int type,int currentRow,CellStyle style) {
		
	if (columnTitleList == null) {
		columnTitleList = new ArrayList<String>(context.getResultObject().keySet());
	}
	SXSSFRow row = sheet.createRow(currentRow);
	
	if(columnTitleList.size() == 0 ) {
		return;
	}
	
	for(int i = 0 ; i < columnTitleList.size() ; i++) {
		SXSSFCell cell = row.createCell(i);
		String tempValue = "";
		
		switch (type) {
			case TITLE:
				tempValue = columnTitleList.get(i);
				break;
			case BODY:
				if(context.getResultObject().containsKey(columnTitleList.get(i))) {
					tempValue = context.getResultObject().get(columnTitleList.get(i)).toString();
				}
				
				break;
		}
//		스타일 객체가 없다면 기본으로
		if(style != null) {
			cell.setCellStyle(style);
		}
		cell.setCellValue(tempValue);	
	}
}
	

 

 

 

3. handleResult Override

 

데이터가(한개의 행이) 호출 될 때마다 workbook에 입력해주고

현재의 행을 나타내는 변수를 ++ 해준다.

 

헤더전용 CellStyle 과 내용전용 CellStyle을 

미리 선언해두어서 write에 마지막 파라미터로 넣어주면

필요에 따라 셀스타일을 변경하면서 입력할 수도 있다.

 

	...

@Override
public void handleResult(ResultContext<? extends T> resultContext) {
	if(resultContext.getResultObject() == null) {
		return;
	}
	this.context = resultContext;
	result =  context.getResultObject();
	
	
	Font headerFont = workbook.createFont();
	headerFont.setFontName("맑은 고딕");
	headerFont.setBold(true);
       
	CellStyle headerStyle = workbook.createCellStyle();
	headerStyle.setBorderTop(BorderStyle.THIN);
	headerStyle.setBorderBottom(BorderStyle.THIN);
	headerStyle.setBorderLeft(BorderStyle.THIN);
	headerStyle.setBorderRight(BorderStyle.THIN);
        
	headerStyle.setAlignment(HorizontalAlignment.CENTER);
	headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
	headerStyle.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
	headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
	headerStyle.setFont(headerFont);

	Font bodyFont = workbook.createFont();
	bodyFont.setFontName("맑은 고딕");
        
	CellStyle bodyStyle= workbook.createCellStyle();
	bodyStyle.setBorderTop(BorderStyle.THIN);
	bodyStyle.setBorderBottom(BorderStyle.THIN);
	bodyStyle.setBorderLeft(BorderStyle.THIN);
	bodyStyle.setBorderRight(BorderStyle.THIN);
        
	bodyStyle.setAlignment(HorizontalAlignment.LEFT);
	bodyStyle.setFont(bodyFont);
		
	if(rownum == 0 ) {			
		write(TITLE, rownum,headerStyle);
		write(BODY, rownum+1,bodyStyle);
	}else {
		write(BODY, rownum+1,bodyStyle);
	}
	rownum++;
}

...

 

 

 

 

 

 

4. 엑셀 다운로드

 

생성자로 Response 객체를 받는 이유는 여기에 있다.

Response 객체의 헤더에 첨부파일을 넣어서 

OutStream으로 파일을 내보낸다.

 

파일 처리는 항상 그렇듯 예외처리를 성실하게 해줘야하고

종료시에 close 를 호출해줘야한다.

 

	...
    
public void download() throws IOException{
	LOGGER.debug("## start excel download : "+filename);
	response.setHeader("Content-Disposition", "attachment; filename=" 
        				+ filename.replaceAll(" ", "_") + ".xlsx;");
	response.setCharacterEncoding(Constants.ENCODING);
		
	ServletOutputStream stream = response.getOutputStream();
	OutputStream out = new BufferedOutputStream(stream);
	
	try {
		response.resetBuffer();
		response.setBufferSize(1024 * 4);
		workbook.write(out);
		
	} catch (Exception e) {
		out.flush();
		out.close();
		stream.close();
	} finally {
		out.flush();
		out.close();
		stream.close();
	}
	
	if (workbook != null) {
		try {
			workbook.dispose();
		} catch (Exception e) {
			workbook.close();
		} finally {
			workbook.close();
		}
	}
	workbook.close();
}

public void close() {
	workbook.dispose();
	try {
		workbook.close();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

...

 

 

 

 

 

도움이 되었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

오랜만에 티스토리에 개발관련 기록을 남기는 것같다.

디비의 값을 엑셀로 다운로드을 수 있도록 해달라는 요청이 있었다.

(요즘은 Android를 파는 중이지만...)

 

요청은 하나의 대상 테이블에 대한 엑셀 다운로드 구현이지만

어차피 개발할 거라면 재사용할 수 있도록 개발했다.

 

 

 

 

 

 

 

 

 

 

0. 의존성 추가 (pom.xml)

 


...
		<!-- excel -->
		<dependency>
		    <groupId>org.apache.poi</groupId>
		    <artifactId>poi</artifactId>
		    <version>4.1.1</version>
		</dependency>
		
		<dependency>
		    <groupId>org.apache.poi</groupId>
		    <artifactId>poi-ooxml</artifactId>
		    <version>4.1.1</version>
		</dependency>
...

 

 

 

 

1. SQL (ExcelMapper.xml)

 

엑셀 다운로드용 쿼리를 모아둘 목적으로 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="kr.co.excel.dao.ExcelMapper">
	<resultMap id="excelListDownloadMap" type="java.util.HashMap" >
    	<result column="CONTENT_NO"     	property="번호"    	jdbcType="INTEGER" />
    	<result column="CONTENT_TITLE"     	property="제목"    	jdbcType="VARCHAR" />
    </resultMap>

    <select id="contentListDownload" resultMap="excelListDownloadMap">
		<![CDATA[
			SELECT CONTENT_NO ,CONTENT_TITLE
			FROM TB_CONTENT
			WHERE CONTENT_NO < 100
		]]>
	</select>
</mapper>

 

 

 

2.  DAO (ExcelMapper.java)

 

현재 개발환경에서는 Mapper 를 사용하기 때문에

interface로 간단하게 DAO를 선언할 수 있다.

반환값을 void, 파라미터를 ResultHandler로 선언하면

Mybatis에서 조회되는 값을 바로 핸들링할 수 있다.

 

(ResultHandler 클래스를 잘 짜놓는다면...ㅎㅎㅎ)

ResultHandler는 너무 길어지니 이어지는 게시글에 자세히 적는걸로!

 

import java.util.Map;

import org.apache.ibatis.session.ResultHandler;
import org.springframework.stereotype.Repository;

@Repository
public interface ExcelMapper {
	public void contentListDownload(ResultHandler<Map<String,Object>> ExcelHander);
}

 

 

 

 

 

 

3. Service (ExcelService.java)

 

난 서비스를 ResultHandler과 DAO의 매핑 공간으로 사용했다.

매핑을 해놓으면 해당 메서드를 호출할때

ExcelHandler에 조회 결과가 매핑되는 걸로 보인다.

난 이 ResultHandler를 통해 다운로드 받을 엑셀 형식을 정의하고

다운로드 로직을 ResultHandler 에 넣어놓았다. (excelHandler.download())

 

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

import kr.co.excel.dao.ExcelMapper;

@Service
public class ExcelService {
	@Autowired
	private ExcelMapper excelMapper;
	
	ExcelHandler<Map<String,Object>> excelHandler;
	
	public void contentListDownload(
			HttpServletRequest request,
			HttpServletResponse response){
		excelHandler = new ExcelHandler<>(response,"테스트");
		excelMapper.contentListDownload(excelHandler);
		
		try {
			excelHandler.download();
		} catch (IOException e) {
			e.printStackTrace();
			excelHandler.close();
		}
	}
}

 

 

 

 

 

4. Controller (ExcelController.java)

 

View페이지가 별도로 필요없는 RestController를 선언했다. 

엑셀 다운로드시 Response 객체가

필요하기 때문에 파라미터로 선언해놓았다.

Request 객체는 추후에 필요할 듯 싶어서

넣어 놓았을 뿐 당장은 필요없다.

 

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import kr.co.excel.service.ExcelService;

@RestController
public class ExcelController {
	
	@Autowired
	ExcelService excelService;
	
	@RequestMapping(value = "/contentListDown.do")
	public void contentListDownload(
			HttpServletRequest request,
			HttpServletResponse response){
		excelService.contentListDownload(request,response);
	}
}

 

 

 

 

도움이 되었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 


 

[SPRING/Mybatis] POI를 이용한 대용량 데이터 추출 (엑셀 다운) -2

 

[SPRING/Mybatis] POI를 이용한 대용량 데이터 추출 (엑셀 다운) -2

[SPRING/Mybatis] POI를 이용한 대용량 데이터 추출 (엑셀 다운) -1 오랜만에 티스토리에 개발관련 기록을 남기는 것같다. 디비의 값을 엑셀로 다운로드을 수 있도록 해달라는 요청이 있었다. (요즘은 A

kyome.tistory.com


 

 

 

 

안녕하세요 ☺️

전 몇 달 전부터 

유튜브 프리미엄을 시작했어요

유튜브 프리미엄을 사용해보니

정말 괜찮아서 추천해볼까 해요!👍

 

 


"Youtube Premium"

 

 

 

 

지인들에게 유튜브 프리미엄을 시작했다고 하면

보통 그냥 광고 보고 영상 보면 되는데

왜 굳이 돈을 쓰냐는 하시는 분들이 많은데

돈을 쓸만하답니다😎

 

 

 

1. 유튜브 프리미엄의 세계

 

 

 

 

유튜브 프리미엄을

그저 광고 없이 유튜브 보기 위해

돈을 쓴다고 생각하시는

분들이 많더라고요

 

유튜브를 광고없이 보는 것뿐 아니라

유튜브 뮤직으로 음악 스트리밍

서비스를 이용할 수 있어요

 

멀티태스킹이 지원되기 때문에

영상을 보다가 잠깐

카톡을 할 수도 있고 

오프라인 저장 기능을

사용할 수도 있답니다.👍

 

유튜브 오리지널도 

추가 비용 없이 볼 수 있어요

K-POP 그룹의 다큐멘터리를

개봉하기도 하고 있는 걸 보면

꾸준히 확장해나갈 것으로 기대가 돼요

 

(현재 저는 유튜브 오리지널을

애용하고 있진 않답니다..😅)

 

링크 참고

(https://www.youtube.com/premium)

 

 

 

 

 

 

 

 

2. 유튜브 프리미엄, 왜 쓸까

1) 절대적인 유튜브 사용량

 

출처 https://it.donga.com/29482/

 

 

 

전 유튜브를 이용하는 시간이

절대적으로 늘었기 때문에 

유튜브 프리미엄이 눈에 들어왔어요

 

저는 주로 출퇴근처럼

이동할 때 핸드폰을 많이 봐요

예전에는 출퇴근 때 노래를 들으며

핸드폰으로 웹서핑을 했지만

 

요즘은 유튜브가 저의 모든

이동시간을 책임지고 있더라고요! 

 

유튜브 광고를 보며 영상을

기다리는 시간도 점점 늘어나면서 

유튜브 프리미엄 가입도

괜찮겠다 싶었어요🤔

 

 

 

 

2) 음악 스트리밍 서비스 대용

 

 

 

하루에 몇 곡 안 듣는데

굳이 매달 음악

스트리밍을 결제해야 할까

라는 생각으로 대안도 없이 가입한

스트리밍 서비스를

해지한 적도 있었어요

 

그리고 알았어요

내가 생각보다 노래도

많이 듣는다는 사실을..😵

 

비록 출퇴근에 노래 듣는 시간은

많이 줄었지만

음악 들을 스트리밍은

필요하더라고요

 

그래서 찾은 답이 유튜브 프리미엄의

유튜브 뮤직이었답니다.

 

 

 

 

 

 

 

 

3) 유튜브 뮤직 따라올 수 없는 음악 추천

 

 

 

유튜브 프리미엄의 진가는

유튜브 뮤직을 만날 때일 것 같아요

 

구글은 추천의 신이라

구글 추천 알고리즘을 경험하면 

유튜브에서 헤어나오 못하고 있지요

 

 

음악도 똑같아요 

유튜브 뮤직에서 듣고 싶은 노래를

하나 찾아서 들으면

추천해주는 플레이리스트가

자동으로 생기고 자연스럽게

다른 곡으로 넘아가면서

계속 노래를 듣게 되더라고요 😮

 

 

예를 들면 피아노곡을 듣고 싶다!

하면 대표적인 피아노곡을

선택해서 플레이하면

비슷한 곡들로 추천해주기 때문에 

나의 플레이리스트를 구성하기 위해

큰 노력이 필요 없어요

정말 진심으로 편하고 좋아요

 

 

 

 

3. 합리적 가격

 

 

 

부가세 포함한 한국 가격

8700원

 

누군가는 너무 비싸다고 하지만

저는 유튜브가 제공하는 서비스에 비해서 

8700원이면 정말 합리적이라고 생각해요

 

자주 듣지 않는 음악 스트리밍에

6000원 쓰는 것보다 

하루 종일도 볼 수 있는 유튜브를 

더 편하고 좋은 환경에서

볼 수 있도록 만드는데

쓰는 8700원이 더 합리적인

소비라고 생각해요!

 

아이폰 사용자는 꼭 PC에서 가입하세요!!

PC로 가입하면 무료 사용기간도 더 길게 주고

아이폰 앱에서 구독 결제하는 것보다 

더 저렴하게 가입할 수 있어요!!

 

 

 

 

4. 저렴하게 하는 방법 (비추)

 

 

유튜브 프리미엄 인도 계정을 만들어서

가입하는 방법이 온라인에서

많이 나오더라고요!

 

인도는 우리나라보다

물가가 낮기 때문에

정말 말도 안 되게 저렴한

가격으로 가입을 할 수 있어요 

 

그렇지만 유튜브 뮤직 감상 중에 

갑자기 인도 노래가 나오고

인도 차트를 추천해준다고 해요.. ㅎㅎ

 

추천이 가장 큰 매력 중에

하나인 유튜브 서비스에서

인도 문화를 추천해준다니...

제가 볼 땐 이러한 부분이 

가장 큰 마이너스 요인이더라고요!

 

그리고 이렇게 인도가 싸서

편법으로 저렴하게

서비스를 이용하고 있다는 걸 

구글이 이미 알고 있고

적극 대응 중이라고 해요

잡히면 계정이 정지당한다고 하니..

 

절대 비추합니다. 😒

 

 

저렴하게 서비스를

이용하려는 노력도 이해는 되지만

서비스의 정당한 금액을 지불하고 

이용하는 것도 올바른 소비자의 길이지

않을까 싶어요ㅎㅎㅎ


 

 

 

흥미로운 추천이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

안드로이드에서 뷰 객체에

이벤트를 만드는 건
패턴화 되어 있어서 

어렵지 않게 개발할 수 있다.

 

TextView textView = (TextView)findViewById(R.id.textView); 

textView.setOnClickListener(new View.OnClickListener() { 
  @Override 
  public void onClick(View view){ 

  } 
}); 

 


그렇지만 왜 이런 패턴과 구조로

작성해야 하는지 궁금했다.

무작정 외우기보단

이해하고 싶어서

안드로이드 이벤트에

공부해보기로 했다

 

 

 

 


이벤트 쉽게 이해하기

 

 

1. 이해하기 어려운 이유

 

안드로이드 이벤트에 대해 찾아보니
이벤트 소스, 이벤트 핸들러, 

이벤트 리스너 같은 낯선 용어들이 

이해를 더 어렵게 하는 것 같다.
그래서 이런 용어들을 빼고

최소한의 용어들로 이해한 것들을

정리해보았다.

 

 

 

 

 

 

 

2. 뷰 (View)는 누구인가

 

 

안드로이드에서 보여지는 

버튼, 텍스트 등은 다 뷰라는 

이름의 객체이다.

 

 

 

특징 - 1. 뚜렷한 목적

 

뷰들은 다 제각기 

생성된 목적이 뚜렷하다.

 

(Ex, Button은 눌림 (Click) 을 당하기 위해, 

CheckBox는 선택 (Checked) 이 되기 위해..)

 

 

특징 - 2. 다양한 이벤트

또 하나의 특징으로는 

뷰는 다양한 이벤트를 가질 수 있다.

즉, TextView도 클릭이벤트를 

가질 수 있다는 말이다.

 

이처럼 다양한 뷰를

효율적으로 관리하기 위해

뷰는 이벤트를 실행할 때마다 

'on이벤트명()'이라는 메서드가 

실행되도록 불러준다.

 

 


3. 우리가 기대하는 뷰

 

 

사용자는 안드로이드 화면에서 

뷰를 눌렀을 때 기대하는 바가 있다

 

만약, 전송 버튼을 눌렀는데 

버튼이 눌린 시늉만 하고 

아무 일도 생기지 않으면

사용자는 앱이 고장 났다고 

볼 것이다.

 

 

 

 

 

 

 

4. 뷰의 입장 이해하기

 

 

클릭 이벤트의

패턴을 이해하기 위해서

뷰의 입장이 되어 보았다.

뷰의 입장이 돼 보면 클릭이벤트 패턴을

이해하기가 한결 수월해진다.

 

 

 

 

뷰 입장 - 1. 난처한 뷰

 

"세세한 것 하나하나 다 해달라고?

너무한 거 아니야?"

 

[그림] 뷰가 알아야 할 것들이 너무 많아진다

 

뷰 입장에서 생각해보면 

사용자가 버튼을 눌렀으면 

눌린 티만 내면 그만이지 


굳이 눌렸을 때 

어떤 일들이 어떻게 진행되는지 

다 알고 있을 이유가 없다. 

뷰가 수행해야 하는 이벤트가 한 개만

있는 것도 아니고 많은 이벤트에 대한 

세세한 내용까지 뷰가 다 관리하자니

뷰 입장에선 너무 부담스럽다.

 

더 정확히 말하자면,

뷰에 로직을 다 넣으면

무거워지고 의존성이 높아지고 등등..

아무튼 지저분해져서 보고 싶지 

않은 코드가 될 것이다. 

 

 

 

 

뷰 입장 - 2. 뷰의 책임감

 

"그래도 기능을 성실하게

수행해야 하긴 할 텐데.."

 

뷰는 사용자와 직접 소통하는 

역할을 담당하기 때문에 
아무 기능을 하지 않는 뷰는

사실 존재의 가치가 없다고

해도 과언이 아니다. 


이벤트를 효율적으로 처리할

방법이 필요한 상황이다. 

 

 

 

 

 

 

5. 뷰의 묘수

 

[그림] 로직을 대신 수행할 객체를 모집

 

뷰의 묘수 - 1. 대리자 모집

 

세세한 로직을

다 알고 싶지 않은 뷰는 로직을

다 알고 있는 대리자를 모집하기로 한다.

 

그렇다고 아무나 불러서 

일을 수행하라고 할 수는 없으니

최소한의 자격요건을 충족하는 

대리자를 모집한다.

 

 

뷰가 내건 최소한의 자격요건


 " 'on이벤트명' 메서드를 

잘 구현해낸 자"


 

이벤트가 일어날 때마다 

이벤트 이름 앞에 on이 붙은 
'on이벤트명' 메서드를 같이 호출할 테니
이벤트에 함께 호출되고 싶다면

'on이벤트명' 메서드를

작성해와서 등록하라고 한 것이다.

 

 

 

 

 

 

뷰의 묘수 - 2. 관리소장 두기

 

이제 지원한 대리자를 

관리하는 일이 남았다.  

 

뷰는 내부에 대리자 관리소장을

하나 두기로 한다.

 

관리소장은 대리자를 등록하는 

일과 이벤트가 일어났을 때 

대리자를 호출해서 로직을 

수행하도록 하는 일을 한다.

 

 

 

6. 결과 

 

[그림] 클릭 이벤트가 발생시 처리

 

이렇게 구조를 정리해 놓으니

뷰는 이제 이벤트가 발생하면

내부에 관리 소장을 불러

어떤 이벤트가 발생했다는 사실만

알려주면 된다.

 

 


 

제가 공부 후 정리한 것에 대해

혹시 다른 의견이나 수정해주실 부분 있으면

꼭 댓글 달아주세요 

저도 아직 공부하는 중이랍니다.

 

 

 

 

도움이 되는 게시글이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

 

 

안녕하세요☺️

상하이를 떠나기로 결정하고, 

날짜를 정하셨다면 

가장 먼저 준비해야 하는 건

비행기겠죠?😌

 

항공편을 준비한 방법을

공유할까 해요😎

 

 


상하이행 비행기

예약하기

 

1. 알찬 여행의 첫 단추 

출발하는 비행기

가장 이른 시간으로,

돌아오는 비행기

가장 늦은 시간으로

예매하는 걸 추천해요

 

왕복으로 미리 구매하는 게

가격면에서도 유리할 수 있고

미리 여행을 계획하기에 좋아요!😌

 

 

 

 

저 같은 경우는 8시 55분이

가장 빠른 비행기였고 

시차로 인해 중국시간

10시에 도착했어요🙊

(한국시간, 11시)

 

중국에서 점심을 먹고 

오후부터 본격적인

일정을 소화했답니다!

 

 

여행의 스타일은

저마다 다르겠지만

일찍 해외로 가고 싶고 

오래 해외에 머물고 싶은 마음은 

비슷할 것 같아요!

 

그러기 위해선 당연히

비행기 시간부터

잘 준비해야겠죠?😉

 

 

 

 

 

 

 

2. 중국 저가항공 OK!

 

중국의 저가 항공을 이용해서 

여행경비를 아끼는 것도 추천해요

당연히 우리나라 항공사가

더 좋고 편하겠죠!

그렇지만 비싸요..!😅

 

 

 

 

인천공항에서 푸동공항까지는 

2시간밖에 걸리지 않는데

가격차이는 대부분

5만원 이상 차이가 나요!

 

이 2시간을 위해서

5만원 이상의

비용을 지출하긴

좀 아까운 것 같아요 🤔

 

 

아, 중국 저가항공의 기내식이 

맛있진 않아요...😭

전 중국동방항공이었는데 

그냥 먹기에 나쁘지 않은 수준??

이었답니다..ㅎㅎ

 

 

 

 

3. 다양한 어플 활용

 

 

 

여행은 정보전인가 봅니다🤓

 더 많은 정보를 모아서

가장 합리적인 선택을 해야겠죠?

 

어느 정도 찾다 보면

비슷한 가격대의

항공권을 찾게 되지만

그래도 조금이라도 더 저렴하고 

나에게 맞는 시간대를

찾기 위해선 노력이 필요해요!

 

 

 

 

 

<추천하는 방법>

 

1) 네이버 활용

 

 

일단 가장 쉽게 찾아볼 수 있는 

네이버 항공권으로 검색해서 

대략의 가격을 알아보기!🤓

 

 

 

2) 각종 어플 활용

 

 

네이버에서 본

가격대를 기준으로 

어플별로 가격을

비교해보면 돼요!😉

 

어플마다 보여주는

방식이 다르기 때문에 

모두 다운받아서 더 편한 어플,

더 합리적인 가격을 제시하는

어플을 이용해서 

예매하면 된답니다!

 

저는 Skyscanner,

Trip.com, 플레이윙즈

이렇게 다운받아서 사용했어요!

 

 

 

 

4. 티켓 구매는 신속하게

어찌 보면 당연한 이야기겠지만

꼭 당부드리고 싶은 정보예요

저도 오늘을

그냥 알아보기만 하고

결제는 내일 해야지!

하다가 그 사이에

가격이 올라버렸어요 😥

 

날짜가 정해졌고 가격이 

충분히 합리적이다! 

하면 바로 결제를

하는 걸 추천해요!

 


 

 

 

 

 

흥미로운 추천이었다면

로그인이 필요 없는 공감 버튼 꾹 눌러주세요! 

 

 

 

 

 

+ Recent posts

"여기"를 클릭하면 광고 제거.