ViewPager, TabLayout

ViewPager는 화면을 양옆으로 밀어서 Page를 바꾸는 슬라이드 동작을 하는 View입니다. 굉장히 많이 사용하는 방식인데요. 보통 TabLayout과 같이 사용하는 것이 일반적입니다. 

아래처럼 1번, 2번과 같이 선택할 수 있게 하는 것이 TabLayout이며 프래그먼트를 포함하는 것이 ViewPager입니다. 물론 둘은 독립적으로도 사용할 수 있습니다.

 

아래와 같은 결과를 만들어내는것이 이번 포스팅의 목표입니다.

 

우선 구현하기에 앞서 TabLayout은 아래의 라이브러리가 있어야 사용할 수 있습니다. 추가합시다.

File - Project Structure - app - '+' - design 검색 후 아래 라이브러리 추가

1) activity_main.xml

MainActivity에서 사용할 activity_main에서는 TabLayout과 ViewPager를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <android.support.design.widget.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


    <android.support.v4.view.ViewPager
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/viewPager"/>
</LinearLayout>

2) fragment1.xml, fragment2.xml

ViewPager에서 사용될 실제 내용을 담는 두 fragment의 layout입니다. 간단하게 TextView만이 존재하는 간단한 layout이네요.

 

fragment1.xml

<?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="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:textSize="28dp"
        android:text="1번째 프래그먼트"/>
</LinearLayout>

fragment2.xml

<?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="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:textSize="28dp"
        android:text="2번째 프래그먼트"/>
</LinearLayout>

 

3) Fragment1, Fragment2

위에서 정의한 layout을 inflate하는 Fragment1과 Fragment2입니다. 역시 아주 간단하게 onCreateView만 정의하였습니다.

 

Fragment1

package com.example.minkug.reakwonapp;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment1 extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        return inflater.inflate(R.layout.fragment1,container,false);
    }
}

Fragment2

package com.example.minkug.reakwonapp;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class Fragment2 extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

        return inflater.inflate(R.layout.fragment2,container,false);
    }
}

 

 

 

4) PagerAdapter

가장 중요한 PagerAdapter를 구현할 차례입니다. 우서 가장 먼저할 것은 FragmentStatePagerAdapter를 상속을 받아야하지요.

이제 1개의 생성자와 2개의 메소드만 Overriding하면 기본적인 Adapter 작성은 끝이납니다.

 

4-1) FragmentManager를 받는 생성자 구현

4-2) 실제 Fragment를 반환하는 getItem 구현

4-3) 페이지의 개수를 반환하는 getCount구현 

 

여기서는 Adapter가 생성될때 fragments라는 list에 우리가 위에서 정의한 2개의 프래그먼트를 추가합니다. 그렇게 되면 getItem은 리스트의 i번재 아이템을 반환하면 될 것이고, getCount 역시 리스트의 사이즈를 반환하면 되기 때문에 구현이 조금 더 간단해집니다.

package com.example.minkug.reakwonapp;


import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;

import java.util.ArrayList;
import java.util.List;


public class PagerAdapter extends FragmentStatePagerAdapter {

    List<Fragment> fragments=new ArrayList<>();

    public PagerAdapter(FragmentManager fm) {
        super(fm);
        fragments.add(new Fragment1());
        fragments.add(new Fragment2());
    }

    @Override
    public Fragment getItem(int i) {
        return fragments.get(i);
    }

    @Override
    public int getCount() {
        return fragments.size();
    }
}

 

5) MainActivity

MainActivity가 코드가 조금 길어보이기는 하나 별다른 것이 없습니다. 우선 ViewPager, TabLayout을 얻어오고, TabLayout에는 Tab을 설정해주네요.

 

조금 눈여겨봐야될 것은 아래 2개의 Listener입니다.

5-1) viewPager.addOnPageChangeListener : Pager가 변경될때 발생하는 이벤트인데, 이때는 TabLayout의 탭까지 변경을 해줘야합니다. Pager를 슬라이딩하여 바꾼다고 하더라도 이 동작을 처리하지 않으면 Tab은 같이 변경되지 않습니다.


5-2) tabLayout.addOnTabSelectedListener : 마찬가지로 tab이 눌려졌다면 page도 같이 변경해주어야합니다. 탭이 선탤될때 발생하는 이벤트는 onTabSelected이며 tab이라는 인자로 선택된 tab의 위치를 알 수 있습니다. 이것을 이용해서 pager를 선택하면 되는 것이죠.

package com.example.minkug.reakwonapp;

import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;


public class MainActivity extends AppCompatActivity {

    private ViewPager viewPager;
    private TabLayout tabLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tabLayout=(TabLayout)findViewById(R.id.tabLayout);

        tabLayout.addTab(tabLayout.newTab().setText("1번"));
        tabLayout.addTab(tabLayout.newTab().setText("2번"));

        viewPager=(ViewPager)findViewById(R.id.viewPager);
        viewPager.setAdapter(new PagerAdapter(getSupportFragmentManager()));

        viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
        tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
            @Override
            public void onTabSelected(TabLayout.Tab tab) {
                viewPager.setCurrentItem(tab.getPosition());
            }
            @Override
            public void onTabUnselected(TabLayout.Tab tab) { }
            @Override
            public void onTabReselected(TabLayout.Tab tab) { }

        });

    }
}

 

구현은 끝났고 여러분이 원하는 방향으로 기능을 추가해보시기 바랍니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

RecyclerView

RecyclerView는 ListView와 비슷하나 조금 더 진보한 View입니다. RecyclerView는 ListView에 비해서 많은 최적화를 거치게 됐고, 수 많은 데이터를 처리하는데 효과적입니다. 

 

그렇다면 RecyclerView가 어떻게 사용되는지 알아보도록 할까요? 여기서는 RecyclerView의 사용법을 위주로 설명할 것이기 때문에 디자인은 저 멀리 버리도록 하겠습니다.

 

1) RecyclerView를 사용하기 전에 라이브러리를 추가

File - Project Structure - App - Dependancies 의 오른쪽 +를 누른 후 recyclerview를 검색하여 

com.android.support:recyclerview를 추가합시다.

androidx를 사용하신다면 이 과정은 건너 뛰어도 상관없겠네요.

 

2) activity_main.xlm에 RecyclerView 추가

간단하게 RecyclerView만을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerView"/>

</LinearLayout>

 

3) item_recycler_view.xlm 추가

RecyclerView에 사용할 item입니다. 간단하게 TextView하나만을 포함하고 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical"
    android:padding="20dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        android:id="@+id/item"/>

</LinearLayout>

 

이제 준비는 끝났습니다. item_recycler_view를 Adapter의 ViewHolder에 이어주고 이 Adapter를 RecyclerView에 설정하면 됩니다.

 

4) MainActivity의 RecyclerView에 Adapter 설정

아직 RecyclerAdapter가 없기 때문에 오류가 발생하는데 우선 할당해놓고 이후에 Adapter class를 만들어봅시다. 

이후 딱히 설명할 건 없습니다.

몇가지 첨언한다면.. 

RecyclerView에 setLayoutManager는 어떤 레이아웃을 사용할 것인지 설정하는데, 여기서는 LinearLayoutManager를 사용했습니다. 

격자형을 사용하고 싶다면 GridLayoutManager를 사용하시기 바랍니다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView=findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(new RecyclerAdapter());

    }
}

 

 

 

 

 

5) RecyclerAdapter(Adapter class) 정의

우리는 우선 RecyclerView.Adapter를 상속을 받아야합니다.

 

5-1) ViewHolder 정의

Adpater안에는 반드시 ViewHolder가 있어야하는데 이 ViewHolder는 RecyclerView의 개별 Item의 대한 view를 가지고 있습니다. item_recycler_view가 이 ViewHolder의 View로 생성되며 필요에 따라 custom할 수 있겠죠? 

비록 우리는 item_recycler_view에 TextView만 추가했지만 뭐 ImageView도 추가할 수 있고 말이죠.

 

5-2) 메소드 Overriding

이제 필수적으로 Overriding할 메소드는 다음과 같습니다.

 

- onCreateViewHolder : ViewHolder를 콘텐츠를 표시하기 위해 사용하는 뷰를 설정하는데, 우리는 이전에 item_recycler_view를 만들었으니 이것을 사용합니다.

- onBindViewHolder : ViewHolder를 binding합니다. 여기서 ViewHolder의 각각 View를 설정할 수 있습니다.

- getItemCount : item의 갯수를 반환합니다. 대부분은 List를 사용하기 때문에 이 List의 size를 반환합니다.

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<String> itemList=new ArrayList<>();

    public RecyclerAdapter(){

        itemList.clear();

        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        private TextView item;
        public ViewHolder(View itemView){
            super(itemView);
            item=(TextView)itemView.findViewById(R.id.item);
        }
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view=LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {
        ViewHolder holder=(ViewHolder)viewHolder;

        holder.item.setText(itemList.get(i));
    }


    @Override
    public int getItemCount() {
        return itemList.size();
    }
}

 

ViewHolder를 정의하는데 여기서는 아까 만들었던 item_recycler_view.xlm의 View를 갖고 있습니다. 이 ViewHolder는 개별 item을 갖고 있는 것이나 같죠. 이 ViewHolder는 onCreateViewHolder에서 객체가 생성됩니다.

 

생성자에는 itemList를 초기화하는데 이 itemList에서 adapter가 binding할때 아이템을 꺼내옵니다. 우선 10개의 아이템을 추가하고 있네요.

이후 notifyDataSetChanged()를 호출하면 이제 데이터의 Set이 바뀌었다고 다시 RecyclerAdapter의 데이터를 표시하게됩니다. 

 

즉, itemList의 아이템이 생기거나 제거됐다면 이후 notifyDataSetChanged()를 호출하세요.

 

onBindViewHolder에는 ViewHolder에 있는 TextView의 문자열 설정을 하고 끝나네요.

 

여기까지 잘따라왔다면 프로젝트 구성은 이렇게 되겠네요.

 

 

 

결과

 

 

 

 

 

이벤트 추가

여기까지만 진행하면 조금 심심하겠죠? 이제 아이템이 클릭이 되면 Toast로 클릭되었다는 메시지를 띄어봅시다.

interface를 사용하여 listener를 설정할때가 왔군요.

 

6) RecyclerAdapter에 OnItemClickListener 설정

여기서는 OnItemClickListener라는 interface를 정의하고 객체를 생성합니다.

onBindViewHolder에서 ViewHolder의 아이템이 클릭되었을때 onItemClickListener의 onClick 메소드를 호출합니다. 이 onClick메소드는 바로 MainActivity에서 구현하거나 객체 생성해서 넘길테지요.

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<String> itemList=new ArrayList<>();
    public interface OnItemClickListener{
        public void onClick(String str);
    }

    private OnItemClickListener onItemClickListener=null;

    public RecyclerAdapter(){

        itemList.clear();

        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        private TextView item;
        public ViewHolder(View itemView){
            super(itemView);
            item=(TextView)itemView.findViewById(R.id.item);
        }
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View view=LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {
        ViewHolder holder=(ViewHolder)viewHolder;

        holder.item.setText(itemList.get(i));
        holder.item.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onItemClickListener!=null){
                    onItemClickListener.onClick(itemList.get(i)+" 클릭!!");
                }
            }
        });
    }


    @Override
    public int getItemCount() {
        return itemList.size();
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener){
        this.onItemClickListener=onItemClickListener;
    }
}

 

 

7) MainActivity에서 onClick 구현

별로 특별한 기능은 하지 않고 onClick시 Toast를 이용해 메시지를 보여줍니다.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView=findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        RecyclerAdapter adapter=new RecyclerAdapter();
        recyclerView.setAdapter(adapter);

        adapter.setOnItemClickListener(new RecyclerAdapter.OnItemClickListener() {
            @Override
            public void onClick(String str) {
                Toast.makeText(MainActivity.this,str,Toast.LENGTH_LONG).show();
            }
        });
    }
}

 

인터페이스를 사용하여 MainActivity에서 Toast 메시지를 실행시킨 이유는 Activity에서만 사용할 수 있는 메소드를 사용하기 위해서입니다(예를 들면 startActivity).

물론 RecyclerAdapter에 MainActivity의 Context를 전달해서 구현할 수도 있었지요. 

 

이제 아이템을 누르면 아래와 같이 메시지가 나타납니다.

 

 

 

특정 위치에 다른 View 설정

맨 처음 또는 특정 position에 다른 item을 할당하고 싶다면 어떻게 해야할까요?

그 경우에는 getItemViewType를 Overriding하여 해결할 수 있습니다.

getItemViewType은 인자로 전달된 position에 따라 ViewType이 무엇인지 결정하고 

onCreateViewHolder에 두번째 인자로 그 ViewType으로 전달됩니다. onCreateViewHolder는 그 뷰타입을 보고 생성되는 ViewHolder를 결정하면 되지요.

 

저는 0번째 item만 다른 view로 보여주려고 합니다.

1) item_header.xml

우선 0번째 아이템을 위해 사용할 layout은 다음과 같습니다. 아주 간단하지요.

<?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="wrap_content">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/header_msg"
        android:textAlignment="center"
        android:paddingTop="20dp"
        android:paddingBottom="20dp"/>
</LinearLayout>

 

2) RecyclerAdapter에 HeaderViewHolder 추가

필요한 부분만 간추려서 보면 아래의 코드와 같습니다. 우선 HeaderViewHolder라는 0번째 아이템을 위한 ViewHolder를 추가하지요. 

 

0번째 아이템이면 TYPE_HEADER라는 ViewType을, 그 외에는 평범한 아이템을 나타내는 TYPE_ITEM이라는 것을 알려주는 getItemViewType을 오버라이딩하고 있네요.

 

이제 이 ViewType은 onCreateViewHolder의 두번째 인자인 i로 전달됩니다. 여기서 ViewHolder를 두 가지로 나눠서 반환하게 됩니다. HeaderViewHolder 또는 ViewHolder(RecyclerAdapter에서 정의한)로 말입니다.

 

이후 onBindViewHolder에서 각각 다른 처리를 하면 됩니다.

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
	...
    private final int TYPE_HEADER=0;
    private final int TYPE_ITEM=1;

    public RecyclerAdapter(){
        itemList.clear();

        itemList.add("Header Message");
        for(int i=0;i<10;i++){
            itemList.add(i+"번째 아이템");
        }
        notifyDataSetChanged();
    }

   ...
    class HeaderViewHolder extends RecyclerView.ViewHolder{
        private TextView headerItem;
        public HeaderViewHolder(View itemView){
            super(itemView);
            headerItem=(TextView)itemView.findViewById(R.id.header_msg);
        }
    }

    @Override
    public int getItemViewType(int position) {
        if(position==0) return TYPE_HEADER;
        return TYPE_ITEM;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        if(i==TYPE_HEADER){
            return new HeaderViewHolder(LayoutInflater.from(viewGroup.getContext())
                    .inflate(R.layout.item_header,viewGroup,false));
        }

        return new ViewHolder(LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.item_recycler_view,viewGroup,false));
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int i) {

        if(i==0){
            ((HeaderViewHolder) viewHolder).headerItem.setText(itemList.get(i));
            return;
        }
        ((ViewHolder) viewHolder).item.setText(itemList.get(i));
    }
	...
}

 

이후 결과는 아래와 같습니다. 0번째 아이템이 다른 view를 가지고 있지요.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

find

find 명령어는 리눅스를 사용함에 있어서 아주 유용한 기능을 제공해주는 명령어입니다. 말 그대로 우리가 원하는 파일이나 디렉토리를 찾아줍니다. 윈도우로 따진다면 아래의 입력란이 되겠지요.

윈도즈에서는 아래에 파일명을 치면 검색이 가능하지만 리눅스에서는 find라는 명령어를 통해서 파일을 찾을 수 있습니다. 잘만 사용하면 너무나 편리한 명령어이기도 합니다.

하지만 find명령은 너무나 사용하기에 광범위한 명령어이므로 그 중 자주 사용되는 몇가지만 소개하도록 하겠습니다.

 

이제부터 find 명령을 통해서 어떻게 파일을 찾을 수 있을지 알아보겠습니다.

 

1) 기본적인 파일 찾기

find는 기본적으로 찾을 파일 경로와 이름으로 구성되면 정말 그 파일만 찾는 기능을 할 수 있습니다. 아래 처럼 말이지요.

#find /home -name "test.c" 

설명

/home : 파일을 찾을 디렉토리를 지칭합니다.

-name "test.c" : test.c 파일을 찾습니다. 

 

결과

/home/centos/code/test.c
/home/centos/test.c

 

2) 와일드카드(*)를 이용한 단어를 포함한 파일 찾기

find 명령어는 파일이름에 와일드카드 문자(*)를 포함하여 특정 단어를 포함하는 파일을 찾을 수 있습니다.

#find /home -name "*test*"

설명

/home : 파일을 찾을 디렉토리입니다.

-name "*test*" : test라는 단어를 포함하는 파일을 찾습니다. ttest이든, test.c이든, aatestbb 파일이든 전부 찾아줍니다.

 

결과

/home/centos/code/test.c
/home/centos/test.c
/home/centos/patience_test.c

 

3) -type 옵션으로 파일의 타입을 포함하여 찾기

저는 test라는 디렉터리를 하나 만들었고 여기서 test라는 단어를 포함하는 디렉터리를 찾아보도록 하겠습니다.

/home/centos/code/test.c
/home/centos/test.c
/home/centos/patience_test.c
/home/centos/test <- directory
#find /home -name "*test*" -type d

설명 

-type d : 파일 type이 디렉터리인것만 찾습니다.

 

결과

/home/centos/test

파일 타입은 다음과 같습니다.

option type
f regular file
d directory
c character special
b block special
l symbolic link
s socket
p fifo

 

4) 파일의 크기에 따른 파일 검색

-size 옵션으로 파일의 크기를 검색할 수 있습니다. 파일 크기 앞에 '+'를 붙이면 그 크기 초과, '-'를 붙이면 그 크기 미만으로 검색이 됩니다. 또한 파일 크기 뒤에 단위가 붙는데요. 아래와 같습니다. 단위를 생략하면 기본적으로 리눅스 블록(b)이 단위가 됩니다.

(b:block, c:bytes, w:2bytes, k:kbytes, M:mbytes, G:gbytes)

옵션 단위
c 1byte 단위
b 1block 단위(1block = 512 bytes)
w 2 bytes 단위
k 1 kilobytes
M 1 metabytes
G 1 gigabytes

몇 가지 예제)

find /home -size 1k 크기가 1k인 파일 검색
find /home -size +100M 100M 초과인 파일 검색
find /home -size -1G 1G 미만인 파일 검색
find /home -size +100M -size -2G 100M 초과 2G 미만인 파일 검색

 

 

5) exec으로 찾을 파일에 명령어 실행

-exec으로 명령어를 실행할 수 있습니다.

find명령을 통한 파일들은 모두 {}에 담겨지게 됩니다. 그러니까 ls -al {} 또는 ls {} -al 인 것이고, cp {} . 인것이 왜인지 생각해보세요.

ls -al {파일}, 또는 ls {파일} -al은 같은 결과를 갖습니다. 전달받는 파일 인자는 1개이니까요 .

반대로 cp는 2개의 파일 인자를 받습니다. 첫번째 인자는 복사가 될 파일, 두번째 인자는 복사가 될 위치이지요. 그래서 cp {파일} . 가 됩니다.

명령어 끝은 항상 \;으로 끝나야합니다. 프로그래밍을 하는 분이라면 ;이 연산의 종료를 의미한다는 것을 아실겁니다. ';'은 특수문자이기 때문에 escape(특수문자화 하지 않는 것)하기 위해 \를 같이 사용합니다. 이해가 안된다면 그냥 외우시거나 프로그래밍 언어를 배우시는 것을 추천드립니다. ㅎㅎㅎ 

 

예제를 통해서 살피도록 하겠습니다.

 

예제 1) 

find /home -name "*test*" -exec ls -al {} \;

설명

test라는 단어가 포함된 파일을 대상으로 ls -al을 실행합니다. {}의 위치를 잘봐두세요.

 

결과

-rw-r--r--. 1 root root 313  2월 29 02:37 /home/centos/code/test.c
-rw-r--r--. 1 root root 0  3월  3 06:57 /home/centos/test.c
-rw-r--r--. 1 root root 0  3월  3 07:04 /home/centos/patience_test.c
합계 4
drwxr-xr-x.  2 root   root      6  3월  3 07:08 .
drwx------. 17 centos centos 4096  3월  3 07:34 ..

 

예제 2)

find /bin/ -name "*gr*" -exec cp {} /home/centos  \;

설명

bin 하위의 gr이라는 파일이나 디렉토리를 복사해 /home/centos로 위치합니다. 실제 실행하고 나서 /home/centos에 위치하여 결과를 보면 이런 파일들이 존재합니다.

 

결과

bzegrep            groff                  grub2-mkstandalone  xzfgrep
bzfgrep            grops                  grub2-script-check  xzgrep
bzgrep             grotty                 grub2-syslinux2cfg  zegrep
chgrp              groups                 lexgrog             zfgrep

..생략..

이뿐만 아니라 rm, mv 등의 명령도 실행 가능합니다. rm은 쓸때 꼭 주의하세요.

 

예제 3)

아까 옮겼던 gr이 포함된 단어의 파일들을 전부 삭제해보도록 하겠습니다.(우선 그전에 gr이 포함된 파일 중 원래 그 자리에 있었던 파일이 있는지 확인하세요.)

find /home/centos/ -name "*gr*" -exec rm -rf {} \;

설명

찾은 파일에 대해서 강제 삭제 명령(rm -rf)을 하게 됩니다. 이 명령어는 전달되는 파일이 하나밖에 없으므로 {}이 어디에나 위치해도 됩니다. rm -rf {} 이건 rm {} -rf이건 상관이 없다 이거죠. 허나 반드시 이 명령어를 실행하지 마시기바랍니다. 잘못하면 진짜 ㅈ돼요.

 

결과

gr이 포함된 모든 파일이 삭제되었습니다.

 

6) 특정 단어 또는 내용을 갖는 파일을 검색하기

끝에 파이프를 연결해서 | xargs grep "검색할 내용"을 덧붙이면 되는데 에러 또는 경고까지 전부 출력하므로 에러(2)는 전부 쓰레기통(/dev/null)에 버려 출력하지 않게 합시다.

find / -name "*.c*" -type f | xargs grep "#include" 2>/dev/null

설명

/디렉토리부터 확장자가 c를 포함하며 파일은 정규파일을 검색합니다.  그때 #include라는 문자열을 포함하는 파일만 검색하며 오류 출력은 전부 쓰레기통인 /dev/null로 갖다 버립니다.

 

결과

뭐 이런것들이 보이네요.

/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include <sys/types.h>
/usr/lib/firmware/isci/create_fw.c:#include <sys/stat.h>
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include 
/usr/lib/firmware/isci/create_fw.c:#include <asm/types.h>
/usr/lib/firmware/isci/create_fw.c:#include 

... 생략 ...

 

 

 

7) 특정 권한을 갖는 파일 찾기

-perm 옵션으로 어떤 파일이 어떤 권한이 있는지 검색할 수 있습니다. 

find . -perm 777

설명

위의 명렁은 유저 권한으로 읽기, 쓰기 , 실행 그리고 그룹 권한으로 읽기, 쓰기, 실행 마지막으로 다른 유저 권한으로는 읽기, 쓰기, 실행 권한인 파일을 모조리 찾는 명령어입니다.

만일 -perm 644라면 유저 권한으로 읽기,쓰기 그룹 권한으로 읽기, 다른 유저 권한으로 읽기의 파일들을 전부 찾습니다.

 

8) 끝까지 찾지 않아도 되는 maxdepth

maxdepth는 찾는 깊이를 뜻합니다. find명령어는 기본적으로 지정된 디렉토리 밑으로 끝까지 search합니다. 만약 maxdepth 옵션을 준다면 지정된 깊이까지만 파일을 검색합니다. 깊이라함은 하위디렉토리를 뜻합니다.

maxdepth의 0은 자기 자신을 의미합니다.

 find / -maxdepth 1

설명

/디렉토리에서 깊이가 1인 파일들만 검색합니다. 결과는 다음과 같습니다.

 

결과

/
/dev
/proc
/run
/swap
/sys
/etc
/root
/var
/usr
/bin
/sbin
/lib
/lib64
/boot
/home
/media
/mnt
/opt
/srv
/tmp

 

지금까지 제가 현업에서 자주 사용하는 find명령어의 옵션을 살펴보았는데요. 가만히보면 이외의 명령어를 사용할 일이 별로 없었습니다. 다른 분들은 잘 모르겠네요. 

 

기능도 막강하고 배우기에는 너무 범위가 넓은 find 명령어는 알면 알 수록 흥미있는 명령어인것 같습니다. 제가 모르거나 놓친 부분이거나, 또는 많이 사용하지만 이곳에 나오지 않은 옵션이라면 댓글 달아주세요!

 

나머지 기능은 꾸준히 업데이트하여 올리도록 하겠습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

정렬

우리는 정렬에 관해서 배우기도 하였고 구현도 해보았습니다. 그래서 어떻게 정렬이 되는지 알고 있죠. 하지만 실제 프로그래밍하는 상황에서 이 정렬을 직접구현해서 프로그램을 만들지는 않을 거에요. 우리에겐 JAVA에서 제공하는 정렬 메소드가 있기 때문이죠. 저희보다 똑똑한 사람들이 만들어 놓은 것이니 우리는 그저 감사하며 사용하면됩니다. 그 전에 사용법을 알아야 잘 사용할 수 있겠죠? 이제부터 어떻게 사용하는지 알아보도록 합시다.

 

Collections.sort

Collections.sort의 메소드를 이용해서 정렬할 수 있습니다. 바로 기본 자료형의 wrapper 클래스의 객체들은 전부 이 Collections.sort를 통해서 알아서 정렬이 가능합니다. 

바로 예제를 통해 확인하도록 하지요.

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class Sort {

	public static void printList(String title,List list){
		System.out.println(title);
		System.out.println(list);
	}
	public static void main(String []ar){
		List<Integer> intList=new ArrayList<>();
		
		intList.add(4);
		intList.add(5);
		intList.add(1);
		intList.add(8);
		intList.add(3);
		
		printList("정수 정렬 전",intList);
		
		Collections.sort(intList);
		
		printList("정수 정렬 후",intList);
		
		System.out.println("==============================");
		
		List<String> strList=new ArrayList<>();
		
		strList.add("de");
		strList.add("dc");
		strList.add("ci");
		strList.add("ad");
		strList.add("aa");
		
		printList("문자열 정렬 전",strList);
		
		Collections.sort(strList);
		
		printList("문자열 정렬 후",strList);
		
		
	}
}

 정렬이 잘되는 것을 아래의 결과를 통해서 확인할 수 있네요. 

실행결과

정수 정렬 전
[4, 5, 1, 8, 3]
정수 정렬 후
[1, 3, 4, 5, 8]
==============================
문자열 정렬 전
[de, dc, ci, ad, aa]
문자열 정렬 후
[aa, ad, ci, dc, de]

 

 

Collections와 ArrayList를 이용한 객체 정렬

하지만 우리는 기본 자료형을 정렬하는 것이 아닌 우리가 만든 클래스의 객체를 정렬하려는 상황이 발생한다면 어떻게 하면 좋을까요?

아래와 같은 User라는 클래스가 있다고 칩시다.

class User{
	public String name;
	public int age;
	public User(String name,int age){
		this.name=name;
		this.age=age;
	}
	@Override
	public String toString(){
		return "(name:"+name+", age:"+age+")";
	}
}

그리고 이 클래스의 객체를 리스트에 5개를 담도록 하겠습니다. 

List<User> list=new ArrayList();

list.add(new User("A",40));
list.add(new User("B",25));
list.add(new User("C",10));
list.add(new User("D",30));
list.add(new User("E",26));

System.out.println(list);

그리고 출력한다면 뭐 뻔한 결과가 나올테죠.

실행결과

[(name:A, age:40), (name:B, age:25), (name:C, age:10), (name:D, age:30), (name:E, age:26)]

이 User의 객체들을 나이순으로 정렬하고 싶습니다(참고로 꼰대는 아니구요). 어떻게 정렬할 수 있을까요?

 

Comparator 구현

Collections.sort를 입력했을때 eclipse에 나오는 recommended method의 2번째 메소드를 보시기바랍니다. Comparator라는 객체를 받고 있는 것을 확인할 수 있습니다.

 

네, 답은 Comparator를 구현하면 이 상황을 해결할 수 있습니다. 

그러면 바로 구현해보도록 합시다. 구현할 것이 메소드 하나 compare밖에 없어요. 그 내부 코드양도 얼마되지 않습니다.

class UserComparator implements Comparator<User>{
	@Override
	public int compare(User a,User b){
		if(a.age>b.age) return 1;
		if(a.age<b.age) return -1;
		return 0;
	}
}

compare는 2개의 인자를 받는데 이 인자들은 우리가 앞서 구현했던 User객체입니다. 만약 a라는 객체가 b라는 객체보다 더 크다면 1, 작다면 -1, 같다면 0을 반환하도록 합니다. 이게 오름차순의 방법이고, 만약 내림차순으로 구현하고 싶다면 반대로 구현하면 되겠죠? (모르면 그냥 찍어서 return하고 원하는 결과와 반대다 싶으면 return을 반대로 해줍시다^^.)

오름차순

첫번째 인자가 더 큰 객체라면 1

두번째 인자가 더 큰 객체라면 -1

같다면 0

내림차순

첫번째 인자가 더 작은 객체라면 1

두번째 인자가 더 작은 객체라면 -1

같다면 0

 

 

이 클래스의 객체를 Collections.sort의 2번째 인자로 전달하기만 하면 됩니다. 이제 전체코드와 결과를 보겠습니다.

 

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Sort {

	public static void main(String[] ar){
		List<User> list=new ArrayList();
		
		list.add(new User("A",40));
		list.add(new User("B",25));
		list.add(new User("C",10));
		list.add(new User("D",30));
		list.add(new User("E",26));
		
		System.out.println("정렬 전 ->"+list);
		Collections.sort(list,new UserComparator());
		//list.sort(new UserComparator()); 이 방법 역시 정렬 방법
		System.out.println("정렬 후->"+list);
	}
}

class User{
	public String name;
	public int age;
	public User(String name,int age){
		this.name=name;
		this.age=age;
	}
	@Override
	public String toString(){
		return "(name:"+name+", age:"+age+")";
	}
}

class UserComparator implements Comparator<User>{
	@Override
	public int compare(User a,User b){
		if(a.age>b.age) return 1;
		if(a.age<b.age) return -1;
		return 0;
	}
}

 

실행결과

정렬 전 ->[(name:A, age:40), (name:B, age:25), (name:C, age:10), (name:D, age:30), (name:E, age:26)]
정렬 후->[(name:C, age:10), (name:B, age:25), (name:E, age:26), (name:D, age:30), (name:A, age:40)]

저희가 원하던 결과를 얻을 수 있습니다. 참고로 Collections.sort를 주석처리하고 list.sort를 실행해도 결과는 같습니다. List도 역시 Comparator를 받을 수 있으니까요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

StringTokenizer

StringTokenizer 클래스는 문자열을 우리가 지정한 구분자로 문자열을 쪼개주는 클래스입니다. 그렇게 쪼개어진 문자열을 우리는 토큰(token)이라고 부릅니다.

 

StringTokenizer를 사용하기 위해서는 java.util.StringTokenizer를 import해야합니다. 사용법은 굉장히 쉽습니다. 사용하는 메소드도 몇개 없는데요. 자주 사용하는 메소드 설명과 예제를 통해 이 클래스를 어떻게 사용하는지 살펴봅시다. 

 

생성자(Constructor)

생성자 설명
public StringTokenizer(String str);

절달된 매개변수 str을 기본(default) delim으로 분리합니다. 기본 delimiter는 공백 문자들인 " \t\n\r\t"입니다. 

public StringTokenizer(String str,String delim); 특정 delim으로 문자열을 분리합니다.

public StringTokenizer(String str,String delim,boolean returnDelims);

str을 특정 delim으로 분리시키는데 그 delim까지 token으로 포함할지를 결정합니다. 그 매개변수가 returnDelims로 true일시 포함, false일땐 포함하지 않습니다.

 

int countTokens() 

남아있는 token의 개수를 반환합니다. 전체 token의 갯수가 아닌 현재 남아있는 token 개수입니다.

 

boolean hasMoreElements(), boolean hasMoreTokens()

다음의 token을 반환합니다. StringTokenizer는 내부적으로 어떤 위치의 토큰을 사용하였는지 기억하고 있고 그 위치를 다음으로 옮깁니다. 두가지 메소드는 모두 같은 값을 반환합니다.

 

Object nextElement(), String nextToken()

이 두가지 메소드는 다음의 토큰을 반환합니다. 두가지 메소드는 같은 객체를 반환하는데 반환형은 다르네요. nextElement는 Object를, nextToken은 String을 반환하고 있습니다.

 

예제

이제 몇가지 예제를 통해서 더 자세히 알아보도록 합시다.

 

0) String 클래스에 있는 split 메소드 이용

public static void main(String[] ar){
	String str="this string includes default delims";
	System.out.println(str);
	System.out.println();
		
	System.out.println("==========using split method============");
	String []tokens=str.split(" ");
		
	for(int i=0;i<tokens.length;i++){
		System.out.println(tokens[i]);
	}
}

String클래스의 메소드인 split 메소드를 사용하여 StringTokenizer를 흉내낼 수 있습니다. split이 반환하는 값은 String 배열입니다.

this string includes default delims

==========using split method============
this
string
includes
default
delims

 

1) Default Delim을 이용

public static void main(String[] ar){
	String str="this string\tincludes\ndefault delims";
	StringTokenizer stk=new StringTokenizer(str);
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

코드의 while문을 보면 토큰이 있는지 확인한 후 있다면 다음 토큰을 가져옵니다. 이렇게 하나씩 토큰을 소비한다고 보면되는데, 이런 패턴이 StringTokenizer를 사용하는 가장 일반적인 사용방법입니다.

실행결과

this string includes
default delims

total tokens:5
================tokens==================
this
string
includes
default
delims
total tokens:0

 

 

 

 

2) 특정 delim을 이용

public static void main(String[] ar){
	String str="this-=string-includes=delims";
	StringTokenizer stk=new StringTokenizer(str,"-=");
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

특정 delim으로 문자열을 분리하는 예제입니다. 여기서는 "-"와 "="으로 분리를 했네요.

실행결과

this-=string-includes=delims

total tokens:4
================tokens==================
this
string
includes
delims
total tokens:0

2-1) String의 split과 비교

public static void main(String[] ar){
	String str="this-=string-includes=delims";
	System.out.println(str);
	System.out.println();
		
	String[] tokens=str.split("-=");
	System.out.println("total tokens:"+tokens.length);
	System.out.println("================tokens==================");
		
	for(int i=0;i<tokens.length;i++){
		System.out.println(tokens[i]);
	}
		
}

split을 이용하면 조금 다른 결과가 나옵니다. split은 정확히 "-="으로 문자를 쪼개기 때문에 "this-=string-includes=delims"에서 빨간 부분을 기준으로 쪼개는 겁니다. 결과를 확인해보세요.

실행결과

this-=string-includes=delims

total tokens:2
================tokens==================
this
string-includes=delims

3) delim까지 포함

public static void main(String[] ar){
	String str="this-string-includes=delims";
	StringTokenizer stk=new StringTokenizer(str,"-=",true);
	System.out.println(str);
	System.out.println();
		
	System.out.println("total tokens:"+stk.countTokens());
	System.out.println("================tokens==================");
	while(stk.hasMoreTokens()){
		System.out.println(stk.nextToken());
	}
	System.out.println("total tokens:"+stk.countTokens());
}

 위의 예제의 생성자에서 세번째 인자를 true로 전달했을때의 예제입니다. 이때 "-"와 "="를 토큰으로 포함하게 됩니다. 이 예제에서 true를 전달하지 않고 false로 전달한다면 위의 예제와 같은 결과가 나오게 됩니다.

실행결과

this-string-includes=delims

total tokens:7
================tokens==================
this
-
string
-
includes
=
delims
total tokens:0

 

이렇게 StringTokenizer의 사용방법을 알아보았습니다. 알아두면 문자열을 조금 더 유연하게 다룰 수 있겠네요.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

UDP(User Datagram Protocol)

TCP가 신뢰성에 기반을 둔 전송 계층 프로토콜이라면 UDP는 빠른 데이터 전송 위주의 프로토콜입니다. 속도를 떨어뜨리는 요소를 없애 TCP처럼 각종 흐름제어, 오류제어, 혼잡 제어를 하지 않습니다. 따라서 UDP는 비신뢰성(Unreliable) 프로토콜입니다.

 

이 UDP 프로토콜, 저와 같은 세대시라면 어디서 많이 보셨죠? 저는 뮤컨을 위해서 스타크래프트의 UDP는 필수였습니다. 이렇게 데이터는 그다지 중요하지 않으면서 속도가 빠른 데이터 전송이 필요할때 UDP를 사용합니다.

 

데이터 전송과정을 보게 되면 이렇습니다.

만일 송신에서 데이터를 1, 2, 3 순으로 전송했다고 할때 네트워크의 사용량에 따라 어떤 데이터그램은 늦게 도착할 수 있습니다. 3번 데이터그램이 가장 좋은 경로를 타고 와서 먼저 도착하게 됩니다. 그 이후에는 1번 데이터그램이 도착하겠네요. 마지막으로 가장 부하가 많은 경로를 타고 들어온 2번 데이터그램이 도착합니다. 

결국 수신자는 3, 1, 2 데이터그램 순으로 전달받게 됩니다. 연결과정 자체를 갖지 않기 때문에 데이터의 순서를 보장하지 않습니다.

 

 

 

헤더 정보(UDP Header)

빠른 전송을 요구할때 쓰이는 UDP는 헤더의 구조가 너무나 간단합니다. 헤더는 단 4가지 정보만 존재합니다.

 

UDP 헤더 정보

너무 간단합니다.

- Source Port : 송신지의 Port 번호입니다.

- Destination Port : 목적지의 Port 번호입니다.

- Total Length : 헤더를 포함한 전체 데이터그램의 크기를 의미하는 필드입니다.

- Checksum : 데이터그램의 오류를 확인하기 위한 필드입니다.

 

UDP 특징

UDP의 특징을 요약하면 다음과 같습니다.

1) TCP와 같이 연결을 성립하는 3-way-handshake와 같은 과정이 없습니다. 따라서 비연결형인 특징을 갖습니다. 연결을 확립하는 TCP는 회선을 연결한듯 정확한 연결을 요구하므로 가상회선 방식이라 하며 UDP는 그런 과정없이 데이터를 쪼개 보내므로 데이터그램 방식이라 합니다.

2) 따라서 데이터의 순서, 유실을 보장하지 않고, 데이터 수신 실패시 재전송을 요구하지 않습니다. 데이터를 수신할때 순서가 뒤바뀔 수도 있습니다. 이는 곧 비신뢰성을 의미합니다. 

3) 데이터를 일방적으로 수신받기 때문에 속도가 매우 빠릅니다.

4) 데이터의 checksum으로 최소한의 오류만을 검출합니다. 그러니까 받는 데이터는 검사를 하지만 유실된 데이터는 모른다는 것이지요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

C언어, C++에서는 메모리를 조금 더 쉽게 다루고자하는 함수가 몇가지 존재합니다. 그것들이 무엇이 있는지 설명과 예제를 통해서 알아보도록 하겠습니다. 메모리 관련 함수를 사용하기 위해서는 string.h를 include해야합니다.

 

0) string.h 헤더파일 추가

메모리 관련 함수를 사용하기 위해서 반드시 추가해주세요.

 

1) void* memset(void* source, int value, size_t n)

메모리 주소 source부터 시작해 n만큼 value로 메모리를 채웁니다. return 값은 메모리의 시작주소입니다.

간단하네요. 그러면 예제를 바로 보도록 하겠습니다.

#include <stdio.h>
#include <string.h>

int main() {

	int nums1[5];
	unsigned char nums2[5];
	int i;

	memset(nums1, 10, sizeof(nums1));
	memset(nums2, 10, sizeof(nums2));
	
	for (i = 0; i < 5; i++) {
		printf("nums1[%d] = %d \n", i, nums1[i]);
	}
	
	printf("\n");

	for (i = 0; i < 5; i++) {
		printf("nums2[%d] = %d \n", i, nums2[i]);
	}
}

 

nums1과 nums2를 5개의 배열로 잡는데 자료형이 다르군요. nums1는 int형(여기서는 4바이트), nums2는 unsigned char형(1바이트)입니다. 이후 둘의 메모리를 memset으로 10으로 초기화합니다.

어떤 결과가 나올까요? 두개의 for문에서 nums1과 nums2의 요소들이 전부 10으로 나올것 같은데 그럴까요?

실행결과

nums1[0] = 168430090
nums1[1] = 168430090
nums1[2] = 168430090
nums1[3] = 168430090
nums1[4] = 168430090

nums2[0] = 10
nums2[1] = 10
nums2[2] = 10
nums2[3] = 10
nums2[4] = 10

 

우리의 예상과는 조금은 다릅니다. memset내부에서 실제 10이란 값은 unsigned char로 변환되어 1바이트의 메모리에 그 값을 집어넣게 되는겁니다. 

그래서 4바이트인 int형은 이런식으로 메모리가 set이 됩니다.

00001010 00001010 00001010 00001010 -> 168430090

memset은 1바이트 단위의 메모리를 세팅합니다. 그래서 unsigned char 형의 nums2는 제대로 된 값을 읽을 수 있습니다.

 

2) void* memcpy(void* destination, const void* source, size_t num)

이 함수는 source의 메모리를 destination으로 num만큼 복사합니다. 이 함수에는 source나 destination이 num바이트 이상인지를 검사하지 않으므로 상당히 취약하며 이진데이터를 그대로 복사합니다. 그러니 중간에 NULL이 있는지 없는지 확인하지 않습니다. 아래의 예제를 봅시다.

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char source[8];
	int destination[2];
	int i;

	memset(source, 10, sizeof(source));

	memcpy(destination, source, sizeof(source));

	for (i = 0; i < 2; i++) {
		printf("destination[%d] : %d\n", i, destination[i]);
	}
}

 

source는 8바이트이고 destination도 8바이트입니다. 우선 source를 10으로 전부 채운 후에 destination으로 메모리 복사를 하면 어떤 결과가 나올까요?

 

이전의 memset에서 보았듯 바이트 단위로 메모리가 복사되어 8바이트가 0000 1010으로 복사되는 것이지요.

00001010 00001010 00001010 00001010 -> 168430090

따라서 168430090의 값이 두 번 출력되게 됩니다.

실행 결과

destination[0] : 168430090
destination[1] : 168430090

 

 

3) int memcmp(const void* ptr1, const void* ptr2, size_t num)

메모리의 바이트를 비교합니다. ptr1과 ptr2가 num만큼 비교했을 때 같다면 0, 아니면 다른 값을 리턴합니다. strcmp와 비슷한 리턴 값을 보이는데, unsigned char으로 ptr1이 ptr2보다 크다면 양수, 작다면 음수를 리턴하게 됩니다.

 

#include <stdio.h>
#include <string.h>

int main() {

	unsigned char a[5] = { 0,1,2,3,4 };
	unsigned char b[5] = { 0,1,2,3,4 };

	printf("memcmp(a,b) = %d \n", memcmp(a, b,5));
	
	a[0] = 100;
	
	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));

	b[0] = 200;

	printf("memcmp(a,b) = %d \n", memcmp(a, b, 5));
}

 

처음 a,b는 정확히 같은 값을 갖고 있으므로 비교했을때 0이 리턴됩니다.

이후 a의 0번째 요소가 100으로 a가 b보다 더 크므로 비교했을때 양수가 리턴됩니다. 

그 다음 b의 0번째 요소가 200으로 a가 b보다 더 작으므로 음수가 리턴되지요.

실행 결과

memcmp(a,b) = 0
memcmp(a,b) = 1
memcmp(a,b) = -1

 

4) void* memchr(void* ptr, int value, size_t num)

memchr은 ptr에서 value를 찾을때 사용합니다. 즉 메모리에서 특정 값을 찾을 때 사용하는 함수입니다. 만약 값이 존재한다면 그 주소를 리턴하고 아니면 NULL을 반환합니다.

 

#include <stdio.h>
#include <string.h>

void printMemory(void *ptr) {
	if (ptr == NULL) {
		printf("메모리에 존재하지 않음\n");
	}
	else {
		printf("메모리에 %d가 존재. addr : %p \n", *((unsigned char*)ptr),ptr);
	}
}
int main() {

	unsigned char arr[5] = { 0,1,2,3,4 };
	unsigned char a = 4;
	unsigned char b = 5;
	int i;

	for (i = 0; i < 5; i++) 
		printf("arr[%d] : %d, %p\n", i, arr[i], &arr[i]);
	
	
	void* ptr=memchr(arr, a, 5);
	printMemory(ptr);

	ptr = memchr(arr, b, 5);
	printMemory(ptr);
}

 

현재 1바이트 배열 arr에는 0,1,2,3,4가 있습니다. 여기에서 a(4)와 b(5)를 찾을 겁니다. a는 존재하니까 ptr이 NULL이 아닌 a가 존재하는 그 주소를 반환하겠지요. b는 존재하지 않으므로 NULL이 반환됩니다.

 

실행결과

arr[0] : 0, 0093F75C
arr[1] : 1, 0093F75D
arr[2] : 2, 0093F75E
arr[3] : 3, 0093F75F
arr[4] : 4, 0093F760
메모리에 4가 존재. addr : 0093F760
메모리에 존재하지 않음

 

4가 있는 주소, 0093F760을 반환하는 것을 알 수 있네요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

TCP(Transmission Control Protocol)

IP 계층 위에서 동작하는 TCP는 연결 지향 프로토콜입니다. 마치 물리적인 선로로 연결되어 있는 것처럼 가상의 연결 통로를 설정하여 통신합니다. 그만큼 정확한 데이터 전송을 요구하며 신뢰성을 보장하기 때문에 신뢰할 수 있는 프로토콜(Reliable Protocol)이라고 합니다. 신뢰성 보장을 위해서 다음의 제어를 수행합니다. 

 

1) 흐름 제어(Flow Control)

데이터를 보내는 사람, 받는 사람이 있다고 할때 줄 수 있는 양, 받을 수 있는 양이 다릅니다. 그래서 상대방이 받을 수 있을 만큼의 양으로 적절히 전송하는 것을 흐름제어라고 합니다. 

 

2) 혼잡 제어(Congestion Control)

네트워크가 혼잡해지면 송신자가 데이터의 전송량을 제어하는 것을 뜻합니다. 혼잡하다라는 것을 어떻게 알 수 있을까요? 데이터의 손실 발생이 많아지면 이는 즉, 네트워크가 혼잡한 상태로 판단하여 전송량을 제어하게 됩니다.

 

3) 오류 제어(Error Control)

데이터의 오류나 손실없는 전송을 보장해주는 것이 혼잡 제어입니다. 오류 발생 시 TCP는 데이터를 제전송합니다.

 

 

 

 

 

TCP 헤더

TCP 프로토콜은 신뢰성을 보장하기 위해서 헤더에 많은 정보를 포함하게 되는데 각 필드에 어떤 정보를 포함하는지 알아보도록 하겠습니다.

TCP 프로토콜의 구조

 

- Source Port: 출발지의 포트, 즉 데이터를 보내는 컴퓨터의 포트 정보입니다. 컴퓨터가 갖을 수 있는 포트는 65536개이므로 사이즈가 2바이트인것을 확인하세요. 

 

- Destination Port: 반대로 목적지의 포트입니다. 

 

- Sequence Number : 송신 데이터의 일련 번호를 담고 있습니다. 

 

- Acknowledgement Number : 그전의 데이터를 잘 받았다는 표시로 상대방이 다음에 전송할 일련번호를 담고 있습니다. 줄여서 ACK라고 하겠습니다.

 

- HLEN(Header Length) : 헤더의 정보를 담고 있습니다. 4 bits의 워드 단위입니다. 헤더의 길이는 최소 20바이트 ~ 60바이트까지입니다.

 

- Reserved : 예약된 비트입니다. 아직 사용하지 않습니다. 나중을 위해서 남겨두는 비트인 셈이지요.

 

- Control Flags 

FLAG 설명

URG

(Urgent Pointer)

Urgent Pointer의 필드가 유요하다는 의미의 FLAG

ACK

(Acknowledgement)

수신 확인 응답 FLAG

PSH

(Request for push)

송수신 버퍼의 있는 데이터 즉시 처리 요청 FLAG

RST

(Reset the connection)

연결을 강제 중단합니다. TCP가 유지되고 있을때 이 FLAG를 사용하면 그 즉시 연결을 끊어 버립니다. 해커들이 Hijacking을 위해 피해자의 연결을 끊어버릴때 사용합니다. 보통의 정상적인 종료는 아래의 FIN FLAG를 설정합니다.

SYN

(Synchronize sequence number)

연결 설정 FLAG

FIN

(Terminate the connection)

정상 종료의 연결 종료 FLAG

 

- Window Size :  수신자에서 송신자로 보내는 수신자의 윈도우 사이즈입니다. 즉, 수신 버퍼의 여유공간 크기를 의미하게 되지요. 송신자는 이 윈도우 사이즈 범위 내에서 수신측의 수신 확인(ACK)을 확인하지 않고 연속적으로 데이터를 보낼 수 있습니다.

 

- Checksum : 오류를 검사하기 위한 필드입니다. 전체 데이터가 오류가 나 변형되었는지 확인합니다. 

 

- Urgent Pointer : 긴급 데이터의 위치값을 담고 있습니다. 

 

 

 

 

 

 

 

 

TCP의 연결 설정 과정(3-way handshake)

TCP연결은 어떻게 연결이 될까요? 3단계 절차에 따라서 연결이 성립됩니다. 이때 사용되는 FLAG는 2개입니다. 바로 SYN과 ACK입니다. 다음의 그림을 통해서 연결과정을 알아보도록 합시다.

 

3-way handshake

 

1. 최초 클라이언트 측에서 동기화를 위해 SYN FLAG와 함께 Seq(uence) Number를 임의로 설정해서 보내줍니다. 이때 최초의 Seq number를 ISN(Initial Sequence Number)라고 합니다.

아직 상대방에게서 데이터를 수신하지 않았으므로 Ack(nowledgement) Number 필드는 비어있네요. SYN 패킷을 보냈으므로 클라이언트의 상태는 SYN_SENT가 됩니다. 서버는 SYN을 받았으므로 SYN_RECV 상태 또는 SYN_RCVD상태가 됩니다.

 

이때 클라이언트가 적극적으로 연결 요청을 하고 있네요. 이것을 Active Open이라 합니다. 서버는 수동적으로 받아들이고 있네요. 이것을 Passive Open이라고 합니다.

 

2. 서버에서 동기화 요청을 받았으면 잘 받았으니 연결하자고 요청합니다. 클라이언트에서 보낸 Ack number에 받은 Seq에 +1을 하여 다음 Seq Number를 요구합니다. 클라이언트의 Seq number가 100이므로 101을 Ack number로 보내는 군요. 또한 자신도 역시 ISN을 설정하여 다시 클라이언트로 보냅니다. 

이때 사용한 플래그는 ACK와 SYN입니다. ACK와 SYN이 유요한 데이터이기 때문이죠. 이 페킷을 보낸 후 서버는 연결 확립(ESTABLISHED)상태가 됩니다. 

 

3. 클라이언트는 이에 대한 응답으로 서버에게 ACK num을 설정하여 보냅니다. 이 패킷을 준 후 클라이언트도 연결 확인 상태가 됩니다.

 

이렇게 보면 초기에 데이터가 왔다 갔다 3번하고 있죠? 이것을 3-Way Handshake라고 합니다.

 

아래는 실제 3 way handshake를 와이어샤크로 찍어본 화면입니다.

wire shark hand shake

 

 

연결 종료(4-way handshake)

정상적인 연결 종료는 FIN, ACK의 플래그를 통해서 이루어집니다. 아래와 같이 4단계를 거쳐 연결이 종료가 됩니다. 

1. 연결상태에 있던 클라언트가 연결을 종료하기 위해 FIN을 보냅니다. 이때 클라이언트의 상태는 FIN_WAIT_1 상태가 되고 서버는 CLOSE_WAIT 상태가 됩니다. 3 way handshake와 마찬가지로 먼저 close요청을 한쪽이 Active Close, 받은쪽이 Passive Close라고 합니다.

 

2. 수신하는 서버는 이에 대한 응답으로 ACK를 보냅니다. 이때 클라이언트는 FIN_WAIT_2의 상태가 됩니다.

 

3. 서버는 이 후 소켓을 받는 시스템 콜(close)을 호출할때까지 대기 상태로 있다가 소켓이 종료되면 FIN을 보냅니다. 마지막 FIN과 함께 ACK를 보냈으므로 LAST_ACK 상태가 됩니다.

 

4. 서버로부터 FIN을 받은 클라이언트는 ACK응답을 하여 2MSL만큼의 시간(보통 1분에서 4분)이후 연결 종료 상태(CLOSED)가 됩니다. 서버 CLOSED상태가 되어 연결이 종료됩니다.

 

이러한 과정을 4-way handshake로 연결이 정상적으로 연결이 종료되는 과정입니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

동기화(Synchronization)

동기화는 특정 객체를 동시에 접근하여 변경하기를 막는 기법입니다. 가장 빈번하게 사용되는 예를 들어볼까요? 은행 입출금과 화장실의 예가 있는데 저는 화장실이 더 좋더군요.

 

우리가 고깃집에서 소주와 맥주를 거나하게 마셔서 화장실을 갈때 열쇠가 있지요? 여기서 화장실은 한개이고 열쇠 또한 한개라고 칩시다. 열쇠를 갖고 화장실에서 볼일을 보려고 문을 잠글겁니다. 문을 잠그고 나서는 각자 취향대로 볼일을 보고 물을 내리고 열쇠를 제자리에 걸어놓습니다.  화장실의 키를 갖고 열고 잠그는 것이 일종의 동기화입니다.

여러분들은 알게 모르게 동기화를 실천하고 있는 것이지요.

 

만약 이 과정에서 동기화가 빠지게 된다면 어떻게 될까요? 제가 화장실을 쓰고 있는데, 다른 사람이 와서 제 무릎에다가 지리는 현상이 발생하겠죠. 

 

이렇듯 멀티쓰레드(Multithread) 환경에서 어떤 쓰레드(사람)가 자원(화장실 열쇠)을 이용하는데 동시에 그 자원을 사용하면 안될때 동기화가 필요합니다.

 

자바에서 이러한 동기화 기법을 적용하는 방법은 두가지가 있습니다.

첫번째로는 특정 객체의 메소드에 synchronized 예약어를 사용하는 방법과 synchronized lock을 사용하는 방법입니다.

 

synchronized 사용하여 동기화

우선 동기화를 걸지 않은 코드를 먼저 봅시다. key라는 객체는 원래 HAS-A 관계가 적절하지만 이해를 돕기위해서 그런건 고려하지 않았습니다. 코드는 그렇게 어렵지 않습니다. 

쓰레드 3개를 생성하고 각각 알기쉽게 사람 이름을 붙여줍니다. 이 쓰레드는 이제 사람이라고 치고 key를 사용하여 볼일을 본다면 어떻게 될까요?

 

 

 

 

class Key {
	
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key(); //이 객체를 쓰레드 3개에서 사용
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

아래 결과처럼 철수가 싸는 중에 영희가 철수 무릎위에 볼일을 보고, 그 다음 영철이가 들어와서 또 한번 철수 무릎에 볼일을 봅니다. 그 다음 철수가 볼일을 보네요.

철수이(가) 화장실 문을 연다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.

 

철수를 위해 동기화를 할 필요가 있겠죠? 가장 간단한 방법은 동기화할 메소드에 synchronized 만 추가하면 됩니다. 

useKey메소드를 다음과 같이 변경합시다.

public synchronized void useKey(String name){
	open(name);
	defecate(name);
	close(name);
}

그리고 실행합시다.

철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

저희는 이렇게 철수를 도울 수 있습니다.

 

Synchronized Block으로 원하는 코드부분만 동기화

Synchronized로 동기화를 거는 건 무척 쉽네요. 하지만 그 메소드 전부를 동기화하기 때문에 불필요한 곳까지 동시에 실행할 수가 없습니다.

다음의 예를 보세요. 위의 코드에서 몇줄밖에 추가하지 않았습니다.

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		lookIntoAMirror(name);
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울을 보는것까지 동기화를 걸었네요. 우리는 그럴 필요없습니다. 거울은 누가 먼저보든 상관없으니까요. 볼일 보는 것만 정확히 동기화를 하려고 한다면 block을 이용해야합니다.

 

 

 

 

아래와 같이 원하는 부분만을 동기화할 수 있습니다. 

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public void useToilet(String name){
		lookIntoAMirror(name);
		synchronized(this){
			open(name);
			defecate(name);
			close(name);
		}
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울은 순서없이 먼저 들어온 사람이 봅니다.

철수이(가) 거울을 본다.
영희이(가) 거울을 본다.
철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
영철이(가) 거울을 본다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

 

synchronized block의 인자는 사실 잠글(lock 시킬) 객체를 말합니다. this이니까 바로 key객체 자신이겠죠.

lock 시킬 객체를 한번 바꿔서 실행하도록 하지요.

 

1) 아래는 this(Counter객체)를 통해서 lock을 시켰습니다. 

class Counter{
	public Integer cnt=0;
	
	public void increase(String threadName){
		synchronized(this){ 
			for(int i=0;i<10;i++)
				System.out.println(threadName+", cnt:"+(cnt++));
			System.out.println();
		}
	}
	
}
class MyThread extends Thread{
	private String threadName;
	private Counter counter;
	public MyThread(String threadName,Counter counter){
		this.threadName=threadName;
		this.counter=counter;
	}
	public void run(){
		counter.increase(threadName);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Counter counter=new Counter();
		MyThread thread1=new MyThread("thread1",counter);
		MyThread thread2=new MyThread("thread2",counter);
		
		thread1.start();
		thread2.start();
		
		thread1.join();
		thread2.join();
		System.out.println(counter.cnt);
	}
	
}

 

 

2) 다음의 코드는 cnt객체로 lock을 시켰습니다.

public void increase(String threadName){
  synchronized(cnt){
  	for(int i=0;i<10;i++)
  		System.out.println(threadName+", cnt:"+(cnt++));
  		System.out.println();
	}
}

 

3) 마지막으로 동기화를 걸지 않은 코드입니다.

public void increase(String threadName){
	
	for(int i=0;i<10;i++)
		System.out.println(threadName+", cnt:"+(cnt++));
	System.out.println();
	
}

 

1)의 결과는 thread1의 increase, thread2의 increase 둘 중 먼저 실행되는 것이 끝난 이후에 다른 쓰레드의 메소드가 실행되며 마지막 결과도 20으로 항상 동일합니다. this로 lock을 걸었기 때문에 다른 쓰레드의 increase가 끝난 이후에 다른 쓰레드의 increase가 실행됩니다. 

 

2)의 결과는 thread1의 increase, thread2의 increase 둘 중 무작위로 실행되며 실행이 끝난 이후 마지막 결과가 20으로 항상 동일합니다. 이 경우 cnt로 lock을 걸었기 때문에 cnt가 쓰이지 않는 순간(cnt++가 되지 않는 찰나의 순간) 다른 쓰레드가 cnt++을 실행할 수 있습니다.

 

3)의 결과는 동기화를 걸지 않았기 때문에 결과가 매 수행마다 다를 수 있습니다.

 

보통은 1)의 방법으로 동기화를 많이 구현하게 됩니다.

 

각각의 결과를 비교해보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

쓰레드(Thread)

쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레드를 여러개 사용하면 동시에 여러 작업을 할 수 있는 뜻이 되는 것이죠. 

우리가 익히 알고 있는 main 함수 역시 쓰레드입니다. 프로그램 실행 시 실행되는 첫번째 쓰레드이기 때문에 main함수를 우리는 메인 쓰레드라고도 합니다.

 

쓰레드를 사용해서 얻을 수 있는 이점이 있을 텐데 어떤 이점들이 있을까요?

 

  • 우선 가장 두드러진 장점은 바로 동시성입니다. 동시에 여러 일들을 할 수 있습니다. 그렇기 때문에 작업의 효율성을 높일 수 있습니다.
  • 쓰레드끼리 메모리를 공유합니다. 그렇기 때문에 메모리가 절약되는 효과를 볼 수 있습니다. 경제적인 것이죠.

그렇다고 해서 장점만 있는 것은 아닙니다. 아래와 같은 상황을 고려해야하죠.

 

  • 프로그램의 실행 단위가 많아지면 프로그램이 상당히 복잡해질 수 있습니다.
  • 예상치 못한 버그가 생길 수 있습니다. 이를 위해서 적절히 동기화를 걸어주어야 하지요.
  • 교착상태나 기아상태로 빠질 수 있습니다.

 

 

 

자바 쓰레드 사용

자바에서는 대표적인 두 가지 쓰레드를 생성하는 방법이 있는데요. 아래의 두 가지 방법을 활용하여 쓰레드를 생성해봅시다.

 

1. Thread 클래스를 상속받는 방법

2. Runnable 인터페이스를 구현하는 방법

 

1. Thread 클래스를 상속받는 방법

아래의 코드처럼 Thread 클래스를 상속합니다.

실행되는 각 쓰레드를 구별 짓기 위해서 쓰레드 이름을 저장하고 단지 for문에서 100까지 도는 아주 간단한 쓰레드입니다. 주의 해야할 것은 쓰레드의 실제 override하는 메소드는 run이지만, 쓰레드 호출 시 실행하는 메소드는 start라는 점을 주의하시기 바랍니다. 

class MyThread extends Thread{
	
	public MyThread(String threadName){
		super(threadName);
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(this.getName()+":"+i);
		}
		System.out.println();
	}
}
public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			new MyThread("Thread"+i).start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

위의 코드에서 프로그램을 예상할 수 있나요? 평소의 프로그램(싱글쓰레드)같았다면 우리는 이렇게 예상을 했을 겁니다. 

MainThread Start

Thread1:0

Thread1:1

Thread1:2

Thread1:3

...

Thread2:0

Thread2:1

Thread3:2

...

Thread3:98

Thread3:99

MainThread End

하지만 우리는 동시성을 원하니까 쓰레드를 돌렸지요. 그래서 위의 결과가 될 수도 있고(진짜 운이 드럽게 좋다거나 없다면) 아닐 수도 있습니다. 

대부분의 결과는 아래와 같이 제 얼굴처럼 뒤죽박죽된 결과가 발생합니다. 아래 결과는 실제 제 똥컴에서 돌린 결과입니다.

MainThread Start 
MainThread End 
Thread1:0 
Thread1:1 
Thread1:2 
Thread1:3

...

Thread1:59 
Thread1:60 
Thread2:0 
Thread2:1

...

Thread2:98 
Thread2:99 

Thread1:61 
Thread3:0 
Thread3:1

...

Thread1:97 
Thread1:98 
Thread1:99

 

쓰레드의 실행 순서는 이렇게 예측할 수 없습니다.

 

 

 

2. Runnable 인터페이스 구현

실제로 쓰레드를 생성할때 많이 사용하는 방법입니다. Runnable 인터페이스를 구현한 클래스를 Thread의 생성자로 주입하여 실행하는 방법이죠. 아래의 코드를 봅시다. 

 

달라진게 있다고 한다면 implements Runnable 쪽인것 같네요. 이렇게 구현한 MyThread라는 클래스를 객체화하여 main 메소드에서 실행시키고 있습니다.

Thread라는 클래스의 생성자로 Runnable 객체를 전달하고 있습니다.

Thread thread=new Thread(new MyThread("Thread"+i));
thread.start();

 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar){
		System.out.println("MainThread Start");
		for(int i=1;i<=3;i++){
			Thread thread=new Thread(new MyThread("Thread"+i));
			thread.start();
		}
		System.out.println("MainThread End");	
	}
	
}

 

결과는 1번의 Thread 클래스를 상속받는 방법과 비슷하게 순서를 예측할 수 없이 실행됩니다.

MainThread Start 
MainThread End 
Thread2:0

...

Thread1:62 
Thread2:93 
Thread2:94

...

Thread3:97 
Thread3:98 
Thread3:99

 

저는 항상 Main 쓰레드가 먼저 끝나버리는군요. 

이점에 대해서 불만을 가지고 있는데요. 메인 쓰레드는 항상 다른 쓰레드를 기다렸다가 종료할 수 없을까요?? 자기 할일 끝났다고 먼저 가버리는 것은 조금 매너가 없으니까요. 

 

그래서 join이라는 쓰레드 메소드가 존재합니다.

 

join

join이라는 메소드를 통해서 분기를 어떤 지점에 합칠 수 있습니다. 그러니까 쓰레드를 생성한 쓰레드는 그 지점에서 기다려야합니다. 아래의 코드를 통해서 알아보도록 합시다.

 

알아보려면 코드를 조금 변경해야하는데, 우선 join을 쓰게 되면 InterruptedException이 발생합니다. 처리하기 귀찮으니 throws를 통해서 그냥 던져줍시다. 누군가 알아서 먹든가 하겠죠.

아래 코드처럼 변경해서 실행하면 메인쓰레드는 자신이 생성한 3개의 쓰레드를 끝날때까지 기다렸다가 자신도 종료하게 됩니다.

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
		}
		
		for(int i=1;i<=3;i++)
			thread[i].join();
		
		System.out.println("MainThread End");	
	}
	
}

 아래의 실행결과처럼 메인쓰레드는 나머지 3개가 종료할때까지 기다립니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

왜 join으로 이름을 정했을까요? 아래의 그림을 보면 이해하기가 쉬울 겁니다. 위 코드의 상황을 그림으로 옮겨놓았습니다.

main은 thread1, thread2, thread3을 수행시킵니다. 이때 thread1, thread2, thread3이 끝나면 다시 자신을 실행시키는 main와 합쳐지지요. 이해하기 쉽죠?

 

 

아 이것도 불만이다. 난 쓰레드를 썼지만 순차적으로 실행하겠다! thread1끝나면 thread2실행하고 thread2끝나면 thread3실행하겠다 하시는 분들은 thread 실행시키자 마자 바로 join거시면 됩니다. 

class MyThread implements Runnable{
	private String threadName;
	public MyThread(String threadName){
		this.threadName=threadName;
	}
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(threadName+":"+i);
		}
	}
}

public class ThreadTest {

	public static void main(String[] ar) throws InterruptedException{
		System.out.println("MainThread Start");
		Thread[] thread=new Thread[4];
		for(int i=1;i<=3;i++){
			thread[i]=new Thread(new MyThread("Thread"+i));
			thread[i].start();
			thread[i].join();
		}
		
		
		System.out.println("MainThread End");	
	}
	
}

 

아래의 그림에서 점선이 join하는 구간입니다.

 

실행결과는 뭐.. 순차적입니다.

MainThread Start 
Thread1:0 
Thread1:1

...

Thread3:99

MainThread End

 

 

근데 이렇게 구현할 거면 쓰레드 안쓰는 것이 낫죠. 단지 join을 어떻게 사용하는지 보여드린겁니다.

 

간단하게 자바에서 쓰레드를 사용하는 방법을 알아보았습니다. 

 

반응형
블로그 이미지

REAKWON

와나진짜

,