[Java] 자바 문법·IO/스트림·스레드·네트워크 총정리
목차
자바 환경 설정과 인코딩
set path=%JAVA_HOME%;%PATH%
echo %path%를 쳐 보면 자바 패스가 들어가 있다.- 윈도우는 ANSI 타입을 기본으로 사용하다가, 윈도우 10 버전부터 포매팅을 UTF-8로 변경했다.
형 변환과 오버플로
다음 구문은 에러가 날까? → 그렇다(O).
public class Main {
public static void main(String[] args) {
byte d;
d=127;
d=d+1;
System.out.print(d);
}
}
byte와 int를 더하는 것이라 에러가 난다.
Main.java:8: error: incompatible types: possible lossy conversion from int to byte
이건 어떨까?
public class Main {
public static void main(String[] args) {
byte d;
d=12;
d+=1;
System.out.print(d);
}
}
정상 실행되어 13이 나온다.
그럼 이건?
public class Main {
public static void main(String[] args) {
byte d;
d=127;
d+=1;
System.out.print(d);
}
}
-128이 출력된다. (오버플로) — 헷갈리는 부분이다.
다음은 무엇이 문제일까? 이건 자바 문제라기보다 시스템적인 문제다.
public class Main {
public static void main(String[] args) {
byte d,e,f;
d=1;
e=2;
f=d+e;
System.out.print(d+"+"+e+"="+f);
}
}
- 두 개의 바이트를 더하기 연산 처리하고 있는데, 시스템은 바이트를 그대로 연산하도록 세팅되어 있지 않다.
d,e는 둘 다byte인데+연산이 등장하면, 더하기 연산을 위해 둘 다int로 바뀐다. 결과는int인3이 된다.- 이
int값을byte인f에 넣으려 하므로 에러가 난다. - 이렇게 자동으로 형이 바뀌는 것을 자바에서 오토 캐스팅(자동 형변환)이라 한다.
public class Main {
public static void main(String[] args) {
double h = 3.14;
System.out.print(h);
float i = 3.14f; //에러 없음.
float i = 3.14; //이건 에러 뒤에 f를 안씀. 실수가 정수(8바이트(double)가 4바이트(float))에 들어가므로 에러가 나는 것.
System.out.print(i);
}
}
float,double처럼 형 크기가 차이 나는 경우도 마찬가지다.3.14f일 때는 에러가 없었지만,f를 제거한3.14(double)를float에 넣으니 에러가 생긴다.
전산에서는 신뢰가 가장 우선이다. 틀린 답을 내놓느니 에러가 나는 것이 백만 배 낫다.
char 타입과 유니코드 문자
class Main{
public static void main(String[] args){
char ch1;
ch1 = 'A';
ch1='\uac00'; 이게 들어가면 '가'로 바뀐다.
//ch1 = ch1+1;
System.out.println(ch1);
}
}
제어문 - if, switch
class Ex06{
public static void main(String[] args){
//제어문-if문
if(false){
System.out.println("참");
}else{
System.out.println("거짓");
int su = 1;
switch(su){
case1:
System.out.println("1입니다");
break;
case2:
System.out.println("2입니다");
break;
case3:
System.out.println("3입니다");
break;
default:
System.out.println("0입니다");
break;
}
}
- 각
case의 값과switch변수의 자료형이 일치해야 한다. switch안에서case 2+1:같은 연산을 취하면 안 된다.
컴파일·실행 배치 파일
@echo off
set filename = Ex08
del %filename%.class
javac %filename%.java
java %filename%
컴파일하여 클래스 파일을 만들고 실행한다.
소스 파일을 쓴다는 건, 남들이 가져다 놓은 클래스 파일을 가져다 쓴다고 생각하면 된다.
메서드와 클래스
클래스는 세 가지 기본 요소를 가지는데, 그중 하나가 메서드다. 언어에 따라 메서드만, 함수만, 또는 둘 다 지원하기도 한다. 자바는 메서드만 지원한다.
class Main{
public static int func01(){
System.out.println("func01 run...");
return 1234;
}
public static void main(String[] args){
System.out.println("호출전");
func01();
System.out.println(func01());
int su = func01();
System.out.println("호출후"+su);
return;
}
}
class Main{
public static int func01(){
System.out.println("func01 run...");
return 1234;
}
public static void func02(int a){
System.out.println("전달 받은 매개변수는 a");
return;
}
public static void main(String[] args){
System.out.println("호출전");
func01();
System.out.println(func01());
int su = func01();
func02(4321);
System.out.println("호출후"+su);
return;
}
}
위처럼 메서드끼리 이을 때는, 소개받는 인자와 넘기는 인자의 개수·타입이 모두 일치해야 한다.
변수
- 기본 변수
- 참조 변수
문자열(참조 변수):
String 변수명;
변수명 = "문자열 ~~~";
String msg = ""; // 문자열은 비어있는 문자가 존재 가능
String 변수명 = 값;
"값java"=msg+'java';
"값true" = msg+true;
연산
- 연산자에는 우선순위가 존재한다.
반복문
for (초기화1; 조건2; 증감 3){반복구문4;}
// 실행 순서: 1->2->4->3->2->4->3->2->4
while(조건){반복구문;};
// 초기화1;
// while(조건2){반복구문4; ~~~~~~~~~~증감식3}
참조와 메모리
JAVA_HOME=D:\Program Files\Java\jdk-15.0.1
PATH=%JAVA_HOME% ; ~~~~~~~~~~~~
- 객체를 쓰면 여러 타입의 변수를 정의할 수 있고, 다른 타입일지라도 하나의 타입에 저장되어 주고받는 것이 가능해진다.
null과 빈 문자열은 다르다. (빈 문자열은 문자열 객체가 존재하는 것이고,null은 가리키는 객체가 없는 것이다. 객체적 입장에서 둘은 완전히 다르다.)- 자바는 80% 이상이 C로 만들어졌다. C언어의 포인터 개념이 어려워 자바에서는 그것을 쓰지 않기로 했지만, 비슷한 역할은 반드시 존재한다. JVM 구조(클래스/스택/힙 영역)에서 포인터가 주소를 가리키듯, 자바에서도 해당 변수의 (메모리) 주소를 가리키는 것이 존재한다.
학습 메모 - 문법은 목적에 맞게
(5일차)
- 메인이 돌다가 메서드를 호출하면, 새 메서드를 찾아 호출해서 스택에 올리고, 리턴하면서 값을 가져온다. 이 과정에서 스택에 올렸다 뺐다 하는 일이 발생한다. 이 경우라면 메인에서 쭉 가는 게 낫지 않을까?
문법을 쓰기 위해 코딩하면, 컴퓨터는 불필요한 일을 하게 되는 경우가 많다. 자동 형변환을 또 하면 느려지는 것도 같은 맥락이다.
- 문제를 낼 때는 필요한 사항과 요구사항만 만족하면 되고, 그 외에는 문제가 되지 않는 한 결과가 잘 나오면 일단 OK다.
- 학습에서는 결과보다 ‘어떤 식으로 접근했느냐’를 더 우선시한다.
객체지향과 재사용
- 객체지향 = 말(개념)을 코드로 옮긴 것.
- 문법을 억지로 써먹으려 하지 말자. 그렇게 하면 불필요한 객체를 쓰게 된다.
- 의미 없이 객체를 찍어내다 보면 메모리·시간 복잡도가 비효율적이게 되어, 디자인 패턴으로 싱글톤 같은 방식도 생겨났다(객체를 마구 찍지 못하게).
재사용 단위:
- 메서드 = 코드의 재사용
- 클래스 = 코드의 재사용 (재사용 단위가 클래스 구성 요소로 확장된 것)
정리하면:
- 메서드를 만드는 건 필요에 따라 코드를 재사용하는 것이다. 반복문으로 쓰기 불편한 코드의 재사용이다.
- 클래스를 만드는 목적은 클래스 단위의 재사용이 필요하기 때문이다.
- 상속을 포함해 상당수의 문법이 코드 재사용을 위해 사용된다.
- 따라서 문법을 쓰려면 재사용될 코드를 먼저 도출해야 한다. 그 코드를 반복문으로 바꿀지, 클래스로 바꿀지 결정하는 과정이 리팩토링이다.
- 개발자는 프로그램을 한 번 짜고 끝내는 게 아니라 계속 수정한다.
모듈화하는 이유: 필요한 부분에서 사용 가능하도록 표준화하기 위함이다. 처음부터 완성된 모듈화 형태로 짜는 것은 어떤 개발자도(경력이 많아도) 한 번에 해내기 어렵다.
유니코드와 문자 체계
- 현존하는 언어의 포매팅은 문자 체계가 가변이다.
- 유니코드는 문자 코드로서의 범주를 말한다. 특정 문자 포맷을 가졌다고 해서 그것을 유니코드라 하는 것은 아니다.
숙제 (day05)
- 자바 정규표현식 조사
- 성적 관리 ver 2.0 — 한 학생의 성적을 출력하시오.
객체 배열이 나오면 String으로 생각해 보자. String도 객체이기 때문이다.
final 키워드 (day06)
- 클래스의
final: 더 이상 상속하지 않는다 (상속 거부). - 메서드의
final: 오버라이딩을 허용하지 않는다 (오버라이드 거부). - 변수의
final: 상수형 변수.
내부 클래스는 웬만하면 잘 안 쓰는데, 잘 쓰는 경우가 한 가지 있다고 한다. (해외에서는 또 잘 쓴다고 한다.)
Wrapper 클래스 (day07)
- 숫자를 다루는 클래스는 Number 클래스를 상속받고 있다.
- 입력값이 정수냐 실수냐에 따라 하나의 변수가 받아야 한다면?
int와double을 모두 받을 수 있는 변수는 Wrapper 클래스(Object, Number)밖에 없다.
배열 복사 (day08)
- 기존 String의 단점: 무언가를 처리하면 새로운 객체를 계속 리턴한다. 내 자신이 바뀌는 게 아니라 조작된 새로운 객체를 리턴하는 것이다.
배열 복사를 가장 쉽게 하는 법:
public static void main(String[] args) {
char [] arr1 = {'a','b','c','d'};
// System.out.println(Arrays.toString(arr1));
System.out.println(new int[] {1,2,3,4});
System.out.println(Arrays.toString(arr1));
char[] arr2 = Arrays.copyOf(arr1, arr1.length);
System.out.println(Arrays.toString(arr2));
System.out.println(arr1==arr2);
//배열복사 가장 쉽게 하는 법
}
Arrays.copyOf를 사용하면 된다.

얕은 복사 vs 깊은 복사:
- 객체 배열은 객체의 주소 값을 담는다. 복사하면 배열은 다르지만, 그 안의 element가 가리키는 객체는 동일하다.
- 즉 객체 배열을 복사하면 새로운 배열이 만들어지지만, element까지 복사되지는 않고 같은 객체를 가리킨다.
- 다른 객체를(새로운 element를) 만들고 싶으면 배열을 반복문으로 돌려서 만들어야 한다.
System.arraycopy는 깊은 복사다.
리팩토링은 어디까지 할지 정해야 한다. 끝없이 하려 들면 끝이 없다.
자료구조 - 정보 저장의 단위
- 정보를 저장하는 단위를 리팩토링해 봐야 한다. 내부에서 하긴 껄끄러우므로(UI까지 겹치므로) 밖으로 빼놓고 생각해 보자.
- 입력·수정·삭제가 자주 일어나는 건 링크드 리스트(LinkedList)가 더 뛰어나고, 한 번 입력하고 보기만 하는 건 어레이 리스트(ArrayList)가 성능이 더 뛰어나다. (표면적인 특성이다.)
Set 타입 (순서 없음, 중복 불가)
(10일차)
- 순서가 존재하지 않음 = 중복을 허용하지 않음. Set 타입의 자료구조(순서가 없는 자료구조)다. 다만 엄밀히 말하면 순서가 아예 없을 수는 없다.
- 이론적: 순서(X), 중복(X)
- 실질적: 순서(O), 중복(X)
- 이들은 모두 자바 util 패키지의 Set 인터페이스를 상속받아 ‘Set 타입’이라 불린다.
- 순서가 있고 중복을 허용하는 것은 List를 상속받는다.
- List에서 디폴트로 쓸 것은 ArrayList다. 인덱스 정순으로 하면 큐, 역순이면 스택이 된다. 원칙만 잘 지키면 ArrayList로 거의 모두 해결 가능하다.
- 내가 만든 걸 남들이 쓸 때는 내가 원하는 대로 안 쓸 수 있어서 다양한 타입이 제공된다.
- Set 타입에서 디폴트로 쓰는 것은 HashSet이다.
HashSet과 Hash 함수
- 정렬을 해야만 중복값 검사가 가능하다. 이때 HashSet을 쓰면 숫자가 나오는데, 이를 가지고 정렬하는 게 Hash 함수다.
- 자료구조에 들어갈 때는 캐스팅되어 Object로 들어간다. 실제로는 Integer 객체 등으로 박싱되어 들어간다.
- 기본 자료형은 주소 값만 기억해서는 바로 들어갈 수 없으므로, 객체로(정확히는 객체의 주소 값을 기억하여) 들어간다.
- 객체를 구분하는 것 자체가 각각의 메모리 주소를 가지고 있기 때문이다. HashSet은 문제를 일으킬 경우가 거의 없다.
TreeSet (이진 트리, 정렬)
- TreeSet은 이진 트리 방식으로 값을 저장한다.
- TreeSet에 무언가를 넣으려면 정렬을 위한 값이 있어야 한다.
- “알아서 정렬하겠지” 하면서 넣으면, 내 클래스 객체는 지정된 정렬 기준 값이 없어 정렬을 해낼 수 없고 Exception 오류가 발생한다.
학습 태도 메모
- 수업만으로는 프로그래밍 능력이 배양되지 않는다. 중요한 것은 반복과 숙달인데, 수업에는 반복·숙달과 실제 활용이 빠져 있다.
- 개별적으로 직접 해 보고 따라와야 한다. 문법을 배우면 써 보는 식으로. 그렇지 않으면 한 번 듣고 지나가 버린다.
테스트 과제는 자료구조 Set 내용. 특정 시간(데드라인)을 주고 의뢰하며, 평가 항목·배점을 작성한다. 최우선은 데드라인, 그리고 공개 평가다. 평가는 고과에 반영되며 기준은 최대한 상대평가에 가깝다고 한다.
- 국내 개발자는 자바를 할 수밖에 없는데, 국내 주도 사업이기 때문(선택과 집중)이다.
- 기존 언어들은 모든 걸 다 하려 했고 웹은 스크립트 언어라 천시했지만, 시대가 바뀌어 하드웨어 쪽을 다루던 언어들이 웹으로 눈을 돌렸다.
- 자바의 UI는 다룰 생각이 없다. 퍼포먼스 좋은 다른 도구가 많기 때문이다. 자바로 UI를 하는 건 부담이라 어설프게 만들어졌고, 그래서 타 언어를 쓰라고 하는 것이 자바가 취하는 자세다.
제네릭스
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>(); //이렇게 쓰게 되면 Integer타입만 list에 넣을 수 있다.(제약을 주는거 이게 제네릭스)
list.add(1111);
list.add(2222);
list.add("3333");
}
- list에는 원래 다양한 형태가 들어갈 수 있는데, 제네릭을 주면 Integer 형태만 들어갈 수 있게 된다(제약을 주는 것이 제네릭스).
- 자바는 다양한 언어에 영향을 주기도 했고, 발전하면서 콜백 등 다른 언어들의 영향을 받기도 했다. 요구를 받아들이는 게 늦어, 역으로 다른 언어에 영향을 받는 스타일이 되었다.
- 그러므로 다른 형태의 언어를 공부하면서 자바를 공부하는 게 좋다. 자바 공부는 스펙트럼을 넓히는 차원이 좋다.
제네릭스까지는 끝날 때까지 계속 반복된다. 꼭 익숙해져야 하며 매우 중요한 부분이다. (스레드는 스프링이나 자바에서 관리를 해 준다.)
IO 입출력
IO 개요
- IO의 주 타깃은 콘솔이었으나, 이제 다룰 대상은 File이다.
프로그램은 다양한 기능도 좋지만 상품성이 중요하다. 현장에 나가면 바로 상품을 만들어야 한다. 문제 자체만 해결하는 것은 별것 아니지만, 기본 기능을 리팩토링하고 완벽하게 잘 되게 하는 것이 우선이다. 간단한 프로젝트라도 생각하지 못한 문제가 엄청나게 나온다(자바로 메모장 만들기 등).
파일과 디렉토리
- 윈도우 콘솔에서 파일·폴더를 생성하려면 관리자 권한이 필요하다. 안 되면 이클립스나 인텔리제이를 관리자 권한으로 실행해 보자.
- 디렉토리도 파일이다. 윈도우로 넘어왔다고 그 원칙이 바뀌지는 않는다. 폴더와 디렉토리가 완벽히 일치하는 개념은 아니지만, 윈도우만 폴더를 디렉토리라고 부른다. 그래서 파일 포매팅을 보면 윈도우는 디렉토리를 특수하게 사용한다.
절대경로를 입력해도, 상대경로를 입력해도 완벽하게 나오게
dir을 처리하는 것이 과제. txt 파일은 루트 노드에서 다 실행해 보자.
임시 파일 (temp)
- 로컬 temp 경로는 절대 겹치면 안 된다. 그래서 지정하는 옵션이 prefix와 suffix다. 파일의 앞부분·뒷부분은 지정하고, 중간은 겹치지 않게 알아서 생성한다. prefix로 시작해서 suffix로 끝나는지 확인하면 된다.
package com.bit.day11;
import java.io.File;
import java.io.IOException;
public class Ex06_tempFile {
public static void main(String[] args) {
File file = null;
try {
file = File.createTempFile("abcdefg", "txt"); //임시 파일 만드는 것
System.out.println(file.getPath());
}catch (IOException e){
e.printStackTrace();
}
}
}
이러면 temp에 abcdef로 시작하고 txt로 끝나는 파일이 생긴다.


- temp 파일은 운영체제가 저장했다가 용량이 부족하거나 필요 없으면 지우기도 한다(예: 디스크 조각 모음). 강제로 지우지 않아도 되며, 기본적으로 30일 기준으로 운영체제가 관리한다. 30일이 지나지 않아도 용량을 벗어나면 가장 오래된 것을 지운다.
IO의 기본 - 읽기와 쓰기
- IO에 한해서 자바는 로우레벨에 가깝다.
- IO의 기본은 쓰고 읽는 것이다. 그것만으로는 불편하고 느리니 기능을 장착하는데, 어디까지나 편의를 위함이다.
- 양수로만 하면
-128~127이0~255가 된다. 126이 끝에 가고, 그다음 빌드에서 끝으로 갔다가 0이 되는 식이다.
메모장과 인코딩
- 메모장은 다른 문서와 달리 바이너리 코드에 해당하는 숫자 값을 그대로 글자로 표현해 준다. 메모리상 65면 화면에
A라고 보여 준다. - 메모장은 그림 첨부나 표가 안 되지만, 워드나 한글은 자체 포매팅 방식이 있어 표현이 가능하다. 메모장은 순수히 바이너리 코드를 화면에 띄워 주는 역할을 한다.
- 폰트는 바꿔도 데이터에 영향을 주지 않는다. 다만 데이터를 어떻게 보여줄지를 정하는 것이 폰트다.
- 인코딩(encoding)에서는 데이터의 변형이 존재한다. 인코딩 패턴에 맞춰 1바이트 또는 2바이트로 조절한다.
바이트 스트림과 문자열의 충돌
- 바이트를 제어해야 하는데, 바이트 스트림으로 바이트가 아닌 문자열을 쓰고 제어하려고 해서 겪는 현상들이다. 이 접근 자체가 잘못된 것이다.
- 바이트를 바꿔서 문자열로 쓰든지, 문자열 스트림을 쓰든지 해야 한다. 바이트 스트림밖에 못 쓰는 상황이면 문자열을 바이트로 처리해야 한다.
버퍼(Buffer)의 개념
- 사용자가 버퍼를 만들어 처리한다는 것: 끝나기 전까지 다음 코드로 안 넘어간다. 항상 확인 후 다음으로 넘어가는데, 그러지 말고 한 번에 몇 바이트를 통째로 써 버리면 성능이 향상된다.
- 읽어들일 때도 한 번에 통째로 읽으면 향상된다. 이것이 BufferedReader, BufferedWriter 방식이다.
- 버퍼를 쓴다 = 바이트 배열에 담아서 한 번에 가져온다. 이 수(read 반환값)는 몇 개를 읽어 왔는지를 리턴한다. 기본적으로 버퍼만큼 꽉 채워서 읽어오다가 마지막엔 남은 만큼만 읽어온다.

- 읽어들인 값은 바이트가 가지고 있으므로, 바이트 배열로 문자열을 조립해야 한다. 버퍼만큼 읽어오고 버퍼만큼 문자열을 만든다.
7바이트 파일에 사이즈 2만큼 주고 실행하면? abcdefg까지만 나와야 하는데 f가 더 나온다. 왜일까?
- 버퍼는 바이트 배열인데, 반복문을 돌 때마다 재사용한다. 한 번 쓰면 다음번에 덮어쓴다.
- 파일 끝을 만나 적은 수만 읽으면, 이전에 읽은 값이 남아 있어 그것까지 가지고 문자열을 만들어 버린다.
이런 현상이 함부로 다운로드한 파일을 정상적으로 열면 안 되는 이유다. 내가 모르는 값(악성 코드)이 숨어 있을 수 있다. 정상적으로 연 순간 운영체제는 권한으로 연 것으로 파악해 악성 코드가 실행될 수 있다.
이를 방지하려면 buf를 주고, 인덱스 0부터 원하는 길이만큼만 읽어들이게 하면 된다.
package com.bit.day11;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Ex11_BufferedReader {
public static void main(String[] args) {
File file = new File("test11.txt");
FileInputStream fis = null;
byte[] buf = new byte[1024]; //쓸때와 달리 읽을때는 얘기가 다르다.
//앞서선 1바이트씩 썼으면 지금은 읽어들이는게 3바이트
// 전체가 2800개면 3번만 읽어들이면 됨.4번째에 나감.
long before , after;
try{
fis = new FileInputStream(file);
before = System.currentTimeMillis();
// while(true){
// int su = fis.read();
// if(su==-1) break;
// }
while(true){
int su = fis.read(buf);
if(su ==-1) break;
// System.out.println(su);
System.out.println(new String(buf,0, su)); //이러면 su 만큼(버퍼만큼) 문자를 읽게 된다.
//읽어들인 값이 test11과 일치 하는지를 봐야한다.
}
fis.close();
after = System.currentTimeMillis();
System.out.println(""+(after-before)+"ms");
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
쓸 때도 마찬가지로 인덱스 0번부터 원하는 숫자만큼만 작성한다.
fos.write(buf,0,buf.length);//쓸때도 마찬가지로 인덱스 0번부터 원하는 숫자만큼만 작성
이미지 복사로 버퍼 성능 확인
package com.bit.day11;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Ex13_BufferedReaderImg {
public static void main(String[] args) {
File origin = new File("big02.jpg");
File copy = new File("copy.jpg");
FileInputStream fis = null;
FileOutputStream fos = null;
byte[] buf = new byte[1024];
try {
if (!copy.exists()) copy.createNewFile();
fis = new FileInputStream(origin);
fos = new FileOutputStream(copy);
while(true){
int cnt = fis.read(buf);
if(cnt ==-1) break;
fos.write(buf);
}
fos.close();
fis.close();
System.out.println();
//성능 향상이 1kb버퍼로 인해 엄청난 성능향상. 2kb를 주면 1/2로 준다. 4kb는 1/4로 줄고. ㅇㅇ
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 앞서는 ‘얼마나 읽을지’를 고민했다면, 여기서는 ‘얼마나 쓸지’를 고민한다.
IO 학습의 핵심
- 우리가 직접 IO를 할 일은 많지 않지만, 개념을 이해하고 내부 동작을 알아야 한다. 구현 자체보다 내부 동작 이해가 중요하다.
- 자바는 쉽게 쓰는 방법도 제공하지만, 코드는 오히려 더 복잡해 보인다. 코드상으로는 복잡한데 개념상으로는 어떻게 쉽게 짜는지가 드러난다.
- 바이트 스트림(byteStream)의 기본은 숫자를 다루는 것(stream으로)이다. 프로그래밍은 결국 숫자열이나 문자열 둘 중 하나를 다룬다. 문자열을 다룰 때가 껄끄럽다.
필터 클래스 (Filter Stream)
- IO는 일방통행 흐름이다. 필터 클래스는 기능을 좀 더 쉽게 쓰게 해 준다. 필터 클래스를 다는 순간 기능이 확장된다.
- 클래스에서 기능은 메서드다. 그 대표적인 게 버퍼다.
- 내가 만든 바이트 배열로 1바이트씩 읽으면 반복문이 2800여 번 돌아간다. 필터 클래스가 버퍼를 가지고 있어 버퍼 양만큼을 읽어온다.
package com.bit.day11;
import java.io.*;
public class Ex13_BufferInputStream {
public static void main(String[] args) {
File file = new File("test11.txt");
FileInputStream fis = null;
BufferedInputStream bis =null;
long before, after;
try{
fis = new FileInputStream(file);
bis = new BufferedInputStream (fis); //inputStream에 필터 제공하는 건 BufferedInputStream이다.
before = System.currentTimeMillis();
while(true){
int su = fis.read();
if(su==-1) break;
System.out.println((char)su);
}
bis.close();
//bufeerdInputStream을 먼저 해제해줘야한다.
fis.close();
after=System.currentTimeMillis();
System.out.println(":" + (after-before)+"ms");
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
}
- 내가 반복문을 돌려 가져오는 건 하드디스크로부터 읽어오는 것이다. 반면 IO의 버퍼 필터 클래스는 메모리에서 메모리를 읽어온다.
- 주기억장치와 보조기억장치의 속도 차이 때문에, 2배의 작업이라도 메모리에서 읽으면 빠르다. 같은 버퍼라도 버퍼의 위치가 다르다.
- 객체를 생성하는 순간 버퍼를 읽어오므로, 제대로
close()하지 않으면 값이 날아간다. 원하는 출력을 하려면 일괄적으로 담아 두었다가 한 번에 처리한다(하나씩 출력하면 너무 느려진다).
package com.bit.day11;
import java.io.*;
import java.util.Arrays;
public class Ex13_BufferInputStream {
public static void main(String[] args) {
File file = new File("test11.txt");
FileInputStream fis = null;
BufferedInputStream bis =null;
long before, after;
byte [] arr = new byte[(int)file.length()] ;
System.out.println(arr.length);
int cnt = 0;
try{
fis = new FileInputStream(file);
bis = new BufferedInputStream (fis); //inputStream에 필터 제공하는 건 BufferedInputStream이다.
before = System.currentTimeMillis();
while(true){ //정삳ㅇ 실행 코드
// int su = fis.read(); //fis 대신 bis 버퍼에 담아서 실행하면 엄청난 속도 향상
int su = bis.read();
if(su==-1) break;
arr[cnt++] = (byte)su;
// System.out.println((char)su);// 하나씩 보내는건 속도저하에 엄청난 영향
if(cnt == 1024){ //배열이 너무 큰 경우 문제가 생겨서 1024가 되면 배열을 줄였다.
cnt = 0;
System.out.println(Arrays.toString(arr));
arr= new byte[1024];
}
}
bis.close();
//bufeerdInputStream을 먼저 해제해줘야한다.
fis.close();
after=System.currentTimeMillis();
System.out.println(new String(arr)+":" + (after-before)+"ms");
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e) {
e.printStackTrace();
}
}
}
- 위 코드는 BufferedInputStream
bis를 만들고 그 안에 넣어 메모리로부터 버퍼를 읽어온다. 버퍼의 의미가 다르며, 두 가지(빠른 읽기 + 담기)를 동시에 할 수 있다. - 이 경우들이 다 되는 것은 아닐 수 있다. OS에 따른 제약 사항도 있고, 문자열에 길이 제한이 있다(운영체제 특성상).
버퍼 달 때 주의할 점
- BufferedInputStream, FileInputStream을
close하지 않으면 안 된다(해제 필수). 이는 read할 때도 마찬가지다. flush는 버퍼가 꽉 차지 않더라도 밀어내고 버퍼를 초기화한다.close를 안 해도 써지긴 한다.- API는 기능을 쓰도록 요청하는 것이며, 그 외에는 노출되어 있지 않다.
특이한 필터 클래스 - PrintStream
- 스트림은 양방향이다(단방향인데 input, output이 존재).
- BufferedInputStream, BufferedOutputStream에는 예외가 있는데, PrintStream은 새로운 기능을 더해 주는 출력 전용 필터 클래스다(오직 쓰기만 제공, outputStream만).
- 지금까지 써 온 것이며, 참조 변수처럼 출력이 안 되는 것은
toString으로 호출한다. - PrintStream조차도 필터 클래스라 무한 장착이 가능하다(정수기에서 각 필터를 거쳐 물을 얻듯이). 이 사이에 BufferedStream을 장착할 수도 있다(성능 향상 목적).
- BufferedStream의 특징 = 버퍼 기능 = 성능 향상.
- PrintStream의 기능 = 문자열 처리를 빠르고 쉽게.
- 둘을 합하면 빠르면서 쉽게 가능하다.
- 기본적으로 ByteStream, OutputStream으로 기능은 하지만 느리고 쓰기 불편하다.
IO에 대해 확장된 개념을 가지자.
문자열 스트림 (Reader/Writer)
- 문자열 제공을 쉽게 하도록 문자열 스트림이 제공된다(필터 스트림은 아님).
- 파일에
write하면int수를 쓰는데, 사실 1바이트만 사용한다.
package com.bit.day12;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Ex01_StringStream {
public static void main(String[] args) {
//문자열 스트림 (2byte)
char ch = 97;
File file = new File("test01.txt");
FileWriter fw = null;
try {
if (!file.exists()) file.createNewFile();
fw = new FileWriter(file);
// fw.write(65);
// fw.write((int)'가');
//어차피 문자열 전달하는데 뭐하러 int를 썼나?
// int와 character는 int로 처리하면 뭐가 반응하나? 오토캐스팅으로 캐릭터가 들어간다.
fw.write((int)'가'); //write에는 character 받도록 되어있지는 않음. int로 받게 되어있지.
fw.write("나");//그럼에도 가능한 건 autoCasting덕분.
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
UTF-8과 유니코드:
- UTF-8은 3바이트 체계로 3개의 데이터가 넘어간다. 자바는 유니코드를 사용하는데, UTF-8도 유니코드 중 하나지만 유니코드는 기본적으로 2바이트다.
- 즉 3바이트를 2바이트로 바꿔 통신하고, 다시 2바이트 체계를 3바이트로 바꾼다.
- 유니코드는 문자 코드라 혼자 못 쓰며 어떤 방식으로든 바꿔야 한다. 그중 하나가 UTF-8이고, 윈도우가 쓰는 것은 MS949다.
- ANSI 타입이 MS949면 일치해서 혼동이 없는데, UTF-8이 되면서 혼동이 생긴다.
package com.bit.day12;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Ex02 {
public static void main(String[] args) {
File file = new File("test01.txt");
FileReader fr = null;
try {
fr = new FileReader(file);
System.out.println(fr.read());
// System.out.println((char) fr.read());
System.out.println((char) fr.read());
System.out.println(fr.read());
//없는데 읽으면? -1이 떨어짐.
//문자열 체계는 2바이트.
// 만약 3바이트면 얘기가 완전히 달라진다.
// UTF-8을 보내고 읽으면 그걸 통해서 char바꾸면?char는 2바이트 니까 3글자 나와야 되는데 2글자가 잘 나온다.
fr.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
인코딩 흐름 정리:
- ‘가’, ‘나’는 입장에선 3바이트씩 보낸 것 같아 3바이트 체계 같지만, char로 변환해 보면 그렇지 않다.
- ‘가나’를 보내면 안에서는 문자 코드로 바꿔 통신을 통해 보낸다. 유니코드 그대로는 못 쓰니 파일 특성에 따라 인코딩한다. 그 과정에서
3, 3으로 보내던 걸2, 2, 2로 바꾼다. - 그대로 전달되는 것처럼 보이지만 이런 흐름이며, 읽어들일 때도 반대다. 파일 특성 또는 IO 대상에 맞춰 이뤄진다.
- 콘솔은 MS949라 그대로 보내서 문제가 발생한다. 그래서 컴파일 단계에서 UTF-8을 2바이트로 바꿀 때 3바이트라 생각하고 바꾸게 하고, 쓰는 IO 대상으로 바꿀 때 2바이트로 바꾼다. 그래서 변환되어 한글이 깨지지 않고 잘 나온다.
javac할 때만 컴파일 옵션을 주고 실행할 땐 안 준다. 자바 내부에서는 문자 코드인 유니코드를 사용하기 때문이다.
바이트 스트림으로 한글 읽기의 함정


byteStream으로 하기 위해 3바이트씩 주고 그 안에 한글을 한 글자씩 넣었다.


‘가나다라마’ 5개를 넣고 5개를 출력해 보면 깨진다(가나다 다음에 !@).
package com.bit.day12;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Ex03_byteStreamReader {
public static void main(String[] args) {
File file = new File("test01.txt");
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
byte[] han = new byte[3];
han[0] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[1] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[2] = (byte) fis.read(); //이러면 한 글자 읽는다.
System.out.println(new String(han)); //한 글자들을 읽는다.
han[0] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[1] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[2] = (byte) fis.read(); //이러면 한 글자 읽는다.
System.out.println(new String(han)); //한 글자들을 읽는다.
han[0] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[1] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[2] = (byte) fis.read(); //이러면 한 글자 읽는다.
System.out.println(new String(han)); //한 글자들을 읽는다.
han[0] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[1] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[2] = (byte) fis.read(); //이러면 한 글자 읽는다.
System.out.println(new String(han)); //한 글자들을 읽는다.
han[0] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[1] = (byte) fis.read(); //이러면 한 글자 읽는다.
han[2] = (byte) fis.read(); //이러면 한 글자 읽는다.
System.out.println(new String(han)); //한 글자들을 읽는다.
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
잘 나오다가 깨진다.
- 모든 문자가 3바이트면 상관없는데, 문제는 동적으로 처리된다는 점이다. 127까지는(인코딩이 뭐든) 자원 낭비를 줄이려 1바이트만 쓴다. 앞에 띄어쓰기(스페이스)가 있으면 스페이스를 읽고 그 두 개를 합쳐서 읽는다.
- 이를 막으려면 모든 문자열을 적재한 뒤 최종적으로 한 번에 바꿔야 한다.
package com.bit.day12;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Ex03_byteStreamReader {
public static void main(String[] args) {
File file = new File("test01.txt");
FileInputStream fis = null;
// byte [] buf = new byte[15];
byte [] buf = new byte[16]; //스페이스 포함해야되서(1바이트) 16
try {
fis = new FileInputStream(file);
for(int i =0; i< buf.length; i++){
buf[i] = (byte) fis.read();
}
fis.close();
System.out.println(new String(buf));
//여기서 끝에 깨지면 마지막 끝에 한바이트 안 받아서 그럼(스페이스 1바이트)
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
전체 값을 받은 뒤 한 번에 바꾸면 잘 나온다.

- 바이트 스트림은 이런 문제 때문에 실시간으로 받아 처리하기 상당히 껄끄럽다.
- 쓰는 입장에서는 이런 고려 없이도 잘 쓸 수 있다. 글자를 6번 썼으면 6번 읽으면 된다. 다만 바이트 스트림화하려면 일일이 계산해야 한다.
- 기본 스트림은 1바이트다. 문자열 스트림도 내부적으로는 바이트 스트림을 사용하지만, 내부를 생각하지 않고 쓰도록 잘 만들어져 있다.
- 이전 스트림은 메모리를 생각해야 했지만, 문자열 스트림은 내용에 집중한다. 글자가 6개니까 6번 읽으면 된다. 그러므로 문자열 스트림으로는 메모리를 생각하면 안 된다.
- 문자열 스트림은 오직 문자열용이다. 이미지·영상 등을 복사하면 문제가 발생한다. 파일이 반드시 짝수 크기가 아닐 수도 있고, 인코딩 과정에서 사이즈가 늘거나 줄어 바이트로 보면 불일치 문제가 생길 수 있다.
문자열 스트림으로 읽고 쓰기
package com.bit.day12;
import java.io.*;
import java.util.Arrays;
public class Ex04 {
public static void main(String[] args) {
File file = new File("test01.txt");
File copy = new File("test02.txt");
FileReader fr = null;
FileWriter fw = null;
char [] cbuf = new char[10];
try{
if(!copy.exists()) copy.createNewFile();
fw = new FileWriter(copy);
fr = new FileReader(file);
while(true) {
// int su = fr.read(cbuf);
int su = fr.read(cbuf,0,2); //2개만 읽어들여서 사용한다고 하면
if (su == -1) break;
System.out.println(Arrays.toString(cbuf));
// System.out.println(Arrays.toString(cbuf));
// System.out.println(new String(cbuf,0,su)); //읽어 들인 만큼 파일에 씀.
fw.write(cbuf,0,su); //바로 파일에 쓴다. cbuf 인덱스 번호부터 (0) 읽어들인 만큼 사용(su개의 버퍼 사용)
}
fw.close();//close를 안핳면 정상 작동이 안됨
fr.close();//close를 안핳면 정상 작동이 안됨
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
위는 다양하게 읽어들이는 경우를 본 것이다. 이 외에도 사용자 버퍼를 달았다면 필터 스트림으로 버퍼를 달 수도 있다.
문자열 스트림에 버퍼 달기 (BufferedReader)
import java.io.*;
public class Ex05_filterStreamReader {
public static void main(String[] args) {
File file = new File("test01.txt");
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(file);
br = new BufferedReader(fr);
int su = -1;
while((su=br.read())!=-1){
System.out.println((char)su);
}
br.close();
fr.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
}
}
- 문자열 스트림에 버퍼를 달 때는 BufferedReader, BufferedWriter를 쓴다.
- 문자열 스트림을 쓸 때는 받는 것을 정말 잘해야 한다. 읽기·쓰기 시
close를 꼭 잘해 줘야 한다. - 문자열 스트림 자체가 기본적으로 어느 정도의 버퍼를 가지고 있다. 버퍼가 있으면 문자가 아니라 문자열을 담을 수 있다(버퍼 사이즈가 크다는 전제하에). 최소 3개면 적어도
ABC3글자를 담을 수 있다. - 버퍼가 꼭 3일 필요는 없다. 만약 3이면
close안에서도 자동으로 밀어낸다. ‘가나다라’를 써도 아무것도 안 써지면 버퍼가 꽉 차지 않았다는 뜻이며, 곧 버퍼가 생각보다 컸다는 의미다.
문자열 스트림으로 바로 작성
package com.bit.day12;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Ex06 {
public static void main(String[] args) {
File file = new File("test03.txt");
FileWriter fw = null;
try {
if(!file.exists()) file.createNewFile();
fw = new FileWriter(file);
fw.write("문자열을 문자열 스트림을 통해 바로 작성"); //왜? 문자열 스트림 자체가 기본적인 버퍼를 달고 있다(별도로 사용자 버퍼를 달지 않아도)
fw.write("\n");
fw.write("다음 줄 작성");
fw.write("\n");
fw.write("또 다음 줄 작성");
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

- 위 경우 버퍼가 1024로 설정되어 있고, 1024가 넘어가면 밀어낸다.
- 그럼 버퍼는 왜 있나? 일반적으로 문자열을 다룰 때 1024면 충분한데, 그보다 많은 양을 쓰면 버퍼가 더 필요하다.
- 문자열 스트림의 장점은 많지만 한계도 명확하다. 오직 문자열, 내용상 문자열만 다뤄야 한다. 바이너리 값을 받으면 값이 변환되어(그림·영상처럼) 문제가 생긴다.
- 백과사전 하나를 한 번에 전송하는 게 아닌 이상 버퍼의 필요성을 못 느껴 잘 안 쓴다.
개행 처리와 BufferedWriter
- 윈도우의 개행은
\r\n으로, 앞에\r을 하고 개행해야 한다. 나머지 운영체제는\n만으로 개행한다. - 파일을 열었을 때 개행이 두 번 되어 있다면, 윈도우에서 작성되었을 확률이 높다.
- BufferedStream에서는 그냥 개행하면 된다.
package com.bit.day12;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Ex07_1_BufferedStream {
public static void main(String[] args) {
File file = new File("test04.txt");
FileWriter fw = null;
BufferedWriter bw = null;
try {
if(!file.exists()) file.createNewFile();
fw = new FileWriter(file);
bw = new BufferedWriter(fw);
fw.write("첫째줄");
fw.write("\n");
fw.write("두번째 줄");
fw.write("\n");
fw.write("세번째줄");
bw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.bit.day12;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Ex07_1_BufferedStream {
public static void main(String[] args) {
File file = new File("test04.txt");
FileWriter fw = null;
BufferedWriter bw = null;
try {
if(!file.exists()) file.createNewFile();
fw = new FileWriter(file);
bw = new BufferedWriter(fw);
bw.write("첫째 줄");
bw.newLine();
bw.write("두번째 줄");
bw.newLine();
bw.write("세번째 줄");
bw.newLine();
bw.write("네번째 줄");
// fw.write("첫째줄");
// fw.write("\n");
// fw.write("두번째 줄");
// fw.write("\n");
// fw.write("세번째줄");
bw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 위처럼 쓰던 걸 BufferedStream으로 바꾸면,
newLine을 만날 때 알아서 개행해 준다. 버퍼를 통해 한 라인을 읽어올 수 있다. 3번 쓰면 3라인을 읽어올 수 있다. - 버퍼는 성능 이점이 있지만, 문자열에서는 성능 메리트를 크게 못 느낀다. 문자열에서는 라인 단위(표현 단위)로 읽어 처리한다. 메모리상 숫자가 아닌 표현으로 읽어 그대로 쓴다. 기준이 개행이 된다(문자열 스트림에서의 버퍼).
- 단, 앞서 바이트 스트림은 ‘몇 개 읽었는지’를 반환했다면, 여기서는 ‘한 라인’을 반환한다.
필터와 스트림 정리
- 필터는 필요한 만큼 달아서 쓰는 것이며, 반드시 쓰는 건 아니다.
- 문자열 스트림은 위와 같은 이유로 사용하고, 바이트 스트림으로는 문자열 스트림이 못하는 나머지를(문자열·바이너리 코드·일반 숫자·자료형까지) 다 할 수 있다.
- 다만 우리는
int를 쓰는데 바이트 스트림은byte를 써서 불편하다. 이를 해결하기 위해 제공되는 것이 DataInputStream이다.
DataInputStream / 데이터 스트림
package com.bit.day12;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Ex12 {
public static void main(String[] args) {
File file = new File("test05.bin");
FileInputStream fis = null;
DataInputStream dis = null;
try {
fis = new FileInputStream(file);
dis = new DataInputStream(fis);
System.out.println(dis.read());
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readBoolean());
System.out.println(dis.readChar());
System.out.println(dis.readUTF());
System.out.println();
dis.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 장점: 자바로 써 놓은 자료형을 그대로 사용할 수 있다.
- 성적 관리 프로그램의 경우, 문자열로 읽고 쓰면 편하지만 나중에 쓸 때 캐스팅해야 한다. 데이터 스트림을 쓰면 자료형 그대로 사용 가능하다.
데이터 스트림 사용 시 주의:
- 순수한 문자열 스트림 제어와 바이트 스트림 제어는 직접 할 수 있어야 한다. 필터는 성능 향상을 위해 부가적으로 올리는 것이고, 나머지는 API로 보면 된다.
- 데이터 스트림은 바이트 스트림이라 1바이트씩 읽는다.
int를 읽을 때 4바이트,double을 읽을 때 8바이트를 따로따로 나눠 써야 하는데, 이를 데이터 스트림이 메서드 호출에 따라 알아서 해 준다. - 반드시 쓴 순서대로 읽어들여야 한다. 순서가 다르면 오류는 안 나지만 엉뚱한 결과가 나온다. 데이터 스트림은 유념해서 써야 한다.
메모리상의 파일 - ByteArrayStream
- 바이트 스트림에서 불편한 것 중 하나가 문자열을 실시간으로 변환하면 안 되고, 다 받아 두었다가 일괄 변환해야 한다는 점이다. 그럼 어딘가에 받아 두면 된다.
- 메모리에 파일을 쓰기 위해 필터 스트림이 제공된다. ArrayStream은 잘 쓰면 정말 좋지만 아니면 정말 쓸모없다(목적을 모르면).

- 길이가 다른 문자열들을 바이트 스트림으로 한 번에 읽으면 가능하지만, 천만 자처럼 크면 한 번에 담을 만큼 주지 않는 한 일정 규격 단위로 읽게 되고, 그 경계선이 하필 문자열 중간을 자르면 깨지기 시작한다.
- 10000자면 그만큼의 버퍼를 만들기 부담된다. 대안으로 하드디스크에 파일을 만들어 작성하면 바이트 값으로 다 저장되므로, 일괄적으로 문자로 바꾸면 해결된다.

- 다만 파일은 물리적인 보조기억장치에 만들어 성능 이슈가 크다. 이를 극복하려고 메모리에 파일을 저장해 통째로 바꿔 저장한다. 단, 메모리 공간에 파일을 직접 만들지는 못한다.
- 여기서 등장하는 IO가 ByteArrayOutputStream이다. 지금까지의 스트림과 다르다. InputStream의 끝은 어쨌든 InputStream, OutputStream의 끝은 어쨌든 OutputStream이다.
- ByteArrayInputStream은 파일을 연결해 놓고 하는 것과 비슷하다. 직접 구현도 가능하다(제네릭으로 byte를 add).
- 메모리상의 파일은 결국 byte 배열이다. byte 배열도 파일이며, 메모리상의 파일이 결국 byte 배열이다.
직렬화 (Serializable)
package com.bit.day12;
import java.io.*;
class Lec16 implements Serializable {
int su =1111;
int su2 = 2222; //직렬화의 대상
public void func(){
// System.out.println("기능 실행");
System.out.println("바꾼 기능 실행");
}
}
public class Ex16 {
public static void main(String[] args) {
File file = new File("test07.bin");
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(file);
oos = new ObjectOutputStream(fos);
// oos.writeObject(new Object());
oos.writeObject(new Lec16());
oos.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.bit.day12;
import java.io.*;
public class Ex17 {
public static void main(String[] args) {
File file = new File("test07.bin");
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(file);
ois = new ObjectInputStream(fis);
// Object obj = ois.readObject();
Lec16 obj = (Lec16)ois.readObject();
ois.close();
fis.close();
System.out.println(obj);
System.out.println(obj.su);
obj.func();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
직렬화 주의사항:
- 직렬화의 대상이 바뀌면 자동으로 미스매치가 난다.
private static final long serialVersionUID = 1L;— 객체 직렬화 대상은 이렇게 버전을 명세해야 한다. 안 해도 자바 내부에서 처리하긴 하지만, 확실하게 명시적으로 해 주는 게 좋다.System.out.println(obj.su);에서su가private이면 접근하지 못하니 getter로 접근한다.- 메서드는 직렬화 대상에서 빠진다. 따라서 소스 코드를 수정하는 순간 버전을 바꿔 줘야 한다.
- 직렬화 대상에서 빼야 하는 것: ID, PASSWORD 등. 단, 직렬화 대상에서 제외하면 값이 전달되지 않고 디폴트값이 나온다.
1차 평가에서의 객체·자료구조 선택:
- 객체: 느리지만 자료형(다양성)을 살리기에 좋다.
- 리스트: 동적이지만 부모 타입이고 인덱스로 구분하기 쉽지 않으며, 중간 수정·삭제 시 뒤가 밀려온다. (학번을 인덱스로 쓰면 학번이 밀려 요구사항에 안 맞음)
- Set / TreeSet: 정렬되고, 중복 시 별도 처리 없이 안 들어간다(동적 할당).
- TreeMap / Map: 동적이고 타입이 다양할 수 있으나, 쓰기 불편하고 중복 키는 값이 덮어써진다(입력이 아니라 수정이 됨). 가장 안 쓰는 편이지만 잘 쓰면 좋다(파이썬 딕셔너리 키값과 유사).
스레드(Thread)
프로세스와 스레드 개념
- CPU가 단일 코어일지라도 시간을 잘라 동시에 일어나는 것처럼 처리한다.
- 운영체제는 커널만 있는 게 아니다. 블루스크린은 결국 데드락이며, 껐다 켜야 한다. 개인용이 아닌 서버는 365일 돌아야 하는데, 윈도우의 약점이 파란 화면(블루스크린)이다.
- 과거 네트워크 담당자는 명절·새벽이건 달려가 코드를 뽑고 세팅했으며, 날아온 로그를 백업해 썼다. 그래도 로스가 났다. 이를 극복하기 위해 등장한 개념이 스레드다.
프로세스 안의 스레드
- 프로그램이 데드락에 빠져도 프로세스 입장에서는 정상으로 살아 있다. 따라서 다음으로 넘어가는 데 문제가 없지만, 데드락에 빠져 동작은 못한다.
- 운영체제에서 스케줄러가 일을 한다. 프로세스 안의 두 스레드가 스케줄링되어 동작하면 하나는 죽을 수 있다.
- 제어권은 커널이, 커널은 OS가 가진다. 즉 제어권은 OS에 있다. 프로그램에 있어 프로세스는 제어력이 전혀 없다.
- 프로세스 안에서의 스케줄링은 그 프로세스 자신이 한다. 따라서 프로그램이 제어하는 멀티 작업은 안의 스레드를 제어할 수 있다는 것이다.

- 하나의 프로세스에 하나의 스레드만 실행되면 동시 작업이 안 된다. 명령을 내리면 끝날 때까지 다음 작업이 안 된다.
- 하나의 프로세스에 스레드를 늘리면 동시 작업이 가능하다. 이 스케줄링은 프로세스 내부에서 실행된다.
- 프로세스 자체는 제어하지 못해도 프로세스 내부는 제어가 가능하다. 병렬 처리는 하나의 프로세스 내부에서 이뤄진다.
스레드 생성 (Thread 상속)
class Lec01 extends Thread{ //이 쓰레드 동작 시키려 그러면 이 코어에선 쓰레드 만들고 동작 시킴.
//그래서 메인이 일을 함. 쓰레드 만들어서 동작 시키려 하면 얘도 메인이 필요.
@Override
public void run(){
Thread thr = Thread.currentThread();
System.out.println(thr.getName());
}
}
public class Ex01_Stream {
public static void main(String[] args) {
Thread thr = Thread.currentThread();
System.out.println(thr.getName()); //쓰레드의 이름 가져옴
//스레드 늘리는 가장 쉬운 방법 = 쓰레드 상속
Lec01 thr2 = new Lec01();
// thr2.run(); //메서드 호출(얘는 스레드 아님. 그저 메서드 호출).
//run은 하나의 객체에서 메서드 생성하고 그냥 메서드 실행한거. 그래서 찍으면 main스레드가 나옴.
thr2.start(); //start 호출하면 내부에서 다 함.
//main을 우리가 전부 직접 다 호출하지 않듯이 이걸 실행하면 run이라는 메서드를 알아서 실행한다.
// 스레드 생성하는 건 객체 만들거 스타트 호출하는 거. 물론 그 객체는 스레드 상속 받아야 함.
}
}
- 스레드를 만들고 실행하는 게 메인이 끝난 다음에 일어난다. 어떻게 해야 할까?
- 메인이 시작하자마자, 메인 스레드는 메인대로 일하고 새 스레드는 새 스레드대로 일하게 해야 한다. 메인 스레드는 객체를 생성하고 스레드를 만든 뒤, 그다음부터는 동시에 일이 일어난다. (하나의 콘솔에 출력을 건다.)
class Lec01 extends Thread{ //이 쓰레드 동작 시키려 그러면 이 코어에선 쓰레드 만들고 동작 시킴.
//그래서 메인이 일을 함. 쓰레드 만들어서 동작 시키려 하면 얘도 메인이 필요.
@Override
public void run() {
Thread thr = Thread.currentThread();
for (int i = 0; i < 50; i++) {
System.out.println(thr.getName()+":"+i);
}
}
}
public class Ex01_Stream {
public static void main(String[] args) {
Thread thr = Thread.currentThread();
// for(int i =0; i<50; i++) {
// System.out.println(thr.getName()+":"+i); //쓰레드의 이름 가져옴
// }
//스레드 늘리는 가장 쉬운 방법 = 쓰레드 상속
Lec01 thr2 = new Lec01();
// thr2.run(); //메서드 호출(얘는 스레드 아님. 그저 메서드 호출).
//run은 하나의 객체에서 메서드 생성하고 그냥 메서드 실행한거. 그래서 찍으면 main스레드가 나옴.
thr2.start(); //start 호출하면 내부에서 다 함.
//main을 우리가 전부 직접 다 호출하지 않듯이 이걸 실행하면 run이라는 메서드를 알아서 실행한다.
// thr2.start(); //하나 더 찍으면 문제 발생
for(int i =0; i<50; i++) {
System.out.println(thr.getName()+":"+i); //쓰레드의 이름 가져옴
}
// 스레드 생성하는 건 객체 만들거 스타트 호출하는 거. 물론 그 객체는 스레드 상속 받아야 함.
}
}
- 병렬 처리는 기본적으로 프로세스로 이뤄진다. 사람으로 따지면, 한 손은 일을 했다 말았다 하고 다른 손은 계속 왕복하는 식이라, 만나는 순간 실행해 랜덤하게 실행되는 꼴이 된다.
- 스레드 여러 개를 운동선수로 가정하면, main은 준비운동 없이 바로 뛰어도 되고 나머지는 준비운동 후 뛰어든다. 메인 스레드가 일반적으로 제일 먼저 실행된다.
스레드 생성법 (Runnable 구현)
- Thread 클래스 상속
- Runnable 인터페이스 구현
class Lec03 implements Runnable{
@Override
public void run(){
Thread thr3 = Thread.currentThread();
System.out.println(thr3.getName());
}
}
public class Ex03_Thread_without_ThreadClass implements Runnable{
public static void main(String[] args) {
Lec03 obj = new Lec03();
Thread thr2 = new Thread(obj); //이렇게 실행하면 잡아낼 수 있어야 하는데?
//이를 위해서는 직접 Lec03이 쓰레드여야 하는데?
//이를 해결하기 위해선 Runnalbe 메서드를 상속받으면 된다.
Thread thr3 = new Thread(obj);
// 얘는 스레드를 2번 찍고 하고자 하는 일만 2번 하는 거.
// 결론은 쓰레드를 만들어 내는 또다른 방법은 Runnable 상속받는게 주다.
thr2.start(); //상속을 하면 상속 받은게 아닌 내객체를 넣어도 가능. 단 이떄 얘도 Runnable 구현해놔야.
thr3.start();
//한 객체는 스레드 하나만 사용가능 근데, 스레드 객체가 2개면 가능.
Thread thr1 = Thread.currentThread();
System.out.println(thr1.getName());
}
@Override
public void run(){
Thread thr3 = Thread.currentThread();
System.out.println(thr3.getName());
}
}
public class Ex02_Thread2 extends Thread{
public static void main(String[] args) {
Ex02_Thread2 me = new Ex02_Thread2();
me.start();
//내가 나일 지라도 이렇게 처리를 해줘야 한다.
Thread thr1 = Thread.currentThread();
System.out.println(thr1.getName());
}
@Override
public void run(){
// Thread thr2 = Thread.currentThread();
// Thread thr2 = this.currentThread(); //그럼 나를 스레드로 받아서 하는거도 가능
// 내가 스레드가 됨
// 반면 main에선 안됨
// main은 static이라 this가 없어서 안된다. 그래서 스레드 먼저 받아내야됨.
// Thread thr2 = this.currentThread(); //그럼 나를 스레드로 받아서 하는거도 가능
//이렇게 하면 쓰레드 직접 안 만들고도 할 수 있다.(내 자신이 쓰레드이기 떄문. 하지만 main은 static이므로 내 자신이 처리할 수 밖에 없다.)
System.out.println(this.getName());
}
}
class Lec03 implements Runnable{
@Override
public void run(){
Thread thr3 = Thread.currentThread();
System.out.println(thr3.getName());
System.out.println(this.getName); //얘는 안됨. Lec03은 현재 쓰레드가 아님.
//Runnable 메서드를 상속받는 건 쓰레드를 통해 하고자 하는 일을 적시하는 거지 얘가 쓰레드가 되는게 아니다.
}
}
public class Ex03_Thread_without_ThreadClass implements Runnable{
public static void main(String[] args) {
Lec03 obj = new Lec03();
Thread thr2 = new Thread(obj); //이렇게 실행하면 잡아낼 수 있어야 하는데?
//이를 위해서는 직접 Lec03이 쓰레드여야 하는데?
//이를 해결하기 위해선 Runnalbe 메서드를 상속받으면 된다.
Thread thr3 = new Thread(obj);
// 얘는 스레드를 2번 찍고 하고자 하는 일만 2번 하는 거.
// 결론은 쓰레드를 만들어 내는 또다른 방법은 Runnable 상속받는게 주다.
thr2.start(); //상속을 하면 상속 받은게 아닌 내객체를 넣어도 가능. 단 이떄 얘도 Runnable 구현해놔야.
thr3.start();
//한 객체는 스레드 하나만 사용가능 근데, 스레드 객체가 2개면 가능.
Thread thr1 = Thread.currentThread();
System.out.println(thr1.getName());
}
@Override
public void run(){
Thread thr3 = Thread.currentThread();
System.out.println(thr3.getName());
}
}
- Runnable을 구현하는 것은 스레드를 통해 하고자 하는 일을 적시하는 것이지, 그 객체(Lec03)가 스레드가 되는 게 아니다. 그래서 Lec03에서
this.getName()은 안 된다(현재 스레드가 아니므로).
익명 클래스로 스레드 만들기
스레드를 하나만, 한 번 쓰고 말려면 익명 클래스를 쓴다.
public class Ex04_ThreadMain {
//메인이 쓰레드 만드는데 하고자하는 일 전달해야
public static void main(String[] args) {
//Runnable한번 주입받고 발꺼면 굳이 이름 지을 필요가 없다.
Thread thr1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
thr1.start();
//쓰레드 클래스를 상속하는데 이름 없는 클래스도 있다.
Thread thr2 = new Thread(){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
thr2.start();
//내부 클래스는 잘 안쓴다고 했는데 이런 이름 없는 클래스는 상당히 많다.(편하므로)
}
}
- 익명 클래스를 여러 번 쓰고 싶으면, 스레드 하나가 객체이므로 그 부분을 객체로 넣으면 된다. 참조 변수로 만들어 재사용할 수 있다(클래스에 이름이 없다고 했지 객체 재사용을 못 한다고는 안 했다).
public class Ex04_ThreadMain {
//메인이 쓰레드 만드는데 하고자하는 일 전달해야
public static void main(String[] args) {
//Runnable한번 주입받고 발꺼면 굳이 이름 지을 필요가 없다.
Runnable obj = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
//이 위 쓰레드가 객체다. 그러므로 객체로 따로 넣을수도 있다.
Thread thr1 = new Thread(obj);
thr1.start();
Thread thr3 = new Thread(obj);
thr3.start();
//쓰레드 클래스를 상속하는데 이름 없는 클래스도 있다.
Thread thr2 = new Thread(){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
thr2.start();
//내부 클래스는 잘 안쓴다고 했는데 이런 이름 없는 클래스는 상당히 많다.(편하므로)
}
}
- 익명 클래스 = 한 번 쓰고 말겠다는 의미다. 클래스는 원래 여러 번 찍어내는 것이지만, 익명 클래스는 반대로 하나만 찍어내고 안 쓰겠다는 의미다.
- 익명 클래스에도
public,static,final등이 다 올 수 있다.

성적 관리 예제 (ArrayList + 객체)
import java.util.ArrayList;
import java.util.Scanner;
class Student {
static int cnt;
String name; //필드 변수
int eng;
int math;
float avg;
int kor;
int num;
public Student() { //기본 생성자에 주면 학생이 생성 될 때마다 학번 증가할 것.
num = ++cnt;
}
}
public class Ex10_FirstTest_F2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("학생관리 프로그램.ver 0.5.1");
ArrayList<Student> data = new ArrayList<Student>();
int num = 0;
while(true){
System.out.println("1.입력 2.보기 3.수정 4.삭제 0.종료");
int input = Integer.parseInt(sc.nextLine());
if(input==0) break;
if(input==1){
num++;
Student stu = new Student();
stu.num = num;
System.out.println("이름>");
stu.name = sc.nextLine();
System.out.println("국어>");
stu.kor = Integer.parseInt(sc.nextLine());
System.out.println("영어>");
stu.eng = Integer.parseInt(sc.nextLine());
System.out.println("수학>");
stu.math = Integer.parseInt(sc.nextLine());
data.add(stu);
}
else if(input==2){
System.out.println("학번\t이름\t\t국어\t영어\t수학\t평균");
for(int i =0; i<data.size(); i++){
Student stu=data.get(i);
System.out.println(stu.num+"\t"+ stu.name+"\t"+ stu.kor+"\t"+ stu.eng+"\t"+ stu.math+"\t"+ (stu.kor+stu.eng+stu.math)*100/3/100.0);
}
}else if(input ==3){
System.out.println("수정할 학번>");
int temp = Integer.parseInt(sc.nextLine());
for(int i =0; i<data.size(); i++){
Student stu = data.get(i);
if(stu.num ==temp){
System.out.println("국어");
stu.kor = Integer.parseInt(sc.nextLine());
System.out.println("영어");
stu.eng = Integer.parseInt(sc.nextLine());
System.out.println("수학");
stu.math = Integer.parseInt(sc.nextLine());
}
}
}
}
}
}
Student 객체의 학번을 한 번 정하면 변동되지 않게 하려면 final을 쓴다.
import java.util.ArrayList;
import java.util.Scanner;
class Student {
static int cnt;
String name; //필드 변수
int eng;
int math;
float avg;
int kor;
final int num; //이제 변동도 불가
public Student(String s) { //기본 생성자에 주면 학생이 생성 될 때마다 학번 증가할 것.
num = ++cnt;
}
}
public class Ex10_FirstTest_F2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("학생관리 프로그램.ver 0.5.1");
ArrayList<Student> data = new ArrayList<Student>();
int num = 0;
while(true){
System.out.println("1.입력 2.보기 3.수정 4.삭제 0.종료");
int input = Integer.parseInt(sc.nextLine());
if(input==0) break;
if(input==1){
num++;
// Student stu = new Student();
// stu.num = num; //final이라서 안됨. final로 주면 이름 생성하고 입력받으면 될듯.
System.out.println("이름>");
Student stu = new Student(sc.nextLine());
// stu.name = sc.nextLine();
System.out.println("국어>");
// Student stu = new Student(sc.nextLine());
stu.kor = Integer.parseInt(sc.nextLine());
System.out.println("영어>");
stu.eng = Integer.parseInt(sc.nextLine());
System.out.println("수학>");
stu.math = Integer.parseInt(sc.nextLine());
data.add(stu);
}
else if(input==2){
System.out.println("학번\t이름\t\t국어\t영어\t수학\t평균");
for(int i =0; i<data.size(); i++){
Student stu=data.get(i);
System.out.println(stu.num+"\t"+ stu.name+"\t"+ stu.kor+"\t"+ stu.eng+"\t"+ stu.math+"\t"+ (stu.kor+stu.eng+stu.math)*100/3/100.0);
}
}else if(input ==3){
System.out.println("수정할 학번>");
int temp = Integer.parseInt(sc.nextLine());
for(int i =0; i<data.size(); i++){
Student stu = data.get(i);
if(stu.num ==temp){
System.out.println("국어");
stu.kor = Integer.parseInt(sc.nextLine());
System.out.println("영어");
stu.eng = Integer.parseInt(sc.nextLine());
System.out.println("수학");
stu.math = Integer.parseInt(sc.nextLine());
}
}
}else if(input ==4){
System.out.println("삭제할 학번");
int temp = Integer.parseInt(sc.nextLine());
for(int i = 0; i<data.size(); i++){
Student stu = data.get(i);
if(stu.num ==temp){
data.remove(i);
}
}
}
}
}
}
- 이 패턴이 좋은 건 자바라서 좋은 것이다. 자바를 많이 쓴 사람은 이를 벗어나려 노력하기도 하고, 동시에 이 경우에 익숙해져야 한다.
- 객체는 가장 느리지만 자료형을 살리기엔 아주 좋다. TreeSet도 사용 가능하며, 자동 정렬해 주므로 아마 가장 좋을 것이다.
과제: 빌드 툴 조사. 결국 Maven을 쓰겠지만 가장 초창기는 Ant다. Ant 빌드 툴을 조사하고, hello world를 만들어 컴파일·빌드까지 해 볼 것.
성적 관리 예제 (HashMap)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
public class Ex12_FirstTest_F3_MAP {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("학생 성적 관리 프로그램 0.5.2");
String[] title = {"학번","이름","국어","영어", "수학"};
ArrayList<HashMap<String,String>> data = new ArrayList<>();
int num = 0;
while(true){
System.out.println("1.입력 2.보기 3.수정 4.삭제 0.종료");
int input= Integer.parseInt(sc.nextLine());
if(input==0) break;
if(input==1){
num++;
// HashMap<String,String> map = new HashMap<String,String>();
HashMap<String,String> map = new HashMap<>();
map.put(title[0], num+"");
for(int i = 1; i<title.length; i++) {
System.out.println(title[i]+">");
map.put(title[i], sc.nextLine());
}
data.add(map);
}else if(input ==2){
System.out.println("------------------------------------");
System.out.println(title[0]+"\t"+title[1]+"\t"+title[2]+"\t"+title[3]+"\t"+title[4]);
System.out.println("------------------------------------");
for (int i =0 ; i<data.size(); i++){
HashMap<String, String> map = data.get(i);
System.out.println(
map.get(title[0])+"\t"+map.get(title[1])+"\t"+map.get(title[2])
+map.get(title[3])+"\t"+map.get(title[4]));
}
System.out.println("----------------------------------------");
}
}
}
}
- Map을 쓰면 내가 원하는 만큼 넣을 수 있다(Map의 장점). 다만 그렇게 마음대로 넣으면 출력이 까다롭다.
- Map은 제어는 가능한데 강제성이 없다. 개발자는 상황의 변수를 싫어해서, 전형적인 자바 개발자는 Map을 선호하지 않는다(관리가 힘듦).
- 어떤 언어들은 Map에 값 저장 시 타입을 유지하지만, 자바는 타입 유지가 안 된다. 타입 유지가 필요한 부분을 객체로 하면 극복된다.
자료구조 선택 정리:
- 동적 할당이 필요하면 ArrayList를 쓴다. 배열로 만들어 앞에서 시작하면 큐, 거꾸로 쓰면 스택, 입력 시 중복 검사하면 Set이 된다.
- 선택권이 너무 많아 모르겠으면 ArrayList, 또는 TreeMap·HashSet 등을 쓴다.
- 각 자료구조의 장단점을 알고 활용하는 것이 중요하다. 자료구조는 끝날 때까지 계속 다루게 될 것이다.
스레드 종료와 join
- IO 작업에서의 스레드는 별다른 대안이 없다. 주어진 명령들로 코드를 짜서 극복한다.
- 메인이 끝나면 메인 스레드가 끝나는가? → 아니다. 모든 스레드가 종료되어야 한다.
class Lec03 extends Thread{
@Override
public void run(){
for(int i =0; i<20; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class Ex03 {
public static void main(String[] args) {
System.out.println("main start");
Lec03 thr1 = new Lec03();
Lec03 thr2 = new Lec03();
thr1.start();
thr2.start();
System.out.println("main end");
}
}
- 메인 스레드는 다른 스레드를 제어하는 역할이며, 다른 스레드가 끝나기 전까지 제어해야 한다.
class Lec03 extends Thread{
@Override
public void run(){
for(int i =0; i<20; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class Ex03 {
public static void main(String[] args) {
System.out.println("main start");
Lec03 thr1 = new Lec03();
Lec03 thr2 = new Lec03();
thr1.start();
thr2.start();
try {
thr1.join();
thr2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
join을 하는 순간, 조인 대상 스레드가 끝나기 전까지 (현재 스레드인) 나는 안 끝난다.- 조인한다고 해서 그 스레드가 마지막에 끝나는 게 아니라, 현재 스레드가 끝나기 전엔 안 끝나는 것이다. 반드시 둘 다 조인해야 하는 법은 없다.
class Lec03 extends Thread{
@Override
public void run(){
// for(int i =0; i<1000000; i++) {
for(int i =0; i<200; i++) {
System.out.println(getName()+":"+i);
//200 ehlfEoRKwl ehfrjt.
//근데 메인에서 조인을 걸면 200 될떄까지 대기 후에 main이 끝난다. main에서 조인에 머물러 있으니 탈출시켜야. 메인에서 탈출시키는 그게 Thread3이다
try {
sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Ex03 {
public static void main(String[] args) throws InterruptedException {
Thread thr0 = Thread.currentThread();
System.out.println("main start");
Lec03 thr1 = new Lec03();
// Lec03 thr2 = new Lec03();
Thread thr3 = new Thread() { //새 쓰레드 생성
@Override
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Exception발생");
thr0.interrupt();//탈출
}
};
thr1.start();
// thr2.start();
thr3.start();
try {
thr1.join(1000); //1초만 조인 됨.
//조인에다 시간을 줌 = 1초동안 다 끝나야 끝남. 이 작업 자체가 1초를 넘어가는데 이 조인은 자동으로 풀림.
// 다음은 어떤게 선택 될지 모르게 됨.
// thr2.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.sleep(1000);
System.out.println("main end");
}
}
sleep과 yield
class Lec04 extends Thread{
@Override
public void run(){
for(int i = 0; i<10; i++){
try {
sleep(1000);
//sleep 주니까 1초 단우로 0,1 한번씩실행 됐다.
//시간을 넣게 되니까
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getName()+":"+i);
}
}
}
public class Ex04 {
public static void main(String[] args) {
Lec04 thr1= new Lec04();
Lec04 thr2= new Lec04();
thr1.start();
thr2.start();
}
}

yield는 스레드를 양보 상태(Runnable 상태)로 만든다. 자기 자신을 또 꺼낼 수도 있다.
class Lec04_1 extends Thread{
@Override
public void run(){
for(int i = 0; i<10; i++){
if(getName().equals("Thread-0") && i>0){ //스레드 0은 한번 실행하고 계속 양보중.ㅁ
yield();//스레드 양보
}else{
System.out.println(getName()+":"+i);
}
}
}
}
public class Ex04_1 {
public static void main(String[] args) {
Lec04_1 thr1= new Lec04_1();
Lec04_1 thr2= new Lec04_1();
thr1.start();
thr2.start();
}
}
- Running 상태일 때는 메인(내)가 일을 하고 있지 않은 것이다. 메인이 출력하는 때는 타깃 스레드가 Runnable 상태일 때다.
- 타깃이 일할 때 한쪽으로 일을 몰아주고 그다음 else로 가면 내가 일을 못하게 되므로(계속 양보만 하니까), 조건을 하나 더 추가한다.
공유 객체와 동기화 문제
public class Ex06 extends Thread{
int su1,su2;
static int sum = 0;
//1+2+3+..9..10
//1+6+2+7..+5..10
@Override
public void run(){
for (int i = su1; i<=su2; i++) {
int temp = sum +i;
sum = temp;
}
}
public static void main(String[] args) {
// int su1 = 1;
// int su2=100;
// int sum = 0;
Ex06 me1 = new Ex06();
Ex06 me2 = new Ex06();
me1.su1 = 1;
me1.su2 = 5000;
me2.su1= 5001;
me2.su2=10000;
me1.start();
me2.start();
try {
me1.join();
me2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("sum= " +sum);
// 쓰레드가 하나일 때는 문제가 전혀 없다.
}
}
- 위처럼 일을 나눠서 하면 잘못된 값이 나온다. 교차로 연산을 하기 때문이다. 숫자가 작은 범위에선 안 생겨도, 크게 되면 값이 제멋대로 나온다(전산에서 치명적).
@Override
public void run(){
for (int i = su1; i<=su2; i++) {
int temp = sum +i;
sum = temp;
try {
sleep(5); //최소한의 시간 주고 처리 = 발생 빈도 낮추기 가능.
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 최소한의 시간을 주면 발생 빈도를 낮출 수 있다. IO는 컴퓨터 작업에서 가장 느린 작업이라, 콘솔에 출력하고 나면 자바 입장에선 잠시 쉬는 셈이다(출력해야 다음으로 넘어가니까). 우리 기준이 아니라 컴퓨터 기준이다.
- 개발 과정에서 로그 메시지를 남겼다가 지우면 문제가 발생하는 것도 같은 이유다. 로그 메시지 때문에 시스템을 쉬게 만들어, 병렬 처리에서 스레드가 넘어갈 확률이 높아진다.
문제의 원인:
- 각 스레드가 각각
sum을 가지면 문제가 안 되는데, 이 경우 둘 다 공용 객체sum에 접근해 덮어쓴다. static을 써서 문제가 생긴 것은 아니다. 공용 객체를 쓰기 위해static을 쓴 것이다.- 동시에 같은 객체(필드)에 접근했을 때 문제가 발생한다. 자바는 객체지향이고 객체는 주소 값을 전달하므로 공용 객체 접근 비율이 높다. 가장 큰 해결책은 공용 객체를 안 쓰는 것이지만, 자바에서는 참조 변수 전달이 많아 쉽지 않다.
기술 면접 시: 몰라도 관련 용어를 알면 그것이라도 말하자. 예시나 상황을 말해도 좋다.
메모리 구조와 synchronized
자바 메모리 영역:
- 클래스 영역: Ready 상태 영역. 자바에 필요한 모든 것을 올려놓는 영역.
- 스택 영역: 실제 일 단위로 코드를 수행하는 위치(자료구조의 스택 구조로 업무 수행).
- 힙 영역: 객체를 찍어내는 영역.
main, run 부분을 현재 저장된 위치까지 해야 하나, 아니면 새로 해야 하나? 좋은 방법은 메모리상에 똑같이 두는 것이다.

- 그러면 기존 스택 영역을 분리(segment)해야 한다. 그림상으로는 붙어 있지만 물리적으로는 다 떨어져 있다. 그래서 파편화된 것을 윈도우는 성능이 떨어지면 조각 모음한다(위 그림은 논리적 그림).

run은 새롭게 할당된 것이다. 실제 메모리 컨트롤은 그림보다 적은 메모리를 소요한다.- 스레드로 메모리를 분리해도, 동일 객체에 접근하면 문제가 발생한다. static 영역뿐 아니라 스택 영역도 동일 객체 접근 시 같은 문제가 생길 수 있다.
- 그래서
synchronized를 사용한다. 스레드가 넘어왔을 때 일을 못 하게 하며, 스레드가 전환되었어도 돌아오면 문제가 없게 한다. synchronized는 스레드 전환은 일어나지만 전환된 스레드가 일을 못(안) 하게 한다. 메서드를 동기화하고 싶으면synchronized를 붙인다.

- 메모리 상황을 알아야 문제가 왜 발생하는지 알고 해결할 수 있다. 객체는 static이 없으면 위의 하나의 객체에 다 접근한다.
- StringBuilder, StringBuffer가 이 이야기다. 다른 스레드가
append로 기존 값에 작업을 넣는데, 넣고 빼는 과정에서 동기화(synchronized)가 되었느냐 아니냐의 차이다.
wait / notify
join은 interrupt를 통해 깨어나게 했는데,wait는 wait 상태로 빠뜨린다. 깨어나게 하려면notify를 써야 하며, waiting이 아닌 다른 스레드에서 깨운다. 단, 깨운다고 원하는 스레드를 지정해 깨우는 게 아니라 waiting 상태 중 하나를 임의로 깨운다.- 모든 객체는 키를 가진다. synchronized는 객체가 키를 넣을 때 들어가게 할지 말지를 정한다.
- synchronized했을 때 키를 가지고 들어가면, 키를 가져야만 코드 내부로 들어간다. 중괄호를 빠져나갈 때 키를 반환하고 다른 게 들어가게 할 수 있다.
비유: 여자/남자 화장실 키가 2개 있는데 화장실이 하나뿐이면, 남녀 누구든 한 명이 들어가면 다음 사람은 누구든 못 들어간다.
- 모든 클래스는 Object를 상속한다.
- 대기한다 = 스레드 교체를 통해 내 순서까지 오긴 하지만 진행을 더 못하는 것. synchronized 자체는 비효율적이지만(일을 안 하고 쉬므로), 일을 함으로써 생기는 문제를 막는다. 아예 못하게 하는 개념이지 스레드 순서를 바꾸는 개념은 아니다.
- 앞서 시간을 안 잡아먹으려면 양보하거나, 조인으로 block 상태에 빠뜨렸다. 문제는 Runnable 상태에 있으니 생긴다. 내 순서가 왔을 때 밖으로 빼버린다(사이드로). Runnable 상태 중 하나로 뽑아내니 내게까지 순서가 안 온다.
- 스레드 전환을 통해 내게 오긴 하나, 올 때까지 멈춰 있어 효율적이지는 않다. 단 코드상으로는 편하다.
wait와notify는 Thread 클래스가 아닌 Object 클래스에서 받는다. synchronized가 내재되어 있어, 이를 상속받지 않으면 Exception이 발생한다.
데몬 스레드
class Lec08 extends Thread {
@Override
public void run(){
while(true){
System.out.println("thread working");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Ex08 {
public static void main(String[] args) {
System.out.println("main start");
Lec08 thr = new Lec08();
thr.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main end");
}
}
- 내가(JVM이) 죽으면 끝이다. 끝나기 전에 데몬들을 정리해야 한다. 자바는 JVM 특성 때문에, JVM이 정상적으로 리턴되면 운영체제 입장에서는 정상 종료가 된다.
- 스레드 자체는 필요하다(한 프로그램에서의 동시 작업). 두 개 이상의 프로그램을 돌려 처리하는 건 프로세스이며, 프로세스 관리는 운영체제가 한다. 프로세스를 관리하고 싶으면 운영체제에 요청해야 한다.
네트워크와 소켓
웹 서버의 기본 역할: 파일을 올려 두면 IO를 통해 자동으로 읽어 준다. 이런 것을 통신에서 진행한다.
IP 주소
첫 번째 기준 - 공인 / 사설
- 공인 IP: 공인된 IP를 찾아가기 위한 IP. 중복되면 안 된다(찾아갈 수 있어야 하므로).
- 사설 IP: 찾아가기 위한 IP는 아니다. 통신하려면 서로 인식하고 통신해야 한다. 일반적으로 내가 나가는 IP지, 찾아 들어오기 위한 IP가 아니다.
두 번째 기준 - 고정 / 유동
- 고정 IP: 고정되어 있는 IP(학교·공공기관 등), 안 바뀌고 항상 고정.
- 유동 IP: 상시적으로 바뀜(핸드폰 IP 등). 알아서 할당되어 바뀐다.
참고: IPv4 - 위키백과
첫 번째 기준과 두 번째 기준은 완전히 다른 것이다.

- C 클래스는 앞 3개가 정해져 있고 뒤를 마음대로 정할 수 있다. 가정은 대부분
192.161.148.xxx형태이며, 우리가 쓰는 공유기는 기본적으로 C 클래스다. - D, E 클래스는 특수한 목적으로 정해진 것이다.
- 사설은 외부로 나가지는 못하지만, 상위 서버(공유기)가 내부 PC 번호를 아니까 내부끼리는 찾아갈 수 있다. 공유기 밖에서 안으로 들어오지 못할 뿐이다. 사설 IP는 중복 가능이라 얼마든지 늘릴 수 있다.

- 루프백은 나 자신을 나타낸다.
- 또 하나의 패턴이
www다. 같은 나라만 가리키는 대역과 해외까지 허용하는 대역이 있다.www는 전 세계적으로 동일한 주소를 가리킨다.www.naver.com과naver.com은 다르다(다른 주소를 가리킴). 전자는 어디서든 같은 곳으로 가지만, 후자는 가장 빠른 곳으로 간다.
서브넷 마스크
- 할당할 수 있는 체계를 말한다. 이를 간략하게 쓰는 게 뒤에 쓰는 특수 용도 주소다.
게이트웨이
- 사설 IP 대역으로 할당받았는데, 네이버와 통신하려면 공유기를 거쳐 나가야 한다.
DNS
- 누구도 IP 주소를 기억하지 않고 도메인 주소만 기억한다. 그것을 서비스해 주는 서버다.
1.1.1.1,8.8.8.8, 시스템 DNS의 차이.
OSI 계층

- 클라이언트가 왼쪽이라면, 애플리케이션(예: 네이버)에서 아래로 요청하면 오른쪽(서버)에서는 아래부터 위로 올라간다.
- 최종적으로 서비스 프로그램을 IO를 통해 읽어 다시 내게 도착하고, 브라우저에 띄워 준다.
- 통신 계층이 꼭 이걸 지킨다는 보장은 없다. 이 모델은 80년대에 “통신을 마음대로 하니 이렇게 하자”는 개념적 모델이다. 그 전에도 통신은 있었다.
4계층 모델:

- 4계층 모델에서는 segment를 빼버린다. 위 그림은 동일한 모델이다. 애플리케이션 부분(5, 6, 7)을 뭉쳐서 얘기하고, 하위 레벨은 하드웨어 기반이다. 세션은 접속과 접속을 같은 접속으로 볼지에 대한 계층이다.

- IP는 물리적 계층을 넘어선 OS 영역이다. IP 자체가 프로그램 영역 중 하나라 다중 할당이 가능하다. IP를 통해 구분하려면 고유한 식별 개체가 있어야 한다.
- 이를 하드웨어적으로 구분하는 것이 MAC 어드레스다.
- 최종적으로 접근하려면 IP가 필요하고, IP 단으로 상위로 가려면 block되어 있어야 한다.
포트 (컴퓨터 네트워킹)
포트 번호는 크게 세 종류로 구분된다.
- 0 ~ 1023번: 잘 알려진 포트(well-known port). 어느 정도 출력이 정해진 포트.
- 1024 ~ 49151번: 등록된 포트(registered port)
- 49152 ~ 65535번: 동적 포트(dynamic port)
웹의 디폴트 포트는 80번이다. 직접 포트를 만들어 써야 하면 3000번 이내는 쓰지 않도록 한다.

자바로 포트(주소) 다루기
import java.net.Inet4Address;
import java.net.*;
public class Ex09 {
//통신의 모든건 자바 Net 패키지에 있다.
public static void main(String[] args) {
// String host = "https://naver.com";
// String host = "localhost";
byte[] arr = {(byte)233,(byte)130,(byte) 200,108};
try {
// InetAddress addr = InetAddress.getByName(host);
InetAddress addr = InetAddress.getByAddress(arr);
System.out.println(addr.getHostName());
System.out.println(addr.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
위 코드 실행 후 나오는 주소를 주소창에 넣으면 옮겨간다. 127.0.0.1은 나 자신이며, localhost와 본인의 IP 또한 나 자신을 가리킨다.
주소가 한 개가 아니라 여러 개일 수도 있다.
import java.net.Inet4Address;
import java.net.*;
public class Ex09 {
//통신의 모든건 자바 Net 패키지에 있다.
public static void main(String[] args) {
// String host = "https://naver.com";
// String host = "localhost";
String host = "naver.com";
// byte[] arr = {(byte)233,(byte)130,(byte) 200,108};
try {
// InetAddress addr = InetAddress.getByName(host);
// InetAddress addr = InetAddress.getByAddress(arr);
InetAddress[] addrs = InetAddress.getAllByName(host);
for(int i = 0; i< addrs.length; i++) {
InetAddress addr = addrs[i] ;
System.out.println(addr.getHostName());
System.out.println(addr.getHostAddress());
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
위 코드를 실행하면 네이버 주소 4개가 다 나온다.
- 브라우저는 문자열을 받는다. 백엔드로 처리할 때는 우선 문자열을 만들고, 그다음 프론트엔드 문법으로 문자열을 처리한다.
- 브라우저가 해석하는 언어 = 프론트엔드
- 뒷단(서버측)에서 구동되는 언어 = 백엔드
- 자바 IO, 스레드, 소켓 프로그래밍으로 이어진다.
Transfer 계층 - TCP vs UDP
- 내 컴퓨터에 IP 주소를 준다고 해서 어떻게 알고 찾아가는가? PC를 조립하면 OS를 설치하고 드라이버를 설치해 하드웨어를 인식시킨다. 라우터는 내부는 알지만 외부는 모를 수 있다.
참고: TCP, UDP 장단점
- TCP: 연결 지향형 = 순서를 지닌다 → 신뢰도가 높다. 대신 속도가 느리다.
- UDP: 비연결 지향형 = 순서가 없다 → 신뢰도가 낮지만 속도가 빠르다.

- 보낼 때와 받을 때 해석이 다르면 엉뚱하게 받아 신뢰할 수 없고 순서를 확신할 수 없다. 단, 연결 과정에서 시간이 줄어든다.
- 내 입장에선 신호를 보내고 응답이 와야 제대로 연결되었다고 판단한다. 이건 ‘나’를 기준으로 신호를 주고받는 것이다. 서버(네이버) 입장에선 어디선가 연락이 와서 신호를 보내는데, 클라이언트가 맞는지 확신하기 위해 응답을 한 번 더 받아야 한다(연결 성공 확인).
- 그래서 신호가 오가는 과정이 위 핸드셰이크와 같다. 연결이 확신되면 데이터를 보낸다.
- 통신도 네트워크를 사용하는 IO라고 보면 된다.
소켓(Socket)
- 통신하려면 대상이 있어야 하는데, 이를 지원하는 게 소켓이다. 소켓을 통해 IO 작업을 한다.
- 최종적으로 통신하는 주체는 내 컴퓨터(OS)이며, 소켓을 지원하는 것도 OS다. 운영체제가 제공하는 소켓을 사용한다.
- 우리가 아는 OS는 파일 시스템이다. OS의 주 역할은 하드웨어를 인식·제어하는 것인데, 파일 다루듯이 다루면 된다. 모든 것을 파일로 관리한다.
- 그래서 IO 작업 시 파일 입출력을 한 이유는, 모든 IO 통신이 OS에서는 파일 입출력으로 다뤄지기 때문이다.

- 지금까지 IO 작업하면서 ‘나’만 생각했지 파일 입장은 생각 안 했다. 소켓을 다룰 땐 파일 입장도 되어 봐야 한다. 하나의 IO가 내 입장에선 OutputStream이지만 네이버 입장에선 InputStream이다.
- 한쪽에선 소켓을 생성하고, 한쪽에선 연결된 소켓을 반환한다. 그 연결된 소켓을 반환해 주는 게 서버 소켓이다.
- 서버 소켓은 처음부터 있는 게 아니라, 연결되면 연결된 소켓을 반환한다. 그 전엔 연결된 소켓이 올 때까지 대기한다. 즉 접속이 들어올 때까지 대기하다가 연결된 소켓을 반환한다. 나는 문을 열어 놓고 대기하면 된다.
- 반대로 접속하는 클라이언트 입장에선, 소켓을 만들면서 접속을 시도한다. 어디로 접근할지에 대한 정보가 필요하다.
직접 구현 시에도 패키지로 구분하자(하나는 클라, 하나는 서버). 에코 서버를 사용하면 다수가 소통 가능해진다.
자바 UI와 통신 규약
- 자바의 장점은 운영체제에 종속되지 않는다는 것이지만, 자바 UI는 운영체제에 종속된다.
- 그래서 나온 게 Swing이다. 운영체제에 종속되지 않게 자바가 직접 Drawing 하는데, 그래서인지 촌스럽다.
- UI를 쓸 때 사용자 인식을 벗어나면 안 된다(UI/UX 개념). UI/UX 부분에서는 운영체제에 종속적인 부분이 있는 게 좋다. 모든 UI 컴포넌트는 화면에 보여줄지 말지 결정해야 한다.
통신상의 규약 (프로토콜)
- 전체에게 보낼 때는
#, 특정인에게 보낼 때는아이디 #식의 약속을 지키지 않으면 안 된다. 통신에는 약속이 정해져 있고, 그 약속이 프로토콜이다. - 통신은 쏘는 대로 받는 게 아니라, 정해진 패턴대로 주고받아야 원하는 출력을 받는다.
- 웹 서버를 구현하면 통신 규약을 지켜야 채팅이 가능하다. 규약을 안 지키면 채팅이 불가능하다.
자료구조와 객체 선택 정리
- 자료구조를 쓰겠다 = 동적 할당을 하겠다는 것. 선택 기준을 정해야 한다.
- 기본적으로 객체를 선택하면 자료형을 살리겠다는 의미다. 최종적으로 처리되는 건 문자열(String)이다.
- 자바에서 객체는 동적이지도 않고 정적인데도 쓰겠다는 건 자료형 여러 개를 쓰겠다는 것이다. 그런데 문자열 스트림을 쓰는 순간 양쪽 말이 안 맞는다. 그럴 바엔 문자열 배열을 쓰는 게 (정적이라) 위와 같은 코드가 된다.
- 그래서 다양한 코드를 짜 봐야 한다.
통신 TCP / UDP (보충)
- UDP를 쓸 일은 거의 없다. IPTV도 UDP를 안 쓴다.
- TCP 방식이면 지직거릴 때 느리게 갔다가 빠르게 따라잡지만, UDP 방식이면 지직거리면 바로 현재 화면으로 넘어간다.
- 일반적으로 다 TCP이며, UDP 방식은 정말 드물다(최소한의 신호를 보낼 때 정도 사용).