Java의 '+'에서 컴파일까지

MeteorLee·어제
0

Java '+'는 JVM에서 어떻게 작동할까?

메모리적 이득이라니??

이 내용의 시작은 내가 듣던 강의에서 강사님이 알려준 방식이었다.

이 방식이 더 메모리적으로 이득이니까 이 방법을 사용하도록 하세요

public void updateArticle(ArticleDto dto) {
        try {
            Article article = articleRepository.getReferenceById(dto.id());
            if (dto.title() != null) article.setTitle(dto.title());
            if (dto.content() != null) article.setContent(dto.content());
            article.setHashtag(article.getHashtag());
        } catch (EntityNotFoundException e) {
            log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}", dto);
        }
    }

위 코드는 게시글을 업데이트하는 메서드다.

메서드는 게시글을 찾을 수 없는 경우 exception이 발생하고 catch에서 로그를 찍는다. 이 로그를 찍는 상황에서 log.warn() 메서드 안에 들어갈 문자열을 적는 방식에서 메모리적 이득과 손해가 발생한다는 것이다.

'+' 방식과 {} 방식

log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}", dto); - 이득
log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: " + dto);  - 손해

문자열을 작성하는 방식에서 메모리적 이득이 발생한다는 것이 이해가 가지 않았기에 이렇게 글을 작성하게 되었다.

이해하지 못하겠으면 찾아봐야지?

평소에 사용하던 방식

문자열과 문자열을 더하거나 문자열 + 변수, 숫자 등을 더하는 방식은 대부분 '+'를 많이 사용하였다. 이유는 간단하다.

편하니까

'+'를 사용하는 방식이 어떤 상황에서든 손해가 없다는 것이 아니라는 것은 알고 있었다. 예시로 코딩 테스트를 하다보면 '+' 연산자를 사용할 때 StringBuilder를 보다 메모리나 연산 시간에서 손해를 보는 것으로 알 수 있다. 하지만 이런 상황은 반복문 안에서 잦은 연산이 발생하는 경우에나 발생하는 것으로 알고 있었다.

그래서 이번 기회에 제대로 한번 파보려고 한다.

String Concatenation

내가 가장 자주 사용하는 '+' 연산자를 사용하는 방식으로 String Concatenation라는 이름을 가졌다. 문자열을 연결한다는 의미를 가졌다.

+를 이용한 STring Concatenation은 이런 특성을 가졌다.

1. 자바에서는 자체적으로 '+' 연산자를 처리하는 것이 아닌 StringBuilder를 이용하여 작동한다.

다른 언어와 다르게 자바는 String이 문자열인 객체다. 따라서 객체와 객체를 더해주는 과정은 단순한 연산으로 처리하는 데 문제가 있다. 하지만 자바에서 단순하게 '+'를 사용해도 문자열을 더하는 데 문제가 없도록 자체적으로 처리를 해준다. 이런 자체적인 처리에서 사용되는 것이 StringBuilder다.

2. String은 불변(immutable) 객체이기에 새로 만드는 String은 메모리적으로 손해

자바 내에서 자체적으로 StringBuilder를 사용하여 변환해주기에 간단한 내용은 사용해도 문제가 없다. 하지만 자바에서 String은 단순한 문자열이 아닌 하나의 객체다. 따라서 객체를 계속해서 만드는 과정이 들어가 있기 때문에 반복문 안에서 사용하는 것은 주의가 필요하다.

String Interpolation

일단 강사님이 알려주신 방식인

메서드(변수가 포함된 문자열, 변수);

이 방식의 이름에 대해서 찾아 보았다.

이 방식은 알고는 있지만 굳이 잘 사용하지 않는 방식이었다. 굳이 라는 느낌이 강한 방식이라 코딩 테스트를 할 때만 가끔 활용할 뿐이었다. 일단 이름을 찾으니 String Interpolation라는 이름의 방식이었다.

String Interpolation은 이러한 특성들을 가졌다.

문자열 보간은 결국 문자열 연결로 구현된다.

문자열 보간은 결국 내부적으로 문자열 연결(String Concatenation)로 구현됩니다. 문자열 보간은 단지 개발자가 더 간결하고 직관적인 구문으로 문자열을 구성할 수 있도록 제공되는 문법적 편의일 뿐, 최종적으로는 문자열 연결을 통해 결과가 만들어지기 때문입니다.

chatGPT를 통해 얻은 답변으로 개발자가 문법적인 편의성을 활욜할 수 있도록 하는 하나의 방법으로 설명된다. 하지만 결국 내부적인 구현은 위의 '+'를 활용한 방법과 같은 StringBuilder와 같은 방법으로 구현된다.

그렇다면 어떻게 두 개의 방식이 메모리적 성능에 영향을 끼치는 걸까?

Lazy Evaluation

Lazy Evaluation

한국말로 하면 지연 연산 정도로 해석될 것 같다. 무엇을 지연한다고 하면 바로

메서드가 실행되기 전 까지는 컴파일 과정을 겪지 않는 것이다.

간단하면서도 어려운 것 같다. 컴파일이 되지 않는 다는 내용이 그러면 이 메서드가 실행되는 상황에서 다시 컴파일 된다는 것인가? 라는 의문을 가지게 할 수 있는 내용이라고 생각한다. 하지만 우리가 여기서 생각해야 할 점은 자바는 단순한 컴파일러 언어가 아니라는 것이다.

자바는 컴파일러 + 인터프리터 언어다.

우리가 자바의 코드를 실행하게 되면 먼저 자바의 컴파일러에 의해 컴파일된다. 이후 인터프리터를 통해 실행된다. 자바를 처음 공부할 때 배운 내용이지만 코드를 작성하거나 실행할 때 한 번도 고려해본 적 없는 지식이었다.

일단 컴파일에 대한 지식이 전무하기에 gpt를 통해서 컴파일 해달라고 해봤다.

log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}", dto); - 이득
log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: " + dto);  - 손해

그러면 위 코드를 컴파일 해보자.

  • String Concatenation
L0
   LINENUMBER 10 L0
   NEW java/lang/StringBuilder
   DUP
   LDC "게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: "
   INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
   ALOAD 1
   INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String;
   INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
   INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
   INVOKEINTERFACE org/slf4j/Logger.warn (Ljava/lang/String;)V
  • String Interpolation
L0
   LINENUMBER 6 L0
   GETSTATIC org/slf4j/LoggerFactory.getLogger(Ljava/lang/Class;)Lorg/slf4j/Logger;
   LDC "게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto: {}"
   ALOAD 1
   INVOKEINTERFACE org/slf4j/Logger.warn (Ljava/lang/String;Ljava/lang/Object;)V

일단 길이나 대략적인 내용만 봐도 +를 이용한 방식이 성능적으로 손해를 볼 것이라고 예상이 가능하다. 좀 더 자세하게 살펴보면 위의 +를 통한 방식은 컴파일 과정에서 StringBuilder를 통해서 이미 문자열을 더하는 과정을 진행한다. 하지만 아래의 String Interpolation는 지연 방식을 이용하기에 문자열을 더하는 과정이 없다.
따라서 자바를 실행하기 위해 컴파일을 하는 시점에서 이미 성능적으로 손해를 볼 수 밖에 없는 것이다.

그러면 log를 찍는 상황에서는 다시 컴파일 하는거야?

내가 공부하면서 많이 헷갈린 부분인데 catch를 통해 log.warn()을 실행하는 상황이 온다면 String Concatenation은 이미 컴파일 했기에 문제가 없지만 String Interpolation는 다시 컴파일 하는 과정을 겪으니까 실행해야하는 상황에서는 오히려 손해가 큰 것이 아닌가 고민을 했었다. 하지만 자바는 인터프리터 또한 가지고 있기에 인터프리터가 런타임 시에 다시 컴파일 하는 과정없이 바이트 코드를 실행해주며 내부에서 문자열을 더하는 연산을 해준다.

따라서 사용도 하지 않는 부분을 컴파일 하는 것을 주의해야한다는 것을 다시 알게 되었다.

응용해보자

Lazy Evaluation을 사용하는 String Interpolation는 사용하지 않는 메서드의 컴파일은 하지 않고 인터프리터를 사용하여 실행

언제 사용하지 않을까?

내가 작성한 코드가 매번 매 순간 다 사용되는 것은 아니지만 나름의 분기점에서 사용과 사용하지 않는 비사용으로 나눌 수 있다고 생각한다.

try - catch

위의 예시처럼 예외 발생 시에만 사용되는 코드가 바로 사용되지 않는 시점이 많은 코드일 것이다. 따라서 예외가 발생하여야만 사용이 되는 코드에는 Lazy Evaluation이 적용되는 코드를 작성하는 것이 좋을 것이다.

log

로그는 보통 레벨에 따라 사용과 비사용이 나뉜다. 만약 로그의 레벨을 warn으로 한다면 info 레벨의 로그는 무슨 일이 있어도 작동하지 않을 것이므로 Lazy Evaluation이 적용되는 코드를 작성하는 것이 좋을 것이다. 그렇지 않으면 모든 로그가 있는 부분에서 컴파일 과정에서 StringBuilder를 통해서 문자열을 더하는 과정을 넣어줄 것이고 이는 메모리적으로 손해를 일으킬 것이다.

if - else

if-else 상황에서도 사용되지 않는 쪽은 StringBuilder를 만들지 않아 메모리적으로 손해를 보는 경우가 발생하지 않을 것이다.

마치며

공부를 하며 느낀 점

강사님이 지나가며 말해주신 메모리적 이득이라는 것에 꽂혀 공부를 시작했는데 생각보다 난해한 부분이 많아서 당황했다. 특히 컴파일 부분에서는 무슨 얘기를 하는 건지 몰라서 이곳 저곳 찾아도 보고 gpt한테 비슷한 질문만 계속해서 한 적도 있었다.

하지만 어렵게 공부한만큼 얻어간 것이 많았다고 생각한다.

자바의 메모리 구조를 다시 한번 살펴볼 수 있었고, 컴파일 + 인터프리터 언어라는 것을 상기했고, 문자열을 더하는 과정에 대해 깊게 생각해 볼 수 있었다. 또한 지연의 개념이 컴파일 과정에서 어떻게 발생하는 지 잘 알 수 있었다.

코드 한 줄에도 나름의 이유를 담아

같은 결과를 내는 코드 한 줄에도 나름의 이유를 담아 선택하는 것이 좋은 코딩이라고 생각했지만 실천해 본 것은 이번이 처음인 것 같아서 기분이 많이 좋았다. 앞으로도 코드 한 줄에도 나름의 이유를 담을 수 있도록 노력하려 한다.

profile
코딩 시작
post-custom-banner

0개의 댓글