본문 바로가기
SK Planet ASAC

JAVA 동작 원리와 객체 지향(은닉화/상속)

 

Java 동작 원리!

우리가 작성한 Java 코드가 어떻게 웹 서버(혹은 프로그램)가 되는 걸까

 

1. 기계어로 변환(ByteCode)

가장 기본 원리는 기계어(프로그램/정적)가 머신에서(해석되어) 실행(CPU 및 메모리가 할당되어 실행되면 프로세스/동적)되는 것 

 

Java가 기계어로 변환하는 방법, 컴파일과 런타임

이렇게 기계어와 같은 저급언어로 변환할 때 Java는 컴파일러를 사용한다. 우리가 작성한 Java(.java) 코드를 자바 컴파일러(Javac)를 통해 Bytecode(.class)로 변경한다. 

 

이후 Runtime 과정에서 Bytecodes(.class)를 기계어(Binary Code)로 변경한다. 이 때 자바 엔진(JVM)을 통해 이뤄진다. 

자바의 인터프리터는 자바 엔진(JVM)이다. Java는 컴파일 단계에서 단일 바이트 코드를 생성하고 JVM이 머신에 맞추어 기계어로 인터프리팅 후 실행한다. 그래서 Java는 호환성이 뛰어나다. 

(우리가 자바는 JVM이 있어 어디서든 실행할 수 있다는 장점을 어디선가(?) 들어봤을 것이다. 교수님이 했던 말이 떠오른다..) C언어의 경우 어떤 머신에 구동할지 컴파일 단계에서 옵션을 주어 머신에 맞는 기계어를 컴파일한다. 

Compile Error와 Runtime Error

Compile Error
컴파일 시 인지되는 문법 오류 등을 발생하는 것을 말한다. 똑똑한 IDE가 이를 파악하고 알려준다. 

Runtime Error
자바 코드가 실행하면서 발생하는 예외인데, 예를 들어 메모리 부족, Null Point와 같은 수많은 예외가 발생했을 때 이를 Runtime Error라고 함!

 

JVM의 구성

1. Class Loader(로더): 실행을 위한 ByteCode(.class)를 가져와서 

2. Runtime Data Area(메모리): 메모리에 얹고

3. Execution Engine(엔진): 구동

 

우리 포크레인 같은 클래스 로더는 javac를 통해 컴파일된 바이트 코드를 Runtime Data Area에 적재해준다. 이때 동적 로딩을 사용하는데(Dynamic Loading), 필요한 바이트 코드만을 Runtime Area에 적재하는 것을 말한다. 

단순히 퍼나른다고 생각하면 쉽긴한데 Loading -> Linking -> Initialization의 3단계를 거친다. 

 

Runtime Data Area(JDK 메모리)는 어떤일이 일어날까. 

이 포스팅에서 스레드에 관해 그려 놓은 게 있는데, 그때와 비슷하다고 생각하면 될듯. 

Thread 영역은 Thread 마다 하나씩 생성된다.이때 Stack 영역은 지역변수, 파라미터, 리턴값으로 이뤄져있으며, 기본 타입 변수는 직접 값, 참조 타입(객체) 변수는 Heap이나 Method 영역에 객체 주소를 가진다. 

 

여기서 Thread 공유 영역은 모든 Thread가 공유한다. 

1. Method 영역: 전역 변수, Static 변수, Final class, Class의 필드와 메서드 정보 등

2. Heap 영역: 객체(인스턴스, 배열) 

Heap 영역에서는 객체(인스턴스, 배열)이 저장된다 했다. 이것들은 GC의 대상이다. 프로그램을 실행하면 메모리에 필요한 함수나 객체를 올려서 실행하게 되는 것인데, 이게 계속 쌓이면 당연히 좋을리가? 없다. C언어의 경우 100년 만에 듣는 것 같은 Malloc, free를 통해 직접 치워야 하는데, JAVA에서는 JVM이 대신 지워준다.

 

 

2. JRE(Java Runtime Envrionment) 

자바 구동을 위한 모든 것. 

JRE = Other Libraries (API) + JVM (Java Virtual Machine)

 

3. JDK(Java Development Kit)

JRE + Java 개발을 위한 모든 것(컴파일러 등) 

다시 말하자면, API 라이브러리들 + JVM + Java 개발을 위한 Development Kit이 되겠다. 우리가 위에서 말했던 모든 것들을 합쳐서 JDK로 부르면 된다.

그래서 우리가 Java를 "개발"하고 싶다면 JDK 설치가 필요하다. Java를 "구동"하고 싶다면 JRE가 필요하다.

예를 들어, Intellij IDE를 통해 자바 개발을 하고 싶다면, 자바 개발에 필요한 모든 도구인 JDK를 설치해야한다. 하지만 똑똑한 Intellij에서는 자체적으로 JDK 다운로드/설치를 제공한다. 

 

4. 빌드 자동화 도구 gradle!

gradle은 JDK가 제공하는 도구를 사용하여 Java 코드를 컴파일하고 패키지화하며, 필요한 라이브러리를 자동으로 다운로드하고 관리하는 등의 작업을 자동화하는 빌드 도구다.

Node.js의 라이브러리 의존성과 프로젝트 세부 설정 역할을 하는 package.json과 같다. 

 

4-1. application(.properies|.yml|.yaml)

여러 config 파일들(라이브러리 설정) 

예를 들면... 뭐 이런 거...

 

 

2. Java 의 객체지향 프로그래밍 패러다임 문법!

객체지향 프로그래밍 OOP 패러다임의 특성에 대해서 기록한다.

객체지향이란, 분업화, 모듈화와 같은 의미이며 모듈의 기반이 클래스와 객체인 것을 말한다. 특정 객체는 특정 타입의 업무만을 수행한다. 아래 4가지 핵심 개념을 기억하자.

 

1. Class: 캡슐화와 상속

JAVA는 함수가 그 자체로 존재할 수 없고 무조건 Class 안에 메서드로서 존재해야 한다.

 

캡슐화(은닉화)

Class는 데이터와 행위(메서드)로 구성되어 있다. 클래스 내 변수에 함부로 접근했다가는 협업 할 때 문제점, 보안상의 문제가 생길 수 있다. 때문에 클래스 내 필드와 메서드 모두 접근 제어자를 통해 노출이 필요한 것만 최소한으로 외부로 노출한다. 

일반적으로 모든 필드를 Private으로 먼저 감추어 놓고, 그 이후로 선택적으로 필요에 따라 Getter/Setter 등을 Public으로 만들어 노출 할 수 있다. 최대한 데이터를 노출하지 않고, 최소한 메서드를 노출한다!

 


상속(Extends)

캡슐화를 그대로 유지하되 (재사용) 데이터나 행위를 확장하기 위해서 사용한다.

그러나 상속을 사용하면 문제점이 몇가지 발생한다. 만약 A 클래스를 B 클래스가 상속받았다고 가정하자. A 클래스에 있는 a 메서드와 b 필드만 사용하면 되는데, B 클래스에서는 A 클래스의 c 메서드와 d 필드까지 접근이 가능해진다. 즉, 상속 시

의도치 않은 부모 클래스의 Public 필드 혹은 메서드의 노출이 발생할 수 있다는 것이다. 

또한, 원치하는 메서드를 강제로 상속 받아야 한다는 단점도 있다. 이는 재사용성을 저하할 수도 있다. 


때문에, 상속보다는 조합을 활용하는 것이 좋다. 상속은 기존의 캡슐화를 그대로 유지하는 반면, 조합은 기존의 캡슐화를 한번 다시 감싸서 새로운 캡슐화를 하는 것이다. 

// 엔진 기능을 가진 클래스
class Engine {
    void start() {
        System.out.println("Engine is starting.");
    }

    void stop() {
        System.out.println("Engine is stopping.");
    }
}

// 자동차 클래스는 엔진 클래스를 포함하여 엔진 기능을 사용
class Car {
    private Engine engine;

    Car() {
        this.engine = new Engine();  // 조합을 통해 엔진 객체를 생성
    }

    void startCar() {
        engine.start();  // 엔진의 start 메서드를 호출
        System.out.println("Car is ready to go!");
    }

    void stopCar() {
        engine.stop();  // 엔진의 stop 메서드를 호출
        System.out.println("Car has stopped.");
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.startCar();
        myCar.stopCar();
    }
}

 

Car에서는 Engine 클래스를 상속받지 않고 생성자에서 Engine 클래스 객체를 생성하고 있다. 엔진 관련 기능을 구현하되, 이를 외부에 노출하지 않는 것이다. 

 

(Add) 객체 생성하는 방법 3가지!

생성자 

1. NoArgs

class Human {
		private String name;
		private int age;

		// 생성자 1) "No" Args Constructor -> 이후에 Setter 함수 호출
		public Human() {}
}

 

2. Custom Constructor

class Human {
		private String name;
		private int age;

		// 생성자 2) "Custom" Constructor -> 선택적 객체 필드 세팅
		public Human(String name) {
				this.name = name;
		}
}

또는 DTO를 통해 생성하기
class Human {
		private String name;
		private int age;

		// 생성자 2) "Custom" Constructor -> 
		public Human(HumanCreateRequestDto dto) {
				this.name = dto.getName();
				this.age = dto.getAge();
		}
}

 

3. All Args 

class Human {
		private String name;
		private int age;

		// 생성자 3) "All" Args Constructor
		public Human(String name, int age) {
				this.name = name;
				this.age = age;
		}
}

 

 

2. 빌더

Builder의 장점으로는 객체 생성을 위해 내가 원하는 필드만 설정할 수 있고, 순서도 뒤죽박죽으로 할 수 있다. 

Person person1 = Person.builder()
        .name("se")
        .age(10)
        .build();

 

객체 생성을 위한 필드값 주입 역할을 마음껏 분리할 수 있다.

Person person1 = Person.builder()
        .age(10)
        .build();

System.out.println(person1.getName()); //NULL

 

아래와 같은 방법으로 name과 age를 set하는 것을 메서드로 분리해서 사용할 수도 있다. 이럼으로써 은닉화도 되는데, age와 name과 같은 필드에 함부로 접근해서 사용하지 못하게 막을수도 있다. 물론, 분리되어 재사용성 또한 높아진다.

    public static Person.PersonBuilder setName(Person.PersonBuilder builder){
        return builder
                .age(10);
    }
    public static Person.PersonBuilder setAge(Person.PersonBuilder builder){
        return builder
                .name("eh........");
    }
    public static void main(String[] args) {
        Person.PersonBuilder personBuilder = Person.builder();
        personBuilder = setAge(personBuilder);
        personBuilder = setAge(personBuilder);
		personBuilder = setName(personBuilder);
		Person person = personBuilder.build();
		System.out.println(person);

    }

 

 

3. 정적 팩토리 메서드

객체 생성 방법을 매우 좁게 정의하고, Private 생성자를 정의해보자.

객체를 생성할 수 있는 방법이 오직 하나의 정적 메서드 방식으로만 가능하게 설정하는 것인데, 이를 싱글톤이라고 한다.

싱글톤은 정적 팩토리 메서드만 사용하며, 좋은 코드는 함수와 파라미터만으로 문장을 완성할 수 있다. 
팩토리 메서드는 객체를 생성하고 반환한다. 

 

@Getter
class ParsedRequestDto {
    private final String hello;
    private final String world;

    private ParsedRequestDto(String hello, String world) {
        this.hello = hello;
        this.world = world;
    }

    public static ParsedRequestDto of(RequestDto requestDto) {
        String calculated = Calculator.caculate(
            requestDto.getHello(),
            requestDto.getWorld()
        );
        ParsedRequestDto created = new ParsedRequestDto(
            requestDto.getHello(),
            requestDto.getWorld()
        );
        return created;
    }
}

 

원래는 첫번째 줄 처럼 객체를 생성할 수 없지만, 아래와 같이 .of를 사용해 싱글톤 방식으로 객체를 생성할 수 있다.

// 바로 아래 코드 - 호출 불가능 : Private 생성자이기 때문
// ParsedRequestDto newlycreated = new ParsedRequestDto(dto.getHello(), dto.getWorld());
ParsedRequestDto newlycreated = ParsedRequestDto.of(dto);