자원이란?

Java를 사용하다 보면 컴퓨터의 구성 요소 중 다양한 부분들을 사양하게 됩니다. JVM에 대한 정보가 있는 메모리부터 데이터베이스 다른 소프트웨어에서 제공 받는 기능들, 그리고 영구적으로 데이터를 할 수 있는 보조 기억 장치까지요. 저희는 이렇게 소프트웨어, 하드웨어적으로 한정된 기능을 제공받는 것들을 자원 이라고 합니다.

자바에서 가장 흔하게 볼 수 있는 자원 관련 객체는 InputStream, OutputStream 그리고 java.sql.Connection 등이 있으며 이 객체들 모두 앞서 말한 요소들에 관한 기능과 연관이 있습니다.

자원 해제의 중요성

컴퓨터의 다양한 기능들을 사용하는 것은 좋지만, 해당 상황들은 우리가 만드는 프로그램 뿐만이 아니라 각각의 자원에 대해서도 영향을 미치게 됩니다.

 

1. JVM에서 차지하는 메모리

외부 자원을 사용하게 되는 것도 JVM에서 필시 메모리가 차지하게 됩니다. 이는 우리의 프로그램이 GC를 통해서 처음부터 끝까지 관리해주는 객체들과 달리 다른 자원과 연결되어있습니다. 때문에 GC를 믿기보단 직접 해제를 해줘야 합니다.

 

2. 관련 객체 오작동

자원에서 필요한 부분들을 다 사용하고 나서 반납을 해주지 않으면 다른 프로세스나 쓰레드에서 해당 객체를 사용했을 때의 예상치 못한 문제가 발생 할 수 있습니다.

 

이러한 이유 때문에 GC를 믿기보다는 해당 자원을 직접적으로 해제해주는 것이 좋습니다.

 

기존의 방법들

자원에 관련된 객체 이해를 돕기 위해 임의로 하나의 객체를 정의 해보겠습니다.

 

public Resource {

	public void run() throws ResourceExcpetion {
    	//해당 자원에 관련된 로직을 실행하는 메소드
    }
    
    public void close() throws ResourceExcpetion {
    	//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
    }
}

가장 원시적으로 사용하게 되는 방법은 자원을 사용하고 난 다음에 일일히 메소드로 닫아주는 형태입니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        resource.run(); 					//자원 사용
        resource.close();					//자원 사용 해제
    }
}

 

굉장히 단순해 보이지만 문제가 있습니다. 만약 run이라는 메서드를 사용하는데, 프로그램 내부의 로직 문제든 외부 자원에서 발생한 문제이든 Exception 이 발생 될 수가 있을 것입니다. 그러면 자원 사용을 해제하는 close()에 대한 메소드는 실행되지 않을 것입니다. 

이러한 예상되는 문제점들을 해결하기 위해서 사람들이 활용한 것이 try-catch-finally로 대표되는 자바의 예외 처리 구문을 사용하기로 하였습니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        try {
        	resoure.run();					//자원 사용
        } finally {
        	resource.close();				//자원 사용 해제
        }
    }
}

해당 예시처럼 try 블록을 사용한 다음에 메소드가 작동 여부에 상관 없이 finally 블록에 진입하여 자원을 닫아주게 됩니다.

하지만 이러한 부분도 문제가 크게 두 가지가 존재합니다.

 

1. 깊어지는 Depth의 문제

try - finally 방법을 사용하게 된다면, 자원의 수에 맞게 try-finally 블록이 계속해서 발생하게 됩니다. 만약 우리가 사용할 자원이 두개 이상이 되면, 사용하는 자원의 수인 2개 이상만큼 try - finally를 사용하는 것이 이상적입니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        SecondResource secondResource = new SecondResource(); 
        try {
        	resoure.run();					//자원 사용
            try { 
            	secondResource.run();		//자원 사용
            } finally {
            	secondResource.close();		//자원 사용 해제
            }
        } finally {
        	resource.close();				//자원 사용 해제
        }
    }
}

 

하지만 이런 방향은 코드의 Depth를 늘리게 되고, 이는 객체 지향 프로그래밍에 맞지 않은 결과를 초래할 뿐만이 아니라 가독성이 떨어지는 문제점이 생기게 됩니다.

 

2. 삼켜져 버린 예외 처리

만약 run()이라는 메소드에서 예외가 발생한다면,이 예외가 로그에 기록되는 것이 우리가 바라는 방향입니다. 하지만 try - finally를 사용하게 된다면, run()에서 발생한 예외보단 finally에서 발생한 예외가 기록이 됩니다. 이는 디버깅이나 기능을 구현할 때 예외 처리에 대한 로그 기록이 남지 않으므로 문제가 생길 수 있습니다.

 

개선된 방법 - try with resource

Try with Resource를 사용한 코드를 먼저 보고 가도록 하겠습니다.

public class Main {
	public static void main(String[] args) {
    	try(Resource resource = new Resource();){ //자원 할당
        	resoure.run();						//자원 사용
        }
    }
}

앞서 보여드렸던 기존의 코드와 다른점은 2가지가 있습니다.

첫 번째로는 try 블록안에 자원 할당을 하는 코드가 들어갔습니다.

두 번째로는 resource를 닫아주는 close() 메소드가 사라지게 되었습니다.

 

이로 인해서 저희는 Depth에 관한 문제와 예외 처리에 관한 문제를 동시에 해결할 수가 있습니다.

만약 여기서 발생한 예외에 대해서 잡고 싶다면

try 뒤의 블록에 

} catch (ResourceException resourceException) {
	//예외 처리 로직
}

을 진행해주시면 됩니다.

 

하지만 이렇게 좋아 보이는 try-with-resource 방법은 자원에 대한 객체면 다 쓸 수 있는 것이 아닙니다.

특정 인터페이스를 구현해주어야하는데 그것이 바로 다음에 언급할 AutoCloseable입니다.

 

 

Interface AutoCloseable, Method close

 

//java.lang의 패키지라 따로 import할 필요없습니다.

public Resource implements AutoCloseable {

	public void run() throws ResourceExcpetion {
    	//해당 자원에 관련된 로직을 실행하는 메소드
    }
    
    @Override
    public void close() throws ResourceExcpetion {
    	//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
    }
}

AutoCloseable의 경우에는 close를 추상 메소드로 가지고 있는 인터페이스 입니다.

이 close를 오버라이딩을 통해서 구현해주면 try-with-resource에서 자원을 자동으로 해제해주는 객체를 만족하게 됩니다.

 

 

 

참고 자료

  • Joshua Bloch, "Effective Java", Insight(2018), 47-50

객체를 비교할 때 한 가지 기준만 있는 것은 아니다.

지난번, 글에서는 자바의 객체가 동일한지 아닌지에 대해서 equals와 hashCode라는 메서드를 오버 라이딩하는 것을 알아보게 되었습니다.

https://ksjm0720.tistory.com/28

 

 

[JavaBasic] 객체를 비교하는 방법들(1) - equals, hashCode

자바의 기본 구성 단위 객체 이 때까지 Python과 Javascript와 같은 다른 프로그래밍 언어를 다루어오다가 처음으로 Java를 다루게 되었습니다. 가장 차이가 나는 것은 바로 객체에 대한 상당한 의존

ksjm0720.tistory.com

하지만 이렇게 객체를 동일성을 비교하는 것뿐만이 아니라 서로의 순서를 비교해야할 때가 있습니다.

예시로 앞서 다루었던 코드를 다시 한번 보도록 하겠습니다.

 

public class People {
    private String lastName; // 성
    private String firstName; // 이름
    private int age; //나이
    
    public String getLastName() {
    	return this.lastName;
    }
    
    public String getFirstName() {
    	return this.firstName;
    }
    
    public int getAge() {
    	return this.age;
    }
}

 

여기서 필드는 lastName, firstName, 그리고 age가 존재를 합니다. 이때, 값을 기준으로 비교해주는 sort와 같은 메서드를 사용하게 되었을 때, 문제가 생기게 됩니다. 바로 객체의 어떤 필드를 기준을 하여 비교를 해줘야 하는지를 모른다는 것입니다.

 

"lastName, firstName 그리고 age 중 어떤 것을 기준으로 비교를해야하는 거지?"

 

이는 비단 이런 객체뿐만이 아니라, 원시값을 포장했을 때 필드가 하나여도, 자바에서는 어떤 값을 기준인지 판단하지 못합니다. 때문에 어떤 필드를 기준으로 하여 크기를 비교를 해줘야 할지 정해주어야 합니다.

 

예시 코드와 함께 문제를 해결해나가 보도록 하겠습니다.

 

import java.util.ArrayList;
import java.util.List;

public class Application {
    public static void main(String args[]) {
        People people1 = new People("Ga", "Ga", 10);
        People people2 = new People("Na", "Na", 20);
        People people3 = new People("Da", "Da", 30);

        List<People> peoples = new ArrayList<>();
        peoples.add(people1);
        peoples.add(people2);
        peoples.add(people3);

        peoples.sort();

        for (People people : peoples){
            System.out.println(people.getLastName() + " "+ people.getAge());
        }
    }
}

 

이때, 아직 비교할 기준을 설정해주지 않았기 때문에 people.sort()에서 문제가 생기게 됩니다.

 

사람의 나이를 내림차순으로 정렬하는 예제를 통해서 두 가지 방법에 대해서 알아보도록 하겠습니다.

 

객체에 비교할 수 있는 메서드를 내장시켜주자 - Comparable

 

public class People implements Comparable<People> {
    private String lastName; // 성
    private String firstName; // 이름
    private int age; //나이

    public People(String lastName, String firstName, int age) {
        this.lastName = lastName;
        this.firstName = firstName;
        this.age = age;
    }

    public String getLastName() {
        return this.lastName;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public int getAge() {
        return this.age;
    }

    @Override
    public int compareTo(People o) {
        return o.getAge() - getAge();
    }
}

 

아까와 다르게 객체 People을 정의하는 과정에서 "implements Comparable <People>"이라는 부분이 추가된 것을 확인할 수가 있습니다.

 

implements Comparable이라는 부분은 객체에서 인터페이스 Comparable를 구현해주겠다는 것을 의미합니다. 이때, 인터페이스는 실질적인 구현 대신에 명세서처럼 메서드 선언을 가지고 있다는 것을 의미합니다.

인터페이스 Comparable을 implements 한 People은 Comparable이 추상적으로 가지고 있는 compareTo를 자신만의 기준에 맞게 구현하게 됩니다.

여기서 Comparable <People>에서 나오는 괄호 기호 <>은 자바의 기능 중 하나인 Generic으로써, 외부에서 타입을 정할 수 있게 해주는 기능입니다. 이때 저희는 <People>로 지정을 하였으니, Comparable라는 인터페이스 기준으로 한 외부의 관점에서 People이라는 타입을 사용하겠다!라는 것이 됩니다.

우리가 볼 수 있는 코드에서 다음의 부분인 

 

    @Override
    public int compareTo(People o) {
        return o.getAge() - getAge();
    }

 

이 부분이 compareTo를 오버 라이딩하여 실질적으로 구현한 부분입니다.

객체에서 비교할 다른 People인 o가 앞에 가게 되면 내림차순, 반대면 오름차순으로 정렬이 되도록 비교를 하게 됩니다.

 

 

나이의 내림 차순으로 비교되는 결과 값

 

또한, compareTo에서 우리가 원하는 데로 기준을 만들 수 있다는 점에 의하여, 어떤 필드를 비교할 기준으로 잡을지, 더 나아가서 String 타입을 가진 필드도 기준으로 잡아 정렬을 할 수가 있습니다.

 

객체를 비교할 수 있는 또 다른 객체를 만들어주자 - Comparator

객체 자체에 비교할 수 있는 기준을 만드는 Comparable과 다르게, Comparator는 특정 객체를 대상으로 비교를 할 수 있는 역할을 가지는 객체를 의미합니다.

이때, 이 객체는 마찬가지로 Generic 기능을 통해 외부에서 데이터 타입을 지정해주게 되며, 여기는 new Comparator <People>로 구현이 되었습니다.

 

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Application {
    public static void main(String args[]) {
        People people1 = new People("Ga", "Ga", 10);
        People people2 = new People("Na", "Na", 20);
        People people3 = new People("Da", "Da", 30);

        List<People> peoples = new ArrayList<>();
        peoples.add(people1);
        peoples.add(people2);
        peoples.add(people3);

        Comparator<People> comparator = new Comparator<People>() {
            @Override
            public int compare(People o1, People o2) {
                return o2.getAge() - o1.getAge();
            }
        };

        //Collcections.sorts(peoples, comparator);
        peoples.sort(comparator);

        for (People people : peoples){
            System.out.println(people.getLastName() + " "+ people.getAge());
        }
    }
}

 

 

여기서 Comparator라는 객체를 가져와서 구현을 하였는데, 오버 라이딩을 통해서 compare라는 메서드를 재 구현한 것을 알 수가 있습니다. 

 

 

Comparator<People> comparator = new Comparator<People>() {
  @Override
  public int compare(People o1, People o2) {
    return o2.getAge() - o1.getAge();
  }
};

 

이때, 내림 차순을 하기 위해서는 뒤의 People o2에서 앞의 People i1를 비교해주어야 하며 (o2.getAge() - o1.getAge())

오름 차순을 위해서는 앞의 People인 o1에서 뒤의 People o2를 빼주어야 합니다. (o1.getAge() - o2.getAge())

 

Comparator<People> comparator = (o1, o2) -> o2.getAge() - o1.getAge();

 

다음과 같이 람다식으로 깔끔하게도 표현이 가능합니다!

 

        //Collcections.sorts(peoples, comparator); //Comparator 사용
        peoples.sort(comparator); //List API 사용

 

Collections.sort()를 바로 List에 사용하던 Comparable과 달리 여기서는 Comparator를 List의 정렬 API인 sort()에 인자로 넣어주거나, Collection.sort에서 마찬 가지로 우리가 원하는 방향으로 정의된 Comparator를 추가해주면 됩니다.

자바의 기본 구성 단위 객체

이 때까지 Python과 Javascript와 같은 다른 프로그래밍 언어를 다루어오다가 처음으로 Java를 다루게 되었습니다.
가장 차이가 나는 것은 바로 객체에 대한 상당한 의존성이 높다는 점입니다.
사소한 함수 하나를 사용하고 싶어도, 객체를 생성한 다음에 메소드로 구현해야한다는 점은 상당히 번거로운 일이었습니다.
우테코 프리코스 때에는 이러한 자바의 특성에 익숙해졌지만, 어려움을 겪었던 점이 있었습니다.



"원시 값을 포장하여 사용하라"



이게 어려웠던 점은 우리가 "같다" 라는 것을 정의하는 방법에는 많은 방법이 있는데, 자바의 객체로 구성되는 특성상 원시 값으로 포장할 경우에 String을 사용할 때 같음을 비교했던 "equals" 나 정수 값을 비교할 때 사용했던 "==" 이 통하지 않을 것을 알고 있었기 때문입니다.

@Override, 상속 등에 대한 개념도 너무나도 어려웠고, 이것을 왜 쓰는지에 대해서 감이 긴가민가 하던 시절이라 프리코스를 진행하면서는 원시 값 포장의 필요성만 깨닫고 넘겨버렸습니다. 하지만 자바를 본격적으로 다루면서 많은 원시 값 포장을 하게 되었고, 각 객체들의 동일성을 확인할 필요성이 생겼습니다.

객체의 구성요소 중 필드

 

저희는 객체에서 상태를 가질 수 있는 필드라는 값이 있습니다. 필드는 객체당 여러 항목들을 가질 수가 있는데요.
사람을 예시로 한 번 들어보겠습니다.

 

public class People {
    private String lastName; // 성
    private String firstName; // 이름
    private int age; //나이
    
    public String getLastName() {
    	return this.lastName;
    }
    
    public String getFirstName() {
    	return this.firstName;
    }
    
    public int getAge() {
    	return this.age;
    }
}

 

사람은 이렇게 이름으로써 성과 이름을 가지게 됩니다. 

이 때, 우리는 getter를 사용하여 어찌어찌 성과 이름을 비교를 할 수 있게 됩니다. 

 

KSJM.getLastName.equals(JMKS.getLastName)
KSJM.getFirstName.equals(JMKS.getLastName)
KSJM.getAge == JMKS.getAge

 

이렇게 String일 때 사용한다면 각각의 필드 값들은 같다고 나오게 될 것입니다. 

 

하지만 이러한 방식에는 몇 가지 문제점들이 예상이 되었습니다.

 

1. 객체들간의 비교를 할 시에는 하나만 비교하는 것이 아닌 다양한 필드 값들을 동시에 한꺼번에 비교해야만 한다.

우리가 사람을 비교할 때에는 성과 이름을 동시에 비교를 해야합니다. (물론 동명이인일수도 있지만)

하나 하나씩 비교하는 것은 이것을 수행하는 함수를 따로 만들어야하고, 이는 복잡함으로 이어집니다.

 

2. 각각의 필드 타입들의 경우에는 다양한 비교 기준을 가지고 있다.

예시에서도 볼 수 있듯이, Int와 String의 경우에는 동일성을 비교할 때, 다른 방법 "equals" vs "=="를 사용하고 있습니다. 만약 필드 값을 다 가져와서 비교를 하게 되면, 다른 비교 기준을 모두 맞추어 주어야합니다.

 

이러한 문제점들을 해결해주기 위해 사용되는 것이 바로 equals와 hashCode입니다.

 

객체의 단순 비교를 도와주는 equals

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        People people = (People) o;
        return age == people.age && Objects.equals(lastName, people.lastName)
                && Objects.equals(firstName, people.firstName);
    }

 

위에서도 볼 수 있듯이, @Override라는 Annotation을 통해서 상위 객체에서 메서드를 상속받아 재구현을 해준 것을 알 수가 있습니다. 이 때, Object는 모든 객체들의 정점에 있는 객체로써, 저희가 구현하는 객체나 가져오는 객체들의 첫 시작이라고 보시면 됩니다. 즉, 저희가 구현한 People도 코드에서는 작성을 하지 않았지만, 자동으로 위에서 상속받아 사용하게 됩니다. Object라는 객체는 상속하되, equals라는 메소드는 우리가 정의한 객체에 맞게 재정의하여 사용하겠다. 자바 코드 내에서도 equals를 사용해달라! 라는 의미를 담고 있습니다.

 

이 때 자세히 코드를 보시면 저희 People 객체의 여러 필드들이 보이게 됩니다.

 

1. int로 이루어진 age의 경우에는 기존의 == 를 사용하였습니다.

2. String으로 이루어진 firstName, lastName의 경우에는 equals를 사용하였습니다.

 

이를 통해서 객체들의 필드의 특성들을 고려해줌과 동시에 한꺼번에 모든 필드 값들을 비교할 수가 있게 되었습니다.

하지만 그럼에도 불구하고, 불안한 요소들이 있는데요. 

객체들이 인스턴스화될 경우에는 각 메모리에 저장이 되는데, 이러한 주소에 대해서는 신경을 쓰지 못한다는 점입니다.

즉, 동명이인에다가 나이가 같은 사람이 있으면 무조건 같은 사람으로 인식할 위험이 있습니다. 

이 점은 여러 자료 구조에서는 치명적인 결과를 낼 수가 있는데요. (중복이 안된다던가, 정렬이 힘들다던가)

이를 해결하기 위해서 같이 사용하는 것이 hashCode입니다.

 

동일한 메모리 주소를 인지를 확인해주는 hashCode

 

    @Override
    public int hashCode() {
        return Objects.hash(lastName, firstName, age);
    }

 

 

hashCode의 경우에는 객체가 동일한 메모리를 참조하고 있는지에 대한 내용입니다.

만약 김, 철수, 30세 라는 사람이 전국에 여러명 있을 경우에는 다르게 인식을 해주어야하는데, 자바에서는 캐싱을 사용하고 있지 않는 이상 필드는 같아도 다른 메모리에 저장이 되게 됩니다.

이 때문에 equals로 겉보기 필드를 비교하면서도 진짜 고유값이 맞는지를 확인해주는 메소드가 hashCode입니다.

저희가 사실 구현에서는 직접 볼 일이 없지만, 동일성을 확인하다가 일어날 수 있는 부작용을 확인하기 위해서 사용됩니다.

자료 구조에서는 HashMap 등 Hash를 사용하여 정렬하고 저장하는 자료구조에서 key로서 사용하게 됩니다. 이 때, 각각의 equals에서 필드의 동일성을 확인했음에도 불구하고, hashCode를 정의해놓지 않으면 다른 객체로 인식하는 문제가 생깁니다. equals를 정의를 해주셨다면, hashCode도 같이 정의를 해주는 것이 좋습니다.

+ Recent posts