Chapter 1. 새로운 조원과 새로운 활동!
1주차 프로젝트를 성공적으로 마무리하고 2주차로 넘어왔다.
2주차는 각자의 주특기 세션을 정하고 거기에 맞는 프로그래밍 언어를 알고리즘 풀이와 개별 과제를 통해 익히는 것이다.
나는 Spring을 선택했고 자바를 익히게 됐다.
그래서 이번 시간에는 자바와 개별 과제를 통해 배운 것을 정리하고자 한다.
Chapter 2. 자바란 무엇인가???
사실 자바를 처음 접한 것은 대학교 2학년 때다. 나는 전공자 출신이고 프로그래밍에 대해 어느 정도 기초는 있는 상태다. 그래서 자바에 대해 부담감이 적었고 알고리즘 풀이도 어렵지 않았다. 그래서 나는 이번 기회에 자바 기초에 대해 공부했다.
JVM
자바를 얘기하려면 크게 두 가지에 대해 얘기해야한다. OOP와 JVM이다. 이 두가지가 자바 철학의 핵심이다. 그 중 JVM에 대해 먼저 얘기해보려고 한다.
JVM은 Java virtual machine의 줄임말로 Java code를 컴파일 시켜주는 환경이다. 이 JVM이 처음 나왔을 때는 정말 큰 혁명이었다. 왜냐하면 그 전까지 프로그래밍 언어는 코드를 작성한 OS 환경에 종속되어 있기 때문에다.
예를 들면 Window에서 작성한 코드는 Window 환경에서만 돌아가고 Linux 환경에선 돌아가지 않는다. 반대로 Linux에서 작성한 코드는 Window에서 돌아가지 않는다.
그러나 JVM이 등장하면서 상황은 바뀌었다. JVM은 JAVA 코드를 각 OS가 이해할 수 있는 언어로 바꿔준다. 즉, OS와 프로그래밍 언어 사이에 위치하면서 "OS와의 의존성을 떨어뜨려준다". 다만 여기서 조심해야하는 점은 JVM은 각 OS에 맞는 JVM을 설치해줘야 한다. 즉 JVM은 OS 종속적이며, JAVA는 OS에 독립적이다.
프로그래밍 코드가 OS에 종속적이라는 것은 큰 문제였고 자바의 JVM은 이 문제를 해결했으니 모든 프로그래밍 언어를 대체할 거라 생각할 수 있다. 그러나 JVM에는 단점이 있는데 그것은 바로 속도다.
프로그래밍 언어를 실행하는 방식은 총 2가지가 있다.
하나는 컴파일 방식이고 하나는 인터프리터 방식이다.
(1) 컴파일 방식 : 소스코드를 한꺼번에 컴퓨터가 읽을 수 있는 native machine (기계)어로 변환
(2) 인터프리터 방식 : 소스코드를 빌드시에 암것도 하지 않다가, 런타임시에 한줄 한줄 읽어가며 변환
두 방식의 결정적인 차이는 코드를 실행할 때, 어느 부분의 비중이 더 큰 가이다.
컴파일 방식은 코드 실행하기 전에 기계어로 변환해야하기 때문에 코드 실행 전에 시간이 오래 걸린다. 하지만 실행하고 나서의 속도는 매우 빠르다!
반면에 인터프리터 방식은 실행 전에 별도의 변환 과정이 없기 때문에 코드가 실행되는 속도가 빠르다! 하지만 실행되고 나서 로직을 수행하는데 속도는 매우 느리다!
오 ~ 그렇다면 코드 실행 전에는 인터프리터 방식, 실행 후에는 컴파일 방식을 사용하면 더 빠르지 않을까????
하지만 자바는 반대다. 코드 실행 전에 컴파일 방식, 코드 실행할 때는 인터프리터 방식을 사용한다.
코드 실행전에는 JAVA COMPILOR로 자바 바이트 코트(.class)를 만든다. 그리고 코드를 실행하면 JAVA INTERPRETOR에서 자바 바이트 코드를 한 줄씩 읽어드려서 실행한다.
다만 JAVA INTERPRETOR는 이전에 읽은 코드를 저장하기 때문에 느리지만 그렇게 크게 느리지 않는다.
JAVA는 타 프로그래밍 언어보다 느리다는 단점이 있기 때문에 속도가 중요한 곳에서는 잘 사용되지 않는다. 하지만 OS에 독립적이라는 것과 차후 설명하겠지만 JAVA는 OOP를 만족시키는 강력한 툴, 그리고 JVM가 제공하는 자동 메모리 관리 등으로 성능보단 안정성을 중시하는 곳에서 많이 사용된다.
JVM 구조
JVM은 크게 클래스 로더, 메모리, 실행 엔진, 기타로 나뉜다.
클래스 로더는 java 코드를 읽어와 클래스가 존재하면 이를 메모리에 적재한다.
메모리는 클래스 로더가 읽은 클래스를 실행 엔진이 실행할 수 있도록 저장하는 곳이다. 스택, PC, 메소드 등이 있지만 여기서 우리는 스택과 힙을 살펴보겠다.
(위 구조는 파이썬을 기준으로 만들어서 조금 다를 수 있다.)
스택은 메소드 단위로 구분되며 내부에는 변수명과 원시값이 저장된다. 힙은 클래스를 통해 만들어진 객체가 저장된다.
예를 들어 User 클래스를 통해 user라는 이름의 User 객체를 만들면 stack에는 user라는 객체 이름과 User 객체의 힙 메모리 주소가 저장된다. 그리고 힙 메모리에는 User 객체가 저장된다.
따라서 user라는 객체에 접근하면 Stack에서 user라는 이름의 변수를 찾고, 그 변수에 저장된 힙메모리 주소를 통해 힙메모리에서 User 객체를 가져오는 방식이다.
마지막으로 실행 엔진에서는 메모리에 적재된 데이터를 가져와서 실행한다. 이때 GC, garbage collection이라는 개념이 나온다.
Garbage Collection
우선 나는 가비지 컬렉션에 대해 잘 알지 못한다. 따라서 간단하게 개념과 왜 중요한 지 대해서만 설명하고자 한다.
가비지 컬렉션은 이름 그대로 쓰레기, 즉 사용하지 않지만 메모리를 차지하고 있는 잉여 자원을 수집한다. C나 C++에서는 사용한 메모리는 프로그래머가 직접 반환을 해야 했지만 Java에서는 JVM이 대신 정리해준다.
오 그러면 훨씬 더 편한 거 아냐? 할 수 있다. 이는 사실이며 매우 편하다. 하지만 이때 중요한 것은 어머니가 청소할 때, 우리보고 다 가만히 있어!!! 하는 것처럼 GC가 동작할 때는 모든 스레드가 정지한다.
이는 매우 중요한데 만약 GC가 자주 청소를 하면 스레드가 자주 정지하고 프로그램 성능이 크게 떨어질 수 있다. 그러면 GC를 자주 실행 안하면 되지! 라고 하는데 이 GC는 JVM이 관리하기 때문에 개발자가 이 GC를 직접 제어하지 못한다.
따라서 개발자는 적절한 모니터링 시스템을 구축한 후, JVM 튜닝을 통해 이 GC의 주기나 타이밍 값을 조절해서 성능을 향상시킬 수 있다.
Chapter 3. OOP란 무엇인가?? (부제 : 개인 과제 - 아래 요구사항을 충족하는 클래스를 구현하라!!)
2주차에는 주특기와 관련된 프로그래밍 언어를 익히기 위해 알고리즘 풀이와 함께 각 언어에 알맞는 개별 과제도 주어졌다.
개인 과제는 "주어진 요구사항을 만족하는 클래스를 구현하고 OOP를 통해 차후 수정 및 확장에 용이하게 만들어라" 였다.
이 OOP는 Java에서 JVM만큼 중요한 내용이기 때문에 이번 기회에 정리해보려고 한다.
객체지향 프로그래밍
OOP는 객체 지향 프로그래밍이란 뜻으로 현실 세계의 존재하는 개념을 객체로 표현하여 문제를 해결하는 것이다.
여기서 중요한 개념이 바로 ‘객체'다
객체란
객체의 정의는 조금 추상적인데 쉽게 말해서 ‘사람이 식별 가능한 것’이다. 다르게 표현하면 컴퓨터 세계에 존재하는 사물이라고 할 수 있다. 그리고 우리는 이 객체를 추상화라는 과정을 통해서 컴퓨터로 구현할 수 있다.
속성과 기능
이러한 객체는 속성과 기능으로 구성되어 있다. 속성은 그 객체가 가지고 있는 고유한 정보로 쉽게 얘기하면 데이터다. 기능은 그 객체가 할 수 있는 행동을 나타낸다. 예를 들어 자동차를 객체로 표현하면 속성에는 자동차의 속력(speed)가 있고 기능에는 출발(start)과 정지(stop)이 있다.
추상화
프로그래밍 내에서 객체는 현실에 존재하는 것을 컴퓨터 내에서 구현한 것이다. 그러나 모든 것을 구현할 수 없다. 만약 자동차의 모든 것을 구현한다고 하면 코드는 몇 천, 몇 만 줄이 될 것이다. 그래서 문제 해결에 ‘필요한 부분만 추출해서 구현’하는데 이것을 추상화라고 한다.
클래스
클래스는 자바스크립트의 prototype과 함께 대표적인 OOP 객체 생성 방법이다. 개발자는 추상화를 통해 객체에 필요한 것을 정의하고 클래스를 통해 구현한다. 즉 클래스는 객체의 도면이다. 클래스에서는 객체의 속성을 클래스 변수, 객체의 기능을 클래스 메소드로 정의한다.
클래스 == 객체?
파이썬과 같은 몇몇 프로그래밍 언어에서는 클래스도 객체이지만 기본적으로 클래스와 객체는 다르다. 클래스는 객체의 설계 도면이고 객체는 그 클래스에서 설계된 대로 만들어진 결과물입니다. 따라서 하나의 클래스에서 여려 개의 객체가 만들어 질 수 있다.
객체, 클래스, 추상화 예시
만약 우리가 자동차라는 객체를 만들어야 하면 우리는 자동차의 어떤 데이터와 기능을 가져와야 할 지 정해야 한다.
만약 우리가 추상화를 통해서 자동차의 현재 속도 속성과, 출발 정지 기능을 구현한다고 정의했다고 하면 우리는 클래스를 통해 객체의 도면을 구현해야 한다.
class Car{
int speed;
public void start(){
// 구현
}
public void stop(){
// 구현
}
}
이렇게 클래스가 만들어 지면, 우리는 new Car 를 통해 객체를 생성해서 메모리에 할당할 수 있다.
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
메시지 전송
객체지향 프로그래밍에서는 객체 간의 상호 작용으로 문제를 해결한다. 이 객체 간의 상호 작용 방식이 메시지 전송 방식이다. 메시지는 해당 객체의 기능을 호출하는 방식으로 이뤄지는데 Input과 Output으로 상호 작용이 이뤄진다. Input은 기능의 parameter로 값을 넘겨주는 방식이고 Output은 기능의 return 값으로 받아오는 방식이다. 이렇게 Input과 Output이 있기 때문에 함수라고도 불린다.
메시지 전송 예시
예를 들어 편의점이라는 객체가 있고, 그 객체에는 빵값이라는 속성과 buy라는 기능이 있다고 하자.
내가 편의점에서 빵을 사는 과정을 정리하면 다음과 같다.
-> 1. 편의점 점주에게 돈을 지불한다
-> 2. 편의점 점주는 빵 값을 뺀 거스름 돈을 돌려준다.
이를 메시지 전송 과정으로 보면
-> 1. 나는 편의점 객체의 buy 메소드를 호출하고 parameter로 돈을 입력한다.
-> 2. 편의점 객체는 parameter로 받아온 돈을 내부의 빵 값이라는 속성 값으로 뺀 나머지 값을 return 한다.
그리고 코드로 표현하면 다음과 같다
// 편의점 객체
class Market(){
int breadPrice = 500;
public int buy(int price){
return price - breadPrice;
}
}
// main 객체
class Main(){
public static void main(string args[]){
Market market = new Market();
int change = buy(1000); // 거스름돈으로 500원을 받을 것이다.
assert charge == 500;
}
}
상속
상속은 다른 클래스로부터 구현된 코드를 물려받는다는 뜻이다.
예를 들어 아빠라는 객체가 있고 say라는 함수를 구현했다고 하자, 그 후 아들 객체를 만들어서 아빠를 상속받으면 아들 객체는 say라는 함수를 구현하지 않아도 say 함수를 사용할 수 있다.
class Father(){
public void say(){
System.out.println("안녕하세요);
}
}
class Son() extends Father{
// say 함수를 구현하지 않아도 된다.
}
// main 객체
class Main(){
public static void main(string args[]){
Father father = new Father();
Son son = new Son();
father.say(); // "안녕하세요"
son.say(); // "안녕하세요"
}
}
다형성
이 상속을 활용할 때, 꼭 알아야 하는 개념이 있다. 바로 다형성이다.
다형성의 정의부터 얘기하면, "같은 뿌리에서 나와 같은 행동을 하지만 그 결과는 다르다" 이다.
예를 들어 보면 다음과 같다.
사람은 모두 인사를 할 수 있고 한국 사람과 미국 사람의 뿌리는 사람이다. 그리고 한국 사람도 인사를 하고 미국 사람도 인사를 한다. 하지만 한국 사람은 "안녕하세요"라고 인사하고 미국 사람은 "hello"라고 인사한다.
프로그래밍 관점에서 살펴보면 한국인과 미국인 둘 다 Person이라는 class를 상속받는다. 그래서 둘 다 인사를 할 수 있다. 하지만 둘 다 인사를 호출하면 한국인은 안녕하세요, 미국인은 Hello을 출력해야 한다.
이를 코드로 구현하면 다음과 같다.
(다형성을 구현하는 방법에는 overriding과 overloading이 있지만 이는 다음에 설명하겠다.)
class Person(){
public void greet(){
System.out.println("이건 인사입니다.");
}
}
class Korean extends Person(){
@override
public void greet(){
System.out.println("안녕하세요");
}
}
class American extends Person(){
@override
public void greet(){
System.out.println("Hello!");
}
}
// main 객체
class Main(){
public static void main(string args[]){
Korean korean = new Korean();
American american = new American();
korean.greet(); // "안녕하세요"
american.greet(); // "Hello!"
}
}
OOP 4대 요소
위에서 OOP의 전부는 아니지만 가장 중요한 요소들을 살펴봤고, 그 중에서 OOP 4대 요소라는 것을 정리하는 것으로 끝내고자 한다.
OOP 4대 요소는 객체, 클래스, 메시지 전송, 상속이며 이를 표로 정리하면 아래와 같다.
요소 | 정리 |
---|---|
객체 | 컴퓨터 세계에 존재하는 사물이며 속성(데이터) + 기능(행동)으로 구성되어 있다. |
클래스 | 객체 생성 틀이며 클래스 변수 + 클래스 메소드 + 생성자(소멸자) 로 구성되어 있다. |
메시지 전송 | 객체 간의 상호작용 방식이며, 메소드 호출로 동작하며 parameter와 return으로 데이터를 주고 받는다. |
상속 | 클래스 재사용 장치이며 기존 클래스 내용을 새로운 클래스에서 가져올 수 있다. |
Chapter 4. 2주차 끝! 하지만 끝나지 않은 항해!!
이번 주차는 Java 공부와 알고리즘 공부를 하였다. 팀원들과는 많이 친해졌지만 뭔가 함께 만든 것이 아니라서 조금 아쉬웠다.
하지만 항해는 아직 끝나지 않았고 다음 주차도 남아있다.
다음 주차에서는 본격적으로 주특기를 배우고 그것을 활용해서 무언가를 만들어본다. 너무 너무 기대가 되며 다음 주에는 무슨 일이 생길 지, 또 누구와 함께 팀이 될 지 너무 너무 기대가 된다.