원본 출처


장고(Django)는 웹 애플리케이션을 만들기 위한 파이썬 기반의 프레임워크로, 튼튼하며, 오픈 소스이다. 장고의 인기는 지난 2년 동안 증가해왔고, 이미 성숙한 단계에 있으며, 실제로 큰 규모의 단체들에게 이용되어진다.

웹 애플리케이션을 제작하기 위한 다른 파이썬 기반의 프레임워크들(Flask, Pyramid) 중에서도 장고는 가장 인기가 많다. 장고는 파이썬 버전 2.7과 3.6을 모두 지원한다. 이 글을 쓰는 시점에서 파이썬 2.7은 커뮤니티, third party packages, 그리고 온라인 문서화의 측면에서 여전히 이용가능한 버전이다. 장고는 적절하게 사용되면 안전하고, 높은 차원의 유연성을 제공한다. 바로 다음 내용이 파이썬을 사용한 서버 사이드 애플리케이션을 개발할 때 더 나아갈 수 있는 방법이다.

한 사람의 숙력된 파이썬 그리고 장고 개발자로써, 장고 설정(setup)에 대해 내가 몇 년동안 배우고 모아왔던 몇 가지 최고의 실행 방법을 여러분들과 공유할 것이다. 당신이 몇 개의 장고 프로젝트를 수행했는지, 혹은 이제 막 하나를 시작하는 단계인지와는 상관없이 여기에 기술된 방법들은 당신이 미래에 더 나은 애플리케이션을 만들도록 도와줄 것이다.

나는 당신들이 몇몇 툴들을 당신만의 개발 도구상자에 즉시 추가할 수 있도록 매우 실용적인 마음으로 이 글을 썼다. 당신은 심지어 다음 프로젝트를 위해 직접 제작한, 더욱 발전된 형태의 장고 표준 문서를 만들 수도 있다.

이 글의 목적을 위해, 당신이 리눅스 우분투(Linux Ubuntu)를 사용하고 있다고 가정한다.
글에서, $ 기호로 시작하는 1줄짜리 코드들은 해당 줄이 터미널에서 입력되어 져야 한다는 것을 강조하기 위한 것이다. 복붙할 때 $ 기호는 제외하라.


(자, 레쓰게릿)



Virtual Environment

파이썬 기반의 애플리케이션을 개발하는 과정에서, 지속적으로 third party package를 이용해왔다. 이러한 패키지들은 자주 업데이트되어지므로, 패키지들을 지속적으로 준비하는 것이 필수이다. 하나의 로컬 컴퓨터에서 다수의 프로젝트를 개발할 때, 각 패키지의 현 버전의 트랙(track)을 유지하는 것이 문제이다. 그 이유는 다른 프로젝트에서 같은 패키지의 다른 버전을 사용하는 것이 불가능하기 때문이다. (프로젝트 A에서 패키지 P의 1.2 버전을 사용하는데, 프로젝트 B에서 패키지 P의 1.4버전을 사용할 수 없다.) 게다가, 한 프로젝트에서 패키지를 업데이트하는 것이 다른 프로젝트에서 하나 또는 여러 개의 기능(functionality)의 역할을 하는 패키지들을 멈추게 할 수도 있다.

이런 문제는 가상 환경을 통해 해결될 수 있다. 직접 가상 환경(Virtual Environment)을 설정해보자.

우선 가상환경을 설치하라.

$ apt-get update
$ apt-get install python-pip python-dev build-essential

$ export LC_ALL="en_US.UTF-8" # 다음 줄에서 에러가 뜰 경우 쉘 환경 변수를 등록하라.

$ pip install --upgrade pip
$ pip install --upgrade virtualenv
$ mkdir ~/.virtualenvs
$ pip install virtualenvwrapper
$ export WORKON_HOME=~/.virtualenvs
$ nano ~/.bashrc



~/.bashrc 의 파일 끝에 아래 줄을 추가하라.

. /usr/local/bin/virtualenvwrapper.sh

(반드시 해당 경로에 해당 파일이 있는지 확인한 후 추가하세요.)
(만약 파일이 없다면, find / -name "virtualenvwrapper.sh" 명령어로 경로를 찾길 바랍니다.)


실행하라.

$ . .bashrc


설치가 끝나면, 프로젝트이름을 타이핑해서 새 가상 환경을 만들어라.

$ mkvirtualenv project_name


가상 환경에 있는 동안은 터미널에서 아래처럼 접두사가 추가된다는 것을 볼 것이다.

(project_name) ofir@playground:~$


가상 환경을 나가기 위해선 아래의 명령어를 사용하라. 이 명령어로 인해 로컬 컴퓨터의 실제 파이썬 컨텍스트로 되돌아 간다.

$ deactivate


특정 가상 환경 컨텍스트를 시작하려면 아래의 명령어를 사용하라.

$ workon project_name


본인의 로컬 머신에 존재하는 가상 환경들의 목록을 보려면 아래의 명령어를 사용하라.

$ lsvirtualenv


로컬 컴퓨터의 가상환경에서 프로젝트 의존성을 갖는 것은 분리된 환경에서 패키지들을 유지하도록 한다. 즉, 하나 또는 다수의 프로젝트만을 위해 특정 패키지들을 사용한다. 새 가상환경을 만들 때, 당신은 설치된 패키지 없이 완전히 새로운 가상환경에서 시작할 것이다. 그 때는 다른 가상환경에 있는 프로젝트와 동일한 패키지를 설치하더라도 다른 버전을 사용할 수 있다.

장고 설치를 예로 들면,

(project_name) $ pip install Django==1.11

(현재 장고 최신 버전은 2.0.5)

위에서 설치한 장고 1.11 버전은 오직 그 환경(여기선 project_name)에서만 이용가능한 것이다. 당신의 주요 파이썬 인터프리터 또는 로컬 컴퓨터의 다른 가상 환경 모두 당신이 그 환경에서 설치했던 새로운 장고 패키지에는 접근할 수 없을 것이다.

가상 환경을 이용하여 서버를 실행하는 명령을 사용하기 위해, 가상환경의 컨텍스트에서 아래와 같은 명령어를 사용하라.

(project_name) $ cd /path/to/django/project
(project_name) $ ./manage.py runserver


마찬가지로, 가상환경에서 파이썬 인터프리터에 들어가려면,

(project_name) $ python

이미 그 환경에서 설치된 패키지들을 이용할 수 있다.




Requirements

Requirements는 프로젝트에서 사용중인 파이썬 패키지의 목록으로, 각 패키지마다 이름버전이 함께 기록되어 있다.

아래는 requirements.txt 파일의 예시이다.

dicttoxml==1.7.4
Django==1.11.2
h5py==2.7.0
matplotlib==2.0.2
numpy==1.13.0
Pillow==4.1.1
psycopg2==2.7.1
pyparsing==2.2.0
python-dateutil==2.6.0
pytz==2017.2
six==1.10.0
xmltodict==0.11.0


requirements.txt 파일을 최신으로 유지하는 것은 다른 개발자들과 적절히 협업하는 데 있어 필수적이다. 또한 당신의 배포 환경을 적절하게 설정하는 것도 중요하다. 이 파일이 Code Repository 에 포함되어 있으면, 단 한 줄만 터미널에 입력하면 가상 환경에서 설치된 모든 패키지들을 업데이트할 수 있다. 그 때는 매우 짧은 시간에 실행하므로, 함께 일할 개발자를 깨울 수 있다.

requirements.txt 파일을 생성하거나 기존의 것을 갱신하기 위해, 가상환경 내에서 아래와 같은 명령어를 사용하라.

(project_name) $ pip freeze > requirements.txt


당신의 편리함을 위해, 이 명령어가 Git 저장소에 의해 트랙되어진 폴더에서 실행하는지 확인하라. 이는 다른 코드의 인스턴스가 requirements.txt 파일에 접근하는 것을 가능하게 한다.

만약 새 개발자가 팀에 합류하면, 또는 당신이 requirements.txt 파일에 기록된 동일한 패키지를 사용하는 새 환경을 설정하길 원한다면, 가상 환경 컨텍스트에서 아래의 명령어를 실행하라.

(project_name) $ cd /path/to/requirements/file
(project_name) $ pip install -r requirements.txt


파일에 기록된 모든 요구사항은 즉시 당신의 가상환경에 설치될 것이다. 오래된 버전들은 갱신될 것이고, 새로운 버전들은 파일의 요구사항에 정확히 맞추고자 다운그레이드(downgrade)될 것이다. 당신이 여전히 유지하길 원하는 환경들 사이에서 차이가 있을지도 모른다는 것에 주의하라!

나는 당신의 Work flow에 이 명령어들을 통합하는 것을 적극 추천한다. 저장소에 코드를 push 하기 전에 requirements.txt 파일을 갱신하라. 그리고 저장소로부터 코드를 pull한 후에 requirements.txt 파일을 설치하라.




Better settings.py configuration

장고는 매우 기본적이고 유용한 settings.py 파일을 즉시 사용할 수 있다. (즉, 기본적으로 제공한다는 소리) 이 파일은 당신의 프로젝트에서 메인이 되고, 매우 유용한 설정들을 정의한다. 또한 매우 간단하다. 다만 가끔, 개발자가 팀에서 일할 때, 배포 환경을 설정할 때, 혹은 당신이 기본으로 제공되는 settings.py 파일을 둘 이상 필요로 할 때 등 다양한 설정 파일이 요구될 수 있다.

다수의 설정 파일들은 개별적으로 각 환경을 위해 쉽게 사용자가 맞춤형으로 설정할 수 있게 한다. (여기서 환경은 위에서 언급한 가상 환경이 아닌, 실행 환경을 의미)

ALLOWED_HOSTS # for production environment
DEBUG
DATABASES # for different developers on the same team


settings.py 파일을 설정하기 위한 확장된 접근법을 제안하겠다. 이 방식은 당신이 다른 버전들을 유지하도록 해주고, 어떤 특정 시간과 특정 환경에서 원하는 패키지를 사용하도록 해준다.

첫번째, settings.py 파일 경로로 들어가라.

(project_name) $ cd /path/to/settings/file

(장고 프로젝트 폴더에 들어가면 project_name/settings.py 경로)

그 다음, settings 라는 이름의  새 모듈(=디렉토리)를 생성하라. 모듈은 __init__.py 파일을 포함하는 폴더이다.

(project_name) $ mkdir settings


settings.py 파일을 base.py 로 이름을 바꾸고 위에서 생성한 새 모듈(settings 폴더)로 그 파일을 옮겨라.

(project_name) $ mv settings.py settings/base.py


이 예시에서는, 당신이 개발환경과 배포환경만을 위한 하나의 settings 파일을 설정하길 원한다고 가정을 한다. 같은 팀 내 다른 개발자들은 다른 settings 파일을 정의하기 위해 정확히 동일한 방법을 사용할 수 있다.

개발 환경을 생성하라.

(project_name) $ nano settings/development.py


해당 파일 내에 아래와 같이 입력하라.

from .base import *

DEBUG = True


배포 환경을 생성하라.

(project_name) $ nano settings/production.py


해당 파일 내에 아래와 같이 입력하라.

from .base import *

DEBUG = False
ALLOWED_HOSTS = [‘app.project_name.com’, ]


이제는 당신이 구체적인 환경의 설정을 갱신하거나 추가하길 원할 때마다, 자신만의 settings 파일에서 쉽게 구현할 수 있다.

여러분들은 "장고는 각 환경에서 어떤 settings 파일을 로드할 지 어떻게 아는가"에 대해 궁금할 지도 모른다. 그것은 바로 __init__.py 파일이 이용되는 이유이다. 예를 들어, 서버를 실행할 때 로드하기 위해 장고가 settings.py를 찾을 때, 장고는 정확히 settings.py 파일 보다는 settings 모듈을 찾는다. 하지만 그 모듈이 __init__.py 파일을 포함해야만 장고가 그 모듈이 settings.py 파일과 같은 것이란 걸 알게 된다. 장고가 __init__.py 파일안에 무엇이 쓰여졌든지 그 파일을 로드해서 실행할 것이다.

그러므로, 우리는 어떤 settings 파일을 __init__.py 파일 안에 로드할 지를 정의할 필요가 있다.

(project_name) $ nano settings/__init__.py


파일 안에 아래와 같이 타이핑 하라. (배포 환경으로 설정할 경우)

from .production import *


이 방식에서, 장고는 매 실행마다 base.py와 production.py 를 모두 로드할 것이다.

이제 남은 유일한 설정은 당신의 .gitignore 파일에 __init__.py를 기록하는 것이다. 그래서 push와 pull을 할 때 포함되지 않게 한다. 일단 새 환경을 설정하면, settings 모듈에 새 __init__.py 파일을 생성하는 것을 잊지말라. 그 다음 전에 수행했던 것처럼 정확하게 요구되는 settings 파일을 임포트(import)하라.



끝으로,

이 글에서는 우리는 장고 프로젝트를 더 효율적으로 설정하기 위한 세 가지 (베스트) 실행 방식을 다루어보았다.

  1. 가상환경에서 작업하는 것

  2. 워크플로우에서 지속적으로 requirements.txt를 이용하고, 파일을 최신의 것으로 유지하는 것

  3. settings 모듈을 사용하여 설정하는 것



'Programming Language > Python' 카테고리의 다른 글

[정리 03] 유용한 기능들  (0) 2021.02.05
[정리 02] 표준 데이터 타입(Standard Data-Type)  (0) 2021.02.04
[정리 01] Python 기초  (0) 2021.02.04
간단한 알고리즘  (0) 2019.06.30
Tip (Reference)  (0) 2019.05.15

  

 

스레드(Thread)

 

멀티 태스킹(Multi-Tasking) : 여러 개의 애플리케이션을 동시에 실행하면서 컴퓨터의 시스템 성능을 높이기 위한 기법이다.

 

 예를 들어, 파일을 인쇄하면서 동시에 문서를 편집하거나 인터넷에서 파일을 다운로드 받는 등의 경우가 있다. 이를 병렬 처리라고 한다.

 

 이러한 경우는 운영 체제가 CPU의 시간을 쪼개서 각 작업들에 할당하여서 작업들이 동시에 수행되는 것처럼 보이게 하기 때문이다. 멀티-코어(Multi-Core)를 가지고 있는 CPU라면 실제로도 동시에 실행될 것이다.

 

멀티 스레딩(Multi-Threading) : 하나의 애플리케이션 안에서도 여러 가지 작업을 동시에 하는 것을 의미한다


 앞서 말한 멀티 태스킹과 유사한 의미이다. 단지 범위를 애플리케이션 내부로 제한한 것이다. 자바는 멀티 스레딩을 프로그래머들한테 언어 수준에서 제공한다.

 

 예를 들어, (1) 음악을 재생하는 애플리케이션은 인터넷을 통하여 (2) mp3 파일을 다운로드 받으면서 동시에 압축을 풀어서 음악을 재생한다고 하면 각각의 작업 (1), (2) 는 스레드(Thread)라고 불린다.

 

스레드(Thread) : 실이라는 의미로, 하나의 실행 흐름(thread of execution)을 의미한다.


하나의 애플리케이션 안에서 동시에 실행되는 여러 스레드를 만들 수 있으며 이 스레드들은 자바 런타임 시스템에 의하여 동시에 실행된다.

 

 

 

프로세스와 스레드

 

프로세스와 스레드는 컴퓨터의 실행 단위이다.

  • 프로세스(process)는 자신만의 데이터를 가진다.

  • 스레드(thread)들은 동일한 데이터를 공유한다.

동시에 데이터를 공유하는 것은 자칫 위험할 수 있으나, 공유함으로서 스레드 간의 통신이 상당히 효율적이게 된다.

 

프로세스(Process)

  • 실행 중인 프로그램을 의미한다.

  • 독자적으로 실행이 가능한 환경을 가진다.

  • 자신 만의 자원을 가진다.

  • 프로세스의 메모리 공간은 다른 프로세스와 완전히 분리된다.

  • 자바 가상 기계는 하나의 프로세스로 실행된다.

  • 하나의 애플리케이션이 여러 개의 프로세스로 이루어질 수도 있다.

 

스레드(Thread)

  • 경량 프로세스(lightweight process)라고도 불린다.

  • 스레드를 생성하는 것은 프로세스를 생성하는 것보다 훨씬 자원이 적게 든다.

  • 스레드들은 프로세스 안에 존재한다.

  • 메모리와 파일을 포함하여 프로세스의 모든 자원을 공유한다.

  • 모든 자바 애플리케이션은 적어도 하나의 스레드를 가진다. 이를 메인 스레드(main thread)라고 부른다.

  • 메인 스레드에서 필요하면 추가적인 스레드를 생성할 수 있다.

 

동시 작업 프로그램은 단일 작업 프로그램보다 신경 써야할 부분이 많은데 그중 하나가 동기화이다. 동시에 여러 작업들이 같은 데이터를 공유하게 되면 발생하는 문제로 자바에는 이 문제를 해결할 수 있는 다양한 툴이 포함되어 있다. 이는 나중에 설명하도록 하겠다.

 

 

 

스레드의 생성과 실행

 

스레드를 나타내는 클래스는 Thread 이다.

 

Thread thread = new Thread();

thread.start();

  • Thread 클래스의 객체를 생성하고 start()를 호출하면 스레드가 시작된다.

스레드의 작업은 Thread 클래스의 run() 메소드를 재정의(overriding)하여 작성한다. 따라서 스레드가 실행하는 모든 작업은 run() 메소드 안에 있어야 한다.


스레드가 시작되면 run() 메소드는 자바 런타임 시스템에 의하여 호출된다.

 

스레드를 생성하여 작업을 실행하는 방법은 아래와 같은 두 가지 방법이 있다.

  • Thread 클래스를 상속하는 방법 : Thread 클래스를 상속받은 후에 run() 메소드를 재정의(overriding)한다.

  • Runnable 인터페이스를 구현하는 방법 : run() 메소드를 가지고 있는 클래스를 작성하고 이 클래스의 객체를 Thread 클래스의 생성자를 호출할 때 매개변수로 전달한다


- Thread 클래스를 상속하는 방법


class MyThread extends Thread {

    @Override

    public void run() {

        System.out.println("MyThread가 해야 할 작업을 명시하는 곳입니다.");

    }

}


public class ThreadTest {

    public static void main(String[] args) {

        Thread thread = new MyThread();

        thread.start(); // 무조건 호출해야 스레드가 실행된다!

        System.out.println("Main Thread 입니다.");

    }

}


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


 자바에서는 단일 상속만 가능하기 때문에 다른 클래스를 이미 상속받은 클래스는 스레드로 만들 수 없다이 경우에 Runnable 인터페이스를 구현하는 방법을 사용한다. Runnable 인터페이스 안에는 메소드 run() 만이 정의되어 있다. 이 방법이 좀 더 일반적인 스레드 실행 방법이다.

 

class MyRunnable implements Runnable {

@Override

public void run() {

...// 스레드의 작업을 명시하는 곳

}

}

 

public class RunnableTest {

public static void main(String[] args) {

Thread thread = new Thread(new MyRunnable());

thread.start();

}

}



멀티 스레드 예제

class MyRunnable implements Runnable {
    String myName;

    public MyRunnable(String name) {
        myName = name;
    }

    public void run() {
        for(int i = 10; i >= 0; i--) {
            System.out.println(myName + i + " ");
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable("A"));
        Thread t2 = new Thread(new MyRunnable("B"));
        t1.start();
        t2.start();
    }
}

 [출력 결과]

 여기서 총 스레드의 개수2개가 아닌 3임에 주의해야 한다. 그 이유는 메인스레드와 t1 스레드, t2 스레드가 실행되고 있기 때문이다세 개의 스레드 모두 서로 다른 실행 흐름을 가진다.

 

- 예제 : 스레드를 정해진 시간 동안만 일시 중지하기

import javax.swing.*; import java.awt.*; public class CountDownTest extends JFrame { public static void main(String[] args) { new CountDownTest(); } private JLabel label; CountDownTest() { setTitle("카운트 다운 예제"); setSize(300, 200); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); label = new JLabel("Start", SwingConstants.CENTER); //글자를 레이블의 중간에 위치시킨다. label.setFont(new Font("Serif", Font.PLAIN, 100)); add(label); Thread myThread = new MyThread myThread.start(); setVisible(true); } class MyThread extends Thread { //스레드를 내부 클래스로 만들면 필드에 접근이 쉽다. public void run() { for(int i = 10; i >= 0; i--) { try { Thread.sleep(1000); //sleep 의 매개변수는 밀리초 단위로 받기 때문에 1초동안 일시중지 } catch (InterruptedException e) { e.printStackTrace(); } label.setText(i + ""); // 1초가 지나면 레이블의 텍스트를 변경한다. } } } }





스레드 활용

 

- Thread 클래스에는 스레드의 생성 및 제어에 관련된 여러 가지 메소드들이 정의되어 있다.


생성자

 Thread()

 매개변수가 없는 기본 생성자이다.

 Thread(String name)

 이름이 name인 Thread 객체를 생성한다.

 Thread(Runnable target)

 Runnable 을 구현한 객체에 해당하는 스레드를 생성한다.

 Thread(Runnable target, String name) 

 지정된 이름과 Runnable 구현 객체의 스레드 생성

 메소드

 static int activeCount()

 현재 활동 중인 스레드의 개수를 반환한다.

 String getName()

 스레드의 이름을 반환한다.

 int getPriority()

 스레드의 우선순위를 반환한다.

 void interrupt()

 현재의 스레드를 중단한다.

 boolean isInterrupted()

 현재의 스레드가 중단되었으면 true를 반환한다.

 void setPriority(int priority)

 스레드의 우선순위를 지정한다.

 void setName(String name) 스레드의 이름을 지정한다.
 static void sleep(long milliseconds) 현재의 스레드를 지정된 시간만큼 일시 중지시킨다.
 void run()

 스레드 시작 시 호출되는 메소드로, 실행할 작업을 작성해야 한다.

 void start() 스레드를 시작한다.
 static void yield() 현재 스레드를 다른 스레드에 양보하게 만든다.



 


스레드 스케줄링

  • CPU 스케줄링에 의하여 하나의 CPU를 여러 스레드가 나누어 쓸 수 있게 스케줄링하는 방법으로, 어떤 원칙과 순서로 스레드를 수행시킬 것인가를 결정한다.

  • 자바 런타임 시스템은 우선순위(priority) 스케줄링을 이용한다. 이는 우선순위가 높은 스레드가 먼저 실행되는 알고리즘이다. 참고로 모든 스레드는 우선순위를 가지고 있다.

  • 스레드는 생성되면 Thread 클래스에 정의된 MIN_PRIORITYMAX_PRIORITY 사이의 숫자인 우선순위를 배정 받는다.

  • 스케줄러는 현재 수행 가능한 스레드 중에서 가장 우선순위가 높은 스레드를 먼저 수행시킨다.

  • 스레드는 생성될 때 자신을 생성한 스레드로부터 우선순위를 상속받는다.

  • 실행도중에는 setPriority()getPriority() 메소드를 이용하여 스레드의 우선순위를 변경하거나 얻는 것이 가능하다.

 

- 아래 4개의 메소드는 스케줄링과 관련된 가장 일반적으로 많이 쓰이는 메소드이다.

    • sleep()

    • join()

    • yield()

    • interrupt()

sleep(long millis)

  • 밀리초 단위로 스레드를 쉬게 할 수 있다.

  • 스레드가 수면 상태로 있는 동안 interrupt 되면 InterruptedException 이 발생한다. 따라서 예외를 처리하여야 한다.

  • 이 메소드는 CPU의 시간을 다른 스레드에게 넘겨주는 효율적인 방법이다.

  • 다른 스레드의 보조를 맞추는 용도로도 사용될 수 있다.

  • sleep()은 매개변수로 지정된 시간을 받을 때 밀리초 단위와 나노초 단위로 지정받을 수 있다.

  • 해당 메소드는 분명히 지켜지는 것은 아니기 때문에 언제라도 중단될 수 있음을 감안해야 한다.

 

- interrupt()

  • 하나의 스레드가 실행하고 있는 작업을 중지하도록 하는 메소드이다.

  • 일반적으로 인터럽트 메소드가 실행되면 해당 스레드는 종료된다.

  • 하나의 스레드가 다른 스레드의 interrupt()를 호출하면 대상 스레드가 중지된다.

  • 그러나 인터럽트에 대한 스레드의 반응은 프로그래머가 설정할 수 있다.

try {

Thread.sleep(1000);

}catch(InterruptException e) {

// interrupt에 대한 스레드의 반응을 서술하는 곳

}

 

 단, 스레드가 실행 중에 한번도 sleep()을 호출하지 않는다면 InterruptedException을 받지 못한다. 그러면 아래와 같이 인터럽트를 검사해주는 것이 좋다.


if(Thread.interrupted()) {

//인터럽트에 대한 스레드 반응 코드

}


- join()

  • 하나의 스레드를 다른 스레드가 종료될 때까지 기다리게 한다.

  • 중복 정의된 join()을 사용하면 기다리는 시간을 지정할 수 있다.

 예를 들어 A라는 스레드가 선언된 객체에서 B라는 스레드의 종료를 기다리게 할려면 클래스 안에 B.join(); 이라고 작성해야 한다.


- yield()

  • CPU를 다른 스레드에게 양보하는 메소드이다

  • 동일한 우선순위를 가지고 있는 다른 스레드를 실행시키고자 할 때 사용된다.


- 예제

public class ThreadControl {
    //내부 클래스
    private static class MessageLoop implements Runnable {
        public void run() {
            String[] messages = { "Pride will have a fall.",
            "Poser is dangerous unless you have humility.",
            "Office changes manners.",
            "Empty vessels make the mose sound." };

            try {
                for(int i = 0; i < messages.length; i++) {
                    print(messages[i]);
                    Thread.sleep(2000);
                }
            }catch(InterruptedException e) {
                print("아직 끝나지 않았습니다!");
            }
        }
    }

    static void print(String message) {
        String threadName = Thread.currentThread().getName();
        System.out.format("%s : %s%n", threadName, message);
    }

    public static void main(String[] args) throws InterruptedException {
        int tries = 0;

        print("추가적인 스레드를 시작합니다.");
        Thread thread = new Thread(new MessageLoop());
        thread.start();

        print("추가적인 스레드가 끝나기를 기다립니다.");

        while(thread.isAlive()) {
            print("아직 기다립니다.");
            thread.join(1000); // thread가 종료하기를 1초동안 기다린다.
            tries++;

            if(tries > 2) {
                print("참을 수 없습니다");
                thread.interrupt();
                thread.join();
            }
        }
        print("메인 스레드 종료!");
    }
}

[출력 결과]



 

 

 

동기화

 

스레드 간섭(thread interference) : 다수의 스레드가 공유된 데이터에 접근할 때 발생한다.

 

 예를 들어, 하나의 스레드가 공유 데이터 값을 변경하고 있는 중간에 다른 스레드가 끼어들면 원치 않은 결과가 나타나게 된다.

 

- 메모리 일치 오류(memory consistency error) : 공유된 메모리의 불일치가 나타나는 현상이다.


동기화(Synchronization) : 위의 2가지의 문제를 해결하는 도구로, 공유된 자원 중에서 동시에 사용하면 안 되는 자원을 막는다공유된 자원을 충돌 없이 사용할 수 있도록 해주는 도구이다예를 들어 하나의 스레드의 작업이 끝나면 다음 스레드가 사용할 수 있도록 한다

 

임계 영역(Critical Section) : 일상을 예로 들면, 공중 화장실, 공동 세미나실, 공동 실험실, 공동 강의실과 같은 곳은 모두 하나의 사용자가 사용중이면 다른 사용자는 사용이 끝날 때까지 기다려야 한다. 이처럼 동시에 접근할 수 없는 공유 자원에 접근하게 해주는 코드의 일부라고 보면 된다.


 

 

스레드 간섭(Thread Interference)

 

서로 다른 스레드에서 실행되는 두 개의 연산이 동일한 데이터에 적용되면서 서로 겹치는 것을 의미한다.

 

- 결과적으로 데이터들이 섞이거나 데이터 값이 이상하게 변경된다.


- 예제

class Counter {
    private int value = 0;
     
    public void increment() { 
        value++; 
    }
    
    public void decrement() { 
        value--; 
    }
    
    public void printCounter() { 
        System.out.println(value); 
    }
} 

 위의 코드를 기반으로 하나의 특수한 상황을 예로 들면, 스레드 Aincrement()를 호출하고 동시에 스레드 Bdecrement()를 호출했다고 가정하자.


value의 초기값이 0이라면 다음과 같은 순서대로 연산이 겹칠 수 있다.


(1) 스레드 A : 변수 value의 현재값을 가져온다.

(2) 스레드 B : 변수 value의 현재값을 가져온다.

(3) 스레드 A : 가져온 값을 1 증가한다. 증가된 값은 1이 된다.

(4) 스레드 B : 가져온 값을 1 감소한다. 감소된 값은 1이 된다.

(5) 스레드 A : value에 값을 저장한다. value 1이 된다.

(6) 스레드 B : value에 값을 저장한다. value 1이 된다.

 

위의 경우 스레드 A가 저장한 값은 사라진다.

 


[전체 코드]

class Counter {
    private int value = 0;
     
    public void increment() { 
        value++; 
    }
    
    public void decrement() { 
        value--; 
    }
    
    public void printCounter() { 
        System.out.println(value); 
    }
}

class MyThread extends Thread {
    Counter sharedCounter;
     
    public MyThread(Counter c) {
        this.sharedCounter = c;
    }
     
    public void run() {
        int i = 0;
        while(i < 20000) {
            sharedCounter.increment();
            sharedCounter.decrement();
            //증가했다가 감소시키는 과정이 순차적이므로 카운터 값은 변화가 없다.
             
            if(i % 40 == 0) sharedCounter.printCounter(); //가끔 카운터 값 출력
             
            try {
                sleep((int) (Math.random()*2)); //난수 시간만큼 스레드를 중지한다.
            }catch (InterruptedException e) { }
             
            i++;
        }
    }
}
 
 
public class CounterTest {
    public static void main(String[] args) {
        Counter c = new Counter();
         
        new MyThread(c).start();
        new MyThread(c).start();
        new MyThread(c).start();
        new MyThread(c).start();
    }
}


결과 : (실행결과는 컴퓨터와 상황에 따라 상당히 달라진다.)


...

-7

-8

-7

-7

-8

-7

-7

...

 

 스레드 간섭이 없다면 모두 0이 출력되어야 하나, 음수 값을 가짐으로서 스레드 간섭이 발생했음을 알 수 있다.

 


 

 

메모리 불일치 오류

 

서로 다른 스레드가 동일한 데이터의 값을 서로 다르게 볼 때, 발생한다.

 

 예를 들어 아래와 같은 정수 변수가 스레드 A와 스레드 B 사이에서 공유된다하자.


int value = 0;


스레드 Avalue++; 하여 value 값이 증가하였다.

잠시 후에 스레드 Bvalue의 값을 출력하였다.

 

 위의 상황이 하나의 스레드 안에서 실행되었다면 아무런 문제가 발생하지 않고 “1”이 출력된다. 그러나 두 개의 문장이 서로 다른 스레드에서 실행되었다면 출력되는 결과는 “0”이 될 수 있다. 여기서 문제는 스레드 A가 변경한 값이 스레드 B에게 보일 것이라는 보장이 없기 때문이다.

 

 

 

동기화된 메소드

 

앞서 업급했던 동기화를 통해 스레드 간섭과 메모리 불일치 오류를 해결할 수 있다고 하였다.

 

동기화의 방법에는 두 가지가 있다.

  • 동기화된 메소드(synchronized methods)

  • 동기화된 문장(synchronized statements)

 

동기화된 메소드를 만들려면 synchronized 키워드를 메소드 선언에 포함하면 된다. 이 키워드의 의미는 동시에 접근할 수 없는 공유 자원에 접근할 수 있게 해주는 영역인 임계 영역 안에서 스레드를 실행하는 것이 보장된다는 것이다. 쉽게 말해 synchronized 키워드가 붙은 메소드는 공유 메소드가 되어 하나의 쓰레드만이 사용할 수 있게 된다.

 

 아래는 위에서 문제가 발생했던 Counter 클래스를 바람직하게 정의한 것이다.


class Counter {

private int value = 0;

public synchronized void increment() { value++; }

public synchronized void decrement() { value--; }

public synchronized void printCounter() { System.out.println(value); }

}


 이처럼 synchronized 키워드를 붙이게 되면, 


 먼저 동기화된 메소드는 동시 호출되더라도 마이크로 단계들이 겹치지 않는다하나의 스레드가 동기화된 메소드를 실행하고 있으면그 스레드가 종료할 때까지 다른 모든 스레드는 중지된다. 따라서 스레드 간섭 문제를 해결한다.


 동기화된 메소드가 종료되면 자동적으로 이후의 동기화된 메소드 호출은 변경된 상태를 볼 수 있다. 따라서 메모리 불일치 오류 문제를 해결한다.

 

- 결과적으로, 어떤 객체를 두 개 이상의 스레드가 사용한다면, 공유된 객체의 변수에 대한 모든 읽기와 쓰기 연산은 동기화된 메소드를 통하여 이루어져야 한다. 이 기법은 효과적이지만 데드락(deadlock)과 아사 문제(starvation)는 해결할 수 없다.

 

 

 

스레드 간의 조정

 

두 개의 스레드 사이에서 데이터를 공유할 때, 데이터를 생산하고 있는 스레드를 생산자 스레드라 하고, 데이터를 사용하여 어떤 작업을 하는 스레드를 소비자 스레드라고 하자. 두 개의 스레드는 공유된 객체를 사용하여 통신을 하게 된다. 이런 경우 스레드 간의 조정이 필요하다.

 

위의 경우를 계속할 때, 소비자 스레드는 생산자 스레드가 데이터를 생산하여 가져다주기 전까지 데이터를 가져가면 안된다. 또한 생산자 스레드는 소비자 스레드가 이전 데이터를 가져가지 않았는데 새로운 데이터를 생산하면 안된다.

 

이와 같은 상황을 코드로 작성할 때, 최악의 경우는 스레드로 하여금 조건과 반복 루프에서 무한정 검사하게 하는 것이다. 이것을 폴링(polling)이라고 한다폴링(polling)CPU의 시간을 심각하게 낭비한다.

 

스레드 간의 조정에 있어 효율적인 방법은 조건이 만족할 때까지 현재의 스레드를 일시 중지시키는 것이다. 조건이 만족한다는 것은 어떤 이벤트의 발생이 될 수도 있고, 단지 하나의 조건일 수도 있다. Thread 클래스에서는 이처럼 다른 스레드가 어떤 이벤트가 발생했다고 알려줄 때까지 스레드를 중지시키는 wait() 메소드를 제공한다.  

  • - wait() : 다른 스레드가 어떤 이벤트가 발생했다고 알려줄 때까지 스레드를 중지시킨다.

public synchronized goodMethod() {

while(!condition) {

try {

wait(); //이벤트가 발생할 때까지 리턴하지 않는다.

} catch(InterruptedException e) { }

}


System.out.println(“조건이 만족되었습니다!”);

}

  • 주의할 점은 wait()에서 리턴한 후에 반드시 다시 조건을 검사하여야 한다는 점이다. 발생된 이벤트가 우리가 원하는 이벤트가 아닐 수도 있기 때문이다.

  • wait() 메소드는 InterruptedException을 발생시킬 수 있으로 try-catch 블록을 통해 예외 처리를 해주어야 한다.


스레드 간의 조정에 있어 synchronized 키워드를 사용하는 이유는(loack)과 관련이 있다


 예를 들어 스레드 Await()의 호출 대상이라고 한다면 스레드 BA.wait()를 호출하면 스레드 BA에 대하여 락(lock)을 소유한다고 표현한다. 그렇지 않으면 오류가 발생한다.

  • 동기화된 메소드 안에서 wait()를 호출하면 간단하게 락을 획득할 수 있다.

  • wait() 메소드가 호출되면 대상 스레드는 가지고 있던 락을 해제하고 실행을 일시 중지한다.

  • 차후에 다른 스레드가 동일한 락을 획득하여서 notifyAll()을 호출하면, 이벤트가 발생하기를 기다리면서 일시 중지된 모든 스레드들이 깨어나게 된다.

public synchronized notifyCondition() {

condition = true;

notifyAll();

}


- wait() 메소드는 어떤 일이 일어나기를 기다릴 때 사용하는 메소드이다.

 

- notifyAll() 메소드는 어떤 일이 일어났을 때 이를 알려주는 메소드이다.

 

- 맨 처음 언급했던 생산자 스레드와 소비자 스레드를 예시로 계속 들면, wait() 메소드를 통해 생산자가 소비자에게 알려주면, 소비자 스레드가 작업이 끝나면 notifyAll()을 통해 생산자 스레드를 실행시킨다.

 

 

- 예제 : 생산자 스레드와 소비자 스레드를 구현해보자

class Buffer { //데이터를 공유하기 위한 버퍼 클래스
    private int data;
    private boolean empty = true;

    public synchronized int get() {
        while(empty) {
            try {
                wait(); //버퍼가 비어있는 동안 소비자 스레드는 기다린다.
            } catch (InterruptedException e) { }
        }

        empty = true;
        notifyAll(); //버퍼로부터 데이터를 가져갔으니 생산자 스레드를 깨운다.
        return data;
    }

    public synchronized void put(int data) {
        while(!empty) {
            try {
                wait(); //아직 버퍼가 비어있지 않으니 생산자 스레드는 기다린다.
            } catch (InterruptedException e) { }
        }

        empty = true;
        this.data = data; //버퍼가 비었으니 추가할 데이터를 저장한다.
        notifyAll();
    }
}


//생산자 클래스
class Producer implements Runnable {
    private Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            buffer.put(i);
            System.out.println("생산자 : "+ i + "번 데이터 생성");

            try {
                Thread.sleep((int) Math.random()*100);
            } catch (InterruptedException e) { }
        }
    }
}

//소비자 클래스
class Consumer implements Runnable {
    private Buffer buffer;

    public Consumer(Buffer drop) {
        this.buffer = drop;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            int data = buffer.get();
            System.out.println("소비자 : "+ i +"번 데이터 소비");

            try {
                Thread.sleep((int) Math.random() * 100);
            } catch (InterruptedException e) { }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        (new Thread(new Producer(buffer))).start();
        (new Thread(new Consumer(buffer))).start();
    }
}


 

 

 

 



Collections 클래스

 

- Collections 클래스는 여러 유용한 알고리즘을 구현한 메소드들을 제공한다.

 

이 메소드들은 제네릭 기술을 사용하여 작성되었으며 정적 메소드의 형태로 되어있다.

 

자주 사용되는 알고리즘으로는 정렬(Sorting), 섞기(Shuffling), 탐색(Searching) 등이 있다

  • 언급한 세 메소드의 첫 번째 매개변수는 알고리즘이 적용되는 컬렉션이다.


 

 

 

정렬(Sorting)

 

데이터를 어떤 기준에 의하여 순서대로 나열하는 알고리즘이다.


퀵 정렬, 합병 정렬, 히프 정렬 등의 다양한 방법이 존재한다.


- Collections 클래스의 정렬은 속도가 비교적 빠르고 안전성이 보장되는 합병 정렬을 이용한다.


- 안전성이란 동일한 값을 가지는 원소를 다시 정렬하지 않는 것을 의미한다.


안전성은 같은 리스트를 반복하여 다른 기준에 따라 정렬할 때 중요하다.

 

 예를 들어 상품 주문 리스트를 날짜를 기준으로 먼저 정렬하고 이후 주문처를 기준으로 정렬한다면 사용자는 같은 주문처가 보낸 주문은 날짜별로 정렬될 것이라고 가정한다. 이처럼 한번 정렬된 것이 유지되는 경우를 안정성 있는 정렬이라고 말한다.

 

- Collection 클래스의 sort() 메소드는 정적메소드로, List 인터페이스를 구현하는 컬렉션에 대하여 정렬을 수행한다.

 

List<String> list = new LinkedList<String>();

list.add(“김철수”);

list.add(“김영희”);

Collections.sort(list);

 

 위의 경우는 원소가 String 타입이므로 알파벳 순서대로 정렬될 것이다. 원소가 Date 타입이라면 시간적인 순서로 정렬될 것이다. 그 이유는 String 클래스와 Date 클래스 모두 Comparable 인터페이스를 구현하기 때문이다.

 

- 정렬은 Comparable 인터페이스를 이용하여 이루어진다. List 인터페이스는 Comparable 인터페이스를 상속하므로

 

public interface Comparable<T> {

public int comparaTo(T o);

}

  • comparaTo() 메소드는 매개변수 객체를 현재의 객체와 비교하여 작으면 음수, 같으면 0, 크면 양수를 반환한다.


- 예제 1

import java.util.*;

public class SortTest1 {
    public static void main(String[] args) {
        String[] example = { "Nice", "to", "meet", "you" };
        List<String> list = Arrays.asList(example); //배열을 리스트로 무조건 변환!
        Collections.sort(list); //리스트 정렬

        System.out.println(list);
    }
}

[예제 1 출력 결과]

meet과 to의 위치가 바뀐 것을 볼 수 있다. 알파벳순이기 때문에 이러한 결과가 나온 것이다.



- 예제 2

import java.util.*;

class Student implements Comparable<Student> {
    int number;
    String name;

    Student(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public String toString() {
        return name;
    }

    @Override
    public int compareTo(Student s) {
        return number - s.number; //학번순서로 정렬한다.
    }
}

public class SortTest2 {
    public static void main(String[] args) {
        Student[] students = {
            new Student(02, "선옥자"),
            new Student(03, "한영희"),
            new Student(01, "김철수")
        };

        List<Student> list = Arrays.asList(students);
        Collections.sort(list);
        System.out.println(list);
    }
}

[예제 2 출력 결과]



- 예제 3 : 역순으로 정렬하기 - Collections.sort(list, Collections.reverseOrder());

import java.util.*;

public class SortTest {
    public static void main(String[] args) {
        String[] example = { "Nice", "to", "meet", "you" };
        List<String> list = Arrays.asList(example); //배열을 리스트로 무조건 변환!
        Collections.sort(list, Collections.reverseOrder()); //리스트 역순정렬

        System.out.println(list);
    }
}

 [예제 3 출력 결과]

 역순정렬로 인해 예제 1의 출력 결과와 반대되는 결과를 볼 수 있다.





섞기(Shuffling)

 

리스트에 존재하는 정렬을 파괴하여서 원소들의 순서를 랜덤하게 만든다.

 

정렬과는 반대 동작을 한다.

 

주로 게임을 구현할 때 유용하게 쓰인다. 예를 들어 카드 게임에서 카드를 섞을 때 사용할 수 있다.


- 예제

import java.util.*;

public class Shuffle {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();

        for(int i = 0; i <= 10; i++) {
            list.add(i);
        }

        Collections.shuffle(list);
        System.out.println(list);
    }
}

 [출력 결과 1]


[출력 결과 2]

메소드를 실행할 때마다 서로 다른 출력 결과를 보인다.

 

 


탐색(Searching)

 

리스트 안에서 원하는 원소를 찾는 알고리즘이다.

 

선형 탐색 : 처음부터 모든 원소를 방문하는 탐색 방법으로, 리스트가 정렬되어 있지 않은 경우 사용한다.

 

이진 탐색 : 리스트가 정렬되어 있는 경우 중간에 있는 원소(m)와 먼저 비교하여, 크면 그다음부(m+1)터 끝까지 비교하고, 작으면 처음부터 그 전(m-1)까지의 원소들과 비교하는 방식을 반복하여 하나의 리스트를 계속해서 두 개의 리스트로 분할한다. 이 방법은 리스트에 하나의 원소가 남을 때까지 반복된다. 이진 탐색은 문제의 크기를 반으로 줄일 수 있기 때문에 선형 탐색보다 효율적이다.

 

- Collections 클래스는 정렬된 리스트에서 지정된 원소를 이진 탐색한다. 


- 이진 탐색 메소드인 binarySearch()는 만약 반환값이 양수이면 찾고자 하는 원소의 인덱스값이고, 음수이면 탐색이 실패한 것이다. , 원소를 찾지 못했음을 의미한다.

 

int index = Collections.binarySearch(list, element);

// list는 리스트, element는 탐색할 원소이다.

 

 단, binarySearch() 메소드는 실패하더라도 도움이 되는 정보를 반환한다. , 반환값에는 현재의 데이터가 삽입될 수 있는 위치 정보가 있다. 반환값이 pos라고 한다면, (-pos-1)이 해당 데이터를 삽입할 수 있는 위치이다.

 

int pos = Collections.binarySearch(list, key);

if(pos < 0) list.add(-pos-1);

 

- binarySearch() 메소드는 위에도 언급했듯이 정렬된 리스트에 한해서 탐색을 진행한다는 점에 주의하여야 한다.

 

 



 Map 인터페이스

 

많은 데이터 중에서 원하는 데이터를 빠르게 찾을 수 있는 자료 구조이다.

 

- 일반적으로 사전에 비유를 하는데, 사전은 단어가 있으면 단어에 대한 설명이 있다. Map에서는 그 단어(데이터) (Key)“라고 부르고 단어(데이터)에 대한 설명을 (Value)“이라고 부른다.

 

중복된 키를 가질 수 없다각 키는 오직 하나의 값에만 매핑될 수 있다.

 

키가 제시되면 Map은 값을 반환한다.


 예를 들어 학생에 대한 정보를 Map에 저장할 때 키를 학번으로 두고, 값을 학생의 이름으로 할 때, 학번이 제시되면 Map은 학생의 이름을 반환한다.


소드

 void clear()

 Map의 모든 항목들을 삭제한다.

 boolean containsKey(Object key)

 Map이 해당 키에 매핑되는 값을 가지고 있다면 true, 없다면 false를 반환한다.

 boolean containsValue(Object value)

 Map이 해당 값에 대한 적어도 하나의 키를 가지고 있다면 true, 없다면 false를 반환한다.

 Set<Map.Entry<K,V>> entrySet()

 Map에 대응되는 집합(Set)을 반환한다.

 boolean equals(Object o)

 Map에 있는 객체들과 매개변수로 넘겨받은 객체와 동등한지 비교한다.

 V get(Object key)

 해당 Key에 맞는 값을 반환한다없으면 null을 반환한다.

 int hashCode()

 해쉬코드를 반환한다.

 boolean isEmpty()

 Map이 어떠한 키와 값도 가지지 않으면 true, 아니면 false를 반환한다.

 Set<K> keySet()

 Map의 Key에만 대응되는 집합(Set)을 반환한다.

 V put(K key, V value) Map에 키, 값을 추가한다.
 void putAll(Map<? extends K,? extends V> m) 매개변수로 넘겨받은 Map을 전체 복사하여 추가한다.
 V remove(Object key) 매개변수로 넘겨받은 Key Map에 존재한다면 삭제한 후 반환한다.
 int size() Map의 키-값 쌍의 개수를 반환한다.
 Collection<V> values() Map의 값에만 대응되는 Collection을 반환한다.



- Map 인터페이스를 구현한 클래스로는 HashMap, TreeMap, LinkedHashMap 등이 있다.

  • HashMap : 해싱 테이블에 데이터를 저장한다. 단, 저장한 순서와는 다르게 Key 값의 순서는 임의적이다.

  • TreeMap : 탐색 트리에 데이터를 저장한다.

  • LinkedHashMap : HashMap의 단점인 임의적인 순서를 보완하여 저장한 순서에 맞게 Key 값을 저장한다.


키들을 정렬된 순서로 방문할 필요가 없다면 HashMap TreeMap보다 빠르다.


- 예제

import java.util.*;

class Student {
    int number;
    String name;

    public Student (int number, String name) {
        this.number = number;
        this.name = name;
    }

    public String toString() {
        return name;
    }
}

public class MapTest {
    public static void main(String[] args) {
        Map<String, Student> stMap = new HashMap<String, Student>();
        stMap.put("01", new Student(01, "김철수"));
        stMap.put("02", new Student(02, "김영희"));
        stMap.put("03", new Student(03, "김자몽"));

        //모든 항목을 출력
        System.out.println("모든 항목을 출력 : " + stMap);

        //하나의 항목을 삭제
        stMap.remove("01"); //김철수 학생의 정보가 삭제된다.

        //하나의 항목을 대치
        stMap.put("03", new Student(03, "김옥자"));

        //값을 참조
        System.out.println("03번 학생의 이름 : " + stMap.get("03"));

        //모든 항목 방문
        for(Map.Entry<String, Student> s : stMap.entrySet()) {
            String key = s.getKey();
            Student value = s.getValue();
            System.out.println("key : " + key + ", value : " + value);
        }
    }
}

[출력 결과]



- Map에 저장된 데이터를 방문할 때는 Map.Entry 라는 인터페이스를 사용한다.

import java.util.*;

public class MapTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();

        String[] example = { "Hi", "yes", "or", "not", "Hi", "yes", "no", "a", "and" };

        //문자열에 포함된 빈도 계산하기
        for(String str : example) {
            Integer freq = map.get(str);
            map.put(str, (freq == null) ? 1 : freq+1);
        }

        System.out.println(map.size() + "개의 단어가 있음"); //결과 : 7개의 단어가 있음
        System.out.println("containKey(\"to\") = " + map.containsKey("to"));
        System.out.println("isEmplty() = " + map.isEmpty());
        System.out.println("Map : " + map);
    }
}

[출력 결과]





+ Recent posts