728x90

개요

당연하게 사용하던 Java의 람다 기능에 대해서 의문을 갖지 않고 막 써댔다.

그렇지만, 면접에서의 질문을 받았을 때 당황했다. 뜬금포로 final을 쓰고 안쓰고의 차이를 물어보셨는데 순간 답변을 하지 못했다.

그래서 정리해본다.

 

람다 캡쳐링에 대해서 알아보기 이전에 Effectively final에 대해 먼저 알아보자.

Effectively Final 이란?

해당 단어를 deepL을 통해 해석해보면 사실상 최종 이라고 해석해준다.

final을 선언한 상수와 같이 변경되지 않았다면 그와 같은 수준으로 컴파일러가 해석해준다.

 

effectively final이 되려면 아래의 3가지 조건을 만족해야 한다.

아래 3가지 조건은 공식문서를 통해서 나와있는 정보들이다.

  1. 명시적인 final을 선언하지 않았다.
  2. 재 할당을 하지 않아야 한다.
  3. 접두 또는 후미에 증감연산자를 추가해서 데이터를 바꾸지 않아야한다.

객체라면 참조 주소값만 바뀌지 않는다면 그대로 계속 effectively final 로 유지할 수 있다.

 

정상적인 Effectively Final

정상적인 lambda식

Effectively Final이 제대로 되지않은 경우

비정상적인 lambda식

자바가 친절하게 설명을 해준다.

 

자, 이제 변수값을 내부에서 변경하지 않으면 잠정 final로 보고 람다식에 데이터를 명확하게 넣어줄 수 있다.

Lambda Capturing에 대해 알아볼 시간이다!

Lambda Capturing이란?

외부에서 정의한 변수를 사용할 때 람다식(익명 클래스의 function)에서 복사본을 생성하게 된다.

외부라는 의미는 지역변수나 전역변수(인스턴스)와 클래스 변수들을 전부 아우르는 표현이다.

그럼 Capturing을 적용하지 않는 경우도 있을거 아닌가?

당연히 사용하지 않을 수 있다. 그래서 변수를 넣지 않고 동작할 수 있는데 이때는 외부의 변수를 주입받아 사용하는게 아니라서 캡쳐링이

적용되지 않아서 non-capturing이라고 한다.

Lambda Capturing은 왜 복사본을 만드는가?

일단 지역변수는 메모리 구조상 스택 영역에 할당된다.

스택은 스레드가 실행됐을 때 고유한 영역으로 가지고 있게된다. 그래서 스레드끼리는 공유할 수 없고, 스레드가 종료되면 해당 스택 영역도 사라진다.

여기서 이제 문제가 발생하는 것이다.

 

아래 코드에서 ss 라는 문자열을 복사해서 갖고있지 않는다면 new Thread 부분에선 지역변수로 묶여있는 test()가 스레드보다 더 빨리 수행되고 끝날 가능성이 존재하기 때문에 null을 줄 수도 있을 것이다. 이렇기 때문에 복사본을 만들어 유지하는 것이다.

public void test() {
    String ss = "test";

    new Thread(() -> {
        try {
            Thread.sleep(1000L);
            System.out.println("thread1 ss : " + ss);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    System.out.println("ss : " + ss);
}

 

여기서 근데 effectively final이어야 하는 이유는 위에서 봤듯이 멀티 스레드 환경에서 람다식이 동작할 수 있기 때문에

지역변수는 또 스레드마다 공유하지 않기도 한다. 때문에 어떤 복사본이 최신인지를 자바 입장에서는 확인할 방법이 없기 때문에 final변수로만 지역변수를 사용해야 하는것이다.

조용하던 인스턴스 변수나 클래스 변수는?

인스턴스 변수는 메모리 구조상 힙에 할당된다.

우리는 알고있다. 힙은? -> 모든 스레드가 공유할 수 있는 메모리이다.

클래스변수는 static 변수가 이 부분에 포함되는데, 메소드 영역에 할당이 된다.

그래서 값이 바뀌던 말던 그 데이터는 항상 같게끔 유지할 수 있기 때문에 할당할 수 있는 것이다.

정리

람다식 내부에서 지역변수를 사용하는 경우 final이나 effectively final 변수를 사용해야 한다.

-> 이유는 메모리 구조의 stack 영역에 저장되기 때문

final이 아니라면 복사되는 값이 어떤 스레드에서 바꾼것이 가장 최신의 복사본인지 알 수 있는 방도가 전혀 없다.

 

728x90

'Java' 카테고리의 다른 글

참조 유형  (0) 2022.09.12
Checked Exception, Unchecked Exception  (0) 2022.09.07
변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
728x90

이펙티브 자바를 읽다가 약한참조에 대한 이야기가 나와서 포스팅한다.

참조에는 아래 4가지가 존재한다.

  • Strong References (강한 참조)
  • Soft References (소프트 참조)
  • Weak References (약한 참조)
  • Phantom References (팬텀 참조)

이 해당 참조 유형에 따라 GC 실행 대상여부, 시점이 달라진다.

강한참조

new 연산자를 사용하여 객체를 인스턴스화 하고 참조하는 방식.

참조가 해제되지 않으면 GC의 대상이 되지 않는다.

Test test = new Test();

해당 test라는 변수가 참조를 가지고 있다면 GC의 대상이 되지 않는다.

test = null이 되는 순간 GC의 대상이 된다.

 

소프트참조

대상 객체의 참조가 SoftReference만 있다면 GC의 대상이 된다.

단, JVM 메모리가 부족한 경우에만 Heap에서 제거된다.

메모리가 부족하지 않은경우에는 제거하지 않는다.

 

public static void main(String[] args) {
        String ss = "문자열";
        SoftReference<String> reference = new SoftReference<>(ss);

        // 이 시점에 GC의 실행 대상이 가능
        ss = null;

        System.gc();

        // JVM의 메모리가 부족하지 않아서 GC 실행 대상이 되지 않은 경우
        // 그대로 유지한다.
        ss = reference.get();
        System.out.println(ss);
}

약한참조

위에서 봤던 소프트참조와 비슷하게 

대상 객체의 참조가 WeakReference만 있다면 GC의 대상이 된다.

다른점은, 메모리가 부족한경우가 아니라 다음 GC가 일어나게 되면 바로 힙에서 제거된다.

 

public static void main(String[] args) {
        String ss = "문자열";
        WeakReference<String> reference = new WeakReference<>(ss);

        // 이 시점에 GC의 실행 대상이 가능
        ss = null;

        System.gc();

        // gc를 명시적으로 호출했지만 컬렉션이 동작하지 않을수도 있음
        // 그래도 무조건 동작한다고 가정
        ss = reference.get();

        // null 로 비어있게 된다.
        System.out.println(ss);
}

팬텀참조

생성시 ReferenceQueue가 필요하며, PhantomReference의 참조값을 수동으로 clear() 메서드를 실행해야 하고, PhantomReference.get() 메서드는 항상 null을 반환한다는 특징이 있다.

 

PhantomReference는 객체 내부의 참조를 null로 설정하지 않고 참조된 객체를 phantomly reachable 객체로 만든 이후에 ReferenceQueue에 enqueue 된다.

 

두가지에서 사용한다.

  1. 자원 정리 (finalizer 보다는 조금 나은 방법) 그렇지만 try-with-resources를 사용하자.
  2. 생성 비용이 비싼 객체가 언제 메모리에서 해제되는지 알 수 있음.

깃허브 바로가기

 

GitHub - lsj8367/laboratory: 뭔가를 연습해보기 위한 연구 저장소

뭔가를 연습해보기 위한 연구 저장소. Contribute to lsj8367/laboratory development by creating an account on GitHub.

github.com

 

728x90

'Java' 카테고리의 다른 글

effectively final 및 lambda capturing에 대해 톺아보기  (2) 2023.12.08
Checked Exception, Unchecked Exception  (0) 2022.09.07
변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
728x90

예외를 알아보기 전에

에러와 예외의 차이부터 알아보도록 하자.

에러

일단 에러(Error)는 시스템이 비정상적인 상황에 발생하게 된다.

수습할 수 없는 상황에 놓이게되어 개발자가 예측하지 못한경우이다.

예외

예외는 개발자가 구현한 로직에서 발생된 실수나 사용자의 영향이 미쳐 발생하게 되는 것이다.

그렇기에 미리 예측해서 방지할 수가 있다.

이펙티브 자바를 회독하며 스터디를 진행하면서 2장에

IllegalArgumentException이 나오게 되어 이 예외 부분을 정리하게 됐다.

Checked, Unchecked Exception

기본적으로 오류, 예외는 Object를 상속받는 Throwable클래스를 상속받아 구현이 되어있다.

이미지 출처 - https://www.programcreek.com/2009/02/diagram-for-hierarchy-of-exception-classes/

그림을보면 Exception을 포함해 아래 뿌리로 빨간색으로 표시된 예외들은 모두

Checked Exception이다.

푸른색은 전부 Unchecked Exception이다.

RuntimeException을 상속받는 클래스들이면 하나같이 Unchecked Exception이라 할 수 있겠다.

보면 알겠지만, 프로그램이 정상 작동중에 실행될 수 있는 예외라는 뜻이다.

차이점

둘의 큰 차이점은 check, uncheck 둘다 throws로 Exception을 처리하게끔 메소드에 달아주어도

명시적으로 컴파일에서부터 에러를 띄워주는것은 Checked Exception이고 throws로 던져지는 예외는 모두 반드시 처리해주어야 한다.
(상위로 던지거나 자신의 위치에서 try~catch로 처리해주거나)

RuntimeException하위 예외들도 throws에 넣어줄 수 있겠지만, 처리해주어야 할 필요는 없을 수 있다.

같이 throws에 넣어주게 되면 이러이러한 예외가 발생한다~ 정도를 나타내는 의미정도로 생각하면 되겠다.

근데 이마저도 너무 많은 예외를 throws에 같이 넣어주면 가독성을 반대로 해칠수도 있다.

예외로 의미있는 어떤 무언가를 반드시 처리해줄 수 있는 로직이라면 Checked Exception

예외 상황이나 문제를 해결할 수 없다면 Unchecked Exception을 활용해 볼 수 있다.

Checked Exception을 던지게 되었을 때 이 예외를 처리하는 무언가의 핸들러까지 던져지게 될텐데 (무지성 throws)

이럴때 그냥 try~catch해주면서 catch부분에 해당 Checked Exception을 받아서 Unchecked Exception으로 바꾸어 던져주는 방법도 있을 것이다.


트랜잭션 처리에 관하여...

많은 글들에서 트랜잭션 처리 시

  • Checked Exception은 Rollback 하지않음
  • Unchecked Exception은 Rollback 한다
    라고 되어있는데

어떤 트랜잭션인지 명시도 되어있지 않고 무작정 Rollback을 한다 안한다의 여부는 조금 잘못되었다고 백기선님이 말씀하시는걸 보았다.

좀 당연시하게 Spring Framework에서 @Transactional이라고 무작정 생각하고 보면 맞다고 느꼈었는데,

Kafka의 트랜잭션, DB의 트랜잭션 등등 트랜잭션은 정말 많다.

이 문서만 잘 읽어봐도 오해라는거다.

범용적으로 사용하는 Spring Framework에서 사용되고 있는 @Transactional 어노테이션의 롤백 기준은

바로 Unchecked Exception 종류는 기본 Rollback을 진행한다. 반대로 Checked Exception 종류는 Rollback을 하지 않는다.

기본값으로 두개를 나눠서 Check, Uncheck에 대해 트랜잭션 전략을 나누고는 있지만,

어디까지나 기본값이라는거고, Check, Uncheck상관없이 언제든 내가 원하면 해당 예외를 통해 Rollback전략을 매겨줄 수 있다는 것이다.

그러니 CheckUncheck라고 해서 트랜잭션을 롤백한다 안한다의 이분법적 사고는 잘못된 사고라고 이해를 하게 되었다.

728x90

'Java' 카테고리의 다른 글

effectively final 및 lambda capturing에 대해 톺아보기  (2) 2023.12.08
참조 유형  (0) 2022.09.12
변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
728x90

자바 변성 (Variance)

자바의 가변성에는 크게 공변, 무공변, 반공변이 존재한다.
제네릭을 잘 사용하려면 이 가변성에 대한 이해가 필요하다.

변성을 제대로 이해하려면 "타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입인가?" 라는 질문에서 시작하는게 좋다.

배열은 공변, 제네릭은 무공변이 기본이라고 다들 알고 있을 것이다.

무공변 (Invariance) or 불공변

기본적으로 제네릭은 무공변이다.

무공변이라고 하니 헷갈리는것 같다. 사전적으로 번역해보면 불공변으로 나오게 된다.

타입 S가 T의 하위 타입일 때, Box[S]와 Box[T] 사이에 상속 관계가 없는 것

쉽게 말하면 너는너, 나는 나 인 느낌이다.
그래서 선언한 유형만 들어갈 수 있게 코드를 구성할 수 있다.

Object에는 Object만, String에는 String만 들어갈 수 있단 얘기이다.

void invariance() {
    // 제네릭은 기본적으로 무공변
    List<Object> objectList = new ArrayList<>();
    List<String> stringList = new ArrayList<>();

    objectList.add(1);
    objectList.add(1.0);

    stringList.add("aaaaa");
}

공변

공변(covariance)는 타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 하위 타입 임을 나타내는 개념

@Test
void arrayTest() {
    Object[] arr = new Long[5]; //배열에서는 공변이고, Long은 Object의 하위타입이기에 할당이 가능하다.
    arr[0] = "arr"; //공변으로 인해 선언한 arr은 Object로 참조가 된상태라 String도 할당 가능.
    // 여기서 런타임에 ArrayStoreException 발생
}

자바에서 이 배열을 공변으로 열어두지 않았다면, 다형성의 이점을 살릴 수 없게 됐을 수 있다.

Arrays.swap()

Arrays의 메소드를 하나를 가져와봤는데,
만약 공변이 아니었다면, 이 배열 스왑 메소드는 객체별로 전부 구현해주어야 했을 것이다.
제네릭이 있기전엔 형변환에 대한 에러가 나더라도,
다형성의 장점으로 얻을 수 있는 이득이 많았을 것 같다.

리스트의 공변

void variance() {
    List<? extends Object> list = new ArrayList<Number>();
    list.add(1); //컴파일 에러
    list.add(1.0); //컴파일 에러
    list.get(0); // 정상 로직
}

이처럼 선언을 했을때 add는 선언된 제네릭으로 변수를 넣게 되어있는데,

무공변으로 만들었을 경우

공변인 경우

위와 같은 경우에는 capture of ? extends Object e Object의 하위타입은 맞지만,

어떤 타입인지는 모른다? 라는 뜻이라고 생각된다.

그래서 list.get(0)이 Object로 형변환은 가능하지만, 반대로 add()를 통해 null을 제외한 무언가를 추가해줄 수는 없다는 소리이다. 안에 들어가는 객체가 정확하게 뭔지 모르기 때문이다.

그래서 정확한 타입이 어떤건지는 모르기 때문에 개발자가 null을 제외하고는 아무것도 추가하지 못하게 막을 수 있다라고 봐도 될 것 같다.
그래서 자주쓰던 Collections 클래스의 UnmodifiableList를 찾아보게 되었다.

생성자에 이런식으로 공변을 이용해서 막아주고 있는것을 볼 수 있었다.
그러면서 List의 구현체이기 때문에 밖에서는 add에 어떤 값을 넣어줄 수는 있기에, 그대로

Override로 재정의 한 뒤에 Exception을 던져주게 만든것을 확인할 수 있었다.

반공변(Contravariance)

반공변 처음 봤을때 반만 된다 이런생각을 했었다.ㅋㅋㅋㅋㅋㅋㅋ

그게 아니라 공변의 반대

타입 S가 T의 하위 타입일 때, Box[S]가 Box[T]의 상위 타입 임을 나타내는 개념입니다.

@Test
void contravariance() {
    List<? super Number> list = new ArrayList<>();
    list.add(1.0);

    final Number number = (Number) list.get(0);
    final Object object = list.get(0);
}

Number를 포함한 Number의 상위 타입들만 들어갈 수 있게 설정한 상태이다.

아까는 하위타입이 뭔지 알 수가 없다는 것이었는데,

이 코드는 Number 상위인건 알겠는데 상위 누구인지를 알 수 없는 상태이다.

super키워드 다음에 붙은 클래스까지의 형은 전부 넣을 수 있다는 소리와도 같다.

다시말하면, 최소 Number 타입은 보장이 된다는 소리와 같다.
그래서 list.get(0); 에서 최상 타입인 Object로 꺼내서

형에 맞는 캐스팅 or instanceof를 통해 값을 읽어오는게 가능하다.

마무리

이렇게 자바의 가변성에 대해 알아보았다.
얼추 정리되면서 감은 잡은것 같다.
PECS(Producer Extends Consumer Super)를 보면서,
일반적으로 소비(Consume)라는게 스타크래프트의 디파일러가 저글링을 컨슘해서 저글링을 잡아먹기때문에,

스타크래프트의 컨슘

어떤 컬렉션이 만들어지는 과정이 컨슘이라고 생각하고 값을 빼내는 과정(get)이 동작한다고 알고 있었다.
반대로 생산자(Producer)는 말그대로 생산이기에 값을 생성해주는(new) or 더해주는(add) 것이 생산자로 알고 있었다.
반대로 알고있던 것이다.

올바른 내용

컬렉션을 뒤져서 어떤 작업들을 처리 해주어야 한다면 그게 바로 컬렉션 값을 빼내(get) 뭔가를 만들기 때문에 생산자가 되어 extends를 사용해야 한다는 것이고,

컬렉션에 값을 추가해야되면 매개변수로 주어진 값이 소비되어 컬렉션에 들어가니(add) 소비자 관점이라고 보는것 같다.

그래서 이 경우에는 super를 사용해주면 되겠다.

휴..되게 어렵다 😇😇😇

아무튼 읽기전용으로 만들고 싶을때에는 extends를 사용하는것.

좀더 안전하게 데이터 삽입을 하고싶다면 super를 사용하는 것만 기억하면 될 것 같다.

728x90

'Java' 카테고리의 다른 글

참조 유형  (0) 2022.09.12
Checked Exception, Unchecked Exception  (0) 2022.09.07
일급 컬렉션  (0) 2022.08.09
변수  (0) 2022.08.07
728x90

일급 컬렉션

일단 나는 넥스트스텝의 TDD, Clean Code with Java 12기를 하면서

여기서도 일급 컬렉션을 사용했었다.

참 웃겼던건 이것을 조금 응용을 했었어야 했는데 개념 자체도 자세하게 정리가 덜 된것 같았다.

도메인에서부터 차근차근 만들어가는 것에서는 어느정도 생각이 잘 들었지만,

기존 레거시 코드에 이런게 적용되어 있지 않고 뚱뚱하게 로직이 작성되어 있으면

그냥 넘어갔던게 흔했다.

객체지향 생활체조 원칙

소트웍스 앤솔러지에서 발췌된 객체지향 생활체조 원칙이다.

  • 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  • 규칙 2: else 예약어를 쓰지 않는다.
  • 규칙 3: 모든 원시값과 문자열을 포장한다.
  • 규칙 4: 한 줄에 점을 하나만 찍는다.
  • 규칙 5: 줄여쓰지 않는다(축약 금지).
  • 규칙 6: 모든 엔티티를 작게 유지한다.
  • 규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 규칙 8: 일급 컬렉션을 쓴다.
  • 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

진하게 설정해 놓은 것들은 웬만하면 거의 적용하고 있는 규칙들이다.

단, 9번 규칙에서 게터는 어쩔수 없이 사용하게 되는것 같다.

여기서 이 8번 규칙을 적용하는게 위에서 말했던 이유이다.

일급 컬렉션을 쓴다.

이 규칙 적용하는 법은 간단하다.

컬렉션을 포함한 클래스는 반드시 다른 멤버 변수는 존재하지 않아야 한다.

그리고 그 컬렉션만 들어있기 때문에 해당하는 컬렉션만의 로직을 구현할 수 있는 분기점이 생성된 것이다.

예를 한가지 들어보겠다.

public class Car {
    private final String name;
    private final int yearModel;

    public Car(final String name, final int yearModel) {
        this.name = name;
        this.yearModel = yearModel;
    }

    public String getName() {
        return name;
    }
}

이런 자동차 객체를 정의 했을 때, 비즈니스 로직에서 이런 행위들을 했다고 쳐보자.

public class CarService {
    public List<Car> findCarName(final String carName) {
        //다른 비즈니스 로직 수행....
        //DB에서 이러한 데이터를 가져왔다고 가정
        List<Car> carList = Arrays.asList(
            new Car("아반떼", 2021),
            new Car("k3", 2018),
            new Car("그랜저", 2019),
            new Car("모하비", 2020),
            new Car("아반떼", 2017),
            new Car("아반떼", 2016),
            new Car("아반떼", 2015);
        );

        //다른 비즈니스 로직 수행....

        return carList.stream()
            .filter(car -> car.getName().equals("아반떼"))
            .map(car -> car.getName())
            .collect(Collectors.toList());
    }
}

이런식으로 하나의 로직이 어떤 곳에서 데이터를 불러오고

차들의 이름이 매개변수로 받는 carName인 애들만 추려서 리스트로 만드는

그런 로직이다.

간단해서 나눌 필요가 없다고 생각이 들 수도 있지만, 메소드를 하나의 일만 하게끔 분리했을 때 이런 모양이 나왔다면,

그리고 이 로직들이 안에 갇혀있으면 점점 뚱뚱해지면서 테스트하기가 힘들어지는 로직이 생기게 될 것이다.

그래서 아래와 같이 개선한다.

일급 컬렉션

public class Cars {
    private final List<Car> cars;

    public Cars(List<Car> cars) {
        this.cars = cars;
    }

    public List<Car> findAllByCarName(final String carName) {
        return cars.stream()
            .filter(car -> car.getName().equals("아반떼"))
            .map(car -> car.getName())
            .collect(Collectors.toList())
    }
}

Car객체의 리스트만을 멤버 변수로 갖고있는 일급 컬렉션인 Cars를 구현했고,

여기서 해당하는 비즈니스 로직을 정의해 주었다.

public class CarService {
    public List<Car> findCarName(final String carName) {
        //DB에서 이러한 데이터를 가져왔다고 가정
        Cars cars = new Cars(Arrays.asList(
            new Car("아반떼", 2021),
            new Car("k3", 2018),
            new Car("그랜저", 2019),
            new Car("모하비", 2020),
            new Car("아반떼", 2017),
            new Car("아반떼", 2016),
            new Car("아반떼", 2015);
        ));

        return cars.findAllByCarName(carName);
    }
}

해당하는 조건으로만 생성된 일급 컬렉션이 생겨서 비즈니스 로직에 의존하지 않고

순수 Car 리스트에 대한 테스트만 진행을 해볼 수 있게 되었다.

그리고 불변으로 만들어 주어야 무분별한 수정, 삽입이 안되게끔 만들어 줄 수 있다.

그러기 위해선 일급 컬렉션 객체에서 Getter, Setter같은 메소드들을 제외시키고,

오로지 생성자를 통한 생성, 별도로 구현한 로직외 값을 수정 불가 하게끔 정의해주고 사용한다.

이렇게 되면 일급 컬렉션 + 불변객체 가 성립이된다.

더 나아가서 지금은 차 브랜드를 현대, 기아 두개를 같이 넣었지만

KiaCars, HyundaiCars처럼 일급 컬렉션을 따로 만들어서 관리할 수도 있다.

이렇게 따로 만든채로 다른 비즈니스 로직을 수행하게끔 해줄 수도 있다.

하나 더 나아간다면 또 공통적으로 ~를 한다. 라는 기능이 존재할 경우 인터페이스를 놓고 구현해줌으로써

또 여러 과정을 거치는 방법도 있을 것이다.

마무리

이런 일급 컬렉션을 사용하는 것에 재미가 들렸고 완벽하게라고는 못하겠지만,

점차 쓰는 빈도가 늘어가면서 객체지향, 그리고 리팩토링에 불이 붙을것 같다.

728x90

'Java' 카테고리의 다른 글

Checked Exception, Unchecked Exception  (0) 2022.09.07
변성  (0) 2022.08.11
변수  (0) 2022.08.07
상태 패턴 적용  (0) 2022.08.07
728x90

여기서 나오는 모든 예제는 깃허브에 있다.

📌 변수

변수는 값을 저장하는 공간이다.

📌 변수란?


프로그래밍에서의 변수란, 값을 저장할 수 있는 메모리상의 공간이라고 한다.

이 공간에 저장된 값은 변할 수 있기 때문에, 변수라고 이름이 붙여졌다.

변수란, 단 하나의 값을 저장할 수 있는 메모리 공간

그렇기 때문에, 새로운 값을 저장하면 기존의 값은 날아가게 된다.

📌 변수의 선언과 초기화


📌 변수의 선언

변수의 선언 방법은

public class Class {
    int age; // age라는 이름의 변수 선언
    // int : 변수 타입
    // age : 변수 이름
}

변수 타입은 변수에 저장될 값이 어떤 타입인지를 지정해주는 것이다.

변수 이름 변수의 이름이다. 이 변수 이름 은 메모리 공간에 이름을 붙여주는 것이다.

int age; 메모리 주소지가 1이라고 가정했을 때,

int 타입의 age 변수를 1 주소지에 할당해라 가 되는 것이다.


📌 변수의 초기화

변수를 선언한 이후에는 변수를 사용할 수 있는데, 그전에 반드시 변수를 초기화 시켜주어야 한다.

메모리는 여러 프로그램이 공유하기 때문에, 다른 프로그램에 의해 그 메모리가 알 수 없는 값이 들어있을 수 있기 때문에

반드시 초기화를 해주어야 한다.

여기서 연산자가 나오는데 바로 대입 연산자(=) 이다.

이 대입연산자로 변수에게 값을 할당해줄 수 있다.

int age = 26 → 변수 age를 선언하고 26으로 초기화 시켰다.

같은 타입이라면

int a;
int b;
int x = 0;
int y = 0;

//같은 코드
int a, b;
int x = 0, y = 0;

하지만 코딩 컨벤션은 한줄에 하나를 할당하는게 대부분의 규칙처럼 정형화 되어있다.

이 부분에 대해선 협업하는 사람들에 따라 갈리겠지만 보통의 개발자들은 전자를 선호할 것이다.

변수의 초기화란, 변수를 사용하기 전에 처음으로 값을 저장하는 것

@Test
void test() {
    int year = 0;
    int age = 21;

    year = age + 2000; //변수 age + 2000
    age += 1; //age 저장된 값 1증가

    assertThat(year).isEqualTo(2021);
    assertThat(age).isEqualTo(22);
}

대입 연산자는 바로 수행되는 것이 아니라, 우변에 모든 계산이 끝난 후에 할당을 진행한다.

메서드 안에서 변수를 초기화하는 경우엔 명시적으로 int a = 0; 이렇게 초기화를 진행해주지만,

클래스 변수 즉, 전역변수는 int a; 이렇게 선언만 해주더라도 int 타입에 String을 넣는다거나

혹은 String 타입에 int를 넣는

참사를 방지하기 위해 컴파일러에서 아래에 출력한 0, null처럼 자동으로 값을 할당해 준다.


📌 두 변수의 값 교환하기

너무 유명한 swap 예제이다. CallByValue, CallByReference

조금 원초적으로 책에서는 설명되어있는데,

int a = 10;
int b = 20;

두 값을 서로 바꾸려고 할 때, b에 저장된 값을 a에 해버리면 둘다 20이 될 것이다.

그러기에 중간 변수를 하나 두어 스왑을 해주어야 한다.

int a = 10;
int b = 20;
int temp = 0;

temp = a;
a = b;
b = temp;

이렇게 해서 값을 바꿔주어야 한다.


📌 변수의 타입

값은 크게 문자, 숫자 두가지 부류로 나눌 수 있다.

숫자는 또 실수와 정수로 나뉜다.

이런 값의 종류에 따라서 값이 지정될 공간의 크기, 저장형식을 정의한 것이 자료형이다.

이 자료형에는 문자형(char),정수형(byte, short, int, long), 실수형(float, double) 등이 있다.

저장하려는 값의 특성에 따라 알맞은 자료형을 맞춰주면 된다.

📌 기본형과 참조형

  • 기본형
    • 변수는 값을 실제 값을 저장
    • boolean, char, byte, short, int, long, float, double
  • 참조형
    • 값이 저장되어 있는 주소를 값으로 가짐
    • 8개의 기본형을 제외한 나머지 타입

변수 타입에 대한 정리는 여기

📌 상수와 리터럴

상수(constant)는 말 그대로 변하지 않는 값. 그래서 변수 앞에 final을 붙여주면 된다.

final은 선언과 동시에 무조건 초기화를 해주어야하며, 그 이후엔 변경할 수 없다.

static 키워드도 같이 붙여주게 되는데,

붙이는 이유는 어떠한 객체에 final 변수가 하나 있다면 그 객체를 생성할 때마다,

final변수는 생기게 된다. 하지만 static 을 사용해서 메모리에 적재하게 두면 1개의 변수만이 여러개의 클래스에서 공유하게 되는 것이다.

이 상수를 쓰는 이유는 나는 2가지라고 생각한다.

첫째, 메서드안에 연산을 수행할 때, 하드 코딩 으로 숫자를 집어넣게 되면 무슨일을 하는 숫자인지 모를 수 있다.

그렇기에 상수로 떼어내어서 어떤 역할을 하는 변수인지 의미있게 이름을 준다.

둘째, 반복되는 숫자를 상수로 도출하면 반복코드를 제거할 수 있고, 들어가 있던 값을 상수만 바꿔주면

수정작업도 용이하기 때문이라고 본다.

📌 형변환

캐스팅(casting) 이라고도 부르며 서로 다른 타입간의 연산을 수행할 때,

같은 타입으로 변환시켜주는 것이 바로 형변환이다.

변수나 리터럴 앞에 변환하고자 하는 타입을 명시해주면 변환이 된다.

(타입)피연산자

기본형에서 boolean을 제외한 나머지 타입들은 서로 형변환이 가능하다.

하지만 기본형과 참조형간의 형 변환은 불가능하다.

그리고 기본형 타입에서도 작은 값을 큰값으로 그러니까 int형 변수를 long 자료형에 넣는다고 한다면

자동 형변환이 일어나게 된다.


2021-11-06 추가 업데이트

자바 공식문서 페이지를 보면서 추가적으로 여러개를 더 정리해보았다.

📌 변수의 종류

📌 인스턴스 변수 (비정적 필드)

static 키워드 없이 선언된 필드에 저장한다.

해당 값이 클래스의 각 인스턴스(각 개체마다)에 고유하기 때문에 인스턴스 변수라고 한다.

📌 클래스 변수(static 필드)

이 변수는 클래스가 인스턴스화 된 횟수에 상관없이 정확히 한개만 있음을 컴파일러에게 알려준다.

static은 초기화 후에 변경되지 않게 하려고 final을 같이 붙여서 사용해준다.

📌 지역 변수

객체가 필드에 상태를 저장하는 방식과 유사하게, 메서드는 임시 상태를 지역변수에 저장한다.

지역변수는 선언된 메서드에서만 볼 수 있다. 나머지 클래스에서는 액세스 불가능

📌 매개변수

흔히 보던 main메서드

public static void main(String[] args){} 에서 args가 바로 매개 변수이다.

매개변수는 항상 필드 가 아닌 변수 로 분류된다는 것이다.

728x90

'Java' 카테고리의 다른 글

변성  (0) 2022.08.11
일급 컬렉션  (0) 2022.08.09
상태 패턴 적용  (0) 2022.08.07
프로젝트 리팩토링  (0) 2022.08.07

+ Recent posts