(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 ... 등 추가적으로 다루어야 할 것들은 이후 포스팅에서 차근차근 정리해보려고 한다.
"객체 지향 프로그래밍에 있어서 불변객체는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변 객체로 생성 후에도 상태를 변경할 수 있다"라고 정의되어 있다. 명료하지만 예시를 바로 떠올릴 수 없어서 다소 추상적이기도 하다.
먼저 예시를 들면 자바에서 제공하는 기본 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의 예시처럼 가변객체 자체가 변경될 수 있다는점에 유의해야 한다.
우선은 문제를 차근차근 읽어보며 문제를 이해하려고 했다. 문제를 읽다보니 구현해야할 기능이 한 문제 내에 꽤 많아보였고, 평소에 이런 문제처럼 구현해야할 기능들이 많은 경우 조건을 놓치고 디버깅하는데에 시간을 많이 할애해야하는 경우가 많았기에 조금 더 꼼꼼히 읽고 기능을 정리 했다.
문제의 입력으로 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. 제초되는 최대 나무가 같은칸이 존재하는 경우 주어진 조건에 따라 인덱스를 계산하는 기능
처음에 문제를 천천히 읽어 보면서 이 문제의 특징을 먼저 파악해 보려고 했다. 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를 활용하여 구현하기로 했다. 이 때 괄호를 사용하지 않는 경우의 값 또한 구해주어야 한다는 점을 고려해야 한다. 연산자 인덱스를 기준으로 계산을 생각해서 구현했다.
사실 처음에는 괄호를 어떻게 처리해야 할 지 감이 잘 오지 않았다. 그 이유는 수식 앞에서 부터 순서대로 괄호를 넣는 케이스를 예시로 생각했었고 아래와 같이 앞에서 부터 케이스별로 괄호를 넣을 수 있는 방법을 구현하려고 하니 복잡하고 잘 떠오르지 않아서 많이 헤맸다.
문득 코딩테스트 공부를 하며 코드를 작성하다가 현타가 왔다. 원래 중간중간 나에게 질문을 하는 버릇이 있는데, 평소에 익숙하게 사용하던 코드를 보다가 단순한 의문이 들었다. HashSet을 사용하다가 타입 파라미터.. -> 제네릭 ..-> Reference Type , Primitive Type 이렇게 의식의 흐름대로 생각을 따라가며 정리를 하다보니 미흡한 부분이 있었고 이 참에 블로그 첫 게시글을 시작으로 부족했던 개념들을 복습할겸 정리해보기로 했다.
Primitive Type(원시 타입) vs Reference Type(참조 타입)
먼저, 래퍼클래스를 이해하기 위해서는 primitive type 변수와, reference type 변수의 차이를 이해해야 한다. 자바에서는 아래 총 8가지의 primitive type을 정의하고 제공한다.
쉽게 얘기하면 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() 생략