Maven으로 첫 걸음

이때까지 Java 및 Spring을 이용하여 개발을 진행할 때, 빌드 도구로써 gradle을 많이 사용하였습니다.

gradle의 좋은 점은 상대적으로 패키지 관리 시에 간결하게 정의된다는 점과 Groovy라는 언어를 사용하기 때문에 자바 개발자들에게는 이해하기 쉽다는 점이 있었습니다.

하지만 현재에도 많은 Spring 프로젝트들은 Gradle 보다는 Maven으로 관리가 되고 있습니다.

필자가 새롭게 공부를 시작한 Spring Boot의 교재에서도 Maven을 사용하고 있기도 하고요.

그래서 Maven으로 프로젝트를 구성하면서 새롭게 학습해보기로 하였습니다.

 

*.jar . 빌드 산출물에 접근하여 실행하기

교재에서는 Intellij에서 바로 결과물을 실행할 수 있는 기능보다는 *.jar라는 실행 가능한 Java 클래스 파일 모음 및 meta 데이터 및 리소스 파일 모음을 통하여 실행하도록 하였습니다. gradle을 통해 터미널에서 빌드하는 것과는 또 다른 실행 방법이 필요하였습니다. 먼저, 인텔리제이에서 빌드 결과물을 가져오는 설정은 다음과 같습니다.

 

먼저, Intellij의 탭에서

[File] - [Project Structure] - [Project Settings] - [Artifacts] 로 이동합니다.

그 이후에 [+] 버튼을 눌러 Artifact를 추가해주는데, [JAR] - [From Modules With Depedencies]를 눌러줍니다.

 

 

그 이후에는 빌드할 모듈 및 MainClass를 선택해줍니다.

빌드할 모듈에 관한 META 데이터 경로도 선택해줍시다.

 

 

[OK]를 누른 다음 [Project Structure] 창을 벗어나도록 합시다.

이후에는 Intellij의 탭에서 [Build] - [Build Artifacts] 를 선택하여 빌드를 진행할 수 있습니다.

 

빌드가 끝나면 현재 프로젝트에 [out] 이라는 경로가 생기면서 해당하는 jar 파일이 만들어진 것을 확인할 수가 있습니다.

 

.jar 파일이 제대로 빌드되지 않았다?

하지만 해당 경로에서 .jar 파일을 실행하면 제대로 실행되지 않는 경우가 있습니다.

빌드할 때마다 조금씩 다르게 나오긴 하는데, 빈이 제대로 설정이 되지 않았다는 결과가 많았습니다.

Maven을 쓸 경우에는 위의 방법처럼 Intellij 에서의 빌드보다는 오른쪽 탭에 뜬 [Maven]을 활용하는 것이 좋다는 정보를 얻게 되었습니다.

저희는 [Maven] - [$Project 명] - [LifeCycle] - [package]를 더블 클릭하여 관련 패키지까지 함께 묶인 .jar 형식의 파일을 생성해보도록 하겠습니다.

 

target 밑에 생성된 .jar 형태의 빌드 결과물

이후에는 target 경로 밑에 .jar 결과물이 나오게 됩니다.

해당 방법을 통해서 만든 .jar 파일은 실행 시에 Argument를 넣어도 잘 동작하는 것을 확인할 수가 있었습니다.

 

 

 

 

 

 

단위 테스트를 진행하다 보면

단위 테스트는 타깃이 되는 객체만을 철저히 검증하는 테스트라고 볼 수가 있습니다. 그 때문에 최대한 의존하고 있는 객체에 영향을 받지 않아야 합니다. 그래서 Mock 객체를 활용하여 의존하고 있는 객체의 행동에 대해서 의도대로 정의를 해주고 단위 테스트를 진행하는 경우가 있습니다.
이때, 특정 행동의 결과로서 메소드에 인자로 받기 전에 해당 값을 중간에 값을 받아 검증하고 싶을 때가 있습니다.
그것을 수행해주는 것이 바로 ArgumentCeptor 입니다.

간단한 예시와 함께 코드를 살펴보도록 하겠습니다.

예시 객체

public class Machine {

    private final TwoInserter twoInserter;

    public Machine(TwoInserter twoInserter) {
        this.twoInserter = twoInserter;
    }

    public void addNumbers(List<Integer> numbers) {
        numbers.add(1);
        twoInserter.insertNumber(numbers);
    }
}
public class TwoInserter {
    public void insertNumber(List<Integer> numbers) {
        numbers.add(2);
    }
}

 

이렇게 의존 관계를 가진 객체의 메소드를 테스트하는 케이스가 있다고 가정합시다.

단위 테스트에서는 객체의 복잡한 의존관계에서 벗어나 하나의 객체만 집중적으로 테스트하게 됩니다.

때문에 Mockito 라이브러리의 @Mock과 같은 어노테이션을 사용하기도 합니다.

이번 케이스에서 Machine을 테스트하게 된다면 TwoInserter를 Mocking을 하여 단위 테스트를 진행할 수 있습니다.

하지만 단위테스트 도중 이런 생각이 들지도 모릅니다.

 

    public void addNumbers(List<Integer> numbers) {
        numbers.add(1);
        twoInserter.insertNumber(numbers);
    }

Mock 객체 이전의 행위는 어떻게 테스트하면 좋을까?

만약 반환없이 void로 메서드 타입이 정해진다면 더욱 테스트하기 까다로울지도 모릅니다.

만약 twoInserter의 insertNumber에 들어오는 인자를 확인할 수 있다면 numbers.add(0) 라는 코드를 테스트할 수 있지않을까? 라는 호기심이 들었습니다.

해당 기능을 해주는 것이 오늘의 주제인 ArgumentCaptor를 이용한 메소드에 들어가는 인자값 검증입니다.

 

ArgumentCeptor : 메소드에 들어가는 인자값 검증

ArgumentCatptor의 기능을 활용하면 메소드에 들어가는 인자들을 중간에 가로채어 테스트를 진행할 수가 있습니다.

먼저, 테스트코드에서 해당 기능을 사용하기 위해서는 다음과 같은 정의를 해주어야합니다.

 

    @Captor
    ArgumentCaptor<List<Integer>> listArgumentCaptor;
    
    # ArgumentCaptor<$T> argumentCaptor;
    # T는 가져오고 싶은 인자의 타입형

 

@Mock이나 @InjectMock을 사용하는 것처럼 테스트 코드에 다음과 같은 어노테이션을 가진 ArgumentCaptor를 추가해주면 됩니다. 이 때, 메소드에서 받는 인자가 List<Integer> 형이기 때문에 저희는 Argument<List<Integer>>를 사용하였습니다.

 

그 다음은 중간에 가져올 인자를 받는 메소드를 지정하는 부분입니다.

verify(twoInserter).insertNumber(listArgumentCaptor.capture());

#verify($Target되는 인스턴스).$Target메서드(정의된ArgumentCaptor.capture());

다음과 같이 twoInserter라는 인스턴스의 insertNumber() 메소드에 들어가는 인자를 받아주기 위해서 선언된 ArgumentCaptor 인스턴스 및 capture() 메서드를 넣어주었습니다.

 

List<Integer> argumentNumbers = listArgumentCaptor.getValue();
# $T 받아온 인자 = 선언된 ArgumentCaptor 인스턴스.getValue();

이후에는 다음과 같이 argumentCaptor의 메서드 getValue()를 활용하여 메서드에 전달된 인자를 값으로 할당할 수가 있습니다.

 

이를 검증하기 위한 전체 테스트 코드는 다음과 같습니다.

 

    @Captor
    ArgumentCaptor<List<Integer>> listArgumentCaptor;    
    
    
    @Test
    void test() {
        List<Integer> numbers = new LinkedList<>();

        machine.addNumbers(numbers);

        verify(twoInserter).insertNumber(listArgumentCaptor.capture());
        List<Integer> argumentNumbers = listArgumentCaptor.getValue();
        assertEquals(1, argumentNumbers.get(0));
    }

 

메서드에 두개 이상의 인자를 받고 싶을 때는

메서드에는 하나의 인자가 아닌 여러 개의 인자가 전달될 수도 있습니다.

그럴 때는 메서드 인자만큼 ArgumentCaptor를 추가를 해주어야합니다.

 

@Captor
ArguemntCaptor<T> firstArgumentCaptor;

@Captor
ArgumentCaptor<T> secondArgumentCaptor;

verify(targetInstance).targetMethod(firstArgumentCaptor.capture(), secondArgumentCaptor.capture());

 

HTTP 요청에 따른 결과물 반환

Spring은 백엔드 프레임워크로 많이 사용하게 된다. 때문에 API서버처럼 특정 요청이 들어오면 HTTP에 맞게, 클라이언트에게 반환을 해주어야 합니다.

여기서 스프링을 접하고 나서 모두 아는 단어들이지만, HTTP는 많이는 들어봤지만 어떤 의미인지 모르는 상황이었습니다.

HTTP는 어떤 것일까요?

 

HTTP 응답 요소의 많은 구성

HTTP는 HyperText Transport Protocol의 약자로 웹 통신을 위한 하나의 약속이라고 보면 됩니다.

REST하게 짜져 있으면, HTTP 규격에 맞춘 요청만 보더라도, 어떤 요청인지를 알 수 있다고 합니다. 

 

HTTP의 경우 응답, 요청 모두 크게 세 가지 요소로 나뉘어져 있습니다.

네이버를 접속했을 때의 예시를 먼저 보도록 하겠습니다.

 

먼저 요청입니다.

GET / HTTP/2
Host: www.naver.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: (중략)
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
TE: Trailers

 

첫줄에 Get / HTTP /2 는 HTTP StartLine이라고 합니다.

메소드 및 요청 주소 그리고 HTTP 버전으로 구성되어 있습니다.

 

두번째로는 HTTP Header입니다.

요청 사항 및 요청 보내는 사람의 부가적인 정보들이 들어 있습니다.

 

세번째로는 Body입니다.

요청에 있어서 POST나 PUT과 같은 정보들을 함께 넣어줄 때 쓰이게 되는데 header에서는 보이지 않지만,

보통은 json 형태로 브라우저에서 확인할 수가 있습니다.

 

두 번째로는 응답입니다.

 

HTTP/2 200 OK
server: NWS
date: Sun, 09 May 2021 18:14:40 GMT
content-type: text/html; charset=UTF-8
cache-control: no-cache, no-store, must-revalidate
pragma: no-cache
p3p: CP= 중략
x-frame-options: DENY
x-xss-protection: 1; mode=block
content-encoding: gzip
strict-transport-security: max-age=63072000; includeSubdomains
referrer-policy: unsafe-url
X-Firefox-Spdy: h2

첫 줄에 HTTP/2 200 OK는 HTTP StatusLine이라고 합니다.

방금 주었던 요청에 대한 상태가 저장되어 있습니다.

 

나머지 HTTP Header와 Body는 응답의 경우와 같은 맥락의 정보를 담고 있습니다.

 

응답의 경우에는 우리 백엔드 개발자들의 주 관심분야가 될 것 같습니다. 실제로 생성해서 반환을 해주어야하니까요.

하지만, 저것을 일일히 지정하는 것은 까다로운 일일 것 같습니다.

이것을 Spring-boot에서 쉽게 적용시켜주는 것이 바로, ResponseEntity입니다.

응답을 자동으로 만들어주는 ResponseEntity

ResponseEntity<T>는 제네릭 형식으로 외부에서 Body로 보낼 데이터 형식을 지정이 가능합니다.

@RestController의 Annotation이 붙은 부분에서 많이 사용이 됩니다.

간단한 ResponseEntity 샘플을 보고 가도록 하겠습니다.

public ResponseEntity<String> createResponse(final String sample) {
	return ResponseEntity.ok()
      .body(sample);
}

위의 형식들의 경우에는 "SampleString"이라는 내용을 Body안에 담으며, Status Code는 200, 즉 OK로 발송하겠다라는 내용을 담게 됩니다.

생성자보다는 Builder를

public ResponseEntity<String> createResponse(final String sample) {
	return ResponseEntity.ok()
      .body(sample);
}

생성자로 사용하는 것도 좋지만, 빌더로 사용하는 것을 추천드립니다.

그 이유는 Status Code인데요.

200, 204, 400 그렇게 차이가 크게 나지 않을 것만 같은 숫자 코드들도 제각각의 상태를 가지고 있습니다.

이 때문에 오류나 실수가 날 수 있는 생성자 패턴보다는

빌더로 직접 ok, bad reqeust, no content 등을 지정해줌으로써, 번호에 대해서 실수를 할 것을 막을 수가 있습니다.

시작부터 예외에 대한 글

 

Exception 발생이란

Exception은 Error와 달리, 구현 로직에서 발생하는 프로그램이 모종의 이유에서 처리할 수 없는 현상을 말합니다. 구현 로직에서 발생하는 경우가 많기 때문에 개발자가 어느 정도 예상이 가능한데요. 하지만 Error와 마찬가지로 프로그램이 돌아가지 않는 것은 같습니다. 때문에 우리는 예외 처리를 통해서 예외의 종류별로 어떻게 처리할 것인지에 대해서 정할 수가 있습니다. 가장 유명한 것은 try - catch 블록이 있습니다.

Spring에서는 조금 더 나아가서 일괄적으로 예외를 처리할 수 기능이 있습니다.

 

Try - catch를 쓰는 것도 좋지만

먼저, Controller Advice를 사용하지 않았을때의 문제를 살펴 보도록 하겠습니다.

@Controller
public class Controller{
    @GetMapping("/temporary")
    public String temporary(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (SampleException e) {
        	//예외 처리
        }
        return "temporary";
    }
}

 

위의 코드는 제가 임의로 짠 코드입니다.

별 문제가 없으면 "sample"이라는 내용을 model에 넣어줘서 랜더링하게 되고 temporary라는 html template 형태를 반환할 것이라고 예상이 됩니다.

하지만 코드를 짜면서 저는 여러 가지 불만을 느끼게 되었는데요.

 

1. 코드의 중복

특정 Exception 특히나, 제가 커스터마이징한 Exception에 대한 처리를 일괄적으로 하고 싶습니다. 하지만, 지금의 로직이라면 다음과 같이 각 url을 처리하는 메소드마다 같은 코드를 계속해서 중복해서 넣어줘야하는 문제가 생깁니다.

 

@Controller
public class Controller{
    @GetMapping("/temporary1")
    public String temporaryOne(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (CustomizedException e) {
        	//커스터마이징된 예외 처리						//중복
        } catch (SampleException e) {
        	//예외 처리
        }
        return "temporaryOne";
    }
    
    @GetMapping("/temporary2")
    public String temporaryTwo(Model model) {
        model.addAllAttributes("sample", "sample2");
        try {
        	//로직
        } catch (CustomizedException e) {
        	//커스터마이징된 예외 처리						//중복
        } 
        return "temporaryTwo";
    }
}

코드의 중복 없이 특정 예외에 대해서 일괄적으로 처리를 할 수 있는 로직이나 객체가 필요합니다.

 

2. 예외 발생 시에 반환 타입 변경

현재 로직에서는 String을 반환하도록 되어 있습니다. 하지만, Exception 발생 시에 Exception에 관련된 메세지를 Exception Dto에 담아서 전달을 하고 싶습니다.

 @Controller
 public class Controller{
    @GetMapping("/temporary")
    public String temporary(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (SampleException e) {
        	return new ExceptionDto(e.getMessage());	// 예외 발생시에 Dto를 전달하고 싶지만..
        }
        return "temporary";
    }
}

하지만 여기서 반환 Type은 String으로 지정을 해놓았기 때문에 반환하지 않을 가능성이 매우 높습니다.

 

이런 두 가지 문제가 저한테 다가왔었고, 저는 저의 코드 리뷰에서 ControllerAdvice, RestControllerAdvice를 사용한 예외처리를 해보라는 조언을 받게 되었습니다.

@ControllerAdvice, @RestControllerAdvice with @ExceptionHandler

@Controller, @RestController와 마찬가지로, ControllerAdvice는 페이지 렌더링만, @RestControllerAdvice는 json에 여러 정보들을 담아서 반환을 할 수가 있습니다.

 

@RestControllerAdvice
public class RestControllerAdvice {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ExceptionResponseDto> illegalArgumentExceptionResponse(
    IllegalArgumentException illegalArugmentException) {
        String message = illegalArgumentException.getMessage();
        ExceptionResponseDto exceptionResponseDto = new ExceptionResponseDto(message);
        return ResponseEntity.badRequest()
            .body(exceptionResponseDto);
    }
    
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<CustomResponseDto> customExceptionResponse(
    CustomException customException) {
        String message = customException.getMessage();
        CustomResponseDto exceptionResponseDto = new CustomResponseDto(message);
        return ResponseEntity.badRequest()
            .body(exceptionResponseDto);
    }
}

 

먼저, 원래의 Controller에서 예외가 발생하게 됩니다. 그러면 ExceptionHandler에서 해당하는 예외를 감지하게 됩니다. 특정 예외들에 대해서 일괄적으로 똑같이 처리를 할 수 있다는 점에서 앞서 언급한 문제 1번은 해결이 됩니다.

그렇게 예외를 잡게 되면, @RestControllerAdvice에서 처리를 할 수가 있는데요.

여기서도 똑같이 반환형 타입을 바꿀 수가 있습니다. 원래 String만 반환을 할 수가 있었는데, Dto를 활용하여 예외에 대한 내용이나 추가 자료를 같이 섞어서 보낼 수가 있습니다.

이렇게 @ExceptionHandler와 함께 @ControllerAdvice, @RestControllerAdvice를 활용하여, 일괄적으로 예외를 처리할 수 있게 되었습니다.

 

자원이란?

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를 추가해주면 됩니다.

+ Recent posts