CQS 에 대해 정리해보려고 한다. 

 

CQS는 먼저 디자인 패턴 중 하나이다. 

디자인 패턴이라는 것이 말로보면 어려운데, 쉽게 얘기하자면 소프트웨어를 개발하는데, 이러이러한 특정 패턴으로 개발을 하니 복잡성을 줄이고 재사용성, 유지보수성 등등이 좋아지더라.. 하고 발견된 여러 패턴들을 묶어서 디자인 패턴이라고 부른다. 

 

1. 커맨드와 쿼리를 분리하라

글 제목과 같이 커맨드와 쿼리를 분리하라가 이 패턴의 핵심이다.

 

해당 개념을 처음으로 소개한 Martin Fowler 는 command와 query를 다음과 같이 정의했다. 

 

Queries: Return a result and do not change the observable state of the system (are free of side effects).

쿼리는 시스템의 상태를 변경하지 않고, 그 결과를 반환하는 함수를 말하며,

 

Commands: Change the state of a system but do not return a value.

커맨드는 시스템의 상태를 변경하지만 그 결과를 반환하지 않는 함수를 말한다.

 

우리가 흔히 쓰는 자바의 getter / setter 함수가 각각 쿼리, 커맨드에 해당한다. 

 

예시를 보자

 

(1) 위 코드는 시스템 내부 상태를 변경하는 update 함수이며, 커맨드에 속한다. 

 

 

(2) 위 코드는 시스템 내부 상태를 변경하면서 Member 객체를 반환한다. CQS가 지켜지지 않은 경우이다.

 

그렇다면 CQS 를 지켰을 때 얻을 수 있는 효과는 어떤 것이 있을까?  

1. 각 메서드의 의미가 더 명확해진다. 

2. 디버깅이 편리해 진다.

3. 유지보수가 편리해 진다. 

 

side effect의 정의는 맥락마다 다르지만 여기서 쓰이는 side effect란 내부에 변경이 일어나는지 아닌지를 말한다. 즉 내부에 변경이 일어나지 않으면 side effect가 없는 것 이다. 

 

먼저 쿼리는 조회만을 하기 때문에 side effect를 발생시키지 않는다. 하지만 커맨드는 상태를 변경시키기 때문에 side effect 에 주의해야 한다.

결국 CQS 의 핵심은 메서드 내의 side effect가 명확히 드러나 인지하고 사용할 수 있게 하는 것이 중요하고 그렇기 때문에 둘의 역할을 분리하는 것이 좋다가 핵심인 것 같다.

 

 

 

 

 

 

참고 

https://martinfowler.com/bliki/CommandQuerySeparation.html

위 그림은 SpringMvc의 아키텍처이다. 전체적인 흐름을 한번 살펴보고 들어가자.

(ArgumentResolver , HttpMessageConverter, Interceptor 에 대한 내용은 차차 다뤄볼 예정이다) 

 

클라이언트와 서버가 통신할 때 요청의 흐름을 생각해보면 

클라이언트의 request ->  WAS(톰캣) -> Filter -> DispatcherServlet -> Interceptor -> 컨트롤러 정도의 흐름으로 정리될 수 있을 것이다. 

 

이 중 스프링 MVC의 핵심 로직인 DispatcherServlet 라는 녀석은 어떤일을 하고 어떻게 구현되어 있을까? 


1. 다양한 종류의 컨트롤러(HandlerMapping)와 어댑터(HandlerMappingAdapter)

스프링은 다양한 종류의 컨트롤러를 지원한다. 

 

1) HttpRequestHandler

2) Servlet

3) Controller

4) Requestmappinghandlermapping (어노테이션 방식의 컨트롤러)

5) ....등등 

 

이러한 다양한 종류의 컨트롤러를 모두 지원하기 위해서는 앞에서 수문장 역할을 하는 녀석이 필요한데 DispatcherServlet이 수문장역할을 하는 녀석이다. 

 

 

위 코드는 dispatcherServlet 코드의 일부분이다. 위 initStrategies 메서드에서 각종 요소들을 초기화 해주는 작업을 진행하는데, 이 중  initHandlerMappings 라는 메서드를 호출하는 것을 알 수 있다. 사실 우리가 스프링 빈으로 등록하여 사용하는 컨트롤러나 , 스프링 내부적으로 등록되어 있는 컨트롤러등 많은 컨트롤러들이 HandlerMaping 객체로써 initHandlerMappings 라는 메서드를 통해 handlerMappins라는 리스트에 담기게 된다. 

 

HandlerMapping 객체를 담는 리스트
dispatcherServlet의 initHandlerMapping 메서드 일부

 

이 후, handlerMappings 에 등록되어진 많은 컨트롤러 중 요청을 처리할 수 있는 핸들러 (컨트롤러)를 가져온 뒤 해당 컨트롤러를 실행시킬 수 있는 어댑터를 찾아서 컨트롤러의 로직을 실행한다. 스프링 내부에는 위에서 언급한 다양한 종류의 컨트롤러를 다룰 수 있는 어댑터들이 존재하는데 다음과 같은 어댑터들이 있다. 

 

1) RequestMappingHandlerAdapter 

     - 우리가 흔히 사용하는 어노테이션 방식의 RequestMappingHandlerMapping 을  다루는 어댑터

2) HandlerFunctionAdapter

3) HttpRequestHandlerAdapter

4) SimpleControllerHandlerAdapter

 

스프링에서는 아래와 같이 HandlerAdapter들을 초기화 한다. 

 

dispatcherServlet의 initHandlerAdapters 메서드 일부

 

이후 dispatcherServlet의 doService -> doDispatch 를 통해 요청에 맞는 핸들러를 찾고 (아래 코드의 getHandler()에서) , 해당 핸들러를 다룰 수 있는 어댑터(아래 코드의 getHandlerAdapter() 에서)를 찾고 우리가 구현한 컨트롤러의 로직이 핸들러 어탭터를 통해 실행되는 흐름이다. 

dispatcherServlet의 doDispatch 메서드 일부

 

HandlerAdapter 인터페이스

 

참고로 HandlerAdapter 인터페이스를 살펴보면 HandlerAdapter를 구현한 구현체들은 supports(), handle() 메서드를 구현해야한다.

supports() 메서드를 통해 핸들러를 처리할 수 있는 어댑터를 찾는다.

HandlerMapping 인터페이스도 마찬가지로 위 supports() 와 같은 메서드를 통해 다형성을 활용하여 요청을 처리할수 있는 핸들러를 찾는다.

 

2. View

 

아래 그림의 handle() 을 보면 ModelAndView 객체를 반환하는 것을 알 수 있다. 이렇게 로직을 처리하고 반환받은 ModelAndView 객체를 통해서 뷰를 렌더링 해야한다. 

dispatcherServler의 doDispatch 메서드 일부

 

렌더링하기 전에 applyDefaultViewName() 메서트를 통해 view resource 의 경로를 얻고, ModelAndView 객체에 세팅해준다.

 

참고로 RequestToViewNameTranslator라는 객체를 통해 resource의 경로를 얻는다.

( RequestToViewNameTranslator도 인터페이스이며,

RequestToViewNameTranslator를 구현한 DefaultRequestToViewNameTranslator 구현체를 사용한다. )

 

이렇게 경로를 세팅해 준 뒤 비로소 processDispatchResult() 메서드를 통해 아래 코드의 순서로 뷰가 렌더링 된다. 

 

 

 

이 때 사용되는 ViewResolver 인터페이스의 구현체들은 아래와 같고, 우리가 사용하는 템플릿엔진에 따라 다른 viewResolver 구현체가 유연하게 끼워져서 사용된다. 

ViewResolver를 구현한 구현체들

 

마지막으로 다시한번 mvc 아키텍처를 살펴보면서 흐름을 정리하면 아래와 같이 정리할 수 있다.

 

     요청에 따른 핸들러매핑을 찾고 

-> 핸들러 매핑을 처리할 수 있는 어댑터를 찾는다

-> 핸들러 어댑터의 handle 함수를 호출하여 우리가 구현한 컨트롤러의 로직을 실행한 뒤 ModelAndView 객체를 반환 받는다 

-> resource 경로가 매핑된 ModelAndView 를 viewResolver 에 넘긴다 

-> 우리가 사용하는 템플릿엔진에 맞는 viewResolver 구현체를 통해 view가 렌더링된다. 

 

추가로 위 구조를 이해하고 살펴보면 스프링 mvc 프레임워크가 인터페이스와 어댑터 패턴을 활용하여 얼마나 유연하게 설계되었는지 알 수 있다.

 

사실 위 내용에 더해서 살펴봐야 할 것들이 아직 많이 남았는데 argumentResolver , HttpMessageConverter ... 등 추가적으로 다루어야 할 것들은 이후 포스팅에서 차근차근 정리해보려고 한다.

자바의 Immutable Object(불변 객체) 에 대해서 다뤄보려고 한다.

불변 객체가 무엇이고 왜 필요한 것일까? 그리고 어떻게 사용되고 있는 것일까? 

1. 불변 객체란 무엇일까?

"객체 지향 프로그래밍에 있어서 불변객체는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변 객체로 생성 후에도 상태를 변경할 수 있다"라고 정의되어 있다.  명료하지만 예시를 바로 떠올릴 수 없어서 다소 추상적이기도 하다. 

 

먼저 예시를 들면 자바에서 제공하는 기본 Wrapper Class ( String, Integer , Boolean ... ) 가 불변 객체에 해당한다. 

 

2.불변객체에 대한 메커니즘 이해 

 

우리가 생성하는 객체와 자바의 String,Integer 와 같은 기본객체들은 어떤 차이가 있을까?  

개발자들은 글을 보는 것보다 코드를 통한 이해가 더 빠르기 때문에 바로 예시를 들어 이야기 해보자.

 

우리가 정의한 A라는 클래스의 인스턴스를 생성하고, 해당 인스턴스를 myObject1,myObject2라는 변수가 참조할 때, 같은 곳을 참조하게 되고, A 오브젝트의 name 필드값이 변경되면 같은곳을 참조하고 있기 때문에 둘다 Bye라는 값을 반환한다. 

 

 

 

우리가 정의한 클래스 말고 자바에 정의된 기본객체들은 아래와 같은 간단한 코드가 있을 때 어떻게 동작할까 ?

우리가 정의한 객체와 같은 메커니즘으로 동작한다면 Hello,hi 가 아닌 hi,hi 가 나와야 한다. 

하지만 불변객체로 설계된 기본객체들은 참조값의 값을 변경하는 것이 아니라 새로운 공간에 새로운 값을 할당한다. 

즉, example1과 exampl2 가 같은 주소값(1번지라고 예시를 들겠음) 을 바라보고 있고 example2를 통해 새로운 문자열 "hi" 로 값을 변경한다해도 example1은 기존과 동일하게 1번지를 바라보게 되고 example2는 hi가 위치하고 있는 새로운 번지수를 바라보게 된다. 

 

3. 왜 불변객체를 사용하는가?

- 스레드 안전성: 여러 스레드가 동시에 같은 불변 객체를 참조하더라도, 어느 한 스레드가 객체의 상태를 변경할 수 없기 때문에 동기화 문제가 발생하지 않는다. 이는 복잡한 동기화 로직 없이도 데이터 일관성을 유지할 수 있게 해준다.

 

이해를 돕기 위한 시나리오를 추가하자면 

  • A가 "Hello"라는 데이터를 가진 String 객체를 읽고 있을 때, 이 데이터는 메모리에 저장된 후 변경될 수 없다. 즉, 객체 내부의 데이터는 생성 시점의 상태로 고정된다.
  • B의 변경 시도: B가 같은 "Hello" 값을 변경하고 싶다면, String 객체는 변경할 수 없기 때문에 새로운 String 객체를 생성해야 한다. 예를 들어, B가 "Hello"에 "Bye"를 추가하고 싶다면 new String("HelloBye")와 같은 새로운 객체를 만들 수 있다. 하지만 이는 원래 A가 참조하고 있는 "Hello" 객체와는 전혀 다른, 새로운 메모리 주소에 저장된 객체이다.
  • 결과: 따라서 A가 읽고 있는 "Hello" String 객체는 그대로 유지되며, B의 작업은 원래 객체에 영향을 주지 않는다. A는 여전히 원본 "Hello" 데이터를 안전하게 읽을 수 있다.

- 데이터 일관성: 불변 객체를 사용하면 데이터의 신뢰성이 보장된다. 데이터가 예측 가능하고, 오류 가능성이 줄어들어 버그 발생 확률이 낮아진다.

 

- 메모리 효율성: 같은 값의 불변 객체는 메모리에서 한 번만 생성되어 여러 참조에서 공유될 수 있다. 이는 메모리 사용을 최적화하고 성능을 개선하는 데 도움이 된다.

 

4.불변객체를 만드는 방법 

 

1. setter를 사용하지 않는다. (객체의 상태를 변경하는 메소드 미제공)
2. private으로 선언한다.

3. final을 선언한다.

 

 

ImmutableCar객체를 보면 필드값을 수정할 수 없게 설계 되었다. 따라서 한번 지정한 값에 대해 불변이다.

 

하지만 불변객체 내에 가변객체를 참조받고 있다면 이야기가 달라진다. 

 

 

 

ImmutableCar 객체를 불변객체로 설계하기 위해 다음과 같이 코드를 작성했다. 하지만 Sonata객체 자체가 가변객체이기 때문에 Sonata 객체의 version에 대한 수정은 열려있다. 그렇기 때문에 불변객체가 되려면 참조객체 역시 불변객체여야 한다는 것을 알 수 있다.

 

5. 자바의 불변 List

자바는 List를 불변으로 만드는 방법을 제공한다. Collections의 unmodifiableList와 List.of 를 통해 불변 리스트를 얻을 수 있다. 

아래 예시와 같이 해당 리스트에 값을 추가하려고 하면 아래와 같은 친철한 Intellij의 설명과 , 오류를 만나게 된다.

 

 

하지만 위 Collections.unmodifiableList() 로 얻는 객체또한 완전한 불변이라고는 할 수 없는데, 만약 unmodifiableList의 element가 가변객체라면 위 Sonata의 예시처럼 가변객체 자체가 변경될 수 있다는점에 유의해야 한다. 

 

 

'Java > Base' 카테고리의 다른 글

[Java] Wrapper 클래스와 Boxing , Unboxing  (0) 2023.09.14

https://www.codetree.ai/training-field/frequent-problems/problems/tree-kill-all/description?page=1&pageSize=20

 

코드트리 | 코딩테스트 준비를 위한 알고리즘 정석

국가대표가 만든 코딩 공부의 가이드북 코딩 왕초보부터 꿈의 직장 코테 합격까지, 국가대표가 엄선한 커리큘럼으로 준비해보세요.

www.codetree.ai

 

 

우선은 문제를 차근차근 읽어보며 문제를 이해하려고 했다. 문제를 읽다보니 구현해야할 기능이 한 문제 내에 꽤 많아보였고,  평소에 이런 문제처럼 구현해야할 기능들이 많은 경우 조건을 놓치고 디버깅하는데에 시간을 많이 할애해야하는 경우가 많았기에 조금 더 꼼꼼히 읽고 기능을 정리 했다. 

 

문제의 입력으로 4개의 정수가 들어온다. 처음부터 각각이 의미하는 변수를 다 기억하는 것이 구현해야할 기능을 생각하고 코드를 구현하다보면 헷갈려서 대충 뭐가있다 정도로만 파악해두었다. 

 

위 그림과 같이 n x n 크기의 보드판에 나무( 0보다 큰 정수) , 벽 (-1 ) , 빈칸 ( 0 ) 에 대한 정보가 입력으로 주어진다. 이후 각각의 나무들은 문제에서 주어진 조건에 따라 성장과 번식을 하게 되고 이후 보드판을 살펴 나무가 있는 곳에 제초제를 뿌리리는데 이 때 제초제를 뿌렸을 때 가장 많은 나무를 박멸할 수 있는 칸을 선택하여 제초제를 뿌려야한다. 이 과정들 (성장, 번식, 제초) 이 1년 주기로 반복된다. 문제는 이 메커니즘으로 작동한다고 보면 되고 추가적으로 고려해야할 상황들이 있다.  

1. 나무의 성장

나무가 있는 칸을 기준으로 상,하,좌,우 방향을 살폈을 때 나무가 존재하는 칸의 수 만큼 해당칸의 나무의 개수가 늘어난다. 즉 위 그림의 46 그루의 나무가 있는 칸을 기준으로 살폈을 때 , 좌,우,하방향에 나무가 존재하고 있으므로 해당칸은 46 + 3 인 49이 된다. 이 때 성장은 성장은 모든 나무에게 동시에 일어난다.

아래의 기능들이 필요하다.

    - 방향을 체크할 때 보드판의 index범위를 벗어 날 수 있으므로 범위를 체크하는 기능 

    - 방향을 살피고 나무를 성장시키는 기능

2. 나무의 번식 

나무가 있는 칸을 기준으로 상,하,좌,우 살폈을 때 빈칸 (벽, 다른 나무, 제초제 모두 없는경우) 이 있는경우 , 각 빈칸에 해당 칸에 있는 나무의수 / 빈칸의 수 만큼의 나무가 번식한다. 이 때 나눌 때 생기는 나머지는 버리고 번식의 과정은 모든 나무에서 동시에 일어난다.

 

다음과 같은 기능들이 필요하다.

 

1. 방향을 체크할 때 보드판의 index범위를 벗어 날 수 있으므로 범위를 체크하는 기능 

2. 빈칸의 개수를 체크하고, 번식을 위한 빈칸의 위치를 기억하는 기능 

3. 번식될 나무의 개수를 계산하는 기능

 

나무의 성장과 번식 과정에서 확인해야 하는 방향이 상,하,좌,우 로 같고 둘다 2중 반복문을 돌아야 하기 때문에 하나의 기능으로 묶어서 구현했다. 

먼저 상,하,좌,우를 나타내는 DIRECTION1 , 대각선 방향을 나타내는 DIRECTION2 ( 이따 제초과정에서 사용됨) 을 정의하고 문제에서 주어진 변수들을 정의해주었다. 

각 변수를 입력받아 문제해결에 필요한 변수들을 초기화 시켜주었다. 

 

성장과 번식

 

cloneMatrix를 선언해준 이유는 나무가 번식할 때 기존 보드판을 업데이트 하게되면 번식한 칸 마저 기존에 나무가 있던 칸으로 인식되는 일이 발생하기 때문에 정보를 저장해줄 임시 보드판이 필요했다. 

deepCopy 는 2차원 배열 깊은 복사를 위해 따로 만들어주었다. 

 

 

이 함수에서 빈칸에 대한 정보를 담고, 나무의 성장이 일어난다. 급하게 코드를 작성한다고 메서드 명이 적절하지 않은 것 같긴하다..

checkMatrixRange 함수는 중복되는 기능이라 따로 분리해주었다. 간단하게 보드판의 indexRange 를 체크해주는 함수이다. 

 

 

 

3. 나무 제초 

각 칸 중 제초제를 뿌렸을 때 나무가 가장 많이 박멸되는 칸에 제초제를 뿌린다. 나무가 없는 칸에 제초제를 뿌리면 박멸되는 나무가 전혀 없는 상태로 끝이 나지만, 나무가 있는 칸에 제초제를 뿌리게 되면 4개의 대각선 방향으로 k칸만큼 전파되게 된다. 단 전파되는 도중 벽이 있거나 나무가 아얘 없는 칸이 있는 경우, 그 칸 까지는 제초제가 뿌려지며 그 이후의 칸으로는 제초제가 전파되지 않는다. 제초제가 뿌려진 칸에는 c년만큼 제초제가 남아있다가 c+1년째가 될 때 사라지게 된다. 제초제가 뿌려진 곳에 다시 제초제가 뿌려지는 경우에는 새로 뿌려진 해로부터 다시 c년동안 제초제가 유지된다.

 

만약 박멸시키는 나무의 수가 동일한 칸이 있는 경우에는 행이 작은 순서대로, 만약 행이 같은 경우에는 열이 작은 칸에 제초제를 뿌리게 된다.

 

편의를 위해 제초제년 수에 따라 -2 부터 작아지는 방향으로 생각하고 구현했다.

다음과 같은 기능이 필요하다.

 

1. 가장 많이 제초되는 칸을 구하는 기능 

2. 전파 될 곳을 확인하고 제초제를 전파하는 기능 

3. 전파된 땅의 값을 제초제 년수에 따라 업데이트하는 기능 

4. 제초된 나무들의 합을 구하는 기능 

5. 제초되는 최대 나무가 같은칸이 존재하는 경우 주어진 조건에 따라 인덱스를 계산하는 기능

 

 

 

 

 

 

https://www.acmicpc.net/problem/16637

 

16637번: 괄호 추가하기

길이가 N인 수식이 있다. 수식은 0보다 크거나 같고, 9보다 작거나 같은 정수와 연산자(+, -, ×)로 이루어져 있다. 연산자 우선순위는 모두 동일하기 때문에, 수식을 계산할 때는 왼쪽에서부터 순

www.acmicpc.net


처음에 문제를 천천히 읽어 보면서 이 문제의 특징을 먼저 파악해 보려고 했다.
1. 연산자의 우선순위가 없다.
2. 중첩 괄호를 사용할 수 없다.

 

위 두가지 특징을 염두해두고 문제 케이스에 대해 살펴 보기로 했다.

 

예제 입력 1에 있는 예시를 가지고 먼저 가능한 케이스에 대해 고려 해보면

  • (3+8)*7-9*2
  • (3+8)*(7-9)*2
  • (3+8)*7-(9*2)
  • 3+(8*7)-9*2
  • 3+(8*7)-(9*2)
  • 등등...

위 케이스들을 고려하면서 괄호를 추가하는 것에 대한 규칙성이 있는지에 대해 고민을 해보았지만 규칙이 보이지 않았다. 문제에서 주어진 N의 최대 길이가 19이고 시간 제한이 0.5초이기 때문에 따로 규칙을 고려하지 않고, 가능한 모든 경우의 수를 다 찾더라도 시간내에 풀 수 있을 것이라고 생각했다. 

 

먼저 주어진 입력을 받기 위한 버퍼를 정의했다. 이 후 연산자와 숫자를 따로 처리하는 것이 편할 것 같아서 각각을 받는 ArrayList 를 정의했다. 입력값에는 (zerobased index 기준) 0을 포함한 짝수번째 값에는 숫자, 홀수번째 값에는 연산자가 들어오는 규칙이 있기 때문에 이를 활용해서 입력값을 저장해 주었다. 

이제 정답값을 구하기 위한 솔루션만 구현하면 되고 이는 완전탐색을 위해 dfs를 활용하여 구현하기로 했다. 이 때 괄호를 사용하지 않는 경우의 값 또한 구해주어야 한다는 점을 고려해야 한다. 연산자 인덱스를 기준으로 계산을 생각해서 구현했다. 

 

사실 처음에는 괄호를 어떻게 처리해야 할 지 감이 잘 오지 않았다. 그 이유는 수식 앞에서 부터 순서대로 괄호를 넣는 케이스를 예시로 생각했었고 아래와 같이 앞에서 부터 케이스별로 괄호를 넣을 수 있는 방법을 구현하려고 하니 복잡하고 잘 떠오르지 않아서 많이 헤맸다. 

e.g.) 
1. 3+8*7-9*2 (= (3+8)*7-9*2))
2. 3+(8*7)-9*2
3. 3+(8*7)-(9*2)

...

 

하루정도 리프레쉬를 하고.. 다시 생각해보니 백트래킹으로 풀면 되는데.. 라는 생각이 들어서 아래와 같이 구현했다. 연산자 인덱스를 기준으로 조건문을 생각했고, 모든 경우의 수를 다 계산할 수 있는 방법이다. 

연산에 대한 함수는 따로 분리해주었다. 위 switch-case 문은 자바 11 에서는 지원하지 않는 문법(자바 11이후 부터 지원하는 문법)이라 백준에 제출할 때는 기존 switch-case expression을 사용해서 제출했다. 물론 if문을 사용해도 무방하다. 

 

 

아직까지 백트래킹에 대한 아이디어가 바로바로 떠오르지 않는 것을 보니 더 연습이 필요하다고 느꼈다. 

잡설 

 문득 코딩테스트 공부를 하며 코드를 작성하다가 현타가 왔다. 원래 중간중간 나에게 질문을 하는 버릇이 있는데, 평소에 익숙하게 사용하던 코드를 보다가 단순한 의문이 들었다. HashSet을 사용하다가 타입 파라미터.. ->  제네릭 ..-> Reference Type , Primitive Type 이렇게 의식의 흐름대로 생각을 따라가며 정리를 하다보니 미흡한 부분이 있었고 이 참에 블로그 첫 게시글을 시작으로 부족했던 개념들을 복습할겸 정리해보기로 했다. 

 

Primitive Type(원시 타입) vs Reference Type(참조 타입)

 먼저, 래퍼클래스를 이해하기 위해서는 primitive type 변수와, reference type 변수의 차이를 이해해야 한다. 자바에서는 아래 총 8가지의 primitive type을 정의하고 제공한다.

  타입 할당되는 메모리크기 기본값 데이터의 표현범위
논리형 boolean 1 byte false true,false
정수형 byte 1 byte 0 -128~127
short 2 byte 0  -32,768 ~ 32,767
int (기본형) 4 byte 0 -2,147,483,648 ~ 2,147,483,647
long 8 byte 0L -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
실수형 float 4 byte 0.0F (3.4 X 10-38) ~ (3.4 X 1038) 의 근사값
double (기본형) 8 byte 0.0  (1.7 X 10-308) ~ (1.7 X 10308) 의 근사값
문자형 char 2 byte (유니코드) '\u000' 0 ~ 65,535

primitive type의 특징으로는 다음과 같다.

  • 모두 소문자로 시작된다.
  • 비객체 타입이므로 null 값을 가질 수 없다. 
  • 제너릭에서 타입파라미터로 사용될 수 없다.
  • stack에 바로 사용할 수 있는 값(실제 값)을 바이트 단위로 저장한다.

 reference type의 특징으로는 다음과 같다.

  • 기본형 과는 달리 실제 값이 저장되지 않고, 자료가 저장된 공간의 주소를 저장한다.
  • 즉, 실제 값은 다른 곳에 있으며 값이 있는 주소를 가지고 있어서 나중에 그 주소를 참조해서 값을 가져온다.
  • 메모리의 힙(heap)에 실제 값을 저장하고, 그 참조값(주소값)을 갖는 변수는 스택에 저장
  • 참조형 변수는 null로 초기화 시킬 수 있다
더보기

쉽게 얘기하면 reference type 은 위 8가지의 primitive type 자료형을 제외한  모든 자료형을 reference type 이라고 볼 수 있다. 

ex) 기본적으로 제공하는 클래스, 프로그래머가 스스로 만든 클래스, 배열, 열거 타입 등...

 

Wrapper Class(래퍼 클래스)

코딩을 하다보면 primitive type의 변수를 reference type 변수로 변경해야할 때가 생기곤 한다. 예를들어 메서드 파라미터의 타입이 reference type 이라면 primitive type의 변수를 그대로 사용할 수 없다. 이럴 때는 기본타입의 변수를 객체타입의 변수로 변환해 주는 작업이 필요하다. 이렇게 8개의 기본 타입에 해당하는 데이터를 객체로 포장해 주는 클래스를 래퍼 클래스(Wrapper class)라고 한다.  

primitive type(기본타입) wrapper class(래퍼클래스)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

 

Boxing & Unboxing

 래퍼 클래스(Wrapper class)는 산술 연산을 위해 정의된 클래스가 아니므로, 인스턴스에 저장된 값을 변경할 수 없다. 단지, 값을 참조하기 위해 새로운 인스턴스를 생성하고, 생성된 인스턴스의 값만을 참조할 수 있다.  그렇기 때문에 값을 변경할 때에는 래퍼 클래스를 언박싱 한 뒤에 값을 변경하고 다시 박싱을 해야하는 중간단계를 거쳐야 한다.

 

  • Boxing : 기본 타입의 데이터 → 래퍼 클래스의 인스턴스로 변환
  • UnBoxing : 래퍼 클래스의 인스턴스에 저장된 값 → 기본 타입의 데이터로 변환
Integer num1 = new Integer(1); // boxing
Integer num1 = 1; // 이런식으로도 표현 가능

Double num2 = new Double(1.11); // boxing
Double num2 = 1.11; // 이런식으로도 표현 가능



int n1 = num1.intValue(); // unboxing

n1 = n1 + 100; // 101 (값변경)
num1 = new Integer(n1); //boxing (재포장)


double n2 = num2.doubleValue(); // unboxing

n2 = n2 + 100.0; // 101.11 (값변경)
num2 = new Double(n2); //boxing (재포장)

 각 Wrapper Class 의 언박싱 메서드들은 다음과 같다.

메서드 반환값 설명
byteValue() byte 객체의 값을 byte 값으로 변환하여 반환
shortValue() short 객체의 값을 short 값으로 변환하여 반환
intValue() int 객체의 값을 int 값으로 변환하여 반환
longValue() long 객체의 값을 long 값으로 변환하여 반환
floatValue() float 객체의 값을 float 값으로 변환하여 반환
doubleValue() double 객체의 값을 double 값으로 변환하여 반환
charValue() char 객체의 값을 char 값으로 변환하여 반환
booleanValue() boolean 객체의 값을 boolean 값으로 변환하여 반환 

 기능적 편의성을 위하여 오토 박싱 / 언박싱을 제공하지만, 다른 타입간의 형 변환은 어플리케이션의 성능에 영향을 미치게 된다. 비록 사소한 차이 일지라도 어플리케이션의 성능 측면에서 봤을때 반드시 필요한 상황인지 한번 생각해 볼 필요가 있을 것 같다.

오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)

 JDK 1.5부터는 박싱과 언박싱이 필요한 상황에서 자바 컴파일러가 이를 자동으로 처리해 준다고 한다. 이렇게 자동화된 박싱과 언박싱을 오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)이라고 부른다.

/* 기존 박싱 & 언박싱 */
Integer num = new Integer(17); // 박싱
int n = num.intValue();        // 언박싱

/* 오토 박싱 & 언박싱 */
Integer num = 17; // new Integer() 생략
int n = num; // intValue() 생략

참조

'Java > Base' 카테고리의 다른 글

[Java] Immutable Object(불변객체)  (0) 2024.04.21

+ Recent posts