AWT는 이름에서도 알 수 있듯이 Window프로그래밍을 하기 위한 GUI(Graphical User Interface)의 도구입니다. GUI가 무엇인지 모르시나요? 여러분이 Windows의 운영체제를 사용한다면 여기 보이는 화면들이 GUI라고 한답니다. 이전의 컴퓨터들은 명령어 방식의 UI를 썼다면 이제 그래픽이 추가되어 X버튼을 눌러 창을 닫고 하고 있지요. 그리고 JAVA는 AWT를 이용해서 윈도우의 GUI로 된 프로그램을 제작할 수 있습니다. 우리가 AWT를 클래스들을 이용하려면 java.awt.*과 동작에 대한 정의를 하려면 java.awt.event.*를 import하여 사용해야합니다.
이제 본격적으로 AWT에 대해서 알아보도록 하겠습니다. GUI에서 각각의 요소들을 우리는 컴포넌트라고 하며 클래스의 상속 계층도에서 메뉴(MenuComponent)를 제외한 모든 컴포넌트는 Component라는 클래스를 상속받고 있습니다. 이 컴포넌트 중에 조금 특별한 컴포넌트가 있는데 그 이름은 컨테이너(Container)입니다. 이름에서도 알 수 있듯이 무엇을 포함하는 클래스인것 같죠? 다른 컴포넌트를 포함하거나 컨테이너도 포함할 수 있습니다. 그래서 우리는 처음 시작할때 이 Container부터 배울 겁니다. 그 중 가장 일반적인 컨테이너는 Frame이라고 합니다.
아참, AWT보다 자바에서 진보적인 GUI는 Swing입니다. Swing이 여러면에서 더 성능이 좋긴한데, 사용법은 AWT와 거의 동일하므로 AWT 먼저 배우고 난 이후에 Swing 컴포넌트를 배워도 상관없습니다. 그리고 AWT 그래픽이 우리한테는 더 친숙할 겁니다. Swing컴포넌트들은 앞에 J가 붙습니다. 예를들어 Frame의 Swing컴포넌트 버전은 JFrame이죠.
이제는 코드를 짜면서 Frame이 무엇인지 알아보고 어떤 기능이 있는지 배워봅시다.
Frame 알아보기
우선 아래의 코드를 쓰고 실행해봅시다.
import java.awt.*;
public class Main {
public static void main(String[] ar){
Frame frame=new Frame("Title!");
frame.setSize(500,300);
}
}
Frame을 생성하는 코드인데 생성자에는 Title명을 받는 생성자를 사용했습니다. 그리고 너비와 높이는 500, 300 pixel로 맞춰줍니다. 그리고 실행을 하면 아무것도 뜨지 않습니다. 왜냐면 프레임을 보이게 하려면 setVisible()을 true로 설정해야 보이기 때문이죠. 그래서 위 코드를 아래와 같이 수정해보면 이제 Frame이라는 것이 나올 겁니다.
자, Frame이 생성되었다는 것을 확인할 수 있습니다. 그리고 Title부분은 우리가 지정한 문자열과 같다는 것을 알 수 있습니다. 이제 구경했으니 닫아봅시다.
닫히시나요? X버튼을 클릭해도 아무런 변화가 없다는 것을 알 수 있을 겁니다. 왜냐면 X버튼이 눌렸을때 JAVA가 이 프로그램을 닫아야하는지, 혹은 멈춰야하는지, 또는 확인 종료를 묻는 대화창을 띄어야하는지 우리가 정의를 해준적이 없죠. 그래서 X버튼을 눌렸을때를 우리는 Event, 즉 X버튼을 누르는 Event가 발생했다고 하고 동작을 처리해주는 것을 우리는 EventHandling이라고 합니다. 아직은 배우지 않았으니 Eclipse의 화면에 아래의 빨간색을 눌러 종료합시다.
X버튼 이벤트 발생시 Handling - WindowListener 추가하기
혹은 잠깐 맛보고 싶다면 아래의 코드를 추가하고 X버튼을 눌러보세요. 추가하기전에 import java.awt.event.*를 하여 import해주세요.
frame.setVisible(true);
frame.addWindowListener(new WindowListener() {
@Override
public void windowOpened(WindowEvent e) {}
@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
e.getWindow().setVisible(false); //보이지 않게
e.getWindow().dispose(); //메모리에서 제거
System.exit(0); //프로그램 종료
}
@Override
public void windowClosed(WindowEvent e) {}
@Override
public void windowIconified(WindowEvent e) {}
@Override
public void windowDeiconified(WindowEvent e) {}
@Override
public void windowActivated(WindowEvent e) {}
@Override
public void windowDeactivated(WindowEvent e) {}
});
setLocation() 으로 위치 조정
Frame이 자꾸 왼쪽 구석에만 뜨는데, 여간 불편한게 아니죠. 이때 위치를 조정할 수 있도록 setLocation() 메소드가 존재합니다. 위치는 Frame의 가장 왼쪽, 위 꼭지점을 위치로 잡습니다. 이것도 역시 pixel단위로 화면 왼쪽(x축)에서의 거리, 그리고 화면 위쪽에서의 거리(y축)를 지정할 수 있습니다. 화면 가운데에 Frame을 위치시키려면 이렇게 하면 됩니다.
x축 위치 = (모니터의 화면 너비/2) - (프레임 너비/2)
y축 위치 = (모니터의 화면 높이/2) - (프레임 높이/2)
모니터의 화면 너비와 높이를 얻어오려면 Toolkit 클래스를 이용해서 아래의 코드처럼 구현하면 됩니다.
frame.setSize(500,300);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
frame.setLocation((dim.width/2)-(frame.getWidth()/2), (dim.height/2)-(frame.getHeight()/2));
frame.setVisible(true);
이 후 실행하게 되면 Frame이 정중앙에 보이는 것을 확인할 수 있습니다. 하지만 이것보다 더 간편한 방법이 존재하는데요. 위의 dim과 setLocation의 두줄이 아래 한줄로 표현될 수 있습니다. 바꿔서 실행해도 정중앙에 보이는것을 확인할 수 있습니다.
frame.setLocationRelativeTo(null);
배경색을 지정하기 - setBackground()
조금 심심할 수도 있으니 setBackground() 메소드로 배경색을 지정할 수도 있습니다. setBackground()에 전달할 Color의 객체를 사용한다면 원하는 색으로 바꿔줄 수 있습니다.
frame.setBackground(new Color(180,211,211));
또는 Color의 static 멤버로도 가능하긴 합니다.
add 메소드로 다른 Component 추가해보기
Frame에서 다른 Component를 추가해보려면 add 메소드를 이용하면 됩니다. 여기서는 Button 컴포넌트를 사용해서 버튼을 추가해봤습니다.
Button button = new Button("My Button");
frame.add(button);
아래처럼 버튼이 Frame을 꽉채운 것을 볼 수 있습니다. 그리고 아무리 눌러봤자 우리는 event를 handling하지 않았으므로 아무런 변화가 없습니다.
이처럼 버튼 뿐만 아니라 다른 Component들도 add메소드로 추가가 가능합니다. 예를들어 체크박스 (Checkbox), 라디오 버튼(RadioButton) 등 말이죠. 한번 JAVA API doc을 참고하여 추가해보고 가지고 놀아보세요.
이상으로 Frame에 관한 간략한 사용법을 알아보았습니다. 다음 포스팅도 GUI의 포스팅을 이어나가도록 하겠습니다.
중괄호의 숫자가 Arguement들의 인덱스를 나타냅니다. Arguement들은 배열의 행태로 위의 args변수에 문자열 배열로 담겨있습니다. 그래서 {0}은 "reakwon", {1}은 "7"로 치환되게 됩니다. 그래서 그 결과는 아래와 같습니다.
Name : reakwon
Age : 7
Name, Age : reakwon, 7
이런 MessageFormat의 사용처는 MySQL과 같은 query에 주로 사용합니다. SELECT * FROM my_table WHERE id = 'reakwon' 이런 쿼리들을 일반화할때 SELECT * FROM {0} WHERE {1} = '{2}' 식으로 사용할 수가 있죠.
MessageFormat format=new MessageFormat(
"SELECT * FROM {0} WHERE {1} = {2}");
String[] args= {"my_table","id","'reakwon'"};
System.out.println(format.format(args));
결과
SELECT * FROM my_table WHERE id = 'reakwon'
static 메소드로 객체 생성하지 않고 사용할 수도 있습니다. 일회성으로 사용하려면 아래의 방법으로 간편하게 사용할 수도 있지요. MessageFormat의 static 메소드를 바로 사용하는 것을 알 수 있죠.
String message="SELECT * FROM {0} WHERE {1} = {2}";
String[] args= {"my_table","id","'reakwon'"};
System.out.println(MessageFormat.format(message,args));
날짜 형식
MessageFormat은 문자열로만 받는 것은 아닙니다. 날짜와 시간도 이런식으로 받아볼 수 있습니다. 아래의 예시처럼 출력할 수도 있습니다.
String message= "오늘 Date: {0,date}, Time :{0,time}";
System.out.println(MessageFormat.format(message, new Date()));
결과
오늘 Date: 2021. 4. 6., Time :오전 12:04:34
쉼표와 date, time 등으로 날짜와 시간도 지정할 수 있습니다. new Date()로 시간과 날짜를 볼 수 있습니다. 이때 더 많은 정보를 보고 싶다면 아래처럼 사용하면 됩니다.
String message= "오늘 Date: {0,date,full}, Time :{0,time,full}";
System.out.println(MessageFormat.format(message, new Date()));
아래와 같이 요일까지 출력이되며 시간 같은 경우에는 나라의 정보도 확인할 수 있습니다.
오늘 Date: 2021년 4월 6일 화요일, Time :오전 12시 7분 11초 대한민국 표준시
반대로 간략한 정보를 원한다면 short로 바꿔서 입력해주면 됩니다. 더 많은 FormatType과 FormatStyle을 보시려면 지원하는 형식은 아래를 참고해주세요.
기존의 형식 패턴을 applyPattern()으로 다시 지정해서 사용할 수도 있습니다. 이런 방식은 static 메소드가 아닌 객체를 만들어서 계속 사용할때 사용하는 방법입니다.
String message= "오늘 Date: {0,date,short}, Time :{0,time,short}";
MessageFormat format = new MessageFormat(message);
Object[] args= {new Date()};
System.out.println(format.format(args));
String newMessage="자세히 \n"+
"오늘 Date: {0,date,full}, Time :{0,time,full}";
format.applyPattern(newMessage);
System.out.println(format.format(args));
결과
오늘 Date: 21. 4. 6., Time :오전 12:14
자세히
오늘 Date: 2021년 4월 6일 화요일, Time :오전 12시 14분 29초 대한민국 표준시
자, 여기까지 MessageFormat에 대한 소개와 사용 예제를 알아보았습니다. 이런 형식 클래스들은 유용한 경우가 많으니 알아두시면 좋을 것 같습니다.
우리의 일상에서 가장 많이 쓰이는 진수, 10진수를 형식화하는 역할을 하는 클래스가 JAVA에서 DecimalFormat이라는 클래스입니다. 이 클래스는 NumberFormat을 상속하고 있는 클래스이죠. 10진수를 다양한 형식에 맞게 출력해줄 수 있습니다. C언어에서는 이미 fprintf 등이 그 역할을 담당하고 있죠. 이제부터 어떻게 사용되는지 배워보도록 하겠습니다.
Decimal Pattern 적용
DecimalFormat은 java.text 패키지안에 존재하므로 따로 import를 해야합니다. DecimalFormat은 문자 '#'과 '0'이 숫자를 나타내는데 쓰이며 쉼표(,), 마침표(.), 대시(-) 등으로 숫자 형식을 나타낼 수 있습니다. 많이 사용패턴은 아래의 표에 정리해놓았습니다.
Format
설명
0
10진수, 값이 없는 자리는 0으로 채움
#
10진수, 값이 없는 자리는 나타나지 않음
.
소수점을 이하 나타냄
-
음수 부호를 나타냄
,
단위 구분자를 나타냄
E
지수 기호를 나타냄, E 이후 0를 써서 표현(ex E0)
%
퍼센트 기호
'
escape문자, 만약 #을 문자로 나타내고 싶다면 '#' 으로 표현
그외 문자
문자로 취급
DecimalFormat을 사용하는 방법은 생성자를 통해서 형식을 지정해주는 방법이 있습니다. 아래처럼 말이죠.
프로그램을 실행하다가 보면 어떤 원인때문에 비정상적인 동작을 일으키며 프로그램이 종료되는 상황을 보신적 있으실 겁니다. 이때 우리는 프로그램이 오류가 발생했다고 합니다. 에러의 종류는 우리가 컴파일할때 발생할 수 있는 컴파일 오류와 실행 중 발생되는 런타임 오류 두 종류가 있지요. 컴파일 오류는 우리가 잡기가 쉽지만, 런타임 오류는 잡기가 까다롭습니다. 자바에서는 런타임 오류를 두 종류로 보고 있습니다. 바로 에러(Error)와 예외(Exception)으로 말이죠.
에러는 프로그램이 코드로 복구될 수 없는 오류를 의미하고 예외는 프로그래머가 직접 예측하여 막을 수 있는 처리가능한 오류라고 보시면 됩니다. 예를 들어 메모리가 부족한 경우 프로그래머가 직접 제어할 수 없으므로 이런 경우는 메모리 부족(OutOfMemoryError) 에러가 발생하고 함수 호출이 많아 스택이 쌓일 경우에는 StackOverFlowError가 발생할 수 있습니다.
그런데 아래의 코드처럼 어떤 수를 0으로 나눈다면 어떤 상황이 발생할까요?
int a,b;
a=10;
b=0;
int c=a/b;
System.out.println(c);
어떤 수를 0으로 나눌수는 없기 때문에 오류를 내보내게 됩니다.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at aa.Main.main(Main.java:11)
하지만 조건문을 통해서 우리는 0으로 못나누게 할 수 있죠. 이처럼 우리가 예측가능한 상황에서 오류를 제어할 수 있는것이 예외입니다.
예외는 Compile시에 발견할 수 있는 예외와 프로그램 실행시에 발생하는 예외 두 종류가 있습니다. Compile시에 발생할 수 있는 예외는 아래의 사진과 같이 Eclipse와 같은 IDE를 쓰신다면 빨간줄로 예외를 처리하라고 욕합니다.
하지만 위에서의 예처럼 Compile시에 발견하지 못하는 에러를 Runtime에러라고 하는데, 이때는 프로그래머가 예측하여 처리해주어야합니다.
그리고 그런 예외가 발생했을때 어떤 동작을 처리해야하는지를 우리는 예외 처리라고 합니다.
예외 처리
1. try, catch
예외가 발생했을때 우리는 try ... catch ... finally 라는 키워드로 예외를 처리할 수 있거나 메소드를 호출한 곳으로 던질 수 있습니다. 한 가지 중요한 점은 자바에서 모든 예외는 Exception이라는 클래스를 상속받습니다. Exception의 상속 트리를 아래에 간략하게 나타내었습니다.
예외 처리하는 방식은 이렇습니다.
try{
//예외가 발생될만한 코드
}catch(FileNotFoundException e){ //FileNotFoundException이 발생했다면
}catch(IOException e){ //IOException이 발생했다면
}catch(Exception e){ //Exception이 발생했다면
}finally{
///어떤 예외가 발생하던 말던 무조건 실행
}
try 블록 : 이 블록에서 예외가 발생할만한 코드가 쓰여집니다.
catch (예외 종류) 블록 : 이 부분에서 예외가 발생되었을때 처리하는 동작을 명시합니다. catch블록은 여러 개가 있을 수 있습니다. 맨 처음 catch 블록에서 잡히지 않는 예외라면 다음 catch의 예외를 검사합니다. 이때 상속관계에 있는 예외 중 부모가 위의 catch, 그리고 그 자식 예외 클래스가 아래의 catch로 놓일 순 없습니다. 예를 들어 아래와 같이말이죠.
Exception 클래스는 모든 예외의 부모이기 때문에 Exception을 IOException보다 위에서 처리할 수는 없다는 뜻입니다. 왜냐면 IOException의 catch블록은 도달할 수 없는 코드이기 때문이죠.
finally 블록 : 여기서는 예외가 발생하건 발생하지 않건 공통으로 수행되어야할 코드가 쓰여집니다. 임시 파일의 삭제 등 뒷정리 코드가 쓰입니다.
이것을 이용해서 우리는 위의 코드를 예외처리할 수 있습니다.
public static void main(String[] ar){
int a,b;
a=10;
b=0;
try {
int c=a/b;
System.out.println(c); //예외발생으로 실행 불가한 코드
}catch(ArithmeticException e) {
System.out.println("ArithmeticException 발생");
System.out.println("0으로 나눌 수는 없습니다");
e.printStackTrace();
}finally {
System.out.println("finally 실행");
}
}
printStackTrace()라는 메소드는 어느 부분에서 예외가 발생했는지 알려주는 추적로그를 보여줍니다. Exception이 발생했을때 기본 동작이죠. 결과는 아래와 같은 것을 알 수 있습니다.
ArithmeticException 발생
java.lang.ArithmeticException: / by zero
at aa.Main.main(Main.java:11) //Main.java에서 11번째 줄에서 발생했다는 printStackTrace
finally 실행
2. throws
아까전에 예외를 그냥 던질 수 있다고 했죠? 그 의미가 어떤 의미냐면 예외를 여기서 처리하지 않을테니 나를 불러다가 쓰는 녀석에게 에러 처리를 전가하겠다는 의미이며 코드를 짜는 사람이 이 선언부를 보고 어떤 예외가 발생할 수 있는지도 알게 해줍니다. 어떤 뜻인지 모르겠다구요? 아래의 코드를 통해서 알아보도록 합니다.
public static void divide(int a,int b) throws ArithmeticException {
if(b==0) throw new ArithmeticException("0으로 나눌 수는 없다니까?");
int c=a/b;
System.out.println(c);
}
public static void main(String[] ar){
int a=10;
int b=0;
divide(a,b);
}
divide()메소드는 a와 b를 나눈 후에 출력하는 역할을 하는데, 이 나누기 부분에서 우리는 예외가 발생할 수 있음을 알았습니다. 그래서 try, catch로 예외 처리를 해야하지만, divide()를 호출하는 부분에서 처리하기를 원합니다. 왜냐면 divide()를 호출한 곳에서 예외가 발생한 다음의 처리를 divide() 메소드가 정하지 않기 때문입니다. 예를 들어 main메소드에서는 예외가 발생하면 다시 divide()를 호출하거나, 프로그램을 끝내거나, b의 값을 다시 입력받거나 해야하기 때문이고, divide()메소드가 그 결정을 할 수 없다는 의미입니다. 그래서 throws ArithmeticException을 divide를 호출한 main에다가 던지는 것(throw)입니다. 여기서 예외를 던지는 방법은 아래와 같습니다.
(아, 참고로 Exception 생성자 중에서 메시지를 받는 생성자가 있는데, 메시지를 보려면 getMessage()메소드를 이용할 수 있습니다. 아래에서 그 메소드를 사용합니다.)
throw 예외객체
ex) throw new Exception("예외 발생!")
예외를 발생시키는 키워드는 throw입니다. 이때 main은 그 예외를 처리하기 위해 try, catch블록을 쓰면 됩니다. 아래처럼 말이죠.
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdio>
using namespace std;
int n, m;
int mine[500001];
int binarySearch(int left, int right, int num) {
if (left == right) return mine[left] == num;
int mid = (left + right) / 2;
if (mine[mid] == num) return 1;
if (mine[mid] > num)
return binarySearch(left, mid, num);
return binarySearch(mid + 1, right, num);
}
int main() {
scanf("%d",&n);
for (int i = 0; i < n; i++)
scanf("%d",&mine[i]);
sort(mine, mine + n);
scanf("%d",&m);
for (int i = 0; i < m; i++) {
int card;
scanf("%d",&card);
printf("%d ",binarySearch(0,n-1,card));
}
printf("\n");
return 0;
}
탐색을 위해 문제의 입력을 갖고 있는 카드를 우선 정렬합니다. 위 코드가 해답인 코드입니다. 그리고 난 후 이진탐색으로 그 카드의 인덱스가 있는지 확인하는 방법으로 카드가 있는지 아닌지 확인할 수 있습니다.
벡터는 ArrayList와 같이 선형적으로 자료를 담을 수 있는 List 컬렉션입니다. ArrayList와는 다르게 동기화 처리를 하기 때문에 ArrayList보다는 속도가 느리다는 단점이 있습니다. 하지만 여러 쓰레드에서 같은 List를 써야할 필요가 있다면 Vector를 사용하시는 것이 동기화 오류를 만들지 않는 방법이겠죠.
동기화에 대한 예는 맨 아래에 설명하도록 하겠습니다. 지금부터 사용법에 대해서 알아보도록 합시다.
사용법
벡터는 Generic을 사용합니다. 꺽쇠 '<', '>' 에 자료 타입을 지정해주고 수행합니다. 꺽쇠 안에는 원소로 사용할 객체의 클래스를 명시해주면 됩니다. 아래는 Integer 형의 자료를 Vector에서 다룬다는 선언을 보여줍니다.
Vector<Integer> v=new Vector();
1 - 원소 추가와 삭제, 읽기(add, remove, get)
선형으로 자료를 담고 있습니다. 그래서 데이터를 넣으면 차례차례 뒤에 데이터들이 붙어서 쌓이게 됩니다. 특정 위치의 데이터도 삭제할 수 있습니다. 그리고 정수형 index로 원소를 가져올 수 있습니다. 아래가 그 예제를 보여줍니다.
import java.util.Vector; //클래스 import
public class Main {
public static void main(String[] ar){
Vector<Integer> v=new Vector();
v.add(20); //0
v.add(50); //1
v.add(70); //2
v.add(100); //3
for(int i=0;i<v.size();i++) {
System.out.println(i+"번째 원소:"+v.get(i));
}
System.out.println("\n index 1 원소 삭제");
v.remove(1);
for(int i=0;i<v.size();i++) {
System.out.println(i+"번째 원소:"+v.get(i));
}
}
}
결과
0번째 원소:20
1번째 원소:50
2번째 원소:70
3번째 원소:100
index 1 원소 삭제
0번째 원소:20
1번째 원소:70
2번째 원소:100
예제를 보면 차례차례 20, 50, 70, 100의 데이터를 넣어주고 있습니다. 그리고 for문을 돌면서 출력해주고 있습니다. 차례대로 출력이 되는 것을 확인할 수 있죠? 그런데 index가 1인 50을 삭제하게 되면 삭제된 뒤의 원소들이 전부 앞으로 당겨지게 됩니다.
만약 size()보다 큰 index를 갖는 원소를 삭제하거나 get()으로 읽어온다면 ArrayIndexOutOfBoundsException이 발생하게 되므로 size() 체크를 항상 해주셔야합니다. 맨 아래 라인에 v.remove(10); 코드를 추가해서 확인해보도록 하세요.
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 10
at java.base/java.util.Vector.remove(Vector.java:844)
at aa.Main.main(Main.java:23)
원소를 모두 삭제하고 싶다면 clear(), removeAllElements()를 사용하면 모두 지워집니다.
2. Vector를 반복할 수 있는 여러 방법
위의 예제는 단순 for문으로 i를 인덱스 삼아서 get()으로 원소를 읽어왔습니다. 이밖에도 여러 방법이 있는데, 그 방법을 소개합니다.
사이즈를 체크하지 않으면서 순회하고 싶다면 Iterator클래스로 객체를 만들어 사용하는 방법이 있습니다. Iterator클래스는 Java.util 패키지에 존재하니 import하여 사용해보세요. 순회에 필요한 메소드는 hasNext()와 원소를 가져오는 next()메소드만 알고 있으면 됩니다.
메소드
설명
hasNext()
이 다음에 원소가 있는지 확인합니다. 있으면 true, 없으면 false를 반환하지요. 대체로 while안의 조건문에 사용합니다.
addAll() 메소드로 벡터와 벡터를 합칠 수 있습니다. 단, 두 벡터는 같은 Generic 형식을 사용해야하고 합칠 벡터 뒤에 그 벡터가 붙습니다. 또는 생성자를 이용해서 객체 생성시에 벡터를 합칠 수 있습니다. 아래와 같은 형식으로 사용할 수 있습니다. 아래 예는 v2에 v1을 객체 생성시에 합칩니다.
Collections.sort() 메소드를 이용해서 Vector의 데이터를 정렬할 수 있습니다. 기본적으로 숫자는 오름차순, 문자열은 사전순으로 정렬됩니다.
결과
hello
reakwon
world
1
3
5
혹은 내가 만든 객체를 정렬하려면 어떻게할까요? 사람 객체를 키순으로 정렬하고 싶다면, 또는 이름 순으로 정렬, 나이순으로 정렬하고 싶다면 어떻게할까요? 우리가 직접 비교해서 Collections에게 알려줘야합니다. 그것에 대한 설명은 아래의 링크를 참고해주세요. 사용법은 동일합니다.
아까 이야기했던 동기화에 대한 부분을 확인해보도록 합시다. 우선 ArrayList로 아래의 코드를 짜면서 관찰해보죠.
public class Main {
static ArrayList<String> list=new ArrayList();
public static void main(String[] ar){
list.add("reakwon");
list.add("hello");
list.add("world");
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
list.forEach((item)->{
//1초마다 원소를 출력
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(item);
});
}
});
thread.start(); //thread 시작
//thread가 forEach문을 먼저 수행할 여유를 주기 위해 1초 기다림
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
//thread가 forEach() 하는 중에 원소추가
list.add("thread-unsafe");
}
}
결과
reakwon
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList.forEach(ArrayList.java:1513)
at aa.Main$1.run(Main.java:19)
at java.base/java.lang.Thread.run(Thread.java:831)
위의 코드는 아주 간단한 코드입니다. thread라는 쓰레드와 메인 스레드는 list라는 ArrayList객체를 공유합니다. thread는 그 list를 forEach()로 1초마다 그 원소를 출력하는 역할을 하고, 메인 스레드는 스레드 생성하고 1초가 지난 다음 list에 원소를 추가하는 상황입니다. 이때 메인스레드가 thread가 forEach()를 수행하는 중에 list에 원소를 집어넣는 접근을 하게 되는데, 이때 두 스레드 동시에 list에 접근하게 됩니다. 그렇게 되면 위처럼 ConcurrentModificationException이 발생하게 되는 상황이 되죠. 어떻게 고칠까요?
ArrayList를 Vector로만 바꿔주면 이 문제는 해결됩니다.
static Vector<String> list=new Vector();
MainThread는 thread가 forEach()를 종료할때까지 Vector객체에 접근할 수 없고, 반대로 메인스레드의 add()가 끝날때까지 thread는 Vector객체에 접근할 수 없습니다.
Vector는 동작마다 동기화를 걸어줍니다. 이런 일은 속도롤 떨어지게 만드는 작업이지만 멀티 스레드 환경에서는 안전한 작업이죠. 그래서 여러 스레드가 있는 환경에서 개발한다면 Vector를 사용하고 단일 스레드 환경에서는 ArrayList를 활용하시면 되겠습니다.
해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에 해당하는 값을 찾습니다.
Hashtable과 HashMap의 차이
Hashtable과 HashMap과의 차이점은 Thread-Safe인지 아닌지가 그 차이점인데요. Hashtable은 동기화가 걸려있어서 Thread-Safe하다고 할 수 있으며 HashMap은 동기화가 없어 unsafe하다고 할 수 있습니다. 그래서 안전성을 추구한다면 Hashtable을 쓰시면 되고, 데이터의 빠른 처리를 위해서라면 HashMap을 사용하시면 됩니다.
여기서 Hastable의 Thread-Safe가 무슨 의미를 하는지 맨 아래의 예제를 통해서 알아보도록 하세요.
보세요. 이 코드의 대한 저의 의도는 생성된 쓰레드(이하 thread라고 칭하도록 하겠습니다.)가 1초마다 run() 메소드의 저장된 키와 값을 순회하면서 출력하는 겁니다. 물론 예외없이 정상적으로요. 그러니 출력은 아래와 같아야하는 것이 이 코드의 의도입니다. 물론 MainThread가 끝나는 시점은 언제인지 예측 불가능하므로 어디서 나오든 상관없습니다. 중요한 것은 ftp~ssh까지만 출력되어야하는 것이 저의 의도입니다.
MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
하지만 우리의 예상과는 다르게 아래와 같이 출력됩니다. ConcurrentModificationException이라는 예외(Exception)가 발생하게 됩니다. 쓰레드들이 동시에 데이터를 변경할때 이 예외가 발생합니다.
MainThread end
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
{dhcp,67}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at aa.Main$1.run(Main.java:18)
at java.base/java.lang.Thread.run(Thread.java:831)
왜 이런 예외가 발생할까요? 코드를 하나씩 해석해보도록 합시다.
자, HashMap은 두 스레드가 공용으로 사용할 수 있도록 static으로 지정해놓았고, 생성된 쓰레드는 해시맵에 http~ftp까지의 키와 값을 put하고 있습니다(여기서 value는 그 프로토콜의 포트번호를 의미합니다. 뭐 여기서는 중요한게 아니지요.) thread는 put으로 키,값을 저장하고 난 후에 forEach() 메소드를 이용해서 1초마다 갖고 있는 키와 값을 출력합니다.
메인스레드에서는 thread가 forEach() 메소드를 수행할 시간을 벌어주기 위해서 1초간 잠재워줍니다. 그 후 thread는 자신의 루틴(routine) 중 forEach()메소드를 실행합니다.
이때 thread가 forEach() 메소드가 수행하는 중에 메인 쓰레드에서 put으로 데이터를 집어넣고 있는 상황이죠. 즉, HashMap에 동시에 접근해서 변경이 발생하게 되는 그런 코드입니다.
우리의 의도대로 동작하게 만드려면 어떻게 해야할까요? 이럴때 synchronized라는 키워드로 우리가 직접 동기화를 처리할 수도 있지만 더 간단한 방법으로는 바로 Hashtable을 사용하는 것입니다. 이 문제를 해결하려면 단 한줄만 바꾸면 됩니다.
해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서 키는 맵에 오직 유일하게 있어야합니다. 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것입니다. 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)를 찾아야하기 때문입니다. 값은 중복된 값이어도 상관이 없습니다.
HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있습니다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전하지 않은 대신 속도가 빠르냐(HashMap)입니다. 여기서는 Thread-Safe하지 않은 HashMap사용법을 가장 아래에서 살펴보도록 하겠습니다. Hashtable의 개념과 사용법, Thread-Safe한 예제를 보려면 아래의 링크를 참고해주세요.
자신이 만든 객체도 키로 사용할 수 있는데, 그 사용법은 가장 아래쪽 부분에 설명되어있습니다.
Map 인터페이스를 구현한 HashMap은 키를 해싱하여 자료를 저장하고 꺼내오기 때문에 속도가 빠릅니다. 이번 포스팅에서는 사용방법 위주로 설명하도록 하겠습니다.
사용법
사용전에 HashMap과 Map은 java.util 안에 위치합니다. 두개를 import 하여 사용준비해주세요.
1. 키,값 저장(put), 읽기(get) 예
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] ar) {
Map<String,Integer> map=new HashMap(); //<키 자료형, 값 자료형>
map.put("A", 100);
map.put("B", 101);
map.put("C", 102);
map.put("C", 103); //중복된 key가 들어갈때는 이전 키,값을 지금의 것으로 업데이트
System.out.println(map);
System.out.println(map.get("A"));
System.out.println(map.get("B"));
System.out.println(map.get("C"));
}
}
결과:
{A=100, B=101, C=103}
100
101
103
map을 그냥 println으로 출력하게 되면 중괄호('{ }')로 묶여서 키와 값들이 출력됩니다. 기본적으로 map의 put과 get이 아주 많이 사용됩니다. map을 사용하려면 반드시 알아야하는 메소드입니다. put을 키와 값을 map에 저장하는 메소드이며 get은 입력받은 key와 대응되는 값을 돌려줍니다. 만약 해당하는 key가 없다면 null을 넘겨주게 됩니다.
2. containsKey 사용예 (이미 HashMap에 키가 있으면 값을 덮어쓰지 않는 예)
public static void main(String[] ar){
Map<String,Integer> map=new HashMap();
map.put("key1", 100);
map.put("key2", 200);
if(!map.containsKey("key2")) //키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
map.put("key2", 300);
System.out.println(map);
System.out.println(map.get("key1"));
System.out.println(map.get("key2"));
}
결과:
{key1=100, key2=200}
100
200
containsKey메소드로 키가 존재하는지 존재하지 않는지 알 수 있습니다. 이것과 비슷한 메소드로는 containsValue가 있죠. 이것은 반대로 값이 존재하는지 알아보는 메소드입니다. 존재시 true, 없을때는 false를 반환합니다. 위의 if문과 put메소드를 한꺼번에 처리할 수 있는 메소드가 존재합니다. 그래서 두 라인을 아래와 같이 바꿔써도 같은 동작을 합니다.
//if(!map.containsKey("key2")) //키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
//map.put("key2", 300);
map.putIfAbsent("key2",300);
3. putAll 사용예 (Map에 다른 Map을 전부 포함)
public static void main(String[] ar) {
Map<String,Integer> map1=new HashMap();
Map<String,Integer> map2=new HashMap();
//map1 put
map1.put("map1-key1", 100);
map1.put("map1-key2", 200);
//map2 put
map2.put("map2-key3", 300);
map2.put("map2-key4", 400);
System.out.println("map1:"+map1);
System.out.println("map2:"+map2);
//map2에 map1을 합침
map2.putAll(map1);
System.out.println("map2 includes map1:"+map2);
//map1의 키, 값 변경
map1.put("map1-key1", 1000);
//map2에는 영향 없음.
System.out.println("map2 includes map1:"+map2);
}
결과
map1:{map1-key1=100, map1-key2=200}
map2:{map2-key4=400, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
아예 map을 통째로 인자로 넘겨주고 싶다면 putAll 메소드를 사용하면 됩니다. 주의해야할 점은 반드시 키와 값의 자료형이 같은 map이어야한다는 점입니다. 다른 자료형의 키, 값은 받을 수 없습니다.
putAll 대신 생성자를 이용해서 생성과 동시에 map의 데이터를 전부 넘겨줄 수도 있습니다.
Map<String,Integer> map2=new HashMap(map1);
4. keySet 사용예 (모든 키를 순회하는 코드)
list처럼 증가하는 index를 사용할 방법이 없지만 keySet메소드를 이용하여 키를 Set으로 넘겨주어 Map에 존재하는 키를 모두 순회할 수 있습니다.
위에 보이는 -> 가 있는 라인이 람다식입니다. key와 value를 사용하며 -> 이후 동작을 구현해주면 됩니다.
결과
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
6. 내가 만든 객체를 Key로 사용하기(나의 객체를 같은 키로 판단하는 방법)
public class Main {
public static void main(String[] ar) {
Person person1=new Person("reakwon","666666-7777777");
Person person2=new Person("putty","123456-1234567");
Person who=new Person("reakwon","666666-7777777");
Map<Person,Integer> map=new HashMap();
map.put(person1, 90);
map.put(person2, 80);
System.out.println("map includes "+who.getName()+"? "+map.containsKey(who));
map.put(who, 70);
System.out.println(map);
}
}
class Person{
private String name;
private String id;
public Person(String name,String id) {
this.name=name;
this.id=id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return this.name;
}
}
결과
map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}
위의 코드에서 약간 불편한 점을 눈치 채셨나요? 저는 Person의 name과 id가 같으면 같은 키로 보고 중복처리를 하지 않을 생각이었습니다. 즉, 위의 map에는 putty와 reakwon만 있는 것이 저의 의도였고, who와 person1은 같은 키로 map이 인식해줬으면 좋겠습니다. 어떻게 구현할까요?
● equals() 메소드 Overriding
Object 클래스의 equals는 서로 같은 객체인지 아닌지 판별해주는 메소드입니다. String이 바로 이 메소드를 오버라이딩했지요. 그래서 문자열이 같은 경우에 equals의 결과는 true입니다. 이처럼 equals를 통해서 같은지 아닌지를 판별해주면 됩니다. 그래서 아래의 코드를 Person에 추가하면 됩니다.
@Override
public boolean equals(Object o) {
if(o instanceof Person) {
Person p=(Person)o;
return this.id.equals(p.id) && this.name.equals(p.name);
}
return false;
}
결과
map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}
분명 equals를 오버라이딩해서 같은 객체라고 명시했는대도 역시 false를 반환하고 있습니다. 어떻게 해결할 수 있을까요?
● hashCode() 메소드 Overriding
equals()와 쌍으로 기억해두셔야할 것은 바로 hashCode()입니다. hashCode는 각 객체가 갖는 유일한 값(Code)을 의미합니다. Object의 hashCode()는 원래 주소값에 의한 hashCode()로 각 객체가 전부 다른값을 가지고 있습니다. HashMap은 우선 hashCode를 비교하고 같을 때만 equals를 수행하여 정말 제대로 같은것인지 판별합니다. 그래서 HashMap은 애초에 hashCode()가 반환하는 값이 다르면 equals는 수행하지도 않습니다.
그래서 위 코드에서 같은 해시코드를 오버라이딩해서 name과 id에 따라 다른 hashCode를 갖게 만들어주어야합니다. 구현의 편의성을 위해서 String클래스를 사용합니다. String 클래스가 이미 그렇게 다 구현(문자열이 같으면 hashCode도 같고 equals도 true) 이 되어있습니다. 우리는 위의 예제에서 String을 키로 쓴적이 있었죠. String 클래스는 hashCode와 equals가 문자열이 같을때 같은 객체라고 판별할 수 있도록 두 메소드를 전부 재정의한 상태입니다.
그래서 우리는 아래의 코드를 위의 코드 대신 추가해주면 됩니다.
@Override
public int hashCode() {
return name.hashCode()+id.hashCode();
}
@Override
public boolean equals(Object o) {
return this.hashCode()==o.hashCode();
}
결과
map includes reakwon? true
{reakwon=70, putty=80}
위의 코드는 대체적으로 잘 동작하지만 만약 운이 굉장히 좋지 않을 경우 다른 객체로 의도했던 것이 같은 객체로 인식될 수 있습니다. String 클래스의 hashCode의 구현 방식때문이지요. 하지만 이해를 돕기 위해서 더 완벽한 구현은 하지 않았습니다. 귀찮기도 하구요.
알아두셔야할 점은 같은 객체를 판별하는 코드를 넣고자하여 equals를 재정의할때 반드시 hashCode도 다시 정의해주어야한다는 점을 기억하세요.
{seoul,02}
{daejeon,042}
{busan,051}
{jeju,064}
{kyeongkido,031}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at aa.Main$1.run(Main.java:17)
at java.base/java.lang.Thread.run(Thread.java:831)
위 코드에 대한 의도는 thread라는 쓰레드에서 hashmap의 저장된 데이터 seoul ~ daejeon의 데이터를 모두 출력하는 의도입니다. 하지만 메인 쓰레드와 thread간의 HashMap에 동시에 접근했기 때문에 위 코드에 예외가 발생하게 된것입니다. 왜 동시에 접근했을까요?
메인 스레드는 thread의 run() 메소드가 forEach가 수행할 수 있도록 잠시 1초간 기다려줍니다. 그리고 thread는 1초마다 저장했던 데이터들을 출력합니다. 그런데 그때 메인스레드에서 put() 메소드로 데이터를 저장하는 상황이 발생했습니다. 그래서 두 쓰레드가 동시에 HashMap에 접근하게 된것이죠.
이처럼 HashMap은 쓰레드에 대해서 안전하지 않은 컬렉션입니다. 오직 단일 쓰레드에서만 사용하면 안전한 컬렉션이죠. 이를 보완한 컬렉션이 바로 Hashtable입니다. 위의 링크로 들어가서 Hashtable로 이 문제를 해결한 코드를 확인해보세요.
이상으로 HashMap에 대한 기본적인 개념과 사용법 위주로 알아보았습니다. 이 정도만 알고 있어도 HashMap을 잘 사용할 수 있을 것으로 생각합니다. 감사합니다.
여러분들이 usb포트를 통해서 펜으로 그림을 그리거나, 무선 마우스를 이용해서 마우스를 조정할때 윈도우즈에서 usb드라이버를 설치해달라고 하지 않던가요? 물론 이미 드라이버가 설치되어있으면 상관없겠지만 처음 사용할 경우에는 usb드라이버를 설치해달라고 컴퓨터가 요청을 할겁니다. 윈도우즈는 그 장치가 무선 마우스인지, 키보드인지 구분할 수가 없으며 어떻게 조정을 해야하는지도 모르기 때문입니다. 그래서 컴퓨터가 이 장치를 동작시킬때 어떻게 동작되어야하는지에 대한 프로그램을 따로 설치해야합니다. 여러분들이 알게 모르게 설치했던 것이 바로 디바이스 드라이버입니다. 구경이나 한번 해볼까요? 시작 프로그램에서 장치관리자를 검색해서 들어가보세요.
이후 사운드쪽에서 디바이스 하나의 속성을 보도록 합시다.
드라이버 탭을 보면 정보와 업데이트, 사용안함 이런 버튼이 존재하는지 알 수 있습니다. 여러분들이 간혹가다가 멀쩡한 스피커에서 소리가 안들릴때 있지 않나요? 그때 한 방법은 컴퓨터를 reboot하는 방법이 있겠지만 아래의 디바이스 드라이버를 다시 구동시켜주면 됩니다. 어떻게 하냐구요? 간단합니다.
'디바이스 사용 안함' 을 눌러서 해제시키고, 다시 '디바이스 사용'을 눌러서 다시 장치를 구동시켜주면 어떨때는 해결되기도 합니다.
지금까지 간단하게 윈도우즈에서 디바이스를 살펴보았는데 자세히 그 내부를 들여다보면 아래의 도식과 같습니다. 사용자와 가장 접점에 있는 사용자 응용 프로그램 혹은 어플리케이션이 동작을 수행할때 내부적으로 System call을 사용합니다. open(), read(), write() 그런것들 있죠? 여러분들이 사용하는 printf()함수는 시스템 콜이 아니라 write를 잘 포장해서 만든 표준 입출력 라이브러리라고 합니다. 시스템 콜은 사용자 영역(kernel space)에서 호출이 가능합니다.
여기서 사용자 영역이라고 하는 것은 실제 앱을 이용하는 사용자가 아니고 여러분들이 코딩할때 실행 프로그램을 만드는 그 영역을 말하는 겁니다. gcc로 실행 파일을 만들고 실행하죠. 여기서 printf나 메모리 할당(malloc), 그리고 포인터를 이용해서 데이터를 바꾸는데 심한 제약이 있었나요? 정말 알수없는 프로그램을 짜지 않는 이상 그런 경우는 없지요. 간단히 말해서 '내가 마음대로 프로그램을 짜고 실행할 수 있는 공간이구나' 라고 생각하시면 됩니다.
시스템 콜이 호출이 되면 이제 커널 영역(kernel space) 내부로 호출이 전달됩니다. 커널 내부에 있는 가상 파일 시스템으로 전달, 그리고 아까 이야기했던 디바이스 드라이버가 인터페이스를 통해서 하드웨어를 제어합니다.
여기서 또 커널 영역이라함은 무엇일까요? 일반 사용자가 접근할 수 없는 커널만의 영역을 이야기합니다. 여러분들이 커널 영역 메모리를 참조하거나 데이터를 변경할 수 없습니다. 디바이스 드라이버가 커널 쪽에 위치해있으므로 커널 영역의 프로그램이라고 보시면 되는데, 커널의 메모리를 손대는 곳이기 때문에 항상 안전성에 주의해야합니다. 혹시나 여러분들이 블루 스크린을 경험해본적 있죠? 디바이스 드라이버나 커널에 의해 시스템이 사용할 수 없는 상태가 됐기 때문입니다.
리눅스 디바이스는 세가지가 존재합니다. 캐릭터 디바이스, 블록 디바이스, 네트워크 디바이스가 있지요.
모듈(Module)
커널의 일부분인 프로그램으로 커널에 추가 기능이 모듈로 관리가 됩니다. 여러분들이 다바이스 드라이버를 만들고 추가할때 커널의 모듈로 끼워넣으면 됩니다. 모듈은 커널의 일부분입니다. 디바이스 드라이버가 하드웨어를 동작해야하기 때문에 커널의 일부분으로 동작해야합니다. 그래서 커널 모듈로 동작되어지는데, 이때 착각하지 말아야할 점은 모듈은 커널의 일부, 그리고 그 모듈에서 디바이스 드라이버가 동작됩니다. 즉, 모듈은 디바이스 드라이버가 아닙니다. 디바이스 드라이버가 모듈로 커널의 일부분으로 추가되어 동작하는 것이지요.
모듈이라는 개념이 없을때 디바이스 드라이버를 만들었다면 커널이 바뀌었기 때문에 다시 커널 컴파일을 해야하는 과정이 있었죠. 커널 컴파일 과정은 오래 걸리는 작업이라서 시간을 많이 소비하는 작업이기도 합니다. 모듈의 개념이 도입된 이후 위의 과정없이 모듈을 설치하고 해제할 수 있습니다. 바로 이 모듈에서 장치를 등록하거나 해제할 수 있습니다. 예를 들어 문자형 장치를 등록할때는 아래의 함수를 이용하죠.
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
아직까지는 이 함수가 어떤 역할을 하는지 몰라도 됩니다. 우선 모듈이 어떻게 등록이 되는지부터 보도록 하겠습니다. 아주 간단한 모듈 하나를 만들어보도록 하겠습니다. 우선 여러분들이 C 프로그래밍을 하듯이 아래의 코드를 작성해줍니다.
커널 모듈 빌드 디렉토리를 이용해야 하기 때문에 그 경로를 지정해줘야합니다. 이 디렉토리는 /lib/modules/커널 릴리즈 버전/build인데요. 여기서 커널 릴리즈 버전은 uname -r을 보면 알 수 있습니다. KERNDIR이 바로 그것이죠. 저의 경우는 아래와 같네요. 여기의 Makefile을 이용해야하기 때문에 기재해줘야하는 겁니다.
또 우리가 만든 모듈은 현재 디렉토리에 있죠? pwd의 결과를 넣어주면 됩니다.
make를 하여 빌드합니다. 혹시 make 실행할 수 없다면 apt-get install make 해서 설치해주세요.
make하고 빌드를 하면 산출물 중에서 .ko 파일이 보이실건데요. Kernel Object의 약자를 확장자로 갖고 있는 이것이 우리들이 설치할 모듈입니다. 설치해볼까요?
insmod
모듈을 설치하는 명령어는 insmod입니다. install module이죠. 자 이것을 이용해서 설치해봅시다. 사용법은 insmod [모듈명]으로 insmod mymodule.ko명령을 쳐보세요.
무소식이 희소식인 리눅스 세상에서 아무런 응답이 없으니 잘 설치된것 같은데요. 우리가 코드에서 init_module에서 printk로 문자열을 출력해주었죠? 모듈이 등록되었으니 분명 메시지가 나올테니 확인해보세요. dmesg | tail -1로 가장 마지막 줄을 확인해보도록 합시다.
lsmod
우리가 설치한 모듈뿐만 아니라 설치된 다른 모듈도 보고싶다면 lsmod 명령을 사용하여 확인할 수 있습니다. 저의 머신에서는 아래와 같은 모듈들이 있습니다. 저의 모듈도 보이는군요.
rmmod
마지막으로 모듈을 제거할때는 rmmod명령을 치면 됩니다. 사용법은 rmmod [제거할 모듈명] 으로 끝 확장자 .ko는 기재하지 않아도 상관없습니다.
제거되었을까요? 우리가 코드에서 exit_module에서 전달한 함수가 있었죠? 그때도 우리는 printk로 메시지를 출력하게 만들었습니다. dmesg로 가장 마지막 메시지를 확인해보도록 합시다.
커널 영역에서의 프로그래밍은 어려운 작업이고, 그것에 따른 세세한 조작을 할 수 있게 만들어줍니다. 예를 들어 커널 모듈을 통해서 우리는 시스템 콜을 후킹할 수도 있습니다. 아까 말씀드렸다시피 모듈은 커널의 일부분이며 반드시 디바이스 드라이버로 동작하지 않을 수 있지요.
이상으로 가장 간단한 리눅스 모듈을 만들어보았습니다. 최대한 쉽게 쓰려고 했는데 이해가 가셨나요?
가양역 근처 오피스텔로 이사를 할 생각이신가요? 그렇다면 가양역 근처에 사는 저의 후기를 읽어보시고 생각해보세요. 우선 저는 가양역앞 근처에 오피스텔에 거주하고 있습니다. 왜 가양역 근처 오피스텔을 선택했을까요? 그 이유부터 말씀드리죠.
1. 바로 앞 홈플러스가 있습니다. 장볼일이 있으면 옷 주섬주섬 걸치고 슬리퍼끌고 홈플러스로 쇼핑하러가면 되니 여러모로 편리합니다. 여기 이사오기 전에는 대형마트가려면 적어도 10분은 걸어가야했습니다. 그래서 물건 가져올때 무거우면 애 많이 먹었죠. 물론 역에서 얼만큼 떨어져있는지에 따라 다르지만 저의 집에서 홈플러스까지 3분이면 도착합니다.
이처럼 대형마트가 가깝기 때문에 가볍게 장보고 올 수 있습니다.
2. 9호선 급행열차를 탈 수 있습니다. 가양역이 급행 열차가 서는 곳인데, 다른 급행역보다 좋은 점은 일반 열차가 급행 열차가 올때까지 기다리고, 급행열차가 떠난 후에 일반 열차가 출발합니다. 그래서 급행열차를 못탈 경우에는 일반 열차를 타면 됩니다. 하지만 도착역이 꽤 멀리 떨어져있는 경우는 급행 열차를 타야겠죠? 가양역에서 신논현역까지 급행으로 25분정도 걸리는 것 같습니다. 참고해주세요.
3.공항과 가깝습니다. 9호선을 타고 공항철도를 탈 수 있는데, 김포공항까지 가는데 지하철타고 15분 내외로 도착할 수 있습니다. 그래서 주변에 항공업계 종사하는 분들이 많이 거주하고 있습니다.
4. 다른 오피스텔보다 전세가가 저렴했습니다. 구하는 사람마다 다르겠지만, 저의 오피스텔 기준 2020년 8월 당시 전세가가 1억 4500만원이었습니다. 가양역 근처에서도 제 오피스텔 전세가 싼편에 속했습니다. 발산역이나 마곡나루보다 싼편이었죠. 월세로는 60만원 정도였는데, 요즘 전제 구하기가 힘들어서 지금은 잘 모르겠네요. 잘 찾아보시면 있을 수 있습니다.
5. 바로 앞에 술집이 많습니다. 가양역에서 좀 밑으로 내려오면 강서구청 사거리가 있는데, 이곳에 술집, 고깃집이 있습니다. 주변 지인들이 오게되면 주로 이곳에서 한잔하곤 합니다. 물론 주변 지인들이 얼마 없어서 잘 나가지는 않습니다.
그 중 제가 사는 오피스텔은 원룸에 복층이 있습니다. 한 두 달 생활한것으로 후기를 적기보다 생활 기간을 오래두어서 후기를 작성하는게 보다 도움이 될 것 같아 그곳에서 6개월 생활해온 후기를 남겨보도록 하겠습니다. 장점을 먼저 이야기해드린 후 제가 느낀 단점을 적어보도록 하겠습니다.
장점
1. 수납 공간이 넉넉하다.
계단이 있어서 계단 밑 공간이 전부 수납공간입니다. 그래서 별도로 수납장을 구할 필요가 없습니다. 워낙 많아서 전부 채우지도 못했습니다. 아마 복층 오피스텔은 대부분 빨래 건조대도 내장되어 있습니다. 계단 밑에 네모난 칸들이 보이시죠? 저것들이 전부 수납 공간입니다. 그리고 가장 오른쪽에는 빌트인 냉장고가 있습니다. 오피스텔들이 전부 이렇게 되어있다고 말할 수는 없지만, 대부분 이럴거에요.
아래의 사진과 같이 별도로 빨래 건조대를 구할 필요가 없습니다. 아래처럼 이것도 내장되어 있거든요. 그렇기 때문에 공간 효율성이 높아지는 효과가 있습니다.
이뿐만 아니라 옷장도 엄청 넉넉합니다. 아랫층에 옷장 4개, 복층에 5개가 있습니다. 옷도 없는데, 옷장만 많습니다. 옷이 많으신 분들한테는 굉장한 장점이 될 수 있을 것 같습니다.
2. 복층을 이용해 생활 공간이 분리된다.
위쪽은 제가 자는 공간입니다. 그래서 아래층은 저만의 다른 공간으로 사용할 수 있습니다. 아래층까지 보여드리고 싶지만 지금 글을 쓰는 이 시점에는 돼지 우리라서 보여드릴 순 없고, 아래층에 쇼파와 TV, 테이블, 그리고 책상에 있습니다. 현관 바로 앞에는 화장실이 있습니다. 저의 오피스텔의 경우 전용면적이 7평정도 되고 복층이 2평정도 됩니다. 자는 공간이 분리되어 있다보니, 아랫층 7평은 넉넉하게 이용할 수 있습니다.
그래서 지인들이 집에 놀러오면 아랫층에 테이블에 앉아 옹기종기 모여 광란의 파티를 열곤 합니다. 물론 나중에 치우는 것은 저의 몫이죠.
3. 천장이 높아 탁트인 느낌이다.
복층이기 때문에 천장이 매우 높습니다. 3.5m정도는 될거 같아요. 그래서 탁트여서 시원스럽습니다. 천장에는 시스템 에어컨이 있습니다. 손을 뻗어도 아무것도 닿지 않습니다.
4. 창문이 크다.
위의 사진처럼 창문이 커서 바람이 잘 들어옵니다. 통풍이 잘되고 쾌적한 느낌이 장점이 되겠죠? 환기도 잘됩니다.
단점
1. 천장이 높아 모기잡기 힘들다.
천장이 높아서 벌레를 잡을 수가 없어요. 모기는 양반입니다. 실제로 제 집에 말벌이 들어온적이 있습니다. 생명의 위협을 느껴서 말벌이 알아서 나갈 수 있도록 창문을 열어두고 집 밖으로 피신했습니다. 2시간이 지났을 무렵, 말벌이 세마리가 되었습니다. 편의점에서 살충제사서 죽일 수 밖에 없었습니다. 여름철 에프킬러는 필수가 되겠습니다.
2. 창문이 커서 커튼 달기가 함들다.
창문이 커서 커튼달기가 힘들었습니다. 키가 작으신 분들은 커튼달려면 의자 올라타서 달아야할 수 있습니다. 커튼달때는 항상 창문 닫고 하세요. 창문이 커서 혹시나 그러면 안되겠지만 밑으로 떨어질 수도 있어요. 아니면 주변에 키큰 분들한테 도움을 요청해서 다셔야할건데, 저는 친구가 없어서 저 혼자 달았습니다. 외롭고 힘들었습니다.
3. 술먹고 2층올라갈때 늘 조심해야한다.
애주가(알콜 중독)라서 술먹고 윗층에 올라갈때는 항상 조심해야합니다. 보통 복층생각하시는 분들이 이런 걱정을 많이 하시더라구요. 다행스럽게 저는 술먹고 올라가다가 떨어져본적은 없습니다. 오히려 술먹고 올라갈때 더 조심조심 올라가는 것 같아요. 제가 조심성 하나는 기가막힙니다. 문제는 올라갈때가 아니라 내려올때가 문제입니다.
4. 계단이 높아 추락해 다칠 수 있다.
보기보다 계단의 높이가 높습니다. 눈대중으로 제어봤을때는 각 계단이 30cm 가까이 되는 것 같습니다. 그래서 항상 내려올때 손잡이 잡고 조심스럽게 내려와야합니다. 그리고 바닥이 보기보다 미끄럽기 때문에 한칸 한칸 조심스럽게 내려와야합니다. 실제로 내려가다가 발을 헛디뎌 발목이 꺾이며 바로 앞 옷장 벽에 머리를 박은적이 있습니다. 머리는 원래 돌이라서 괜찮았는데, 발목이 좀 많이 꺾여 거동이 불편해 연차냈습니다.
5. 사랑을 나눌때 구사할 수 있는 자세가 제한적이다.
여러분들은 여자친구, 남자친구 있으시죠? 복층에 주로 잠을 잘 수 있는 공간으로 두셔서 침대가 윗층에 있는데요. 복층은 높이가 130cm정도 되는 것 같습니다. 이때 사랑을 나눌때 일어설수가 없습니다. 무릎을 구부린 자세가 최대 허용자세입니다. 뭐, 자세를 자유자제로 구사할 수 있는 분이라면 상관없을 수 있겠습니다. 참고로 저는 해당사항 없습니다.
6. 윗층의 물소리가 적나라하게 들린다.
복층에서 잘때 윗층에서 물떨어지는 소리가 곧장 고막에 울려퍼집니다. 왜냐면 윗층 화장실 바로 밑이 자고 있는 공간이기 때문인데요. 특히 늦은 밤에 씻을때 물떨어지는 소리, 설겆이할때 물떨어지는 소리, 그리고 아침일찍 씻을때 물떨어지는 소리, 설겆이할때 물떨어지는 소리가 들립니다. 윗층 분은 저보다 부지런한 분인지 일찍일어나셔서 씻으시는지 물떨어지는 소리가 들립니다. 그래서 아침에 알람 맞출 필요없이 물떨어지는 소리들으면 저도 일어나서 씻으러 갑니다.
7. 창문이 크기 때문에 커튼을 필수.
창문이 커서 밖에서 다 보입니다. 저의 경우 아파트와 정면에 있어 커튼은 필수입니다. 안그러면 사생활이 밖에 전부 공개될 수 있습니다. 그러니까 큰 커튼으로 전부 가려줘야합니다. 그래야 안부끄럽습니다.
8. 화장실이 멀다
복층에서 자고 있다가 신호가 올때가 있습니다. 그럴때 화장실을 가야하는데, 동선이 조금 깁니다. 과정은 이렇습니다. 일어난다. → 계단을 조심조심 내려간다. → 화장실 들어가서 싼다. → 다시 계단을 조심조심 올라온다. → 누워서 다시 잠을 청한다. → 잠이 다 깼다. 화장실 가느라 잠이 다 깰 수 있습니다. 이런 복잡한 과정이 싫으시다면 요강 하나 준비하셔서 해결하시길 추천드립니다.
9. 보일러가 닿지 않는다.
만약 복층이 침실이라면 겨울에는 조금 추울 수 있습니다. 보일러가 아랫층바닥만 데워주기때문에 복층바닥은 차가울 수 있습니다. 저같은 경우에는 추위를 많이 타지 않아 괜찮았지만 추위에 취약하신 분들은 복층을 자는 공간이 아닌 창고 아니면 다른 공간으로 활용해주셔야할 수 있습니다.
이상으로 제가 반년동안 복층 오피스텔에서 생활한 후기를 남겨보았습니다. 적다보니 장점보다 단점이 더 많네요. 그렇긴 해도 장점이 단점을 커버할 수 있으니까 저는 만족하면서 지내고 있습니다. 느끼는 점은 사람마다 다르니까 그저 참고만 해주시고, 결정은 알아서 최선의 선택을 해보시기 바랍니다.