Java의 컴파일 과정
자바 프로그래머가 `.java`의 확장자를 갖는 소스 파일을 생성한 뒤 `javac` 컴파일러를 이용해
`.class`의 확장자를 갖는 바이트 코드를 만들어내는 것을 컴파일 과정이라고 한다.
Java 프로그램은 바로 실행이 가능할까?
Java 프로그램은 완전한 기계어가 아닌 중간 단계의 바이트 코드를 사용한다.
그렇기에 이것을 해석하고 실행할 수 있는 가상의 운영체제가 필요하다.
해당 가상 운영체제는 JVM으로, JVM 덕분에 우리는 다른 운영체제에서도 JVM만 설치되어있으면,
동일한 결과를 내는 Java 프로그램을 수행할 수 있게 된다.
즉, 운영체제마다 Java 프로그램을 개발하는 것보다 운영체제와 자바 프로그램을 중계하는 JVM을 두어,
어떤 운영체제에서도 동일한 실행 결과가 나오도록 설계가 되어있다.
하지만, 기계어는 운영체제마다 다르기 때문에 JVM은 운영체제에 종속적이어서 운영체제에 맞는 JVM을 설치해야만 한다.
JRE - Java Runtime Enviroment(JVM + 표준 클래스 라이브러리)
JDK - Java Development Kit(JRE + 개발에 필요한 도구(컴파일러))
Java 프로그램 동작 원리
1. 자바 프로그래머가 `.java` 형식의 소스 파일을 생성한다.
2. 생성한 소스 파일을 `javac`라는 컴파일러로 컴파일하여 바이트코드(.class)를 생성한다.
3. JVM 실행 단계에서 `.class` 파일을 클래스 로더에 전달한다.
4. 클래스 로더는 해당 바이트 코드 파일을 읽어 JVM 실행 단계에서 JVM 메모리에 적재 및 초기화를 수행한다.
5. 동적 클래스 로딩을 통해 클래스 로더가 동적으로 .class 파일을 읽고 클래스를 로딩 및 링크해 런타임 데이터 영역인
JVM의 메모리 영역에 올린다.
6. JVM의 핵심 컴포넌트인 실행 엔진이 메모리에 올라와있는 바이트 코드를 읽어 Interperter 방식 혹은 JIT 컴파일러를
사용해 기계어로 번역하여 실행한다.
Java의 실행엔진
JVM의 핵심 컴포넌트로 바이트코드를 인터프리터 방식 혹은 JIT 컴파일러 방식으로 읽은 뒤 기계어로 번역하여 실행한다.
=> 이를 통해 Java 프로그램이 플랫폼 종속적으로 실행된다.
Compiler vs Interpreter
Compiler는 전체 소스코드를 한번에 컴파일한 뒤 실행 파일을 생성한다.
즉, 나중에 실행만 하면 된다.
장점은 속도가 빠르다는 것이고, 단점은 컴파일 과정에서 시간이 조금 걸린다는 것이다.
ex) C, C++, Java
Interpreter은 프로그램을 실행하는 동안에만 소스코드를 한 줄씩 기계어로 변환하고 바로 실행한다.
장점은 개발과 디버깅에 용이하다는 것이고, 단점은 실행속도가 컴파일러 방식에 비해 느리다.
ex) Python, Ruby, JavaScript
오버라이딩과 오버로드
오버라이드는 Override(재정의, 무효화)를 의미한다.
따라서, 상속 후 부모 클래스의 메소드를 오버라이드 할 수 있는데
부모 메서드와 반환값, 메소드 이름, 매개변수가 전부 같아야 한다.
(다르다면, 컴파일 시 오류)
오버로드는 하나의 메소드 이름으로 여러 매개변수를 받아 처리할 수 있는 것을 말한다.
대표적으로 System.out.println()이 있다.
반환 타입과 이름이 같아야 하며, 매개변수값은 달라도 된다.
Java에서의 생성자
Java는 기본적으로 생성자가 없다면 컴파일러가 기본 생성자를 만들어준다.
하지만, 생성자가 1개라도 있으면 기본생성자가 생기지 않으므로, 상속 시 유의해야 한다.
상속받은 클래스는 기본적으로 부모 클래스의 생성자를 호출하고 생성한다.
그렇기에 부모 객체의 생성자가 없다면 컴파일 시 오류가 발생한다.
또한, 상속을 해도 부모 생성자는 상속받지 않음에 유의해야 한다.
Java의 컬렉션 프레임워크
Java의 컬렉션 프레임워크는 데이터를 효율적으로 처리하고 저장하기 위한 클래스와 인터페이스의 집합이다.
다양한 자료구조를 지원하여 개발자는 데이터의 효율적인 관리와 조작이 가능하다.
컬렉션 인터페이스
- 컬렉션 프레임워크들의 루트 인터페이스로 컬렉션 프레임워크 내의 모든 컬렉션 클래스는 해당 인터페이스를 구현해야한다.
List(리스트)
- 순서가 있는 데이터의 집합으로 요소의 중복이 허용된다. 그리고 인덱스를 통한 접근이 가능하다.
- 구현체
- ArrayList(동적으로 크기가 조정되는 배열 기반의 리스트이다. 빠른 탐색이 가능하고, 중간 삽입/삭제가 느리다.)
- LinkedList(연결 리스트를 기반으로 각 요소가 노드로 연결되어 있다. 중간 삽입/삭제가 빠르지만, 탐색이 느리다.)
- Vector(Thread-Safe하지만, 사용이 권장되지 않는다.)
Set(셋)
- 순서가 없는 데이터의 집합으로, 중복을 허용하지 않는다.
- 구현체
- HashSet(중복 허용 X, 내부 해시 테이블 사용을 빠른 검색 O(1)
- LinkedHashSet(중복 허용 X, 순서를 보장)
- TreeSet(중복 허용 X, 값이 오름차 순으로 정렬되어있다. 내부적으로 Red-Black Tree O(log n) 사용
Map(맵)
- 키와 값의 쌍으로 데이터를 저장하는 자료구조이다.
- 키의 중복을 허용하지 않으며, 키에 관해 하나의 값을 갖는다.
- 구현체(Set과 거의 동일)
- HashMap, LinkedHashMap, TreeMap, HashTable
- HashTable(스레드 안전, 하지만 성능이 좋지 않음)
컬렉션 프레임워크의 주요 특징
- 다양한 자료구조를 지원한다.(List, Map, Set, PriorityQueue, Queue, Deque 등)
- 제네릭 지원(타입 안정성 보장, 타입에 의존하지 않는 컬렉션 사용)
- 스레드 안전성
- 컬렉션 프레임워크가 기본적으로 제공하는 클래스들은 스레드 안전하지는 않지만,
Collections.synchronizedList(), ConcurrentHashMap 등을 제공한다.
- ConcurrentHashMap은 HashMap의 스레드 안전 버전
동등과 동일
동등
"같은 값이나 같은 기능을 가진"것을 의미한다.
즉, 두 개체가 서로 같은 성질이나 특성을 가진 상태를 말한다.
=> 하지만, 형태나 구현은 다를 수 있다.
동등성은 주로 값이나 속성에 관한 비교를 말한다.
- equals 메소드
동일
"서로 완전히 같은 것"을 의미한다.
즉, 두 개체가 완전히 동일한 것을 의미하며 참조하고 있는 래퍼런스도 같은 경우를 말한다.
동일성은 주로 객체의 참조에 관한 비교이다.
- ==
String, StringBuffer, StringBuilder
String
불변 객체로, 한 번 생성된 객체의 값을 변경할 수 없다.
그래서 문자열 연산 작업이 많은 경우에 계속해서 새로운 객체가 생성되기 때문에 작업 효율이 떨어질 수 있다.
동시에 새로운 문자열이 계속 생성되므로 메모리를 계속하여 차지하게 된다.
하지만, 불변 객체인만큼 Thread-Safe하며 String 객체를 자유롭게 공유할 수 있어 성능상 이점이 있을 수 있다.
StringBuffer
가변 객체로, 한 번 생성된 문자열 객체의 값을 변경하는 것이 가능하다.
또한, 멀티 스레드 환경에서 동기화를 지원하여 Thread-Safe 하다는 특징을 갖고 있다.
하지만, 동기화 기능에 의한 오버헤드가 있기에 단일 스레드 환경에서는 성능 효율이 조금 떨어질 수는 있다.
StringBuilder
가변 객체로, 한 번 생성된 문자열 객체의 값을 변경하는 것이 가능하다.
멀티 스레드 환경에서 동기화를 지원하지 않기 때문에 Thread-Safe하지 않다.
하지만, 동기화를 지원하지 않으므로, StringBuffer에 비해 단일 스레드 환경에서는 성능 효율이 조금 더 좋다.
Thread-Safe
Thread-Safe 하다는 말은 공유되는 데이터나 코드에 관해 동시에 여러 스레드가 접근하여도 경쟁 조건이 발생하지 않아
실행 결과가 올바르게 유지되는 것을 의미한다.
Thread-Safe를 달성하기 위해서는 여러 스레드가 공유 데이터에 동시에 접근하는 것을 제한하고 한번에 하나의 스레드만
해당 공유 자원을 사용할 수 있도록 하면 된다.
- ex) 뮤텍스/세마포어(운영체제 수준), Lock-Free, 원자적 연산 등
오버헤드
작업을 수행하는 데 필요한 추가적인 시간이나 연산을 의미한다.
위의 Thread-Safe를 예시로 한 스레드가 작업을 처리함에 있어 다른 스레드는 대기 상태에 놓이게 되며,
멀티 스레드 환경에서 동기화 기능을 지원해줌에 따른 동시성 이슈 해결에 대해
추가적인 시간이나 연산이 존재하므로, 이걸 오버헤드라고 할 수 있다.
컨텍스트 스위칭
각 프로세스나 각 스레드에서 CPU의 할당 작업이 번갈아 일어남에 따라 본인의 현재 상태를 PCB 등에 저장해놓고
다른 프로세스나 다른 스레드로 변경되는 문맥 교환 작업을 컨텍스트 스위칭이라고 할 수 있다.
위의 Thread-Safe 상황에서 공유되는 데이터나 코드에 대해 한 번에 하나의 스레드만 접근할 수 있으므로,
새로운 스레드가 선택될 때마다 컨텍스트 스위칭이 일어나게 된다.
Java의 접근제어자의 종류와 특징
Java의 접근제어자로는 public, default, protected, private이 있다.
public로 선언된 메서드, 클래스, 멤버 변수는 같은/다른 패키지의 모든 클래스에서 접근이 가능하다.
protected로 선언된 메서드, 멤버 변수는 같은 패키지 및 다른 패키지의 자식 클래스에서 접근이 가능하다.
default의 접근제어자로 선언된 경우 클래스, 멤버 변수, 메소드는 같은 패키지의 클래스에서만 접근이 가능하다.
private로 선언된 메서드, 멤버 변수는 해당 클래스 내부에서만 접근이 가능하다.
클래스, 객체, 인스턴스
클래스
객체지향 프로그래밍에서 클래스는 특정 개체의 속성과 행위를 정의하는 설계도라고 할 수 있다.
클래스는 멤버 변수, 생성자, 메소드들로 구성되며 멤버 변수는 객체들이 가질 수 있는 상태를 의미하고,
메소드는 해당 객체들이 수행할 수 있는 행위를 의미한다.
객체
객체는 클래스의 인스턴스라고 할 수 있다. 클래스라는 설계도에 따라 생성되어 JVM 메모리에 적재되어 실체로 존재한다.
해당 객체는 멤버변수라는 상태를 가지며, 메소드를 통해 여러 행위를 할 수 있따.
인스턴스
인스턴스는 클래스의 객체라고 할 수 있다. new 키워드로 계속해서 새로운 인스턴스를 만들 수 있으며 그렇게 만들어진 객체는
객체마다 특정 상태를 갖고 정의된 행위의 수행이 가능하다.
즉, 클래스는 객체의 설계도를 의미하며 객체는 클래스에 정의된 속성과 행위를 가진 실체를 의미한다.
그리고 인스턴스는 클래스로부터 생성된 객체를 의미한다고 생각하면 된다.
Call by Reference vs Call by Value
Call by Value
Call by Value는 함수에 인자를 전달할 때, 복사하여 전달하는 것을 의미한다.
값을 복사하여 전달하기 때문에 주소값이 달라 함수 내에서 인자의 값을 변경해도 원본 데이터에 영향을 미치지 않는다.
ex) C, C++, Java, Python
Call by Reference
Call by Reference는 함수에 인자를 전달할 때, 인자의 메모리 주소를 전달한다.
그렇기 때문에 함수 내에서 인자의 값을 변경하게 되면 원본 데이터의 값도 변경되게 된다.
ex) C++, Swift, php
Java는 Call by Reference?
Java의 일반 변수는 Call by Value, 참조 변수는 Call by Reference?
아니다.
Java의 참조 변수를 전달할 때 참조 변수는 참조하는 값의 주소값을 가지고 있는데 그걸 다른 메서드로 전달할 때,
해당 주소값을 복사하여 전달하는 것이다.
즉, 래퍼런스를 복사하여 전달하는 것이다.
그렇기 때문에 메서드 내에서 해당 래퍼런스의 속성 변경 시 변경이 정상적으로 이루어지지만,
참조 변수 자체를 변경하게 되면 원본 데이터에는 수정이 일어나지 않는다.
Java8의 특징
Java8에서 함수형 프로그래밍이 처음 적용되었다.
- Optional 클래스
- Stream API
- Lambda Expression
- Method Reference, Default Method
Optional 클래스
Optional 클래스는 Java8에서 도입된 Wrapper 클래스로 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황에서
관련 메서드를 제공해서 명시적으로 처리할 수 있게 도와준다.
NPE 해결, 가독성 높은 코드
Stream API
자바에서 일련의 데이터 요소인 컬렉션, 배열에 관한 데이터 처리 메소드를 제공하는 API이다.
멀티 스레드를 활용해 병렬로 연산을 수행한다.
내부적으로 반복 연산을 수행해줘 코드의 가독성이 올라간다.
Lambda Expression
따로, Functional Interface를 구현하는 객체를 만들지 않고도 메서드로 전달할 수 있는
익명 함수를 하나의 식으로 표현할 수 있도록 단순화한 것이다.
특정 메서드 사용을 위해 일회용 객체를 만들지 않아도 되어 간단하게 작성 가능하고 가독성이 증가된다.
Java 프로그래밍이란?
Java라는 객체지향 언어를 활용해 JVM을 활용해 운영체제에 종속되지 않은 프로그래밍을 하는 것을 Java 프로그래밍이라고 할 수 있다.
웹, APP, 서버 등 다양한 분야에서 사용된다.
Jave SE vs Jave EE
Java SE - 소규모 웹 개발
Java EE - 대규모 웹 개발
Java와 C++와의 차이점
Java의 경우는 GC의 존재로 메모리 관리를 따로 해줄 필요가 없다.
C++은 개발자가 메모리의 할당과 해제를 진행해야 한다.
Wrapper class
기본 타입의 데이터를 객체를 취급해야 하는 경우가 있을 때,
Wrapper 클래스를 사용하여 참조 타입으로 변경이 가능하다.
Collection Framework에서는 기본 타입의 데이터를 허용하지 않기 때문에,
그때 Wrapper 클래스를 많이 사용한다.
Java의 장단점
장점:
1. 운영체제 독립적이다.
2. 객체지향 언어이다.(캡슐화, 추상화, 상속, 다형성이 잘 충족된 언어)
3. 자동으로 메모리 관리를 해준다.
- JVM의 GC가 데몬 스레드에 의해 Garbage Collection을 수행하며 메모리 관리를 해준다.
4. 오픈소스이다.
5. 멀티 스레드를 지원한다.
- 스레드의 생성과 제어를 위한 API를 지원하여 멀티 스레드 환경을 지원한다.
6. 동적 로딩을 지원
- 필요한 경우에 클래스 로더가 동적 로딩을 하여 클래스를 링크 및 메모리에 적재를 수행한다.
단점:
1. 한 번의 컴파일링으로 기계어로 번역되는 C, C++과는 다르게, JVM을 통해 기계어로 번역된 뒤 실행하는 한 단계의 과정이 더있어서
속도가 느리다는 단점이 존재한다.
- JIT 컴파일러로 일부 개선했다고 한다.(하드웨어 성능 향상, 바이트 코드 변환)
OOP의 4가지 특징
추상화
추상화는 특정 개념이나 객체를 보았을 때 특정 관점에서 관심있거나 중요한 부분만 추려내는 작업이라고 할 수 있다.
중요한 부분은 인터페이스로 표시하고 구현 세부 정보는 숨기는 OOP의 주요 특징 중 하나이다.
캡슐화
데이터와 프로세스를 하나의 객체에 위치하도록 만드는 것을 의미한다.
이를 통해 객체는 자율적으로 다른 객체와 협력할 수 있는 객체가 되어 시스템의 결합도는 내려가고 응집도는 올라간다.
즉, 유지보수에 용이하고, 변경에 유연하게 대처가 가능하다.
상속
부모 객체의 특징을 그대로 물려받는 것을 의미한다.
인터페이스 상속과 클래스 상속이 존재한다.
모듈의 재사용성이 올라가고, 다형성의 구현이 가능하다.
하지만, 무분별한 상속은 오히려 결합도를 증가시킨다.
다형성
같은 요청에 대해 객체의 타입에 따라 다른 결과를 내는것이 다형성이다.
OOP의 5대 원칙
S: SRP(단일 책임 원칙)
하나의 클래스는 하나의 책임/역할만을 수행해야 한다는 것을 의미한다.
Spring의 Controller 클라이언트의 요청 처리라는 역할이 이미 있는데, Controller에서 비즈니스 로직도 처리하면
이건 단일 책임 원칙에 위배된다고 할 수 있다.
O: OCP(개방 폐쇄 원칙)
자신의 확장에는 열려있고, 주변의 변경에는 닫혀있다는 것을 의미한다.
기존의 코드 변경없이 새로운 기능이 추가되어야 한다.
대표적으로, JDBC 인터페이스로 표준화하지 않았다면 DB를 바꿀 때마다 각자 제공하는 API에 따라 코드를
수정해야 한다.(Driver만 변경하면 된다.)
L: LSP(리스코프 치환 원칙)
하위 객체는 상위 객체의 역할을 수행할 수 있어야 한다.
I: ISP(인터페이스 분리 원칙)
하나의 인터페이스보다 여러 개의 구체적인 인터페이스를 클라이언트에 맞게 분리해야 한다.
D: DIP (의존 역전 원칙)
추상적이지 않은 것을 추상적인 것에 의존하게 해야한다.
즉, 변경이 잘 일어나지 않는 것을 의존하게 해야된다는 것을 말한다.
클래스보다 인터페이스
객체 지향이란?
객체란 현실 세계의 개념 혹은 개체의 상태와 행위를 정의한 데이터의 집합을 말한다.
객체지향 프로그래밍이란 위와 같이 설명된 각자의 역할을 가진 객체가 서로 메시지를 주고 받으며 동작하는 것을 말한다.
Java의 non-static 멤버와 static 멤버
Java의 non-static 멤버는 인스턴스 멤버로 객체 생성 시 Heap 영역에 저장된다.
=> 즉, 객체마다 다르게 존재한다.
Java의 static 멤버는 클래스 멤버로 static 영역에 생성된다.
=> 객체가 생성되지 않아도 존재하는 객체의 고유값이라고 할 수 있다.
Java의 메인 메서드가 static인 이유
main() 메서드는 프로그램의 시작과 끝이 되는 메서드로, `JRE`는 프로그램 안에 main()이 있는지 확인하고 이를 실행한다.
main() 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 메모리에서 사라진다.
public
- 어디서나 실행할 수 있다.
static
- 프로그램이 실행되는 순간에 static 영역에 설정되어 GC의 메모리 관리 제거 대상으로부터 벗어나야 한다.
void
- main() 메서드가 끝나면 프로그램도 종료되므로, 반환값이 필요 없다.
main
- 관례적인 프로그램의 시작
String[] args
- main 메서드가 실행될 때 전달할 인자값들을 말한다.
WAS와 웹서버
WAS는 Web Application Server로 웹서버와 데이터베이스 서버 사이에서 동작하는 서버를 말한다.
클라이언트의 요청에 따라 비즈니스 로직을 처리하고 데이터베이스로부터 데이터를 가져온다.
즉, 비즈니스 로직 처리 및 동적인 웹 페이지와 서비스를 제공한다.(Tomcat)
웹서버는 클라이언트의 요청을 받아 html, 이미지 등의 정적인 컨텐츠를 제공한다.
Gradle이란?
대표적인 빌드 시스템으로 다양한 빌드 작업을 수행한다.
1. Source Compilation(컴파일 단계)
- Java 컴파일러가 .java를 컴파일 해 .class 형식의 바이트 코드를 생성한다.
- 이때, Spring Boot에서는 java 플러그인이 호스트 컴퓨터의 JAVA_HOME에 있는 javac 컴파일러로
JavaCompile 태스크를 수행하게 한다.
- 컴파일이므로, JDK가 필요. build.gradle에 있는 의존성을 다운로드 한다.
- Gradle은 JavaCompile 태스크로 컴파일 결과를 build/classes/java/main 디렉터리에 저장한다.
- 이때, 모든 Java 파일과 의존성을 확인해 오류가 있다면 빌드가 실패된다.
2. Resource 처리
- src/main/resources/ 디렉토리에 있는 정적 리소스 파일(이미지, 설정 파일)을 build/resources/ 디렉터리로 복사한다.
- .class 파일과 함께 .jar에 포함된다.
3. 테스트 단계
- src/test/java/에 작성된 테스트 코드를 컴파일하고 실행한다. Gradle은 Test 태스크를 실행하여 테스트 실패 시 프로세스를 중단한다.
4. Packaging
- 성공적으로 컴파일된 .class 파일과 리소스 파일을 .jar 파일로 묶는다.
- jar 태스크로 .class와 리소스를 압축해 build/libs/ 디렉터리에 .jar 파일을 생성한다.
5. build 완료
- build/classes/ -> 컴파일된 바이트 코드
- build/resources/ -> 복사된 리소스 파일
- build/libs -> 최종 .jar 파일
- build/reports/ : 테스트 및 코드 품질 보고서
'Language > Java' 카테고리의 다른 글
[Java] 자바 구성 (0) | 2024.03.31 |
---|