Don't Panic
in Java ☕ 18분 읽기

[Java]Java 정석 기본편 제네릭스 12장 이후

목차

제네릭스, 열거형, 어노테이션

제네릭스

제네릭스는 다양한 타입의 객체를 다루는 메서드나, 컴파일 시 타입 체크를 해주는 기능이다. 객체 타입을 컴파일 시점에 체크하여 타입 안정성을 높이고, 형 변환의 번거로움을 줄여준다.

제네릭스의 장점

  1. 타입 안정성을 제공한다.
  2. 타입 체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

타입 변수

  • ArrayList 클래스 선언에서 클래스 이름 옆 <> 안에 있는 E를 타입 변수라고 하며, 일반적으로는 Type의 첫 글자인 T를 쓴다.
  • 물론 T만 쓰는 게 아니라 다른 글자를 써도 된다. ArrayList<E>처럼 Element의 E를 쓰거나, Map<K,V>처럼 여러 개일 경우 콤마로 구분할 수 있다.
  • ArrayList와 같은 제네릭 클래스를 생성할 때는, 참조 변수와 생성자에 타입 변수 E 대신 Tv와 같은 실제 타입을 지정해 줘야 한다.
ArrayList<Tv> tvList = new ArrayList<Tv>();

제네릭스의 용어

class Box<T> {}라는 제네릭 클래스 Box가 선언되어 있을 때:

  • Box<T>: 제네릭 클래스. ‘T의 Box’ 또는 ‘T Box’라고 읽는다.
  • T: 타입 변수 또는 타입 매개변수 (T는 타입 문자)
  • Box: 원시 타입(raw type)

와일드 카드

  • 제네릭 클래스를 생성할 때, 참조 변수에 지정된 제네릭 타입과 생성자에 지정된 제네릭 타입은 일치해야 한다.
ArrayList<Tv> list = new ArrayList<Tv>()

만약 일치하지 않으면 컴파일 에러가 난다. 그렇다면 제네릭 타입에 다형성을 쓰는 방법은 없을까? 제네릭 타입으로 와일드 카드를 사용하면 된다.

와일드 카드는 기호 ?를 사용하는데, extendssuper로 상한, 하한을 제한할 수 있다.

와일드 카드의미
<? extends T>상한 제한. T와 그 자손들만 가능
<? super T>하한 제한. T와 그 조상들만 가능
<?>제한 없음. 모든 타입이 가능 (<? extends Object>와 동일)

어노테이션이란

  • 주석처럼 프로그래밍 언어에 영향을 미치지 않으면서도, 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다. (어노테이션의 뜻은 주석, 주해, 메모다.)

쓰레드

프로세스(Process)와 쓰레드(thread)

프로세스란 간단히 말해서 실행 중인 프로그램이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원, 그리고 쓰레드로 구성되어 있으며, 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드다.

그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 ‘멀티쓰레드 프로세스’라고 한다.

멀티쓰레딩의 장단점

DOS와 같은 OS는 한 번에 한 가지 작업만 할 수 있다. 반면 윈도우와 같은 멀티 태스킹이 가능한 OS는 동시에 여러 작업을 수행할 수 있다. 싱글 쓰레드 프로그램과 멀티 쓰레드 프로그램의 차이도 이와 같다고 생각하면 된다.

멀티쓰레드의 장점

  • CPU 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

메신저로 채팅하면서 파일을 다운로드받거나 음성 대화를 나눌 수 있는 이유가 바로 멀티쓰레드로 작성되어 있기 때문이다. 만약 싱글 쓰레드로 작성되어 있다면 파일을 다운받는 동안 다른 일(채팅)을 전혀 할 수 없을 것이다.

여러 사용자에게 서비스를 해주는 서버의 경우 멀티스레드로 작성하는 것은 필수적이다. 하나의 서버 프로세스가 여러 개의 스레드를 생성해서, 스레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.

만일 싱글스레드로 서버 프로그램을 작성한다면 사용자의 요청마다 새로운 프로세스를 생성해야 하는데, 프로세스 생성은 쓰레드 생성에 비해 더 많은 시간과 메모리 공간이 필요하므로, 더 많은 수의 사용자 요청을 서비스하기 어렵다.

멀티쓰레드의 단점

멀티스레드가 꼭 좋은 것만은 아니다. 멀티쓰레드는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에, 동기화·교착 상태와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다.

쓰레드의 구현과 실행

쓰레드를 구현하는 방법은 두 가지가 있다.

  1. Thread 클래스를 상속받는 방법
  2. Runnable 인터페이스를 구현하는 방법

어느 쪽을 선택해도 별 차이는 없지만, Thread 클래스를 상속하면 다른 클래스를 상속받을 수 없으므로, Runnable 인터페이스를 구현하는 방법이 일반적이다. Runnable 인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이다.

쓰레드의 실행 - start()

쓰레드 객체를 생성했다고 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다.

t1.start(); // 쓰레드 1을 실행시킨다.
t2.start(); // 쓰레드 2를 실행시킨다.

사실 start()가 호출되어도 바로 실행되는 것이 아니라, 실행 대기 상태에 있다가 자기 차례가 되어야 실행된다. 물론 대기 중인 쓰레드가 하나도 없으면 곧바로 실행 상태가 된다. 쓰레드의 실행 순서는 OS의 스케줄러가 작성한 스케줄에 의해 결정된다.

또한, 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 하나의 쓰레드에 대해 start()는 한 번만 호출할 수 있다. 즉, 쓰레드를 한 번 더 수행하려면 쓰레드 객체를 new로 다시 생성해서 수행해야 한다.

start()와 run()의 차이

왜 쓰레드 실행 시 run이 아닌 start를 호출할까?

  • main에서 run을 호출하는 것은 생성된 쓰레드를 실행하는 게 아니라, 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
  • 반면 start()는 새로운 쓰레드가 작업을 실행하는 데 필요한 호출 스택(call stack)을 생성한 뒤 run을 호출해서, 생성된 호출 스택에 run()이 첫 번째로 올라가게 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자기만의 호출 스택을 필요로 한다. 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출 스택이 생성되고, 쓰레드가 종료되면 작업에 사용된 호출 스택은 소멸된다.

main 쓰레드

main 메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다. public static void main 메서드를 사용하면서 이미 쓰레드를 쓰고 있었던 셈이다.

지금까지는 main이 수행을 마치면 프로그램이 종료되었으나, 다른 쓰레드가 있으면 모든 쓰레드가 종료되기 전까지 프로그램이 종료되지 않는다. 즉, 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

쓰레드는 사용자 쓰레드(‘user thread’, non-daemon thread)와 데몬 쓰레드(daemon thread) 두 종류가 있다.

싱글 쓰레드와 멀티 쓰레드

앞에서 멀티쓰레드의 장점을 다뤘지만 마냥 장점만 있는 건 아니다. 멀티쓰레드가 싱글쓰레드보다 더 오래 걸릴 수 있는데, 그 이유는 프로세스나 쓰레드 간 작업 전환인 컨텍스트 스위칭(context switching) 때문이다.

작업 전환 시에는 현재 진행 중인 작업 상태(다음에 실행할 위치, 프로그램 카운터 등)를 저장하고 읽어오는 시간이 소요된다. 참고로 쓰레드 스위칭에 비해 프로세스 스위칭이 더 많은 정보를 저장해야 하므로 더 많은 시간이 소요된다.

그래서 싱글 코어에서 단순히 CPU만 쓰는 계산 작업이라면, 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 효율적이다.

쓰레드의 I/O 블락킹

두 쓰레드가 서로 다른 자원을 쓰는 작업의 경우에는, 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다.

예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부 기기와의 입출력을 필요로 하는 경우가 이에 해당한다.

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 속성(멤버 변수)을 가지고 있는데, 이 우선순위 값에 따라 스레드가 얻는 실행 시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 우선순위를 서로 다르게 지정하여, 특정 쓰레드가 더 많은 작업 시간을 갖도록 할 수 있다.

void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority()                 // 쓰레드의 우선순위를 반환한다.

데몬 쓰레드

데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드다.

일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제로 자동 종료된다. 왜냐하면 일반 쓰레드가 종료되면 데몬 쓰레드의 존재가 의미가 없기 때문이다. 이 점을 제외하고는 데몬 쓰레드와 일반 쓰레드는 다르지 않다.

데몬 쓰레드의 예로는 가비지 컬렉터, 워드프로세서의 자동 저장, 화면 갱신 등이 있다. 데몬 쓰레드는 일반 쓰레드와 작성 방법·실행 방법이 같으며, 다만 쓰레드를 생성한 뒤 실행 전에 setDaemon(true)를 호출하기만 하면 된다. 그리고 데몬 쓰레드가 생성한 쓰레드는 자동으로 데몬 쓰레드가 된다.

boolean isDaemon()          // 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드면 true를 반환한다.
void setDaemon(boolean on)  // 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다. on을 true로 지정하면 데몬 쓰레드가 된다.

쓰레드의 상태

쓰레드는 생성된 후부터 종료까지 여러 상태를 가질 수 있다.

상태설명
New쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
Runnable실행 중 또는 실행 가능한 상태
Blocked동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
Waiting, Timed_Waiting작업이 종료되지는 않았지만 실행 가능하지 않은(unrunnable) 일시정지 상태. Timed_Waiting은 일시정지 시간이 지정된 경우를 의미
Terminated쓰레드의 작업이 종료된 상태

쓰레드의 실행 제어

쓰레드가 어려운 이유는 동기화와 스케줄링이다. 우선순위를 통한 쓰레드 간 스케줄링을 앞에서 다뤘지만 이것만으로는 부족하다. 효율적인 멀티쓰레드 프로그램을 만들기 위해서는, 보다 정교한 스케줄링으로 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비 없이 사용하도록 프로그래밍해야 한다.

메서드설명
sleep지정된 시간 동안 스레드를 일시정지시킨다. 지정된 시간이 지나면 자동으로 다시 실행 대기 상태가 된다.
join지정된 시간 동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면, join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
interruptsleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행 대기 상태로 만든다.
stop쓰레드를 즉시 종료시킨다.
suspend쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행 대기 상태가 된다.
yield()실행 중에 자신에게 주어진 실행 시간을 다른 쓰레드에게 양보(yield)하고, 자신은 실행 대기 상태가 된다.

쓰레드의 동기화(synchronization)

싱글쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에, 프로세스 자원을 가지고 작업하는 데 별 문제가 없다. 하지만 멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에, 서로의 작업에 영향을 주게 된다.

만일 쓰레드 A가 작업 중에 다른 쓰레드 B로 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경하였다면, 다시 쓰레드 A가 제어권을 받아 나머지 작업을 마쳤을 때 원래 의도했던 것과 다른 결과를 얻을 수 있다.

이러한 일을 방지하기 위해, 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 것이 임계 영역(critical section)과 잠금(락, lock)이다.

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해두고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행하게 한다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만, 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다.

이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화(synchronization)라고 한다.

wait()과 notify()

synchronized로 동기화해서 공유 데이터를 보호하는 것까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요하다.

만약 계좌에 출금할 돈이 부족해서 한 쓰레드가 락을 보유한 채로 돈이 입금될 때까지 기다리면, 다른 쓰레드들은 모두 해당 객체의 락을 기다리느라 다른 작업도 원활히 되지 않을 것이다.

이러한 상황을 개선하기 위해 고안된 것이 waitnotify이다. 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 된다.

wait(), notify(), notifyAll()의 특징:

  • Object에 정의되어 있다.
  • 동기화 블록(synchronized 블록) 내에서만 사용할 수 있다.
  • 보다 효율적인 동기화를 가능하게 한다.

람다와 스트림

람다식

람다식은 간단히 말해서 메서드를 하나의 식(expression)으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확하게 표현할 수 있게 해준다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명 함수라고도 한다.

스트림

지금까지 우리는 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 iterator를 이용해서 코드를 작성했다. 그러나 이런 방식은 너무 길고 알아보기 어려우며 재사용성도 떨어진다.

또한 데이터 소스마다 다른 방식으로 다뤄야 한다. 예를 들어 List를 정렬할 때는 Collections.sort를 써야 하고, 배열을 정렬할 때는 Arrays.sort를 쓰는 것처럼, 컬렉션을 다루는 방식을 표준화했어도 각 컬렉션에는 같은 기능의 메서드가 중복 정의되어 있다.

이러한 문제점을 해결하려고 만든 것이 스트림이다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는 데 자주 사용되는 메서드를 정의해 놓았다. 데이터 소스를 추상화했다는 것은, 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있게 되었다는 것과, 코드의 재사용성이 높아진 것을 의미한다.


입출력

입출력(I/O)과 스트림(stream)

I/O란 입출력을 말하며, 컴퓨터 내부 또는 외부의 장치와 프로그램 간에 데이터를 주고받는 것을 말한다. 예를 들면 System.out을 이용해서 화면에 데이터를 출력하거나, 키보드로부터 데이터를 입력받는 것이 가장 기본적인 입출력의 예다.

스트림(Stream)

자바에서 입출력을 수행하려면, 즉 어느 한쪽에서 다른 쪽으로 데이터를 전달하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림이라고 정의했다.

참고: 앞서 정의한 (람다와 스트림에서의) Stream과 입출력에서의 스트림은 같은 용어를 쓰지만 다른 개념이다.

스트림이란 데이터를 운반하는 데 사용되는 연결 통로다. 스트림은 연속적인 데이터의 흐름을 물에 비유해서 붙여진 이름인데, 여러 가지로 유사한 점이 많다.

  • 물이 한쪽 방향으로 흐르는 것과 같이, 스트림은 단방향 통신만 가능하다. 따라서 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다.
  • 입력과 출력을 동시에 수행하려면, 입력을 위한 입력 스트림(input stream)과 출력 스트림(output stream), 모두 2개의 스트림이 필요하다.
  • 스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며, 중간에 건너뜀 없이 연속적으로 데이터를 주고받는다. 큐와 같은 FIFO 구조로 되어 있다고 생각하면 된다.

보조 스트림

위에서 언급한 스트림 외에도, 스트림의 기능을 보완하기 위한 보조 스트림이 제공된다.

  • 보조 스트림은 실제 데이터를 주고받는 스트림이 아니어서 데이터를 입출력하는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.
  • 보조 스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 후에 이를 이용해서 보조 스트림을 생성해야 한다.

문자 기반 스트림 - Reader, Writer

지금까지 본 스트림은 바이트 기반의 스트림이었다. 바이트 기반은 입출력 단위가 1바이트라는 뜻이다.

C언어와 달리 Java에서는 한 문자를 의미하는 char형이 1byte가 아니라 2byte이기 때문에, 바이트 기반의 스트림으로 2byte인 문자를 처리하는 데에는 어려움이 있다. 이를 보완하기 위해 문자 기반의 스트림이 제공된다.

따라서 문자 데이터를 입출력할 때는 바이트 기반의 스트림 대신 문자 기반의 스트림을 사용한다.

표준 입출력 (Standard I/O)

표준 입출력은 콘솔(console, 도스창)을 통한 데이터 입력과, 콘솔로의 데이터 출력을 의미한다.

자바에서는 표준 입출력을 위해 3가지 입출력 스트림 System.in, System.out, System.err를 제공한다. 이들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동으로 생성되기 때문에, 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않고도 사용할 수 있다.

System.in : 콘솔로부터 데이터를 입력받는데 사용(표준 출력)
System.out : 콘솔로 데이터를 출력하는데 사용(표준 입력)
System.err: 콘솔로 데이터를 출력하는데 사용(표준 입력)

File

파일은 기본적이면서 가장 많이 쓰는 입출력 대상이기 때문에 중요하다.

직렬화(Serialization)

직렬화란 객체를 데이터 스트림으로 만드는 것을 말한다. 다시 말하면 객체에 저장된 데이터를 스트림에 쓰기(write) 위해 연속적인(serial) 데이터로 변환시키는 것을 말한다. 반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라 한다.

직렬화라는 용어 때문에 어렵게 느껴질 수 있지만, 사실 객체를 저장하거나 전송하려면 이렇게 할 수밖에 없다. 여기서 객체란 무엇이고 객체를 저장한다는 것은 무엇인지 정리하고 넘어가는 것이 좋다.

보통 예제 그림에서 이해를 돕기 위해 객체를 생성하면 인스턴스 변수와 메서드를 함께 그리곤 했지만, 사실 객체에는 메서드가 포함되지 않는다. 인스턴스 변수는 인스턴스마다 다른 값을 가질 수 있어야 하기 때문에 별도의 메모리 공간이 필요하지만, 메서드는 변하는 것이 아니라서 메모리를 낭비해가며 인스턴스마다 같은 내용의 코드를 포함시킬 필요가 없기 때문이다.

그래서 객체를 저장한다는 것은 객체의 모든 인스턴스 변수의 값을 저장한다는 것과 같은 의미다. 어떤 객체를 저장하고자 한다면 현재 객체의 모든 인스턴스 변수의 값을 저장하기만 하면 된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스 변수에 저장하면 된다.


네트워킹

네트워킹이란

두 대 이상의 컴퓨터를 케이블로 연결하여 네트워크를 구성하는 것을 말한다.

java.net 패키지를 쓰면 네트워크 어플리케이션의 데이터 통신 부분을 쉽게 작성할 수 있으며, 간단한 네트워크 어플리케이션은 단 몇 줄의 자바 코드만으로 작성할 수 있다.

클라이언트와 서버

클라이언트/서버는 컴퓨터 간의 관계를 역할로 구분하는 개념이다. 서버(server)는 서비스를 제공하는 컴퓨터이고, 클라이언트는 서비스를 사용하는 컴퓨터다.

일반적으로 서버는 다수의 클라이언트에게 서비스를 제공하므로 고사양의 하드웨어인 경우가 일반적이지만, 사양으로 서버와 클라이언트를 구분하는 것은 아니다. 서비스를 제공하는 소프트웨어가 실행되는 컴퓨터를 서버라고 한다.

서비스는 서버가 클라이언트로부터 요청받은 작업을 처리하여 그 결과를 제공하는 것을 뜻한다. 서버가 제공하는 서비스의 종류에 따라 파일 서버(file server), 메일 서버(mail server), 어플리케이션 서버 등이 있다. 서버에 접속하는 클라이언트의 수에 따라, 하나의 서버가 여러 가지 서비스를 제공하기도 하고, 하나의 서비스를 여러 대의 서버로 제공하기도 한다.

서버가 서비스를 제공하려면 서버 프로그램이 있어야 하고, 클라이언트가 서비스를 제공받으려면 서버 프로그램과 연결할 수 있는 클라이언트 프로그램이 있어야 한다. 예를 들어 웹 서버에 접속해서 정보를 얻으려면 웹 브라우저(클라이언트 프로그램)가 있어야 하고, FTP 서버에 접속해서 파일을 전송받으려면 알FTP 같은 FTP 클라이언트 프로그램이 필요하다.

일반 PC의 경우 서버에 접속하는 클라이언트를 수행하지만, FTP Serv-U 같은 FTP 서버 프로그램이나 Tomcat 같은 웹 서버 프로그램을 설치하면 서버 역할도 수행할 수 있다. 파일 공유 프로그램인 토렌트 같은 프로그램은 클라이언트 프로그램과 서버 프로그램을 하나로 합친 것으로, 이를 설치한 컴퓨터는 클라이언트인 동시에 서버가 되어, 다른 컴퓨터에서 파일을 가져오는 동시에 또 다른 컴퓨터에 파일을 제공할 수 있다.

네트워크를 구성할 때 전용 서버를 두는 것을 서버 기반 모델(server-based model)이라고 하고, 별도의 전용 서버 없이 각 클라이언트가 서버 역할을 동시에 수행하는 것을 P2P(peer to peer) 모델이라고 한다.

모델특징
서버 기반 모델안정적인 서비스 제공이 가능 / 공유 데이터의 관리와 보안이 용이 / 서버 구축 비용과 관리 비용이 든다
P2P 모델서버 구축 및 운용 비용을 절감 / 자원의 활용을 극대화 / 자원 관리가 어렵고 보안이 취약

IP 주소 (IP address)

IP 주소는 컴퓨터(host)를 구별하는 데 사용되는 고유한 값으로, 인터넷에 연결된 모든 컴퓨터는 IP 주소를 갖는다.

  • IP 주소는 4byte(32비트)의 정수로 구성되어 있으며, 4개의 정수가 마침표를 구분자로 ‘a.b.c.d’와 같은 형식으로 표현된다.
  • a, b, c, d는 부호 없는 1byte 값, 즉 0~255 사이의 정수다.
  • IP 주소는 다시 네트워크 주소와 호스트 주소로 나눌 수 있는데, 32bit(4byte)의 IP 주소 중 네트워크 주소와 호스트 주소가 각각 몇 bit를 차지하는지는 네트워크를 어떻게 구성했는지에 따라 달라진다.
  • 서로 다른 두 호스트의 IP 주소에서 네트워크 주소가 같다는 것은, 두 호스트가 같은 네트워크에 포함되어 있다는 뜻이다.
  • 윈도우 OS에서 호스트 IP 주소를 확인하려면 콘솔에서 ipconfig.exe를 실행하면 된다.

URL(Uniform Resource Locator)

URL은 인터넷에 존재하는 여러 서버들이 제공하는 자원에 접근할 수 있는 주소를 표현하는 것으로, 다음 형태로 이루어져 있다. (URL에서 포트번호, 쿼리, 참조는 생략할 수 있다.)

프로토콜://호스트명:포트번호/경로명/파일명?쿼리스트링#참조

예시:

http://www.codechobo.com:80/sample/hello.html?refere=codechobo#index1
구성 요소설명예시
프로토콜자원에 접근하기 위해 서버와 통신하는 데 사용되는 통신 규약http
호스트명자원을 제공하는 서버의 이름www.codechobo.com
포트번호통신에 사용되는 서버의 포트번호80
경로명접근하려는 자원이 저장된 서버상의 위치/sample/
파일명접근하려는 자원의 이름hello.html
쿼리(query)URL에서 ’?’ 이후의 부분refere=codechobo
참조(anchor)URL에서 ’#’ 이후의 부분index1

URLConnection 클래스: 어플리케이션과 URL 간의 통신 연결을 나타내는 클래스의 최상위 클래스로, 추상 클래스다.

소켓 프로그래밍

소켓 프로그래밍은 소켓을 이용한 통신을 의미한다. 소켓(socket)이란 프로세스 간의 통신에 사용되는 양쪽 끝단을 의미한다.

서로 멀리 떨어진 두 사람이 통신하기 위해서 전화기가 필요한 것처럼, 프로세스 간의 통신을 위해서는 무언가가 필요하고 그것이 바로 소켓이다.

자바에서는 java.net 패키지를 통해 소켓 프로그래밍을 지원하는데, 소켓 통신에 사용되는 프로토콜에 따라 다른 종류의 소켓을 구현하여 제공한다.

TCP와 UDP

TCP/IP 프로토콜은 이기종 시스템 간의 통신을 위한 표준 프로토콜로, 프로토콜의 집합이다. TCP와 UDP 모두 TCP/IP 프로토콜에 포함되어 있으며, OSI 계층의 전송 계층(transport layer)에 해당하는 프로토콜이다. TCP와 UDP는 전송 방식이 다르며, 각 방식에 따른 장단점이 있다.

항목TCPUDP
연결방식연결기반(connection-oriented)비연결 기반
연결 후 통신(전화기), 1:1 통신 방식연결없이 통신(소포), 1:1,1:n, n:n통신방식
특징데이터의 경계를 구분 안함(byte-stream), 신뢰성 있는 데이터 전송, 데이터의 전송 순서가 보장됨, 데이터의 수신여부를 확인함, 패킷을 관리할 필요가 없음, UDP보다 전송속도가 느림데이터의 경계를 구분함(datagram), 신뢰성 없는 데이터 전송, 데이터의 전송순서가 바뀔 수 있음, 데이터의 수신여부를 확인 안함, 패킷을 관리해줘야함, TCP 보다 전송속도가 빠름
관련 클래스socket,serversocketDatagramSocket, DatagramPacket, MulticastSocket

TCP를 이용한 통신은 전화에, UDP를 이용한 통신은 소포에 비유된다.

  • TCP는 데이터를 전송하기 전에 먼저 상대편과 연결한 후에 데이터를 전송하며, 잘 전송되었는지 확인하고 전송에 실패했다면 해당 데이터를 재전송한다. 따라서 신뢰성 있는 데이터 전송이 요구되는 통신(예: 파일 주고받기)에 적합하다.
  • UDP는 상대편과 연결하지 않고 데이터를 전송하며, 데이터가 바르게 수신되었는지 확인하지 않으므로 데이터가 전송되었는지 확인할 길이 없다. 또한 데이터를 보낸 순서대로 수신한다는 보장도 없다. 대신 이런 확인 과정이 없으므로 TCP에 비해 빠른 전송이 가능하다. 게임이나 동영상 데이터 전송처럼, 데이터가 중간에 손실되어 조금 끊겨도 빠른 전송이 필요할 때 적합하다. 이때 전송 순서가 바뀌어 늦게 도착한 데이터는 무시하면 된다.

각각 장단점이 있으므로 필요에 따라 사용하면 된다.

TCP 소켓 프로그래밍

TCP 소켓 프로그래밍은 클라이언트와 서버 간의 일대일 통신이다. 먼저 서버 프로그램이 실행되어 클라이언트 프로그램의 연결 요청을 기다리고 있어야 한다. 프로그램 간 통신 과정은 다음과 같다.

  1. 서버 프로그램에서는 서버 소켓을 사용해서, 서버 컴퓨터의 특정 포트에서 클라이언트의 연결 요청을 처리할 준비를 한다.
  2. 클라이언트 프로그램은 접속할 서버 IP 주소와 포트를 가지고 소켓을 생성해서 서버에 연결을 요청한다.
  3. 서버 소켓은 클라이언트의 연결 요청을 받으면, 서버에 새로운 소켓을 생성해서 클라이언트의 소켓과 연결되도록 한다.
  4. 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은, 서버 소켓과 관계없이 일대일 통신을 한다.

서버 소켓은 포트와 결합되어 포트를 통해 원격 사용자의 연결 요청을 기다리다가, 연결 요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결한다. 여기까지가 서버 소켓의 역할이고, 실제 데이터 통신은 서버 소켓과 관계없이 소켓과 소켓 간에 이뤄진다.

이는 마치 전화 시스템과 유사해서, 서버 소켓은 전화 교환기에, 소켓은 전화기에 비유할 수 있다. 전화 교환기(서버 소켓)는 외부 전화기(원격 소켓)로부터 걸려온 전화를 내부의 전화기(소켓)로 연결해주고, 실제 통화는 소켓 대 소켓으로 이뤄진다. 즉, 서버 소켓은 소켓 간의 연결만 처리하고, 실제 데이터는 소켓끼리 서로 주고받는다.

소켓들이 데이터를 주고받는 연결 통로는 입출력 스트림이다. 소켓은 입력 스트림과 출력 스트림 두 개를 가지고 있으며, 이 스트림들은 연결된 상대편 소켓의 스트림과 교차 연결된다. 한 소켓의 입력 스트림은 상대편 소켓의 출력 스트림과 연결되고, 출력 스트림은 입력 스트림과 연결된다. 그래서 한 소켓에서 출력 스트림으로 데이터를 보내면, 상대편 소켓에서는 입력 스트림으로 받게 된다. 앞서 비유한 전화기처럼, 소켓이 두 개의 입출력 스트림을 갖는 것처럼 전화기 역시 입력과 출력을 위한 두 개의 라인을 가지고 있다.

자바에서는 TCP를 이용한 소켓 프로그래밍을 위해 Socket과 ServerSocket 클래스를 제공한다.

  • Socket: 프로세스 간 통신을 담당하며 InputStream, OutputStream을 가지고 있다. 이 두 스트림을 통해 프로세스 간 통신(입출력)이 이뤄진다.
  • ServerSocket: 포트와 연결(bind)되어 있어 외부의 연결 요청을 기다리다가, 연결 요청이 들어오면 Socket을 생성해서 소켓과 소켓 간의 통신이 이뤄지도록 한다. 한 포트에 하나의 서버 소켓만 연결할 수 있다(프로토콜이 다르면 같은 포트를 공유할 수 있다).

UDP 소켓 프로그래밍 - Client

TCP에서는 Socket과 ServerSocket을 쓰지만, UDP 소켓 프로그래밍에서는 DatagramSocket과 DatagramPacket을 사용한다.

  • UDP는 연결지향적인 프로토콜이 아니므로 서버 소켓이 필요하지 않다.
  • UDP에서 쓰는 소켓은 DatagramSocket이며, 데이터를 DatagramPacket에 담아서 전송한다.
  • DatagramPacket은 헤더와 데이터로 구성되어 있으며, 헤더에는 DatagramPacket을 수신할 호스트의 정보(호스트의 주소와 포트)가 저장되어 있다. 소포에 수신할 상대편의 주소를 적어서 보내는 것과 같다고 이해하면 된다.
  • 그래서 DatagramPacket을 전송하면, DatagramPacket에 지정된 주소(호스트)의 DatagramSocket에 도착한다.