[Kakao Cloud School] 8번째 회고록

lango·2022년 12월 28일
0
post-thumbnail

Intro


몰입하는 것은 어렵다.

교육과정 속에서 학습하는 것도 학습하는 것이지만, 코어타임 외의 시간을 별도로 투자하여 세미 프로젝트를 진행하는 것이 생각보다 쉽지 않았다.

짧은 시간에 몰입하기란 어려웠고 단순히 컴퓨터 앞에 오래 앉아 있거나, 모니터를 오래 들여다보고 있다고 해서 저절로 집중력이 생기거나 몰입되지는 않았다.

그래서 하나하나씩 구체화시키면서 집중해보기를 시도했다. 예를 들자면 현재 진행하고 있는 세미 프로젝트의 기획과 설계부터 시작해 실제 구현을 위한 팀원들과의 소통과 일 단위의 일정표 작성하는 등의 시간을 보낼 때 어떻게 더 나은 구조로 만들 수 있을까에 대해 고민하니 자연스럽게 몰입할 수 있었고, 더 효율적으로 시간을 투자할 수 있었다고 생각한다.

하루에 온전히 몰입하는 시간이 많아봐야 얼마 되지는 않겠지만, 다른 사람들과 함께 진행하는 프로젝트인만큼 지속적으로 몰입하는 시간들을 만들어서 정해진 일정안에 기대한 것보다 나은 산출물을 만들도록 노력해야겠다.




Day - 34


접근 제한자 protected는 언제 써야할까?

Java의 접근제한자에는 public, package, protected, private 총 4가지의 종류가 있다. 이 접근제한자 중에서 protected에 대해서 이해도가 낮고 활용해본 적이 없어 이번에 protected에 대해서 짚고 넘어가려 한다.

접근제한자 protected란?
자신의 클래스와 하위클래스 안에서 접근할 수 있게 해준다. 중요한 점은 동일한 패키지의 외부에서도 접근이 가능하다는 것이다.

접근 제한자는 public > protected > default(package) > private 순으로 접근을 제한하게 되는데, public과 default의 중간 정도의 수준으로 접근을 제한한다고 보면 된다.

protected에서 가장 중요한 점은 바로 동일한 package에서는 default와 동일하게 접근 제한을 두지 않는다는 것인데, 다른 패키지에서 접근하려면 자식 클래스에서만 접근할 수 있다는 것이다.


옷의 종류 중 하나인 아우터와 상의의 관계를 통해 예시를 살펴보자.

아우터의 종류에는 코트와 패딩이 있고, 상의의 종류에는 맨투맨과 니트가 있다. 코트는 울 소재로 만들어 질 수 있기에 코트라는 객체에 울이라는 protected 속성을 추가하였다.

울 속성으로는 패딩도 만들 수 있다. 그래서 같은 아우터 패키지의 패딩에서도 울 속성에 대해서 접근이 가능하다. 그리고 울 속성은 아우터 종류의 옷에서만 쓰이지는 않기 때문에 같은 소재로 다른 종류의 옷을 만들 수도 있다.

아우터 패키지와 다른 패키지인 상의 패키지를 보자. 맨투맨은 울로는 만들어지지 않기 때문에 울 속성에 접근할 수 없다.

하지만, 상의 종류 중 하나인 니트의 경우 울로도 만들어 질 수 있다.
니트 클래스는 코트를 상속받아서 코트 클래스의 하위클래스가 되면 울 속성에 접근할 수 있다.

코트를 가지고 니트를 만들 수 있는 것은 아니지만 protected를 이해하기 위해 편의대로 예시 그림을 그렸다.


클래스 구조 구현하기

그렇다면 이제 직접 코드로 작성해보자. 먼저 outer 패키지에는 Coat 클래스와 Padding 클래스를 작성한다.

package kakao.study.outer;

public class Coat {
    // 속성
    protected int wool;
    // 생성자
    protected Coat() {

    }
    // 메서드
    protected void coat() {

    }
}
package kakao.study.outer;

public class Padding {
    public void padding() {
        Coat coat = new Coat();
        coat.wool = 60;
        coat.coat();
    }
}

Coat 클래스의 속성과 생성자, 메서드를 protected로 선언하였다. 이로인해 Padding 클래스의 padding() 메서드에서 Coat의 속성, 생성자, 메서드에 접근이 가능함을 알 수 있다.


그리고 outer와는 다른 top 패키지에 Sweatshirt 클래스와 Knit 클래스를 작성한다.

package kakao.study.top;

import kakao.study.outer.Coat;

public class Sweatshirt {
    public void sweatshirt() {
        Coat coat = new Coat(); // Error
        coat.wool = 30; // Error
        coat.coat(); // Error
    }
}

Sweatshirt 클래스는 Coat 클래스의 생성자로 인스턴스를 생성하려고 할 때, Coat와는 관계없는 다른 패키지에서 호출했기에 에러가 발생한다.

package kakao.study.top;

import kakao.study.outer.Coat;

public class Knit extends Coat {
    private Knit() {
        super(); // No Error
        this.wool = 40; // No Error
        this.coat(); // No Error
    }
}

그런데, Coat 클래스를 상속받은 knit 클래스에서는 Coat에게 문제없이 접근할 수 있음을 확인할 수 있었다.

public와 private은 명확하게 접근의 제한 유무를 가지고 구분할 수 있었다.

protected의 경우 같은 패키지와 다른 패키지의 하위클래스에서는 문제없이 접근 가능하다는 점을 위에서 만든 예시를 통해 입증할 수 있었고, 앞으로 객체간에 접근을 구분할 때 protected를 더욱 적극적으로 활용해야겠다고 다짐하였다.



싱글톤 패턴(Singleton Pattren)에 대해서..

지금까지 클래스를 가지고 인스턴스를 생성할 줄만 알지, 생성한 인스턴스를 관리하거나, 인스턴스 간 데이터를 효율적으로 공유하기 위한 방법에 대해서는 바로 정답을 내릴 수 없었다.

한 클래스의 인스턴스를 최초 한 번만 생성하고 이후 여러 클래스에서 해당 인스턴스로 접근하여 정보를 보관하고 공유하는 방법에 대해서 찾아보니 싱글톤 패턴을 가장 먼저 찾을 수 있었다.

싱글톤 패턴은 이전부터 많이 들어왔지만 정확히 어떤 패턴인지는 모르는 상태였기에 싱글톤 패턴에 대해서 알아보려 한다.

싱글톤 패턴(Singleton Pattern)이란?
생성 패턴 중의 하나로 오직 하나의 객체(인스턴스)만을 생성해두고 프로그램 어디에서나 이 객체에 접근하여 사용할 수 있도록 구성하는 패턴이다.

간단히 말하자면 애플리케이션 전체에서 하나의 인스턴스만 생성해두고 필요할 때마다 이 인스턴스에 접근하거나 반환해주어 정보를 공유할 수 있도록 한다는 것이다.

싱글톤 패턴을 이해하려하니 static에 대해서도 알아볼 수 밖에 없었다. static 키워드를 사용하면 메모리에 최초 한 번 할당되고 프로르램이 종료될 때 소멸되기 때문에 싱글톤 패턴과 밀접한 관계가 있다.

따라서 static을 통해 객체(인스턴스)를 생성한다면 프로그램 종료 전까지 메모리에 남아있기 때문에 어디서든 이미 만들어둔 객체에 접근할 수 있다는 것이다.

간단한 코드를 작성하여 싱글톤 패턴의 맛만 보도록 하자. 먼저 FirstSingleton 라는 클래스를 작성하였다.

public class FirstSingleton {
    public FirstSingleton() { }
}

메인클래스 SingletonMain 를 만들어 FirstSingleton의 인스턴스를 여러 개 만들어 비교해보자.

public class SingletonMain {
    public static void main(String[] args) {
        FirstSingleton singleton1 = new FirstSingleton();
        FirstSingleton singleton2 = new FirstSingleton();
        
        System.out.println(System.identityHashCode(singleton1));
        System.out.println(System.identityHashCode(singleton2));
    }
}
// 1586600255 출력
// 359023572 출력

하나의 클래스를 가지고 인스턴스를 만들었지만 서로 관계가 없는 다른 인스턴스를 가지기에 해시코드가 다름을 알 수 있었가.

이제 싱글톤 패턴처럼 구현하기 위해 FirstSingleton 이라는 클래스를 수정해보자.

public class FirstSingleton {
    // 외부에서 직접 인스턴스 생성을 못하도록 생성자의 접근지정자를 privatea로 변경한다.
    private FirstSingleton() { }

    // 하나의 인스턴스 참조를 저장하기 위한 속성을 생성한다.
    private static FirstSingleton singleton;

    // 인스턴스의 참조를 리턴하는 메서드를 생성한다.
    public static FirstSingleton sharedInstance() {
        if(singleton == null) singleton = new FirstSingleton();
        return singleton;
    }
}

가장 먼저 FirstSingleton 객체와 생성자는 외부에서 접근 및 생성하지 못하도록 private으로 선언해주었다. 그리고 static 키워드를 추가하여 동일한 인스턴스를 반환하도록 하였다.

생성자가 private으로 선언되었기 때문에 우리는 메인 클래스(외부)에서 FirstSingleton의 객체를 생성할 수 없다. 또한 동일한 인스턴스를 반환해주기 위해 static 메서드인 sharedInstance를 만들어 주었다.

그렇게 본인의 타입으로 만들어진 static 속성을 정의하고 static 속성에 데이터를 만들어서 주입하고 리턴하는 static 메서드를 생성하였다.

그리고 싱글톤 패턴을 구현할 메인 클래스 SingletonMain 도 수정한다.

public class SingletonMain {
    public static void main(String[] args) {
        FirstSingleton singleton1 = FirstSingleton.sharedInstance();
        FirstSingleton singleton2 = FirstSingleton.sharedInstance();

       	System.out.println(System.identityHashCode(singleton1));
        System.out.println(System.identityHashCode(singleton2));
    }
}
// 1586600255 출력
// 1586600255 출력

두 클래스의 해시코드를 확인해보면 싱글톤 패턴으로 디자인했기에 같은 인스턴스의 참조를 가지고 있어 해시코드가 같음을 알 수 있었다.

싱글톤 패턴을 통해 서버에서는 웬만하면 하나의 클래스마다 인스턴스를 1개만 만들어서 관리하는 것이 여러모로 개발자 입장에서 편리할 수 있을 것 같았고, 특정 클래스들에 대해서 동일한 인스턴스를 보장해야 하는지 등의 여부를 충분히 생각하고 고민해봐야겠다고 느꼈다.



Day - 35


박싱(Boxing)과 언박싱(Unboxing)은 무엇인가?

Java에서는 기본 타입 데이터를 래퍼클래스를 이용하여 객체로 다룰 수가 있다.

그렇기에 기본 타입 데이터를 래퍼클래스로 변환하거나, 래퍼클래스를 기본 타입 데이터로 변환해야 할 경우가 많은데, 박싱(Boxing)과 언박싱(Unboxing)을 통해 기본 타입의 데이터를 대응되는 래퍼클래스로 만들거나, 래퍼클래스에 대응하는 기본 타입의 데이터로 변환할 수 있다.

박싱(Boxing)이란?
기본 자료형의 데이터를 대응되는 래퍼클래스로 변환하는 동작이다.

박싱을 간단하게 코드로 표현해보자.

int point = 5;
Integer score = new Integer(point);

// 5 
// 5

기본 타입 int형 변수 point에 5를 저장한 후 score에 point를 매개변수로 전달하여 Integer 객체를 만들었다.

언박싱(Unboxing)이란?
래퍼클래스에서 기본 자료형 데이터로 변환하는 동작이다.

언방식도 간단한 예제를 작성해보았다.

Integer score = new Integer(5);
int point = score.intValue();

// 5
// 5

Integer 객체 score를 만들고, score의 intValue() 메서드를 통해 score의 값을 기본 타입 int형 변수 point에 저장하였다.


오토 박싱(Auto Boxing)과 오토 언박싱(Auto Unboxing)

JDK 1.5 버전부터 Java에서 박싱과 언박싱을 자동으로 지원해주는 기능인 오토박싱과 오토언박싱 기능이 추가되었다.

오토 박싱(Auto Boxing)이란?
기본 타입의 데이터를 래퍼클래스로 변환하는 것은 박싱과 동일하지만 변환 과정에서 모리의 동적 할당과 기본 타입에 대한 객체 초기화가 포함되어 객체를 명시적으로 생성할 필요가 없다.

오토박싱을 이용하면 기본 타입의 데이터를 래퍼클래스로 쉽게 변환할 수 있다.

int point = 5;
Integer score = point;

// 5
// 5

오토 언박싱(Auto Unboxing)이란?
오토박싱의 반대되는 기능으로, 래퍼클래스를 기본 타입의 데이터로 변환하는 동작을 의미한다. 마찬가지로 변환 과정에서 언박싱과 다르게 intValue()등의 메서드를 이용할 필요없이 변환할 수 있다.

오토언박싱을 이용해도 손 쉽게 래퍼클래스를 기본 타입의 데이터로 변환할 수 있게 된다.

Integer score = new Integer(5);
int point = score;

// 5
// 5

성능은 어떨까?

오토박싱과 언박싱을 통해 편하게 변환하며 사용할 수 있지만 결국 내부적으로 추가적인 연산 작업이 이루어지기 때문에 성능은 저하될 수 있다.

직접 오토박싱 연산과 비교하여 성능이 얼마나 차이나는지 확인해보자.

오토박싱 연산

long start = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < 100000; i++) {
	sum += i;
}
long end = System.currentTimeMillis();
System.out.println("걸린 시간: " + (end-start));
// 걸린시간: 12

동일타입 연산

long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < 100000; i++) {
	sum += i;
}
long end = System.currentTimeMillis();
System.out.println("걸린 시간: " + (end-start));
// 걸린시간: 3

각각 반복문 안에서 100,000번의 연산을 진행해보았는데 소요된 시간은 대략 4배 정도 차이가 났다. 순회할 데이터 양이 많아질 수록 이 성능의 차이는 더 부각될 것이라 생각이 들었다.

그래서 오토박싱과 오토언박싱보다는 동일한 타입으로 연산을 수행해야 함을 인지하고 사용해야겠다고 느꼈다.



인스턴스 생성 없이 Math.max()로 사용할 수 있었던 이유는?

int a = 10;
int b = 20;
int max = Math.max(a, b);
System.out.println(max);
// 20 출력

위 코드를 보면 Math 클래스의 max() 메서드를 인스턴스 생성없이 사용하는 것을 볼 수 있다. Math math = new Math(); 구문을 통해 인스턴스를 생성하지 않고 어떻게 바로 클래스에 접근하여 max() 메서드를 사용할 수 있는 것일까?

Math 클래스란?
Math 클래스는 java.Lang 패키지에 포함된 클래스로 수학과 관련된 일련의 작업들을 처리할 수 있는 클래스이다. Math 클래스의 다양한 메소드들은 전부 static으로 구현되어 있으므로 따로 객체를 생성하지 않고 사용할 수 있다.

여기서 핵심 키워드는 바로 static이다. Math 클래스의 모든 멤버가 static으로 선언되어 있기 때문에, 바로 해당 클래스에 접근하여 사용할 수 있던 것이었다.

바로 Math 클래스의 메서드 종류들을 살펴보았다. Java Documenet를 살펴보면 모든 메서드에 static이 붙어있고 생성자를 제공하지 않는다.

결국 static 메서드만으로 구성된 클래스는 별도로 객체(인스턴스)를 생성하지 않고 바로 클래스에 접근하여 사용할 수 있던 것이다.



Day - 36


로또번호 입력받는 프로그램 구현하기

List 구조를 이용하여 로또번호를 입력받아 출력하는 예제를 작성해보려 한다.

로또 입력 프로그램 요구사항은 다음과 같다.

  • 1~45까지의 숫자 6개를 입력받아서 저장한 후 출력한다.
  • 입력시 1~45사이의 숫자가 아니면 다시 입력하도록 한다.
  • 중복되는 숫자를 입력하면 다시 입력하도록 한다.
  • 데이터 출력시에는 정렬하여 출력한다.

먼저 배열을 이용하는 것보단 Set을 활용한다면 중복 검사에 유리하기 때문에 Set를 이용하여 구현하되, 중복된 데이터를 저장하지 않고 데이터를 정렬해서 저장하는 TreeSet을 활용하여 구현해보겠다.

Scanner sc = new Scanner(System.in);
Set<Integer> lotto = new TreeSet<>();

먼저 입력받기 위한 스캐너 인스턴스를 생성하고 숫자 6개를 저장할 TreeSet을 하나 만들자.

while (lotto.size() < 6) {
	System.out.print("로또 번호 입력: ");
    int num = sc.nextInt();
    if(num < 1 || num > 45) {
    	System.out.println("1부터 45 사이의 로또 번호만 입력 가능합니다. 다시 입력하세요.");
        continue;
    }
    // 로또 번호 중복 검사하기
    // 삽입 성공하면 true, 실패하면 false를 리턴한다.
    boolean dup = lotto.add(num);
    if(dup == false) {
    	System.out.println("이미 입력한 로또번호입니다. 다시 입력하세요.");
    }
}

위 코드를 살펴보면 다음과 같다.

  1. TreeSet에 정수 데이터가 6개가 저장되지 않았다면, 6개가 저장될 때까지 반복하여 정수를 입력받는다.
  2. 단, 입력받는 정수가 1보다 작거나 45보다 크면 continue; 구문을 통해 다음 반복으로 넘어간다.
  3. 입력한 로또 숫자의 중복 검사는 TreeSet에 삽입한 boolean 결과값으로 구분한다. (TreeSet은 새로운 숫자를 TreeSet에 삽입했다면 true, 중복된 숫자를 삽입하려하면 false를 반환한다.)
System.out.println(lotto);

마지막으로 lotto를 출력하도록 하였는데, TreeSet은 저장된 데이터의 순서를 보장해주기 때문이다.

이렇게 입력받은 정수의 중복 여부와 데이터의 정렬을 보장받는 TreeSet을 활용해 손쉽게 로또 입력기를 구현할 수 있었다. 이 구현과정을 통해 알고리즘의 난이도보다는 자료구조의 선택이 굉장히 중요함을 알게 되었다.

전체코드

import java.util.Arrays;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

public class LottoSetMain {
    public static void main(String[] args) {
        /* TreeSet을 활용한 풀이 */

        // 입력받기 위한 스캐너 인스턴스를 생성한다.
        Scanner sc = new Scanner(System.in);

        // 숫자 6개를 저장할 공간을 생성한다.
        // 중복된 데이터를 저장하지 않고 데이터를 정렬해서 저장하는 TreeSet을 활용한다.
        Set<Integer> lotto = new TreeSet<>();

        // set에 6개의 데이터가 저장되지 않았다면 6개가 저장될 때까지 반복한다.
        while (lotto.size() < 6) {
            System.out.print("로또 번호 입력: ");
            int num = sc.nextInt();
            if(num < 1 || num > 45) {
                System.out.println("1부터 45 사이의 로또 번호만 입력 가능합니다. 다시 입력하세요.");
                continue;
            }
            // 로또 번호 중복 검사하기
            // 삽입 성공하면 true, 실패하면 false를 리턴한다.
            boolean dup = lotto.add(num);
            if(dup == false) {
                System.out.println("이미 입력한 로또번호입니다. 다시 입력하세요.");
            }
        }

        // TreeSet의 데이터 출력
        System.out.println(lotto);

        // 스캐너 정리하기
        sc.close();
    }
}



Day - 37


JVM에 대해서 알아보자.

Java를 공부하고 있지만, JVM에 대해서 설명도 못하기에 JVM에 대해서 알아보려 한다.

JVM(Java Virtual Machine)이란?
Java Virtual Machine을 직역하면 자바를 실행하기 위한 가상 컴퓨터라고 하는데, OS와 Java 애플리케이션 사이의 중개자 역할을 한다. 즉, OS에 종속받지 않고 CPU가 Java를 인식하고 실행할 수 있게 해주는 가상 컴퓨터이다.

JVM은 자바 바이트코드를 실행할 수 있는 환경을 제공해준다. 이를 통해 자바 바이트 코드가 플랫폼에 독립적으로 어디서든 실행될 수 있게 한다. 결국 어디서든 JAVA 애플리케이션을 실행할 수 있게 된다는 강점이 있다.

JVM의 구조

JVM의 구조는 다음과 같이 이루어져있다.

  • 클래스 로더(Class Loader)
  • 실행 엔진(Execution Engine)
    • 인터프리터(Interpreter)
    • JIT 컴파일러(Just-in-Time)
    • 가비지 콜렉터(Garbage collector)
  • 런타임 데이터 영역 (Runtime Data Area)

각각의 구조들이 뭔지 하나씩 살펴보도록 하자.

클래스 로더
JVM 내로 클래스 파일 *.class을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. 런 타임 시점에 클래스를 로드하고 jar 파일 내 저장된 클래스들을 JVM 위에 탑재한다. 즉, 클래스를 처음으로 참조할 때, 해당 클래스를 로드하고 링크하는 역할을 한다.

실행 엔진
실행엔진은 로드된 클래스의 바이트코드를 실행시키는 역할을 한다. 클래스 로더가 JVM 내의 런타임 데이터 영역(Runtime Data Area)에 바이트 코드를 배치시키고, 실행 엔진은 자바 바이트 코드를 명령어 단위로 읽어서 실행한다. 이 때 인터프리터(Interpreter) 방식과 JIT(Just-In-Time) Compiler 방식을 사용하게 된다.

인터프리터(Interpreter) 방식이란?
인터프리터는 프로그래밍 언어 소스코드를 바로 실행하는 프로그램을 말한다. 원시 코드를 기계어로 번역하는 컴파일러와는 대비된다. Java는 인터프리터 방식을 사용하여 자바 바이트 코드를 명령어 단위로 읽어서 실행한다. 그런데 한 줄씩 수행하기 때문에 수행 속도가 느리다는 단점이 있다.

JIT(Just-In-Time) 방식이란?
바이트코드를 컴파일하여 native code(네이티브 코드)로 변환하여 사용하는 방식이다. 인터프리터 방식의 단점을 개선하기 위해 도입되었다. 한 번 컴파일된 코드는 빠르게 수행하게 되어 수행 속도가 빠르게 된다. 하지만 컴파일하는 과정에 비용이 들게 된다. 따라서 한 번만 수행할 코드라면 컴파일하지 않고 인터프리팅 하는 것이 유리하다. 따라서 JVM은 인터프리터 방식을 사용하다가 적절한 시점에 JIT 컴파일러를 함께 사용한다.

가비지 콜렉터(Garbage Collector)
가비지 컬렉터는 유효하지 않는, 즉 사용되지 않는 메모리를찾아 삭제하는 역할을 한다.

런타임 데이터 영역(Runtime Data Area)

Runtime Data Area는 JVM이 프로그램을 수행하기 위해 OS로부터 별도로 할당받은 메모리 공간을 말한다. Runtime Data Area는 크게 5가지 영역으로 구분할 수 있다.

PC Register
JVM의 PC Register는 CPU 내의 기억장치인 레지스터와 다르게 작동하는데, 스레드가 시작될 때 생성되고, 생성될 때마다 생성되는 공간의 스레드마다 PC Register가 하나씩 존재한다. PC Register는 스레드가 어떤 명령을 실행할지 기록하는 역할을 수행한다고 보면 된다.

JVM 스택 영역(Stack Area)
프로그램 실행과정에서 임시로 할당되었다가 메소드의 호출이 종료되면 바로 소멸되는 특성을 지닌 데이터들을 저장하기 위한 영역이다. 메소드(method)가 호출될 때 메서드와 메서드의 정보는 JVM Stack에 쌓이게 된다. 즉, 메서드의 매개변수(parameter), 지역 변수(local variable), return 주소, 임시 변수 등의 정보를 기록하는 스택이다. 각 스레드 별로 생성되기 때문에 다른 스레드는 접근할 수 없다. 메서드 호출이 종료되면 스택에서 정보들이 소멸된다.

Native Method Stack
자바 외의 언어 즉, 바이트코드가 아닌 실제 실행할 수 있는 기게어로 작성된 네이티브 코드들을 위한 스택 영역이다. Java Native Interface를 통해 호출되는 C/C++ 등의 코드를 수행한다.

Method Area(=Class Area =Static Area)
클래스 정보를 처음 메모리 공간에 생성할 때 초기화되는 대상들을 저장하기 위한 메모리 공간이다. 모든 스레드가 공유하는 메모리 영역으로 클래스, 인터페이스, 메서드, 필드, Static 변수 등의 바이트 코드를 보관한다. Method Area에는 Runtime Constant Pool이라는 별도의 관리 영역도 존재한다. 이는 상수 자료형을 저장하고 참조하여 중복을 막는 역할을 수행한다.

Heap
객체를 저장하는 가상 메모리 공간으로 new연산자로 생성되는 객체와 배열을 저장하게된다. JVM Heap 영역으로 Runtime 시점에 동적으로 할당하여 사용하는 영역이다. 클래스를 이용해 인스턴스를 생성하면 Heap에 저장된다. 즉, new연산자를 이용해 생성된 객체를 저장하는 영역이다. Heap은 크게 New/Young 영역, Old 영역, Permanent Generation 3 영역으로 나뉜다. 참고로 java8 이후에는 Permanent 영역이 Metaspace 영역으로 바뀌었다.

JVM의 구조를 살펴보다보니 엄청 길어졌다. 클래스로더, 실행 엔진, 가비지 콜렉터, 런타임 데이터 영역의 역할이 무엇인지, 왜 이렇게 동작하는지를 확실하게 숙지해두자.



Day - 38


Try-with-resources 구문이란?

지금까지 try/catch 구문을 사용하며 try/catch 구문 안에서 자원을 사용하고 반납하도록 구현해왔다. 그래서 항상 try/catch 구문안에서 자원을 생성하고 반납해야했다.

BufferedReader를 이용해 입력받고 출력하는 간단한 예제를 작성해보았다.

public static void main(String[] args) {
        
        // BufferedReader로 입력받기
        BufferedReader br = null;
        
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            System.out.println(br.readLine());
            br.close();
        } catch (Exception e) {
            try {
                br.close();
            } catch (Exception ex) {
                System.out.println(ex.getLocalizedMessage());
            }
        }
    }

위 코드를 살펴보면 BuffredReader를 사용하기 위해 try/catch 문을 2번이나 작성하게 되고, br.close() 메서드를 통해 사용했던 BufferedReader를 반드시 반납해야 함을 알 수 있다.

try/catch 구문의 단점

  • 자원을 사용한 후 반납을 해야하기 때문에 별도의 반납 구문이 추가된다.
  • try/catch 구문이 여러 번 반복되는 등 작업이 번거롭다.
  • 실수로 자원을 반납하지 못할 수도 있다.
  • 에러로 자원을 반납하지 못할 수도 있다.
  • 에러 스택 트레이스가 누락되면 디버깅하기 어려워질 수 있다.

이러한 문제점을 개선하기 위해 Java7에서부터는 Try-with-resources라는 문법을 추가로 제공하였다.

try-with-resources 문법이란?
try 구문에 리소스를 선언하고, 리소스를 다 사용하고 나면 자동으로 반납(close) 해주는 기능이다.

AutoCloseable 인터페이스를 구현하고 있는 자원에 대해 try-with-resources를 적용할 수 있기에 코드의 가독성은 높아지고 누락되는 에러없이 모든 에러를 잡을 수 있다고 한다.

위에서 작성한 코드를 try-with-resources 구문을 활용하여 다시 고쳐보자.

package kakao.lango.java.io;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class TryCatch {
    public static void main(String[] args) {
        // try-with-resources 문법을 통해 BufferedReader로 입력받기
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            System.out.println(br.readLine());
        } catch (Exception e) {
            System.out.println(e.getLocalizedMessage());
        }
    }
}

try의 괄호 () 안에서 BufferdReader를 생성함으로써, 복잡했던 코드를 간결하게 고칠 수 있었다. 또한 생성했던 BufferedReader를 별도로 반납하는 코드를 작성하지 않아도 된다.

앞으로는 자원을 반납해야 할 때는 일반적인 try/catch 구문 보다는 아닌 try-with-resources 문법을 사용해야겠다.






Final..

카카오 클라우드 스쿨에서의 교육 8주차가 흘러갔다.

Java에서의 내포클래스(Nested Class), Java API안 JDK 1.8의 내용들, 스레드를 두어 비동기로 작업하는 방법, 다양한 입출력(I/O) 방식들과 네트워크에 대해서 배우게 되었다.

정말 많은 내용을 짧은 시간에 머릿속에 집어 넣으려 하니 굉장히 수업시간이 빠르게 느껴지고, 날이 갈수록 부담은 늘어가고 있다.

그럼에도 불구하고, 모든 CS 지식들을 외우려고 하지말고, 근본적인 원리부터 이해하고 넘어가려고 노력하고 있다. 이런 자세로 배움에 임하니 당연하게도 파생되는 다양한 지식들이 왜 이런 구조로 만들어졌는지 남득이 되는 순간들도 있었다.

다음주부터는 네트워크와 람다, 오픈소스, DB 연동하는 과정들에 대해서 배울텐데 지식적인 내용에 집중하기보다는 해당 지식으로 하여금 무엇을 할 수 있는 것인지를 생각하면서 공부해보자.



혹여 잘못된 내용이 있다면 지적해주시면 정정하도록 하겠습니다.

게시물과 관련된 소스코드는 Github Repository에서 확인할 수 있습니다.

참고자료 출처

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글